Discover Intent
ASecurityTurn whatever a person shares to explain what they want — a prototype (a file, a project folder or a deployed site), a document, or a plain statement — into the person''s Business Intents: each need the product meets for its user, at the level of the product and in its users'' words, with why the business needs it, who asks for it as a persona, and a measurable proof — the play drafts these with their basis — each confirmed or dropped by them. Writes business intents only (ADR 043): no ICE, n...
- 3 stars
- 0 votes
- 0 copies
- 0 views
- Added November 7, 2026
Works with
Security analysis
100/100Pro scans all 18 files and shows the line behind each finding
npx -y skills add intent-driven-ai/garura --skill discover-intent --agent claude-codeAre you the author of Discover Intent?
Add the live security badge to your README. It updates with every re-scan.
[](https://www.skillsdirectory.com/skills/intent-driven-ai-discover-intent)---
name: discover-intent
position: both
description: 'Turn whatever a person shares to explain what they want — a prototype (a file, a project folder or a deployed site), a document, or a plain statement — into the person''s Business Intents: each need the product meets for its user, at the level of the product and in its users'' words, with why the business needs it, who asks for it as a persona, and a measurable proof — the play drafts these with their basis — each confirmed or dropped by them. Writes business intents only (ADR 043): no ICE, no capability, no domain — /discover-domains writes domains and /discover-capabilities writes capabilities and features with their ICE; the links are made later. Keeps what they shared as Sources, exactly as they were when read, and links intents and Sources both ways. The product model is the hand-off to later plays. A play of the Discovery loop, and the hand-in from a person to the product model. Run by hand it opens and lands its own change; handed a loop''s JSON contract it opens nothing of its own.'
user-invocable: true
---
# intent
Turn what a person shares into the person's **Business Intents**, and keep what they shared
as its **Source**:
- **Business Intents** — the person's level: each need the product meets for its user, at the
level of the product and in its users' words — one per thing the product offers, following
its main areas — with why the business needs it, who asks for it (a persona) and how we will
know it is met (a measurable result). The play drafts why, asked by and proof with their
basis, never leaves them open and never asks for them (C17). The person confirms or drops
each; nothing else does.
This play writes business intents only (ADR 043). It writes no ICE, no capability and no
domain: `/discover-domains` writes the domains, and `/discover-capabilities` writes the capabilities and
features with their ICE. Each kind is written on its own and linked later — by the play
that links a part to an intent, or, inside a loop, by the loop's gap skills. Run on its own,
this play leaves the rest of the model to those plays.
The Business Intent and the Source are kinds of the product ontology
(`standards/schemas/product-os/ontology.md`), and the product model is the hand-off.
The source is the input, never the intent. A prototype or a document shows one way to build
what the person wants; this play pulls out the *want* and leaves the *way* behind (IDD
Principle 1).
**Pipeline position: both.** Run by hand, the D2 rule prepends `start-change` and appends
`commit-change → propose-change → check-change → merge-change`, so `/discover-intent` opens and lands
its own change. **Inside a loop** the loop hands it a JSON contract (`--contract`); then it
opens no issue or branch of its own, skips the end sequence, and writes on the loop's branch
(ADR 030 P4; #612 D3).
## Compiled From
This play was compiled from the intent ICE (`reference/ice.md`) by create-play (#612).
Intent defines constraints (C1–C17) and failure conditions (F1–F17); the expectation defines
success scenarios (S1–S11), a Done means (D1–D5, baked to `stop-condition.yaml`), and one
recovery entry per failure condition. To modify this play, update `reference/ice.md` and
recompile with edit-play.
Do NOT edit this file manually — it is a compiled artifact.
## Role
You are the orchestrator. You own the step order and the one human checkpoint. You run the
mechanical work through bundled scripts — capturing the sources, checking the draft, saving
the intents, guarding the model — and you hand the one piece of judgment, drafting the
business intents from the sources, to the `business-intent-keeper` agent, which invokes
the `author-business-intent` skill and checks the draft, through a JSON contract over files
on disk. You ask the person every question the sources cannot answer — never why, asked by or
proof, which the agent drafts with their basis (C17) — each with example answers, and only
their typed reply confirms or drops each intent.
**Forbidden:** confirming or dropping an intent yourself or through any agent (C4); filling a
title, outcome or limit the source does not show and the person did not say (C5); an intent
that names the process or tools behind the screens, or the sample data's specifics, instead of
a need the product meets for its user, or a main section left without an intent of its own, or a why, asked by or proof left open, asked, or without its basis (C17); describing the source
inside an intent (C3); merging two needs into one intent, or leaving a part of the source
unmapped (C12); asking a question with no example answers (C13); writing another kind — an
ICE, a capability, a domain or a spine entry — or any model file other than the new intents
and their sources (C8); opening an issue or a branch while
running inside a loop (C9); writing short-term memory outside the issue being worked on, or
before `start-change` gives it the issue (C14); closing COMPLETED without the stop condition
held (C11).
**Agent boundaries:**
| Agent | Domain | Skill it invokes | Phases |
|-------|--------|------------------|--------|
| `business-intent-keeper` | Read every Source's snapshot and the person's answers, and the ontology through the memory tool (by its name, the config's `product.ontology`); draft the business intents — one per need the product meets — with provenance, a coverage map from every part of the source to its intent, and questions with example answers — and check the draft (coverage, merges and restated intents by judgment, the rest by script) before handing it back | `author-business-intent` | Draft |
| `prototype-reader` | Walk a prototype Source in journeys — who it serves, what each sets out to do, every step and what it showed — through one kept-open browser per journey, and check that every captured view is on a journey (Sonnet, medium effort) | `walk-prototype` | Walk |
| `template-manager` | Load the close's templates (the evidence file, the delivery report, the Next-line map) through the memory tool into the run's short-term memory | — | Close |
`business-intent-keeper` and `prototype-reader` are the **domain agents**: the walker uses the
prototype, the keeper drafts from what the walk found. It drafts business intents only; it
never writes the model. `template-manager` is a utility
agent of the shared close: the play reads no memory itself (ADR 039 §2).
The start and end sequence are injected
sub-plays. Everything else is a script.
## Pre-flight
| Check | Constraint | Action on Failure |
|-------|-----------|-------------------|
| Resolve config + `product_base` (`.garura/core/config.yaml`) | — | Hard halt |
| A source was given: a file path, a project folder, an http(s) link, or the text of a statement | C1 | Ask the person for it (gather, not a halt) |
| Loop contract: when `--contract <file>` is passed, the shared checker `check_loop_contract.py` accepts it — valid JSON naming `loop_run_id` and `branch` (and the rest of the loop contract), and the current branch equals that branch | C9 | Hard halt (REC9) |
| **Clean model tree** — after Step 0 (or at once, inside a loop), `git status --porcelain -- <product_base>product-os` is empty | C8 | Hard halt (REC8) |
**The issue comes first (C14).** Short-term memory is created in the issue being worked on.
Inside a loop that is the contract's `issue`, known now. Run by hand it is the issue Step 0
(`start-change`) opens or resolves. The run's working folder `<working>` is
`{stm_base}<issue>/context/intent/<run-ts>/`, and its status markers sit in
`{stm_base}<issue>/status/`; the stop condition reads `<working>` there.
Run the pre-flight checks and the bundled resolver — not inference — before Step 0, so a hard
halt leaves no issue and no branch. Run by hand, its files go to a scratch folder outside
`.garura/` (`<pre>`, made with `mktemp -d`), because no issue exists yet; inside a loop they go
straight to `<working>`. A halt before Step 0 deletes the scratch folder, so it leaves nothing
behind:
```
pre=$(mktemp -d) # run by hand; inside a loop, write to <working> instead
git branch --show-current
git status --porcelain > "$pre/porcelain.txt"
python3 scripts/preflight.py --play discover-intent --config .garura/core/config.yaml \
--branch "<current branch>" --porcelain-file "$pre/porcelain.txt" > "$pre/preflight.json"
python3 scripts/check_loop_contract.py --play discover-intent \
--branch "$(git branch --show-current)" [--contract <contract.json>] --out "$pre/run.json"
```
`preflight.py` takes no contract; the shared checker does (`standards/rules/loop-contract.md`).
A refused contract is a hard halt (REC9).
`in_loop` is true exactly when the checker accepted a `--contract` (it writes `run.json`). Once the issue is known (at
once inside a loop; right after Step 0 run by hand), create `<working>`, move the scratch
files into it, delete the scratch folder, run `preflight.py` again on the new branch so
`<working>/preflight.json` names the branch and the issue (run by hand), add the issue Step 0 gave to `<working>/run.json` (run by hand; inside a loop the checker
already wrote the contract's `loop_run_id`, `branch` and `issue`), and start the
session stamp:
```
python3 scripts/session_stamp.py --phase start \
--marker "{stm_base}<issue>/status/session-stamp-intent.json" \
--cwd "$(pwd)" --branch "$(git branch --show-current)"
```
Each Source's snapshot goes to the issue's evidence folder
`{stm_base}<issue>/evidence/intent/<run-ts>/source-<name>/` — committed with the run, so it
outlives the link or the file.
## Task DAG
Create ALL tasks immediately after pre-flight (before Step 0), before any domain work. Inside a loop, T0 and
E1–E4 are created and marked skipped (`in_loop`), never run.
```
[T0] start-change (injected — start; skipped in a loop) blockedBy: []
[T1] Capture the sources blockedBy: [T0]
[T1b] Walk the prototype in journeys (prototype only) blockedBy: [T1]
[T2] Draft the business intents blockedBy: [T1]
[T3] Ask and check (≤3 rounds) blockedBy: [T2]
[T4] Confirm or drop each intent (by hand; none in a loop) blockedBy: [T3]
[T5] Save the intents, link the sources blockedBy: [T4]
[T6] Guard the model blockedBy: [T5]
[T7] Commit the delta blockedBy: [T6]
[E1] commit-change (injected — end; skipped in a loop) blockedBy: [T7]
[E2] propose-change (injected — end; skipped in a loop) blockedBy: [E1]
[E3] check-change (injected — end; skipped in a loop) blockedBy: [E2]
[E4] merge-change (injected — end; skipped in a loop) blockedBy: [E3]
[T8] Scenario Validation blockedBy: [E4]
[T9] Close blockedBy: [T8]
```
Mark each task in progress before its step and completed right after its eval passes. No
runtime reordering. On resume, skip completed and reset in-progress to pending.
## Workflow
### Phase: Start (injected — D2 position: start)
**Step 0 — start-change** · Owner: `start-change` (sub-play) · Depends on: pre-flight
*Skipped when `in_loop`.* Run the start-of-pipeline member as a sub-play with
`parent_run_id`: it resolves or creates the issue, cuts the branch off fresh main, and inits
STM. `{issue}` in its result path is the issue it gave. Then create `<working>` in that issue,
move the pre-flight's scratch files into it and delete the scratch folder, write `run.json`,
start the session stamp (above), and run the clean-tree assertion.
{ "play": "start-change", "parent_run_id": "<this run id>",
"inputs": { "title": "intent: <a few words of what the person wants>" },
"outputs": { "result": "{stm_base}{issue}/context/intent/start/start-change.json" } }
**SE-14 (F14/C14):** after Step 0 (at once inside a loop) every file this run wrote to
short-term memory — `run.json`, the pre-flight files, the status markers, the sub-play results —
sits under `{stm_base}<issue>/`; run by hand, nothing was written under the short-term root
before Step 0, and the scratch folder is gone. Each Source's snapshot (Step 1) and
`answers.yaml` (Step 3) are written under the same issue.
### Phase: Capture
**Step 1 — Capture the sources** · Owner: play (script) · Depends on: Step 0
*Inside a loop that hands a capture (`inputs.intent.capture`, ADR 043), skip Steps 1 and 1b: the
Sources are the capture's two source manifests (the prototype and the person's write-up), and
the journeys are the capture's `journeys` — hand both to Step 2 and to the checks. On a later
round, `inputs.intent.targets` names the screens whose need no intent names yet: draft for those.*
Keep each source exactly as it was when read — one capture per Source, each with its own
`<name>`. For a statement, first write the person's exact words to `<working>/statement.txt`.
The given-by is the git user running the play (`git config user.name` and `user.email`):
```
python3 scripts/capture_source.py \
( --source <file path or link> [--also <more pages>] [--click <tab label> ...] \
| --statement <working>/statement.txt ) \
--out-dir {stm_base}<issue>/evidence/intent/<run-ts>/source-<name> \
--given-by "<git user.name> <<git user.email>>" \
--manifest <working>/source-manifest-<name>.json
```
A deployed site is run through in a headless browser and captured **view by view**: the
script finds every view the page offers by itself — each tab, button or link that changes what
is shown, two clicks deep — and keeps each new one (`view-NN-….png` and `.txt`). It never clicks
a control that answers or changes data (accept, drop, delete, finish …) and never follows a link
out of the prototype. `--click` still captures a named control as well. Every view keeps its
picture and its visible text, and the manifest's `views` records what was found, what was kept
and every view that could not be reached, with why. The agents draft from every view, not from
the first screen. An HTML file
is copied and rendered; a document is copied; a statement is kept byte for byte. A **project
folder** is two Sources: capture the folder (its written docs — README, SPEC — are kept), then
start the project the way its README says, capture the running app as a `site` Source, and
stop it. Both Sources link to every intent the run confirms.
**SE-1 (F1/C1):** every capture exited 0 and each `source-manifest-<name>.json` reads
`snapshot_saved: true` with a `kind`; a source that could not be read (or run through) halted
the run (REC1); for a prototype, `views.explored` is true, `views.kept` is at least 1, and every
view found and not kept is either a repeat of a kept view or listed in `views.unreached` with
why — and when any is unreached, the person was told which.
**SE-6 (F6/C6):** each `source-manifest-<name>.json` lists the snapshot files with their
hashes, `read_on`, and `given_by` (the git user); each snapshot sits under the evidence folder.
**Step 1b — Walk the prototype in journeys** · Owner: `prototype-reader` · Depends on: Step 1
*Only for a prototype Source (a file, a project's running app, or a deployed site); a document
or a statement skips it.* A screen shows what a prototype has; a journey shows what it is for
(C15). The walk is high-volume, low-judgment work, so it has its own agent, `prototype-reader`
(Sonnet, medium effort); the drafting stays with `business-intent-keeper` (Opus, high effort).
The walker invokes `walk-prototype`: it works out who the prototype serves and what each sets
out to do — every main section is some journey's goal — walks each journey a step at a time
through `scripts/walk_prototype.py` (click, type, press, back; one kept-open browser per
journey; each step tells only what changed — every step's picture and text kept), and writes `journeys.yaml`
with every captured view on a journey or named as not walked, with why. On a deployed site a
step that changes data is taken only when the person said the site is safe for it: run by hand
ask them once, before the walk; inside a loop read `answers.may_change_data` from `run.json`
— when it is not there, the walk takes no such step and names what it could not walk.
{ "task": "walk this prototype in journeys, the way the people it serves would",
"inputs": { "source": "<the prototype: file path or link>",
"source_manifest": "<working>/source-manifest-<name>.json",
"may_change_data": "<true for a file or local prototype; else the person's word>",
"walk_script": "<play-dir>/scripts/walk_prototype.py",
"check_script": "<play-dir>/scripts/check_journeys.py" },
"outputs": { "journeys": "<working>/journeys.yaml",
"evidence_dir": "{stm_base}<issue>/evidence/intent/<run-ts>/source-<name>/journeys/",
"check_report": "<working>/journeys-check.json" },
"task_id": "walk-prototype" }
**SE-15 (F15/C15):** for a prototype, `journeys-check.json` reads `ok: true` — every journey
has who, what they set out to do, two or more steps each with what it showed, and what they
end with; every captured view is on a journey or named as not walked, with why; every main
section is a journey's `section` of its own — and, run again after Step 2 with `--draft`, every
business intent names the journey it came from and every main section has a business intent
of its own (C17); no
data-changing step ran on a deployed site the person had not cleared.
### Phase: Draft
**Step 2 — Draft the business intents** · Owner: `business-intent-keeper` · Depends on: Step 1
{ "task": "draft the person's Business Intents from these sources and their answers",
"inputs": { "source_manifests": ["<working>/source-manifest-<name>.json", "<one per Source>"],
"answers": "<working>/answers.yaml (absent on the first pass)",
"journeys": "<working>/journeys.yaml (a prototype Source only)",
"round": "<the loop's round, from run.json — absent run by hand>",
"model": "<product_base>product-os/ (read only — on a later round, what /discover-domains and /discover-capabilities wrote)",
"issue": "<issue>",
"ontology": "<product.ontology from .garura/core/config.yaml — the ontology's place in memory>",
"check_script": "<play-dir>/scripts/check_intent.py" },
"outputs": { "draft": "<working>/intent-draft.yaml",
"check_report": "<working>/intent-check.json" },
"task_id": "draft-intent" }
The agent invokes `author-business-intent`, which writes the draft: the business intents —
one per need the product meets for its user, at the level of the product, in its users' words,
never the process or tools behind the screens (C17) — each property with a provenance
(`source`, `journey`, `memory`, `model`, `person`, or `proposed` with its `basis` for why, asked
by as a persona and a measurable proof, which are never left open or asked); no ICE, no
capability and no domain (C8, ADR 043); what each source shows; the coverage map from every
part of a source to the business intent it serves; and the questions the sources cannot
answer, each tagged with its intent and carrying up to three example answers. The agent then
checks the draft — every page, tab and section mapped, nothing merged, no intent that restates
the source or names the process behind it, each at the product's level in its users' words, by
judgment; the rest by the check script — and sends problems back to the skill
(at most twice). It returns only its JSON contract: `completed`, or `blocked` with
`blocked_reason: needs_person` when only the person's questions are left, with both paths and
the counts.
### Phase: Ask and check
**Step 3 — Ask and check** · Owner: play (script + the person) · Depends on: Step 2 · ≤3 rounds
Check the draft mechanically:
```
python3 scripts/check_intent.py --draft <working>/intent-draft.yaml \
--source-manifest <working>/source-manifest-<name>.json [--source-manifest <one per Source>] \
[--answers <working>/answers.yaml] \
--source-text <a snapshot's readable text: statement.txt, a document, or a view's .txt> \
[--source-text <one more per readable snapshot file, across every Source>] \
--out <working>/intent-check.json
```
Why, asked by and proof are never a question for the person (C17): the check fails one that is
empty, `open` or asked, and Step 2 is re-dispatched with the report to draft it with its basis.
**Inside a loop (C16) — no stop, the questions go to the loop.** When the check's only
problems left are questions for the person (missing properties, open questions), do not ask:
go on to Step 5, which saves the intents as proposed with each gap named open and writes the
questions to `<review_dir>/open-intent.json` for the loop. On a later round (`run.json`
`round` above 1) Step 2's agent first reads what `/discover-domains` and `/discover-capabilities` wrote in the
model, and memory, and fills what they answer (provenance `model` or `memory`); the loop asks
the person only what no play could answer. Every other problem the check finds is fixed as
below, in a loop too.
**Run by hand:** for every open question and every missing property, ask the person — plainly, one at a time,
naming the intent it belongs to, with the question's example answers (pick one, change one, or
write your own). Record each reply in `<working>/answers.yaml` exactly:
`{ about: <intent> — <the property>, reply: <their words>, offered: [<the examples>], picked: true }`
— `picked: true` only when the reply is one of the examples. Something the person says
unprompted (an intent they add, a correction) is recorded as `volunteered: true`, with no
examples. Then re-dispatch Step 2. For any
other problem the check found (a copied run of words, code-like text, a bare label, a part
mapped to no intent, another kind in the draft), re-dispatch Step 2 with the check report. Stop when the check is clean; after 3 rounds still unclean, halt and show the person
what is left.
**SE-2 (F2/C2):** the final `intent-check.json` reports, for every intent, no missing property
and no bare label.
**SE-3 (F3/C3):** it reports no copied run from the source and no code-like text.
**SE-4 (F5/C5):** it reports every filled property's provenance as `source`, `journey`,
`memory`, `model`, `person` or `proposed` (each `proposed` one with its basis), and no
unanswered question.
**SE-17 (F17/C17):** it reports no why, asked by or proof empty, `open`, asked by a question,
or proposed without its basis; and the agent's contract reads `completed` — its judgment found
every business intent a need the product meets for its user, at the product's level, in its
users' words, with asked by a persona and a measurable proof.
**SE-12 (F12/C12):** it reports a coverage map with every part mapped to an existing business
intent and every intent the source shows mapped from a part, every Source summarised, and no
`ice`, `domains`, `capabilities` or `features` in the draft; the agent's contract reads
`completed` (its judgment found no unmapped page, tab or section, no two needs merged into one
intent, and no intent that restates the source).
**SE-13 (F13/C13):** it reports every question carrying 1–3 example answers, and — run with
`--answers` — every recorded answer listing the examples it was offered, with every reply that
equals an example marked `picked: true`.
### Phase: Confirm (human checkpoint — run by hand only)
**Step 4 — Confirm or drop each intent** · Owner: play · Depends on: Step 3 · Checkpoint
(class: standard, pinned) · **run by hand: never skipped by config or policy. Inside a loop: skipped — no approval stop**
*Inside a loop (`in_loop`), skip this step: the loop may still ask its questions (Step 3),
but nothing waits for approval. Step 5 saves every drafted intent as `proposed`, and the person
confirms or drops each at the loop's final review — the review and the pull request at the
loop's end. Until then no ICE built for it is workable (C4).*
First show what each source shows and the coverage map — every part of the sources and the
business intent it serves — so the person sees nothing was left out or merged. The person may type **missing: <what>** or **merged: <item>** here; add it to
`answers.yaml` and go back to Step 2. Then show each intent, one at a time, exactly as it will
read — title, outcome, why, asked by, proof it is met, must not — with the parts of the source
mapped to it, and ask the person to type one of: **confirm**, **drop**, **change: <what>**, or
**stop**.
- **confirm** / **drop** → record the decision with their exact words; go to the next intent.
- **change** → add the change to `answers.yaml` and go back to Step 2 (decisions already
recorded stand unless the change touches that intent).
- **stop** → save nothing, and close HALTED with the reason (S4).
When every intent is decided, write `<working>/confirmation.yaml`:
`confirmation: { by: person, confirmed_by: <git user>, at: <date>, decisions: [ { intent: <id>, decision: confirmed | dropped, reply: <their exact words> } ] }`.
**SE-5 (F4/C4):** run by hand, `confirmation.yaml` reads `by: person`, holds one decision for
every drafted intent, each with the person's own non-empty reply, and was written only after
each intent was shown to them; no agent wrote or edited it. Inside a loop, no
`confirmation.yaml` exists, the play did not wait, and every saved intent reads `proposed`.
### Phase: Save
**Step 5 — Save the intents, link the sources** · Owner: play (script) · Depends on: Step 4
```
python3 scripts/persist_intent.py --draft <working>/intent-draft.yaml \
( --confirmation <working>/confirmation.yaml | --proposed --at <date> ) \
--source-manifest <working>/source-manifest-<name>.json [--source-manifest <one per Source>] \
--product-base <product_base> --working <working> \
[--loop-run-id <loop_run_id> --open-out <review_dir>/open-intent.json [--update-proposed]]
```
Inside a loop add `--loop-run-id` and `--open-out`; on a round above 1 also
`--update-proposed` (it rewrites only intents this loop saved that are still proposed).
**SE-16 (F16/C16):** inside a loop, no question went to the person from this play;
`open-intent.json` exists and lists every open gap with what it is about, the gaps it blocks
and example answers; every saved gap reads "open" on its page with the loop named; on a round
above 1, `intent-manifest.json` `updated` lists only pages this loop saved, and no gap the
model or memory answers is left open.
Run by hand it passes `--confirmation`; inside a loop it passes `--proposed` and saves every
drafted intent as `proposed`. It writes `<product_base>product-os/intents/<id>.md` for each
confirmed (or, in a loop, proposed) intent, and each Source record `source.md` beside its
snapshot, and links intents and Sources both ways. It writes no ICE, no capability, no domain
and no spine entry, and refuses a draft that carries any of them (C8, ADR 043). An intent page
has no Touches list from this play; on a later round of a loop it keeps a Touches list
another play wrote. It never overwrites a page (beyond `--update-proposed`); a dropped intent
is not saved. It writes `intent-manifest.json`, and no hand-off file: later plays read the
model. It refuses, and writes nothing, unless the person decided every intent and confirmed at
least one.
**SE-7 (F7/C7):** `intent-manifest.json` reads `linked: true` — each intent page names every
Source and each `source.md` names every confirmed intent; on a later round, a Touches list
already on a page is still there.
**SE-10 (F10/C10):** every confirmed intent has its page under `product-os/intents/` in the
ontology's shape; no `vision-goal` file exists.
**Step 6 — Guard the model** · Owner: play (script) · Depends on: Step 5
```
python3 scripts/scoped_write_guard.py --product-base <product_base> --base-ref HEAD \
--add-only 'product-os/intents/*.md' --modify-only 'product-os/intents/*.md' \
--out <working>/guard-report.json
```
On a non-zero exit, re-run with `--restore` and apply REC8. (`--modify-only` covers only a
later round's update of a page this loop saved.)
**SE-8 (F8/C8):** `guard-report.json` reads `ok: true` — only intent pages were added (or, on a
later round, a page this loop saved was updated) under the product model; no spine, capability
or domain file changed; no existing doc changed.
**Step 7 — Commit the delta** · Owner: play · Depends on: Step 6
Make the intents and their Sources durable on the branch (the loop's branch inside a loop):
```
git add -- <product_base>product-os/intents <stm_base><issue>/evidence/intent/<run-ts>
git commit -m "feat(model): business intents — <n> from <what the source is> (#<issue>)"
```
### Phase: End sequence (injected — D2 position: end)
*Skipped when `in_loop` — the loop lands its own change.* After the commit, the D2 rule
injects the close sequence, each a sub-play with `parent_run_id`:
**Step E1 — commit-change** · blockedBy: Step 7
{ "play": "commit-change", "parent_run_id": "<this run id>", "inputs": {}, "outputs": { "result": "{stm_base}{issue}/context/intent/end/commit-change.json" } }
**Step E2 — propose-change** · blockedBy: E1
{ "play": "propose-change", "parent_run_id": "<this run id>", "inputs": {}, "outputs": { "result": "{stm_base}{issue}/context/intent/end/propose-change.json" } }
**Step E3 — check-change** · blockedBy: E2
{ "play": "check-change", "parent_run_id": "<this run id>", "inputs": {}, "outputs": { "result": "{stm_base}{issue}/context/intent/end/check-change.json" } }
**Step E4 — merge-change** · blockedBy: E3
{ "play": "merge-change", "parent_run_id": "<this run id>", "inputs": {}, "outputs": { "result": "{stm_base}{issue}/context/intent/end/merge-change.json" } }
Each end member owns its own evals; a review reject stops the chain before merge; the land on
main keeps its pinned human approval.
**SE-9 (F9/C9):** by hand, `start-change` ran at the head and `merge-change.json` reports the
PR merged; inside a loop, `run.json` reads `in_loop: true`, neither `start-change` nor any
end member ran, and the branch the commit landed on equals the contract's branch.
### Phase: Scenario Validation
**Step 8 — Scenario evals** · Owner: play · Depends on: the end sequence (or Step 7 in a loop)
- **SCE-1 (S1 — business owner, deployed prototype):** `intent-manifest.json` reads
`decided: true`, `stage: confirmed` and `confirmed_by`; every required property is on
each intent page; the site's `source-manifest-<name>.json` lists at least one screenshot;
the manifest reads `linked: true`; the guard reads ok; `merge-change.json` reports the PR
merged.
- **SCE-2 (S2 — founder, plain statement):** the `source-manifest-<name>.json` reads `kind: statement`;
`statement.txt` in the snapshot matches the person's words byte for byte (its hash matches);
`intent-check.json` reports no copied run.
- **SCE-3 (S3 — business owner, nothing without a basis):** every property of every intent has
provenance `source`, `journey`, `memory`, `model`, `person` or `proposed`, each `proposed`
one with its basis, none empty, `open` or `agent`; `intent-check.json` reports no question
for why, asked by or proof.
- **SCE-4 (S4 — business owner, does not confirm):** when the person typed **stop**, or
dropped every intent, no intent page exists, `persist_intent.py` refused, the model paths are
clean, and the run closed HALTED with the reason.
- **SCE-5 (S5 — Discovery loop):** `run.json` reads `in_loop: true`; no `start-change` or end
result file exists for this run; the commit is on the contract's branch; no
`confirmation.yaml` exists; `intent-manifest.json` reads `stage: proposed`; the run wrote no
spine, capability or domain file.
- **SCE-6 (S6 — product strategist, hand-over through the model):** every confirmed intent
is a page under `product-os/intents/` in the ontology's shape; `guard-report.json` shows no
spine, capability or domain file changed; no `vision-goal` file exists.
- **SCE-7 (S7 — business owner, a prototype that does several things):** the draft holds one
business intent per need; the coverage map names every part and an existing intent for
each; every intent no part maps to was stated by the person; `confirmation.yaml` holds a
decision for every drafted intent.
- **SCE-8 (S8 — business owner, answering a question):** every answer in `answers.yaml`
lists the examples it was offered, and every reply that equals an example is marked
`picked: true`.
- **SCE-10 (S10 — Discovery loop, a gap only the loop can fill):** round 1's
`open-intent.json` lists every gap and no question went to a person from `/discover-intent`; round 2's
`intent-manifest.json` `updated` names the intents it filled, and every filled property's
provenance is `source`, `journey`, `memory`, `model`, `person` or `proposed`.
- **SCE-11 (S11 — product owner, a prototype rebuilt from its intents):** run on the saved model
(the installed `.garura/core/tools/rebuild-eval/run_eval.py`, stages
`prepare,inventory,check,review`), the static check reads
no `intent-field` or `intent-proof-open` finding, and the review reads every intent test
passed (`need_the_product_offers`, `outcome`, `asked_by_persona`, `why_business`,
`proof_measurable`, `product_words`).
- **SCE-9 (S9 — product owner, a prototype with many views):** `journeys-check.json` reads
`ok: true` with the draft checked (`--draft`): every captured view is on a journey or named
as not walked, and every business intent names the journey it came from.
### Phase: Evidence & Close
**Step 9 — Close** · Owner: play · Depends on: Step 8
Run the Standard Play Close. `/discover-intent` writes its evidence in the issue being worked on,
beside the Sources' snapshots, so the record of the run and what it read stay together.
**SE-11 (F11/C11):** the close is stop-condition gated — `check_stop_condition.py` over the
baked `stop-condition.yaml` (D1 intents saved; D2 decided — by hand, at least one confirmed and
every one decided; in a loop, every one saved as proposed for the loop's final review;
D3 every Source snapshot saved; D4 intents and Sources linked both ways; D5 guard ok) reads **held** before any COMPLETED
close.
```bash
# --- Standard Play Close (shared; see ../_shared/play-close.md) ---
# Run the close in ../_shared/play-close.md. Its Step L loads the templates through
# template-manager and sets ts and evidence_dest; then run this play's own lines below;
# then Steps C0-C2.
# evidence_base, slug:
# evidence_base="${stm_base}${issue}/evidence/intent/" ; slug="${run_ts}"
# Session identity stamp (#463) — close phase; start phase ran at pre-flight
session_stamp=$(python3 scripts/session_stamp.py --phase close \
--marker "${stm_base}${issue}/status/session-stamp-intent.json")
# Stop-condition gate (#464) — Step C0: this play carries a baked manifest, so the
# gate is LIVE. Evaluate the Done means here as the close's authoritative input.
python3 scripts/check_stop_condition.py \
--manifest "<play-dir>/stop-condition.yaml" \
--base "<working>/" \
--out "${stm_base}${issue}/status/stop-condition-intent.yaml"
sc_exit=$? # 0 held · 1 unmet · 2 error
```
`/discover-intent`'s evidence lives in the issue being worked on: `evidence_base="${stm_base}${issue}/evidence/intent/"`
and `slug="${run_ts}"` (one run can save several intents).
**Step C0 — bind the verdict.** `sc_exit == 0` (held) permits `status: COMPLETED`. Anything
else closes `HALTED` with `exit_reason: stop_condition_unmet` and the evidence's Stop
Condition section names every unmet clause. A run the person stopped at Step 4 closes HALTED
with `exit_reason: not_confirmed`; so does a run where the person dropped every intent. An unevaluable verdict is never a pass.
**Step C1 — Write evidence file.** Gated by the resolved `evidence.record` flag (global +
per-play `evidence.plays.intent`; first match wins, absent ⇒ record). When false, skip the
write and record `evidence skipped (record=false)` in the report's pointer line. Otherwise
fill the `evidence-file.md` slots (play `intent`, run_id `intent-${ts}`, the confirmed and
dropped intent ids, started_at/completed_at, status per C0, exit_reason; artifacts
produced: the intent pages,
each Source's snapshot and `source.md`, every
`source-manifest-<name>.json`, `intent-draft.yaml`, `intent-check.json`, `answers.yaml`,
`confirmation.yaml`, `intent-manifest.json`, `guard-report.json`, the model-delta commit sha;
`in_loop`; step and scenario eval results SE-1…SE-17 / SCE-1…SCE-11; `open-intent.json` (in a loop); `journeys.yaml` and `journeys-check.json` (a prototype); the checkpoint decisions from Step 4
with the person's exact replies;
the session identity stamp fields from $session_stamp (#463): session_id, ledger_file,
ledger_start_offset, ledger_end_offset (null when unresolved — never blocks the close); and
stop_condition per C0) and write to `$evidence_dest`. Do NOT hand-author the body.
**Step C2 — Render delivery report.** Also render the **Next** line: resolve this play in the Next-line map (`$next_map`, from `template-manager`) and emit `**Next:** /<command> — <why>.` (omitted when the mapped command is null), per `play-close.md`. Fill the `delivery-report.md` slots and output the
report: `## intent Delivered — <n> business intents`, the Run Summary table, the
Pipeline Steps table from the task DAG, the Artifacts Produced table (the intent pages and the
Sources), Next Steps (run `/discover-domains` for the domains and `/discover-capabilities` for the capabilities and
features, or the Discovery loop for all of it — the links are made there), and a pointer to
`$evidence_dest`. Always emitted; never gated.
```bash
# --- end Standard Play Close ---
```
## Scenario Validation
| Scenario | Persona | Eval |
|----------|---------|------|
| S1 — from a deployed prototype | business owner | SCE-1 |
| S2 — from a plain statement | founder | SCE-2 |
| S3 — nothing without a basis | business owner | SCE-3 |
| S4 — does not confirm | business owner | SCE-4 |
| S5 — inside a loop | Discovery loop | SCE-5 |
| S6 — hand-over to /discover-domains | product strategist | SCE-6 |
| S7 — a prototype that does several things | business owner | SCE-7 |
| S8 — answering a question | business owner | SCE-8 |
| S9 — a prototype with many views | product owner | SCE-9 |
| S10 — a gap only the loop can fill | Discovery loop | SCE-10 |
| S11 — a prototype rebuilt from its intents | product owner | SCE-11 |
## Recovery
| For | Trigger | Direction | Handoff |
|-----|---------|-----------|---------|
| F1 | the source could not be read, or a runnable source could not be run through, or a prototype was kept from fewer views than it offers | halt and tell the person exactly what could not be opened, and ask for a readable copy or a working link; for missing views, capture again with every view found, and name to the person each view that still cannot be reached | human |
| F2 | an intent misses a required property, or a section does not read for a business reader | ask the person for the missing property; rewrite the unclear section in plain words; re-check before confirmation | autonomous |
| F3 | an intent describes the source instead of the outcome | strip the description and restate the outcome the person wants; re-check | autonomous |
| F4 | an intent was marked confirmed without the person's typed yes, or by an agent | reset to proposed, present it to the person, and record only their own confirmation | human |
| F5 | a title, outcome or limit has no provenance in the source or the person's answers, or a drafted property names no basis | remove the title, outcome or limit and ask the person for it; give the drafted property its basis or redraft it from one | human |
| F6 | a Source has no snapshot, is not saved through the evidence method, or lacks when it was read, what it shows, or who gave it | take the snapshot again and re-save it through the evidence method with every field before the close | autonomous |
| F7 | an intent and a Source are not linked both ways, or a later round dropped a Touches list | write the missing link on whichever side lacks it, and restore the Touches list from the page as it was — through the save script | autonomous |
| F8 | the run wrote another kind, a model path outside the run's scope changed, or an existing doc was changed | drop the other kind from the draft (the play that owns it writes it); the guard's restore reverts the offending paths; re-write only the run's own records, after a human confirms the restore | human |
| F9 | run by hand without opening or landing its change, or opened its own issue or branch inside a loop | by hand, run the missing opening or end-sequence step; inside a loop, close the stray issue and branch and continue on the loop's branch | human |
| F10 | an intent is missing from the model, or not in the ontology's shape | re-write the missing or misshapen page from the confirmed draft | autonomous |
| F11 | the close would read COMPLETED with the Done means unmet | close HALTED with the unmet clauses named; fix the state and re-evaluate | autonomous |
| F12 | two needs share one intent, a part maps to no intent, or an intent restates the source | re-draft with the problem named — one intent per need, every part mapped; ask "why?" once for an intent that restates the source; at confirmation the person can name a missing or merged intent | autonomous |
| F13 | a question went out with no examples, or a picked example was recorded as the person's words | ask again with examples; mark the picked reply as picked | autonomous |
| F14 | a working file, an answer, a marker, a sub-play result or a snapshot sits outside `{stm_base}<issue>/`, or something was written to short-term memory before Step 0 gave the issue | move it into the issue's working folder (or status or evidence folder), delete the stray copy, and continue; when the issue is not known yet, hold it in the scratch folder until Step 0 | autonomous |
| F15 | the intents were drafted without journeys, a journey is incomplete, a view is on no journey and not named, a business intent is a rule rather than what a person gets, or a data-changing step ran on a deployed site not cleared | walk the missing journeys (or name each view not walked, with why), redraft the business intents from the journeys' goals; for a deployed site, ask the person whether a data-changing step is safe before taking it | autonomous |
| F16 | inside a loop `/discover-intent` stopped to ask, saved an unnamed gap, wrote an open question without its context, left a gap the model or memory now answers, or changed an intent this loop did not save | hand the questions to the loop instead; name every gap as open with its question; on the next round re-read the model and memory and fill what they answer; restore any intent this loop did not save | autonomous |
| F17 | an intent names the process or tools behind the screens, a main section has no intent of its own, sits above the product's level, or uses words its users would not; asked by is not a persona; or why, asked by or proof is open, asked or not measurable | redraft the intent from the product's main area and the journey it serves, in its users' words; draft asked by, why and proof with their basis; drop the question | autonomous |
## Pause and Resume
Steps run top to bottom. On entry, resolve config, the issue (from the loop contract, or from
the branch), and the in-progress run from its status marker in `{stm_base}<issue>/status/`, skip completed steps, reset any in-progress step to pending, and continue. A resume
after Step 1 reuses the captured snapshot; a resume after Step 3 reuses `answers.yaml` rather
than asking again; a resume never re-asks a decision already recorded, and never re-asks any
once `confirmation.yaml` exists.
Inside a loop, a resume keeps `in_loop` from `run.json`.
## Compilation Metadata
| Field | Value |
|-------|-------|
| fingerprint | sha256:0cf2249a72b85d11987b20b4542e0dbaafe8f51392d9926c58e772bb2df6e49a (of `reference/ice.md`) |
| compiled_by | edit-play (#689 ADR 043: business intents only — no ICE, capability or domain); edit-play (#689 the product at the product's level: C17); edit-play (#661 reads no memory; the run lives in its issue); edit-play (#637 one product model with /discover-domains); create-play (#612) |
| pipeline_position | both (start-change head; commit → propose → review → merge close; both skipped inside a loop) |
| position_exception | model-writing both play — writes the intent on its started branch (or the loop's) and commits its own model delta (Step 7) BEFORE the injected end sequence lands it |
| workflow_structure | B with one human checkpoint — pinned when run by hand (the person confirms or drops each intent), none inside a loop (intents saved as proposed for the loop's final review); ask-and-check loop ≤3 rounds; stop-condition gated close |
| stop_condition | stop-condition.yaml (D1–D5), gate live at Step C0 |
| domain_agents | 1 (business-intent-keeper) |
| utility_agents | 1 (template-manager — the shared close's templates) |
| skills_used | walk-prototype, author-business-intent |
| scripts | 13 (walk_prototype.py, check_journeys.py, preflight.py, check_loop_contract.py (the shared loop-contract checker), session_stamp.py, check_stop_condition.py, scoped_write_guard.py, capture_source.py, check_intent.py, persist_intent.py, intent_touches.py — copied from create-play references, keeps a Touches list on a later round; lint_grounding.py — copied from /discover-domains and check_ice_workable.py — copied from create-play references, kept as copies, not called since ADR 043) + test_intent_scripts.py |
| step_evals | 17 (SE-1…SE-17) |
| scenario_evals | 11 (SCE-1…SCE-11) |
| recovery_entries | 17 (one per failure condition; 12 autonomous / 5 human) |
Files in this skill
- SKILL.md
- reference/ice.md
- scripts/capture_source.py
- scripts/check_ice_workable.py
- scripts/check_intent.py
- scripts/check_journeys.py
- scripts/check_loop_contract.py
- scripts/check_stop_condition.py
- scripts/intent_touches.py
- scripts/lint_grounding.py
- scripts/model_names.py
- scripts/persist_intent.py
- scripts/preflight.py
- scripts/scoped_write_guard.py
- scripts/session_stamp.py
- scripts/test_intent_scripts.py
- scripts/walk_prototype.py
- stop-condition.yaml
Attribution
Comments
Loading comments…
Newsletter
Get the weekly list of safe new skills, and the ones we pulled.