@gr8ful/spf 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +321 -0
- package/assets/defaults/spf.config.yaml +141 -0
- package/assets/prompts/builder/system.md +13 -0
- package/assets/prompts/builder/user.md +34 -0
- package/assets/prompts/documenter/system.md +17 -0
- package/assets/prompts/documenter/user.md +48 -0
- package/assets/prompts/planner/system.md +21 -0
- package/assets/prompts/planner/user.md +45 -0
- package/assets/prompts/reviewer/system.md +16 -0
- package/assets/prompts/reviewer/user.md +44 -0
- package/assets/prompts/scout/system.md +20 -0
- package/assets/prompts/scout/user.md +34 -0
- package/assets/skill/SKILL.md +80 -0
- package/assets/skill/cookbooks/authoring_chains.md +193 -0
- package/assets/skill/cookbooks/how_to_prompt_for_the_eng.md +109 -0
- package/assets/skill/cookbooks/roster.md +197 -0
- package/assets/skill/cookbooks/run_adw.md +92 -0
- package/assets/skill/cookbooks/spf_overview.md +111 -0
- package/assets/skill/references/config.md +188 -0
- package/assets/skill/references/handoff.md +162 -0
- package/assets/skill/references/observability.md +184 -0
- package/dist/chains/adw_build.d.ts +12 -0
- package/dist/chains/adw_build.js +27 -0
- package/dist/chains/adw_build_review.d.ts +21 -0
- package/dist/chains/adw_build_review.js +55 -0
- package/dist/chains/adw_build_test.d.ts +21 -0
- package/dist/chains/adw_build_test.js +67 -0
- package/dist/chains/adw_document.d.ts +23 -0
- package/dist/chains/adw_document.js +59 -0
- package/dist/chains/adw_plan.d.ts +12 -0
- package/dist/chains/adw_plan.js +27 -0
- package/dist/chains/adw_plan_build.d.ts +12 -0
- package/dist/chains/adw_plan_build.js +30 -0
- package/dist/chains/adw_plan_build_test.d.ts +16 -0
- package/dist/chains/adw_plan_build_test.js +65 -0
- package/dist/chains/adw_plan_build_test_quality.d.ts +18 -0
- package/dist/chains/adw_plan_build_test_quality.js +66 -0
- package/dist/chains/adw_prompt.d.ts +12 -0
- package/dist/chains/adw_prompt.js +25 -0
- package/dist/chains/adw_quality.d.ts +12 -0
- package/dist/chains/adw_quality.js +32 -0
- package/dist/chains/adw_scout.d.ts +12 -0
- package/dist/chains/adw_scout.js +27 -0
- package/dist/chains/adw_simple_sdlc.d.ts +43 -0
- package/dist/chains/adw_simple_sdlc.js +147 -0
- package/dist/chains/context.d.ts +19 -0
- package/dist/chains/context.js +1 -0
- package/dist/chains/index.d.ts +19 -0
- package/dist/chains/index.js +116 -0
- package/dist/cli/bin.d.ts +15 -0
- package/dist/cli/bin.js +24 -0
- package/dist/cli/commands/abort.d.ts +1 -0
- package/dist/cli/commands/abort.js +42 -0
- package/dist/cli/commands/doctor.d.ts +1 -0
- package/dist/cli/commands/doctor.js +165 -0
- package/dist/cli/commands/eject.d.ts +1 -0
- package/dist/cli/commands/eject.js +55 -0
- package/dist/cli/commands/events.d.ts +1 -0
- package/dist/cli/commands/events.js +49 -0
- package/dist/cli/commands/init.d.ts +1 -0
- package/dist/cli/commands/init.js +62 -0
- package/dist/cli/commands/install-skill.d.ts +1 -0
- package/dist/cli/commands/install-skill.js +122 -0
- package/dist/cli/commands/list.d.ts +1 -0
- package/dist/cli/commands/list.js +13 -0
- package/dist/cli/commands/migrate.d.ts +1 -0
- package/dist/cli/commands/migrate.js +167 -0
- package/dist/cli/commands/phases.d.ts +1 -0
- package/dist/cli/commands/phases.js +25 -0
- package/dist/cli/commands/run.d.ts +3 -0
- package/dist/cli/commands/run.js +27 -0
- package/dist/cli/commands/sessions.d.ts +1 -0
- package/dist/cli/commands/sessions.js +20 -0
- package/dist/cli/commands/trace.d.ts +8 -0
- package/dist/cli/commands/trace.js +10 -0
- package/dist/cli/commands/ui.d.ts +1 -0
- package/dist/cli/commands/ui.js +35 -0
- package/dist/cli/commands/version.d.ts +1 -0
- package/dist/cli/commands/version.js +8 -0
- package/dist/cli/commands/watch.d.ts +8 -0
- package/dist/cli/commands/watch.js +210 -0
- package/dist/cli/gitignore.d.ts +1 -0
- package/dist/cli/gitignore.js +14 -0
- package/dist/cli/index.d.ts +1 -0
- package/dist/cli/index.js +156 -0
- package/dist/core/agent_cc.d.ts +75 -0
- package/dist/core/agent_cc.js +322 -0
- package/dist/core/agent_flue.d.ts +66 -0
- package/dist/core/agent_flue.js +321 -0
- package/dist/core/agents.d.ts +64 -0
- package/dist/core/agents.js +456 -0
- package/dist/core/changes.d.ts +35 -0
- package/dist/core/changes.js +98 -0
- package/dist/core/console.d.ts +36 -0
- package/dist/core/console.js +156 -0
- package/dist/core/data_types.d.ts +562 -0
- package/dist/core/data_types.js +382 -0
- package/dist/core/gates.d.ts +33 -0
- package/dist/core/gates.js +144 -0
- package/dist/core/git_helper.d.ts +59 -0
- package/dist/core/git_helper.js +115 -0
- package/dist/core/issues/github_provider.d.ts +52 -0
- package/dist/core/issues/github_provider.js +211 -0
- package/dist/core/issues/provider.d.ts +90 -0
- package/dist/core/issues/provider.js +14 -0
- package/dist/core/paths.d.ts +78 -0
- package/dist/core/paths.js +108 -0
- package/dist/core/permissions.d.ts +78 -0
- package/dist/core/permissions.js +187 -0
- package/dist/core/prompts.d.ts +4 -0
- package/dist/core/prompts.js +17 -0
- package/dist/core/quality.d.ts +65 -0
- package/dist/core/quality.js +194 -0
- package/dist/core/runner.d.ts +72 -0
- package/dist/core/runner.js +168 -0
- package/dist/core/session.d.ts +16 -0
- package/dist/core/session.js +60 -0
- package/dist/core/sqlite.d.ts +55 -0
- package/dist/core/sqlite.js +106 -0
- package/dist/core/tracer.d.ts +56 -0
- package/dist/core/tracer.js +246 -0
- package/dist/core/utils.d.ts +40 -0
- package/dist/core/utils.js +108 -0
- package/dist/core/watch.d.ts +44 -0
- package/dist/core/watch.js +194 -0
- package/dist/test/agent_cc.test.d.ts +1 -0
- package/dist/test/agent_cc.test.js +95 -0
- package/dist/test/agent_flue.test.d.ts +1 -0
- package/dist/test/agent_flue.test.js +83 -0
- package/dist/test/data_types.test.d.ts +10 -0
- package/dist/test/data_types.test.js +49 -0
- package/dist/test/ui_server.test.d.ts +1 -0
- package/dist/test/ui_server.test.js +119 -0
- package/dist/test/watch.test.d.ts +1 -0
- package/dist/test/watch.test.js +227 -0
- package/dist/ui/server/app.d.ts +3 -0
- package/dist/ui/server/app.js +98 -0
- package/dist/ui/server/db.d.ts +82 -0
- package/dist/ui/server/db.js +333 -0
- package/dist/ui/server/serve.d.ts +13 -0
- package/dist/ui/server/serve.js +80 -0
- package/dist/ui/server/static.d.ts +5 -0
- package/dist/ui/server/static.js +55 -0
- package/dist/ui/shared/types.d.ts +263 -0
- package/dist/ui/shared/types.js +8 -0
- package/package.json +52 -0
- package/web/assets/index-C7nF068F.css +1 -0
- package/web/assets/index-mzSArcnQ.js +11 -0
- package/web/assets/play-latin-400-normal-GKW-4YV7.woff2 +0 -0
- package/web/assets/play-latin-700-normal-DyPlLDbb.woff2 +0 -0
- package/web/index.html +14 -0
- package/web/logo.svg +6 -0
|
@@ -0,0 +1,162 @@
|
|
|
1
|
+
# Handoff Reference
|
|
2
|
+
|
|
3
|
+
The envelope schema, the two-channel output contract, and the session
|
|
4
|
+
directory layout — how context transfers in code, not in conversation.
|
|
5
|
+
|
|
6
|
+
## Two output channels, exactly
|
|
7
|
+
|
|
8
|
+
An agent may produce output in two ways and no others:
|
|
9
|
+
|
|
10
|
+
1. **Reference files** written into `context_handoff/` — plans, notes,
|
|
11
|
+
artifacts for the agents that follow.
|
|
12
|
+
2. **A final valid-JSON response** — the envelope, its direct response and
|
|
13
|
+
nothing else, validated by Flue's injected `sf_report` tool against the
|
|
14
|
+
call's declared Valibot schema before the tool's handler ever fires.
|
|
15
|
+
|
|
16
|
+
Code does the rest: capture the validated data, persist it as
|
|
17
|
+
`envelope.json`, inject it into the next agent's user prompt.
|
|
18
|
+
|
|
19
|
+
## Envelope schema
|
|
20
|
+
|
|
21
|
+
Every output type is built with `envelopeType()` over the base shape
|
|
22
|
+
(`src/core/data_types.ts`):
|
|
23
|
+
|
|
24
|
+
```ts
|
|
25
|
+
const EnvelopeBaseEntries = {
|
|
26
|
+
status: v.picklist(["success", "fail"]), // the only required field
|
|
27
|
+
summary: v.optional(v.string(), ""), // one sentence: what happened
|
|
28
|
+
artifacts: v.optional(v.array(v.string()), () => []), // paths written, usually in context_handoff/
|
|
29
|
+
notes_for_next_agent: v.optional(v.string(), ""), // what the next agent must know
|
|
30
|
+
};
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
`status` is load-bearing: an envelope that parses but reports `status:
|
|
34
|
+
"fail"` throws, failing the phase. An agent declaring its own failure is not
|
|
35
|
+
a successful phase.
|
|
36
|
+
|
|
37
|
+
The built-in types:
|
|
38
|
+
|
|
39
|
+
- `GenericOutput` — fallback for an agent with no sharper contract yet.
|
|
40
|
+
- `PlanOutput` — `commit_message` (the plan file's own commit subject).
|
|
41
|
+
- `BuildOutput` — `changed_files`, `commit_message`.
|
|
42
|
+
- `ScoutOutput` — `findings: [{file, note}]`.
|
|
43
|
+
- `ReviewOutput` — `approved`, `findings: [{requirement, met, evidence}]`, `blocking: string[]`.
|
|
44
|
+
- `DocumentOutput` — `document_path`, `documented_files`, `commit_message`.
|
|
45
|
+
|
|
46
|
+
**Each `commit_message` describes its own agent's work product, never the
|
|
47
|
+
next one's.** A chain that commits once uses whichever fits; one that
|
|
48
|
+
commits per step needs all three, and reusing one agent's sentence for
|
|
49
|
+
another's diff is how a commit log starts lying.
|
|
50
|
+
|
|
51
|
+
Two types are code-shaped-as-envelope adapters, not agent reports: `VerifyOutput`
|
|
52
|
+
(a quality-suite result, from `quality.asEnvelope`) and `ChangesOutput` (a
|
|
53
|
+
captured `git diff`, from `changes.asEnvelope`). The consuming agent can't
|
|
54
|
+
tell the difference, which is the point.
|
|
55
|
+
|
|
56
|
+
The envelope is a **manifest of claims**. Gates verify those claims after
|
|
57
|
+
the fact — declared artifacts exist and are non-empty, declared changed
|
|
58
|
+
files appear in the diff, a review's verdict agrees with its own findings.
|
|
59
|
+
See `authoring_chains.md` for adding a gate or an output type.
|
|
60
|
+
|
|
61
|
+
## The typed-output rule — the synced triad
|
|
62
|
+
|
|
63
|
+
**Every agent call passes a concrete output type**, and the model's final
|
|
64
|
+
JSON is validated against exactly that type by the injected `sf_report`
|
|
65
|
+
tool.
|
|
66
|
+
|
|
67
|
+
```ts
|
|
68
|
+
const plan = await run.phase(
|
|
69
|
+
makePhaseParams({ name: "plan", kind: "agent", owner: "planner", description: "..." }),
|
|
70
|
+
(ph) => ph.call(makeAgentCall({ output_type: PlanOutput, prompt, gates: [gates.artifactsExist] })),
|
|
71
|
+
);
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
Three things are one fact: the envelope schema in `data_types.ts`, the exact
|
|
75
|
+
JSON shown in that agent's `user.md` `## Report` section, and the
|
|
76
|
+
`output_type:` named at the call site. Edit them together, or the agent's
|
|
77
|
+
next response silently stops matching what code expects.
|
|
78
|
+
|
|
79
|
+
**Parse failure is not a restart.** If the model never calls `sf_report`
|
|
80
|
+
(or calls it with data that fails validation), the harness falls back to
|
|
81
|
+
extracting JSON from the response text, and if that still fails, re-prompts
|
|
82
|
+
the **same session** with a correction — bounded by `JSON_FIX_ATTEMPTS` in
|
|
83
|
+
`agents.ts`. Gate violations use the identical mechanism, bounded instead by
|
|
84
|
+
the phase's `retries`. A cold restart would throw away the context that
|
|
85
|
+
produced the near-miss.
|
|
86
|
+
|
|
87
|
+
## Injecting the previous envelope
|
|
88
|
+
|
|
89
|
+
`prompts.render()` substitutes into a `user.md`:
|
|
90
|
+
|
|
91
|
+
| Placeholder | Value |
|
|
92
|
+
|---|---|
|
|
93
|
+
| `{{prompt}}` | the engineer's ask (or the chain's per-call prompt) |
|
|
94
|
+
| `{{previous_envelope}}` | the upstream envelope JSON, from `AgentCall.previous` |
|
|
95
|
+
| `{{context_handoff_dir}}` | absolute path to this session's `context_handoff/` |
|
|
96
|
+
|
|
97
|
+
A `user.md` declares one h3 per incoming datum, then the task, then the
|
|
98
|
+
output contract:
|
|
99
|
+
|
|
100
|
+
````markdown
|
|
101
|
+
## Task
|
|
102
|
+
|
|
103
|
+
Find what `prompt` asks about. Write findings into `context_handoff_dir`,
|
|
104
|
+
then emit your `Report` JSON.
|
|
105
|
+
|
|
106
|
+
## Report
|
|
107
|
+
|
|
108
|
+
Respond with ONLY valid JSON matching `ScoutOutput` — no prose before or after:
|
|
109
|
+
|
|
110
|
+
```json
|
|
111
|
+
{
|
|
112
|
+
"status": "success",
|
|
113
|
+
"summary": "<one sentence on what you found>",
|
|
114
|
+
"findings": [{ "file": "src/server.ts", "note": "<why this file matters>" }],
|
|
115
|
+
"artifacts": ["<context_handoff_dir>/scout_findings.md"]
|
|
116
|
+
}
|
|
117
|
+
```
|
|
118
|
+
````
|
|
119
|
+
|
|
120
|
+
The `## Report` section shows the exact JSON shape of the declared output
|
|
121
|
+
type — the agent's output contract lives in `user.md` because the shape
|
|
122
|
+
belongs to the *use*, not the identity. The matching `system.md` stays
|
|
123
|
+
static: Purpose + Instructions only.
|
|
124
|
+
|
|
125
|
+
## Session directory layout
|
|
126
|
+
|
|
127
|
+
```
|
|
128
|
+
.spf/data/sessions/{adw_id}/
|
|
129
|
+
├── agent_map.json agent name -> Flue conversation id + model
|
|
130
|
+
├── context_handoff/ the ONE place agents write files for the agents that follow
|
|
131
|
+
└── {agent_name}/
|
|
132
|
+
├── prompts/ exact prompts sent (system.md + user.md), saved before execution
|
|
133
|
+
└── envelope.json the final valid-JSON response — captured, validated, persisted by code
|
|
134
|
+
```
|
|
135
|
+
|
|
136
|
+
`session.ensure(cfg, adwId, cwd)` mints or joins the id and creates these
|
|
137
|
+
dirs. One `context_handoff/` per session, shared by every agent.
|
|
138
|
+
|
|
139
|
+
## agent_map.json and resuming
|
|
140
|
+
|
|
141
|
+
```json
|
|
142
|
+
{
|
|
143
|
+
"planner": {"session_id": "a1b2c3d4-planner", "model": "google/gemini-3.6-flash", "coding_agent": "flue"},
|
|
144
|
+
"builder": {"session_id": "a1b2c3d4-builder", "model": "google/gemini-3.6-flash", "coding_agent": "flue"}
|
|
145
|
+
}
|
|
146
|
+
```
|
|
147
|
+
|
|
148
|
+
This map is what lets a later run rejoin each agent's **existing Flue
|
|
149
|
+
conversation**. Run a `build` chain with `--adw-id a1b2c3d4` after a `plan`
|
|
150
|
+
chain used the same id, and the builder resumes its own conversation rather
|
|
151
|
+
than starting cold.
|
|
152
|
+
|
|
153
|
+
The map records the model each session was created with. If config drift
|
|
154
|
+
changes an agent's model, that agent starts a **fresh** conversation and the
|
|
155
|
+
map is updated — never a bad resume. `agent_sessions` in `spf.db` is the
|
|
156
|
+
queryable mirror of this file. Flue's own durable conversation state lives
|
|
157
|
+
separately, in `.spf/data/flue.db` — a sibling with a different lifetime
|
|
158
|
+
(resumed, not archived) from `spf.db`.
|
|
159
|
+
|
|
160
|
+
**Files are the raw record; the db is the queryable mirror.** Losing
|
|
161
|
+
`spf.db` loses nothing that can't be rebuilt from `envelope.json` and
|
|
162
|
+
`agent_map.json` (plus Flue's own `flue.db` for conversation replay).
|
|
@@ -0,0 +1,184 @@
|
|
|
1
|
+
# Observability Reference
|
|
2
|
+
|
|
3
|
+
The event schema, the tables, and the polling contract — one data path,
|
|
4
|
+
always: **agents → sqlite → CLI / web UI.**
|
|
5
|
+
|
|
6
|
+
## Two stores, one truth
|
|
7
|
+
|
|
8
|
+
**Files are the raw record** (`envelope.json`, `agent_map.json`, Flue's own
|
|
9
|
+
`.spf/data/flue.db` conversation store); **SQLite (`.spf/data/spf.db`, via
|
|
10
|
+
`node:sqlite`) is the queryable mirror** the CLI and UI read. `tracer.ts`
|
|
11
|
+
writes both. Losing the db loses nothing that can't be rebuilt from files.
|
|
12
|
+
|
|
13
|
+
Location comes from `observability.db` in `spf.config.yaml`, default
|
|
14
|
+
`.spf/data/spf.db` — inside the target repo, always gitignored.
|
|
15
|
+
|
|
16
|
+
## Event schema
|
|
17
|
+
|
|
18
|
+
`tracer.ts` emits these types, every one logged against its `adw_id` **and**
|
|
19
|
+
`phase_id`:
|
|
20
|
+
|
|
21
|
+
| Type | Emitted when |
|
|
22
|
+
|---|---|
|
|
23
|
+
| `phase_start` | a `run.phase(...)` block is entered |
|
|
24
|
+
| `agent_start` | an agent's Flue conversation is created or continued for `ph.call(...)` |
|
|
25
|
+
| `tool_call` | a tool call completes — **one event per real call**, named `bash: ls -la src`, payload `{tool, tool_call_id, args, result_snippet, ok, duration_ms}` |
|
|
26
|
+
| `handoff` | an envelope crosses from one agent to the next |
|
|
27
|
+
| `gate_pass` | a gate found no failed checks — payload carries `attempt`, `checks` (the evidence), and an empty `violations` |
|
|
28
|
+
| `gate_fail` | a gate found at least one failed check — payload carries `attempt`, `checks`, and `violations` |
|
|
29
|
+
| `log` | an explicit `ph.log(...)` from the chain script |
|
|
30
|
+
| `agent_end` | the agent's turn completes; envelope parsed or not — payload carries `cost`, `usage` (the per-component breakdown), `context_tokens`, `context_window` |
|
|
31
|
+
| `phase_end` | the block exits; carries the resolved status |
|
|
32
|
+
| `error` | a throw inside a phase block |
|
|
33
|
+
|
|
34
|
+
`parent_id` nests spans, so an agent phase expands into its tool-call spans
|
|
35
|
+
in the UI.
|
|
36
|
+
|
|
37
|
+
**Spend is itemized per phase.** `agent_end.usage` carries tokens *and*
|
|
38
|
+
dollars for each component Flue reports (matching pi-ai's field names
|
|
39
|
+
one-for-one) — `input`, `output`, `cache_read`, `cache_write` — summed
|
|
40
|
+
across every send the phase made, so a phase that retried on a bad envelope
|
|
41
|
+
or a failed gate shows what all its attempts cost, not just the last one.
|
|
42
|
+
|
|
43
|
+
`reasoning_tokens` is the thinking share and is **inside** `output_tokens`,
|
|
44
|
+
not a fifth component. It bills at the output rate, so the panel nests it
|
|
45
|
+
under output rather than adding it. Runs predating the breakdown have no
|
|
46
|
+
`usage` key at all; the lump `cost` and the event's own `tokens` still
|
|
47
|
+
stand.
|
|
48
|
+
|
|
49
|
+
**Context is occupancy, not spend.** `events.tokens` and
|
|
50
|
+
`sessions.total_tokens` bill every turn, so they only grow. `context_tokens`
|
|
51
|
+
is how full the window actually was when the agent's last turn settled —
|
|
52
|
+
computed the same way as pi's own auto-compaction trigger: the last *valid*
|
|
53
|
+
turn's `usage.totalTokens`, falling back to `input + output + cacheRead +
|
|
54
|
+
cacheWrite`, skipping aborted/errored turns. `context_window` — the model's
|
|
55
|
+
ceiling — is backend-dependent: a Flue-backed agent always reports **0**
|
|
56
|
+
(Flue exposes no model-registry lookup the way pi's
|
|
57
|
+
`~/.pi/agent/models.json` did), while a `claude_code`-backed agent reports
|
|
58
|
+
a real ceiling straight from the CLI's own result message. Either way, `0`
|
|
59
|
+
means "unknown," not "no context used" — the lane draws no ceiling bar
|
|
60
|
+
rather than a wrong one.
|
|
61
|
+
|
|
62
|
+
**Gates record evidence, not just a verdict.** A gate returns one
|
|
63
|
+
`{item, ok, note}` check per thing it looked at; `violations` are derived
|
|
64
|
+
from the failed ones. Both land in `gate_results` (`checks_json` +
|
|
65
|
+
`violations_json`) and in the `gate_pass`/`gate_fail` payload.
|
|
66
|
+
|
|
67
|
+
**A `tool_call` is the one event that spans time**, so it fills both
|
|
68
|
+
`started_at` and `ended_at` on the row. Every other type is a point in
|
|
69
|
+
time. Lay tool calls out on a time axis from those columns, never by
|
|
70
|
+
parsing `payload_json`.
|
|
71
|
+
|
|
72
|
+
**Streaming is solved by construction.** `agent_flue.ts`'s `run()` forwards
|
|
73
|
+
Flue's `ConversationStreamChunk`s to the tracer as they arrive — never
|
|
74
|
+
batched at phase end. This is why the whole call chain from `agent_flue.run()`
|
|
75
|
+
up through `agents.execute`, `run.phase`, and every chain's `main()` is
|
|
76
|
+
`async`.
|
|
77
|
+
|
|
78
|
+
## Tables
|
|
79
|
+
|
|
80
|
+
```sql
|
|
81
|
+
sessions (
|
|
82
|
+
adw_id TEXT PRIMARY KEY, adw_name TEXT, request TEXT, status TEXT, engineer TEXT,
|
|
83
|
+
started_at TEXT, ended_at TEXT, total_tokens INTEGER DEFAULT 0, total_cost REAL DEFAULT 0,
|
|
84
|
+
archived INTEGER DEFAULT 0 -- review triage, set by the UI; never by a run
|
|
85
|
+
);
|
|
86
|
+
phases (
|
|
87
|
+
phase_id TEXT PRIMARY KEY, adw_id TEXT REFERENCES sessions, seq INTEGER,
|
|
88
|
+
name TEXT, kind TEXT, owner TEXT, description TEXT,
|
|
89
|
+
status TEXT DEFAULT 'fail', -- success must be earned
|
|
90
|
+
attempt INTEGER DEFAULT 0, retries INTEGER DEFAULT 0, error TEXT,
|
|
91
|
+
started_at TEXT, ended_at TEXT
|
|
92
|
+
);
|
|
93
|
+
events (
|
|
94
|
+
event_id TEXT PRIMARY KEY, adw_id TEXT REFERENCES sessions, phase_id TEXT REFERENCES phases,
|
|
95
|
+
parent_id TEXT, type TEXT, name TEXT, payload_json TEXT, tokens INTEGER,
|
|
96
|
+
started_at TEXT, ended_at TEXT -- ended_at set only on events that span time
|
|
97
|
+
);
|
|
98
|
+
envelopes (
|
|
99
|
+
envelope_id TEXT PRIMARY KEY, adw_id TEXT REFERENCES sessions, phase_id TEXT REFERENCES phases,
|
|
100
|
+
agent TEXT, output_type TEXT, payload_json TEXT, valid INTEGER, attempt INTEGER, created_at TEXT
|
|
101
|
+
);
|
|
102
|
+
gate_results (
|
|
103
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT, adw_id TEXT REFERENCES sessions, phase_id TEXT REFERENCES phases,
|
|
104
|
+
attempt INTEGER, gate TEXT, passed INTEGER,
|
|
105
|
+
violations_json TEXT, -- derived: the failed checks, as "item: note"
|
|
106
|
+
checks_json TEXT, -- [{item, ok, note}] — everything the gate looked at
|
|
107
|
+
created_at TEXT
|
|
108
|
+
);
|
|
109
|
+
processes ( -- adw_id -> pid, so a stuck run can be stopped
|
|
110
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT, adw_id TEXT REFERENCES sessions,
|
|
111
|
+
kind TEXT, -- 'adw' (the chain process) | 'agent' (a real child pid for claude_code; same as the chain's own pid for Flue, which is in-process)
|
|
112
|
+
name TEXT, pid INTEGER,
|
|
113
|
+
command TEXT, -- what the pid WAS; pids get recycled, verify before killing
|
|
114
|
+
started_at TEXT, ended_at TEXT -- ended_at NULL = believed alive
|
|
115
|
+
);
|
|
116
|
+
agent_sessions ( -- the queryable mirror of agent_map.json
|
|
117
|
+
adw_id TEXT REFERENCES sessions, agent TEXT,
|
|
118
|
+
coding_agent TEXT, model TEXT, color TEXT, session_id TEXT,
|
|
119
|
+
context_tokens INTEGER, -- window occupancy after the agent's last turn
|
|
120
|
+
context_window INTEGER, -- backend-dependent; 0 = unknown — see "Context is occupancy" above
|
|
121
|
+
created_at TEXT, last_used_at TEXT,
|
|
122
|
+
PRIMARY KEY (adw_id, agent)
|
|
123
|
+
);
|
|
124
|
+
```
|
|
125
|
+
|
|
126
|
+
`processes.kind = 'agent'` rows carry the actual `claude` subprocess pid for
|
|
127
|
+
a `claude_code`-backed agent — genuinely killable independently of the
|
|
128
|
+
chain that spawned it. For a Flue-backed agent (in-process, no child) the
|
|
129
|
+
same row carries the chain's own pid, since there's no separate process to
|
|
130
|
+
record. `spf abort <adw_id>` signals every live row it finds either way.
|
|
131
|
+
|
|
132
|
+
**Derived, never stored:** phase durations (`ended_at − started_at`),
|
|
133
|
+
session phase-progress (query `phases` by `adw_id`), lane layout (`kind` +
|
|
134
|
+
`owner`).
|
|
135
|
+
|
|
136
|
+
Phase status invariants: `queued` only for manifest-declared phases not yet
|
|
137
|
+
entered; `running` on enter; only a clean exit writes `success` — agent
|
|
138
|
+
phases additionally need the envelope validated and gates green; everything
|
|
139
|
+
else resolves to `fail`.
|
|
140
|
+
|
|
141
|
+
## WAL pragmas
|
|
142
|
+
|
|
143
|
+
Every connection — writer and reader — opens with:
|
|
144
|
+
|
|
145
|
+
```sql
|
|
146
|
+
PRAGMA journal_mode=WAL;
|
|
147
|
+
PRAGMA synchronous=NORMAL;
|
|
148
|
+
PRAGMA busy_timeout=5000;
|
|
149
|
+
```
|
|
150
|
+
|
|
151
|
+
WAL allows readers during writes. `src/core/sqlite.ts`'s `node:sqlite` shim
|
|
152
|
+
sets these explicitly and also disables `enableForeignKeyConstraints`
|
|
153
|
+
(`node:sqlite` defaults it **on**, unlike `bun:sqlite`/plain SQLite — left
|
|
154
|
+
on, a session's first `events` row inserting before its `sessions` row
|
|
155
|
+
commits throws). `spf ui` opens its connection with `{readOnly: true}` —
|
|
156
|
+
verified the shim translates that correctly; `readonly` (lowercase) is a
|
|
157
|
+
silent no-op in `node:sqlite` itself, which is exactly the footgun the shim
|
|
158
|
+
exists to close.
|
|
159
|
+
|
|
160
|
+
## Polling contract
|
|
161
|
+
|
|
162
|
+
**Nothing pushes.** No ingest endpoint, no WebSocket. Live view polls a
|
|
163
|
+
rowid cursor:
|
|
164
|
+
|
|
165
|
+
```sql
|
|
166
|
+
SELECT ... FROM events WHERE adw_id = ? AND rowid > ? ORDER BY rowid LIMIT 500;
|
|
167
|
+
```
|
|
168
|
+
|
|
169
|
+
`spf events <adw_id> --follow` and `spf ui` both do this at
|
|
170
|
+
`observability.poll_ms` (default 500). History is the same queries with
|
|
171
|
+
filters, lazy-paged — one mechanism serves both live and past runs.
|
|
172
|
+
|
|
173
|
+
## Finding and stopping a stuck run
|
|
174
|
+
|
|
175
|
+
```bash
|
|
176
|
+
spf phases <adw_id> # which phase is still "running"
|
|
177
|
+
spf abort <adw_id> # SIGTERM the chain's process
|
|
178
|
+
```
|
|
179
|
+
|
|
180
|
+
A hung agent produces no events at all — the trace goes quiet rather than
|
|
181
|
+
red, which is exactly when `processes` (not `events`) is the table that
|
|
182
|
+
answers "is this run doing anything." A killed run finalizes its own trace:
|
|
183
|
+
SIGTERM/SIGINT are handled to close out the session as `fail` with its
|
|
184
|
+
process rows closed, rather than leaving it reading `running` forever.
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ADW Build — one-shot implementation workflow.
|
|
3
|
+
*
|
|
4
|
+
* Usage:
|
|
5
|
+
* spf build "<prompt or path/to/prompt.md>" [--config <path>] [--adw-id a1b2c3d4]
|
|
6
|
+
*
|
|
7
|
+
* Phases: engineer(request) -> builder
|
|
8
|
+
*/
|
|
9
|
+
import type { ChainContext } from "./context.ts";
|
|
10
|
+
export declare const REQUIRED_AGENTS: string[];
|
|
11
|
+
export declare const REQUIRED_SUITES: string[];
|
|
12
|
+
export declare function main(ctx: ChainContext): Promise<number>;
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ADW Build — one-shot implementation workflow.
|
|
3
|
+
*
|
|
4
|
+
* Usage:
|
|
5
|
+
* spf build "<prompt or path/to/prompt.md>" [--config <path>] [--adw-id a1b2c3d4]
|
|
6
|
+
*
|
|
7
|
+
* Phases: engineer(request) -> builder
|
|
8
|
+
*/
|
|
9
|
+
import * as agents from "../core/agents.js";
|
|
10
|
+
import * as gates from "../core/gates.js";
|
|
11
|
+
import * as session from "../core/session.js";
|
|
12
|
+
import { BuildOutput, makeAgentCall, makePhaseParams } from "../core/data_types.js";
|
|
13
|
+
export const REQUIRED_AGENTS = ["builder"];
|
|
14
|
+
export const REQUIRED_SUITES = [];
|
|
15
|
+
export async function main(ctx) {
|
|
16
|
+
const { prompt, config_paths, adw_id, cwd } = ctx;
|
|
17
|
+
const cfg = agents.loadConfig(config_paths);
|
|
18
|
+
agents.validate(cfg, REQUIRED_AGENTS, REQUIRED_SUITES, cwd);
|
|
19
|
+
const run = session.ensure(cfg, adw_id, cwd);
|
|
20
|
+
await run.phase(makePhaseParams({ name: "request", kind: "engineer", owner: run.engineer, description: "Capture the incoming ask" }), async (ph) => {
|
|
21
|
+
ph.log({ input: prompt });
|
|
22
|
+
});
|
|
23
|
+
await run.phase(makePhaseParams({ name: "build", kind: "agent", owner: "builder", retries: 1, description: "Implement the request" }), async (ph) => {
|
|
24
|
+
await ph.call(makeAgentCall({ output_type: BuildOutput, prompt, gates: [gates.diffMatchesClaims] }));
|
|
25
|
+
});
|
|
26
|
+
return run.finish();
|
|
27
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ADW Build Review — implement, then confirm it is what was asked for.
|
|
3
|
+
*
|
|
4
|
+
* Usage:
|
|
5
|
+
* spf build-review "<prompt or path/to/prompt.md>" [--config <path>] [--adw-id a1b2c3d4]
|
|
6
|
+
*
|
|
7
|
+
* Phases: engineer(request) -> builder -> reviewer [-> builder(revise) -> reviewer ... bounded]
|
|
8
|
+
*
|
|
9
|
+
* Review is not testing. Tests answer "does it run"; the reviewer answers "is this
|
|
10
|
+
* the thing that was asked for" — it reads the spec (`plan.md` from a prior plan
|
|
11
|
+
* phase if the session has one, else the prompt verbatim), reads the code that was
|
|
12
|
+
* written, and rules on each requirement.
|
|
13
|
+
*
|
|
14
|
+
* Like the tester, the reviewer's phase succeeds when it RUNS and REPORTS. A
|
|
15
|
+
* rejection does not fail the phase; it fails the run, checked at the end, after
|
|
16
|
+
* the bounded revise loop has had its chances.
|
|
17
|
+
*/
|
|
18
|
+
import type { ChainContext } from "./context.ts";
|
|
19
|
+
export declare const REQUIRED_AGENTS: string[];
|
|
20
|
+
export declare const REQUIRED_SUITES: string[];
|
|
21
|
+
export declare function main(ctx: ChainContext): Promise<number>;
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ADW Build Review — implement, then confirm it is what was asked for.
|
|
3
|
+
*
|
|
4
|
+
* Usage:
|
|
5
|
+
* spf build-review "<prompt or path/to/prompt.md>" [--config <path>] [--adw-id a1b2c3d4]
|
|
6
|
+
*
|
|
7
|
+
* Phases: engineer(request) -> builder -> reviewer [-> builder(revise) -> reviewer ... bounded]
|
|
8
|
+
*
|
|
9
|
+
* Review is not testing. Tests answer "does it run"; the reviewer answers "is this
|
|
10
|
+
* the thing that was asked for" — it reads the spec (`plan.md` from a prior plan
|
|
11
|
+
* phase if the session has one, else the prompt verbatim), reads the code that was
|
|
12
|
+
* written, and rules on each requirement.
|
|
13
|
+
*
|
|
14
|
+
* Like the tester, the reviewer's phase succeeds when it RUNS and REPORTS. A
|
|
15
|
+
* rejection does not fail the phase; it fails the run, checked at the end, after
|
|
16
|
+
* the bounded revise loop has had its chances.
|
|
17
|
+
*/
|
|
18
|
+
import * as agents from "../core/agents.js";
|
|
19
|
+
import * as gates from "../core/gates.js";
|
|
20
|
+
import * as session from "../core/session.js";
|
|
21
|
+
import { BuildOutput, ReviewOutput, makeAgentCall, makePhaseParams } from "../core/data_types.js";
|
|
22
|
+
export const REQUIRED_AGENTS = ["builder", "reviewer"];
|
|
23
|
+
export const REQUIRED_SUITES = [];
|
|
24
|
+
const MAX_REVISION_LOOPS = 3;
|
|
25
|
+
export async function main(ctx) {
|
|
26
|
+
const { prompt, config_paths, adw_id, cwd } = ctx;
|
|
27
|
+
const cfg = agents.loadConfig(config_paths);
|
|
28
|
+
agents.validate(cfg, REQUIRED_AGENTS, REQUIRED_SUITES, cwd);
|
|
29
|
+
const run = session.ensure(cfg, adw_id, cwd);
|
|
30
|
+
await run.phase(makePhaseParams({ name: "request", kind: "engineer", owner: run.engineer, description: "Capture the incoming ask" }), async (ph) => {
|
|
31
|
+
ph.log({ input: prompt });
|
|
32
|
+
});
|
|
33
|
+
let previous = await run.phase(makePhaseParams({ name: "build", kind: "agent", owner: "builder", description: "Implement the request" }), (ph) => ph.call(makeAgentCall({ output_type: BuildOutput, prompt, gates: [gates.diffMatchesClaims] })));
|
|
34
|
+
let review = null;
|
|
35
|
+
for (let i = 1; i <= MAX_REVISION_LOOPS; i++) {
|
|
36
|
+
review = await run.phase(makePhaseParams({
|
|
37
|
+
name: `review_${i}`,
|
|
38
|
+
kind: "agent",
|
|
39
|
+
owner: "reviewer",
|
|
40
|
+
description: "Rule on every requirement in the spec, against the code on disk",
|
|
41
|
+
}), (ph) => ph.call(makeAgentCall({ output_type: ReviewOutput, prompt, previous, gates: [gates.artifactsExist, gates.verdictConsistent] })));
|
|
42
|
+
if (review.approved)
|
|
43
|
+
break;
|
|
44
|
+
if (i === MAX_REVISION_LOOPS)
|
|
45
|
+
break;
|
|
46
|
+
previous = await run.phase(makePhaseParams({
|
|
47
|
+
name: `revise_${i}`,
|
|
48
|
+
kind: "agent",
|
|
49
|
+
owner: "builder",
|
|
50
|
+
retries: 1,
|
|
51
|
+
description: "Close every blocking finding the reviewer named",
|
|
52
|
+
}), (ph) => ph.call(makeAgentCall({ output_type: BuildOutput, prompt, previous: review, gates: [gates.diffMatchesClaims] })));
|
|
53
|
+
}
|
|
54
|
+
return run.finish(review !== null && review.approved, `the reviewer never approved after ${MAX_REVISION_LOOPS} revision(s)`);
|
|
55
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ADW Build Test — implement, then verify; failures flow back into the builder.
|
|
3
|
+
*
|
|
4
|
+
* Usage:
|
|
5
|
+
* spf build-test "<prompt or path/to/prompt.md>" [--config <path>] [--adw-id a1b2c3d4]
|
|
6
|
+
*
|
|
7
|
+
* Phases: engineer(request) -> builder -> code(test) [-> builder(fix) -> code(test) ... bounded]
|
|
8
|
+
*
|
|
9
|
+
* Testing is CODE. The suite's command is written down in core/quality.ts,
|
|
10
|
+
* so running it needs no judgement — only repairing it does. Failures reach the
|
|
11
|
+
* builder as an envelope through `quality.asEnvelope`, which is the same door an
|
|
12
|
+
* agent's report came through, so the repair loop is unchanged.
|
|
13
|
+
*
|
|
14
|
+
* A failing suite does NOT fail its phase: the runner did its job, the code is
|
|
15
|
+
* what failed. It fails the run, checked at the end, after the bounded fix loop
|
|
16
|
+
* has had its chances.
|
|
17
|
+
*/
|
|
18
|
+
import type { ChainContext } from "./context.ts";
|
|
19
|
+
export declare const REQUIRED_AGENTS: string[];
|
|
20
|
+
export declare const REQUIRED_SUITES: string[];
|
|
21
|
+
export declare function main(ctx: ChainContext): Promise<number>;
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ADW Build Test — implement, then verify; failures flow back into the builder.
|
|
3
|
+
*
|
|
4
|
+
* Usage:
|
|
5
|
+
* spf build-test "<prompt or path/to/prompt.md>" [--config <path>] [--adw-id a1b2c3d4]
|
|
6
|
+
*
|
|
7
|
+
* Phases: engineer(request) -> builder -> code(test) [-> builder(fix) -> code(test) ... bounded]
|
|
8
|
+
*
|
|
9
|
+
* Testing is CODE. The suite's command is written down in core/quality.ts,
|
|
10
|
+
* so running it needs no judgement — only repairing it does. Failures reach the
|
|
11
|
+
* builder as an envelope through `quality.asEnvelope`, which is the same door an
|
|
12
|
+
* agent's report came through, so the repair loop is unchanged.
|
|
13
|
+
*
|
|
14
|
+
* A failing suite does NOT fail its phase: the runner did its job, the code is
|
|
15
|
+
* what failed. It fails the run, checked at the end, after the bounded fix loop
|
|
16
|
+
* has had its chances.
|
|
17
|
+
*/
|
|
18
|
+
import * as agents from "../core/agents.js";
|
|
19
|
+
import * as gates from "../core/gates.js";
|
|
20
|
+
import * as quality from "../core/quality.js";
|
|
21
|
+
import * as session from "../core/session.js";
|
|
22
|
+
import { BuildOutput, makeAgentCall, makePhaseParams } from "../core/data_types.js";
|
|
23
|
+
export const REQUIRED_AGENTS = ["builder"];
|
|
24
|
+
export const REQUIRED_SUITES = ["test"];
|
|
25
|
+
const MAX_FIX_LOOPS = 3;
|
|
26
|
+
function record(ph, result) {
|
|
27
|
+
const passed = result.checks.filter((c) => c.passed).length;
|
|
28
|
+
ph.log({ passed: result.passed, checks: `${passed}/${result.checks.length}`, artifacts: result.artifacts.join(", ") });
|
|
29
|
+
}
|
|
30
|
+
export async function main(ctx) {
|
|
31
|
+
const { prompt, config_paths, adw_id, cwd } = ctx;
|
|
32
|
+
const cfg = agents.loadConfig(config_paths);
|
|
33
|
+
agents.validate(cfg, REQUIRED_AGENTS, REQUIRED_SUITES, cwd);
|
|
34
|
+
const run = session.ensure(cfg, adw_id, cwd);
|
|
35
|
+
await run.phase(makePhaseParams({ name: "request", kind: "engineer", owner: run.engineer, description: "Capture the incoming ask" }), async (ph) => {
|
|
36
|
+
ph.log({ input: prompt });
|
|
37
|
+
});
|
|
38
|
+
await run.phase(makePhaseParams({ name: "build", kind: "agent", owner: "builder", description: "Implement the request" }), (ph) => ph.call(makeAgentCall({ output_type: BuildOutput, prompt, gates: [gates.diffMatchesClaims] })));
|
|
39
|
+
let test = null;
|
|
40
|
+
for (let i = 1; i <= MAX_FIX_LOOPS; i++) {
|
|
41
|
+
test = await run.phase(makePhaseParams({
|
|
42
|
+
name: `test_${i}`,
|
|
43
|
+
kind: "code",
|
|
44
|
+
owner: "quality",
|
|
45
|
+
description: "Run the suite — a known command, so code runs it and no agent has to rediscover it",
|
|
46
|
+
}), async (ph) => {
|
|
47
|
+
const result = quality.runTests(run);
|
|
48
|
+
record(ph, result);
|
|
49
|
+
return result;
|
|
50
|
+
});
|
|
51
|
+
if (test.passed)
|
|
52
|
+
break;
|
|
53
|
+
await run.phase(makePhaseParams({
|
|
54
|
+
name: `fix_${i}`,
|
|
55
|
+
kind: "agent",
|
|
56
|
+
owner: "builder",
|
|
57
|
+
retries: 1,
|
|
58
|
+
description: "Repair what the suite reported, from its verbatim output",
|
|
59
|
+
}), (ph) => ph.call(makeAgentCall({
|
|
60
|
+
output_type: BuildOutput,
|
|
61
|
+
prompt,
|
|
62
|
+
previous: quality.asEnvelope(test, "tests"),
|
|
63
|
+
gates: [gates.diffMatchesClaims],
|
|
64
|
+
})));
|
|
65
|
+
}
|
|
66
|
+
return run.finish(test !== null && test.passed, `the suite still failed after ${MAX_FIX_LOOPS} fix attempt(s)`);
|
|
67
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ADW Document — write up the work that was just done, from the diff.
|
|
3
|
+
*
|
|
4
|
+
* Usage:
|
|
5
|
+
* spf document "<prompt or path/to/prompt.md>" [--base main] [--config <path>] [--adw-id a1b2c3d4]
|
|
6
|
+
*
|
|
7
|
+
* Phases: engineer(request) -> code(changes) -> documenter
|
|
8
|
+
*
|
|
9
|
+
* This runs AFTER a build, and the guard is structural rather than advisory: the
|
|
10
|
+
* change capture is a code phase, and an empty diff raises there — before the
|
|
11
|
+
* documenter is ever spawned. There is nothing to document until something was
|
|
12
|
+
* built, and the phase says so instead of paying an agent to discover it.
|
|
13
|
+
*
|
|
14
|
+
* `git diff` against `--base` (main by default) is what "the latest changes"
|
|
15
|
+
* means here; see core/changes.ts for how the base commit is resolved on a
|
|
16
|
+
* branch, on main, and on a clean tree right after a chain committed.
|
|
17
|
+
*/
|
|
18
|
+
import type { ChainContext } from "./context.ts";
|
|
19
|
+
export declare const REQUIRED_AGENTS: string[];
|
|
20
|
+
export declare const REQUIRED_SUITES: string[];
|
|
21
|
+
export declare function main(ctx: ChainContext, options?: {
|
|
22
|
+
base?: string;
|
|
23
|
+
}): Promise<number>;
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ADW Document — write up the work that was just done, from the diff.
|
|
3
|
+
*
|
|
4
|
+
* Usage:
|
|
5
|
+
* spf document "<prompt or path/to/prompt.md>" [--base main] [--config <path>] [--adw-id a1b2c3d4]
|
|
6
|
+
*
|
|
7
|
+
* Phases: engineer(request) -> code(changes) -> documenter
|
|
8
|
+
*
|
|
9
|
+
* This runs AFTER a build, and the guard is structural rather than advisory: the
|
|
10
|
+
* change capture is a code phase, and an empty diff raises there — before the
|
|
11
|
+
* documenter is ever spawned. There is nothing to document until something was
|
|
12
|
+
* built, and the phase says so instead of paying an agent to discover it.
|
|
13
|
+
*
|
|
14
|
+
* `git diff` against `--base` (main by default) is what "the latest changes"
|
|
15
|
+
* means here; see core/changes.ts for how the base commit is resolved on a
|
|
16
|
+
* branch, on main, and on a clean tree right after a chain committed.
|
|
17
|
+
*/
|
|
18
|
+
import * as agents from "../core/agents.js";
|
|
19
|
+
import * as changes from "../core/changes.js";
|
|
20
|
+
import * as gates from "../core/gates.js";
|
|
21
|
+
import * as session from "../core/session.js";
|
|
22
|
+
import { DocumentOutput, makeAgentCall, makeChangeCapture, makePhaseParams } from "../core/data_types.js";
|
|
23
|
+
export const REQUIRED_AGENTS = ["documenter"];
|
|
24
|
+
export const REQUIRED_SUITES = [];
|
|
25
|
+
const DOCUMENT_NOTES = "Read diff_path in full before writing. Document only what the diff shows, then copy the write-up into app_docs/ as your task describes.";
|
|
26
|
+
export async function main(ctx, options = {}) {
|
|
27
|
+
const { prompt, config_paths, adw_id, cwd } = ctx;
|
|
28
|
+
const base = options.base ?? "main";
|
|
29
|
+
const cfg = agents.loadConfig(config_paths);
|
|
30
|
+
agents.validate(cfg, REQUIRED_AGENTS, REQUIRED_SUITES, cwd);
|
|
31
|
+
const run = session.ensure(cfg, adw_id, cwd);
|
|
32
|
+
await run.phase(makePhaseParams({ name: "request", kind: "engineer", owner: run.engineer, description: "Capture the incoming ask" }), async (ph) => {
|
|
33
|
+
ph.log({ input: prompt });
|
|
34
|
+
});
|
|
35
|
+
const changeset = await run.phase(makePhaseParams({ name: "changes", kind: "code", owner: "git", description: `Diff the working tree against ${base} — the change to be written up` }), async (ph) => {
|
|
36
|
+
const result = changes.capture(run, makeChangeCapture({ base }));
|
|
37
|
+
ph.log({
|
|
38
|
+
base: `${result.base.label} @ ${result.base.commit.slice(0, 7)}`,
|
|
39
|
+
reason: result.base.reason,
|
|
40
|
+
files: result.files.length + result.untracked.length,
|
|
41
|
+
lines: `+${result.insertions} -${result.deletions}`,
|
|
42
|
+
diff: result.diff_path,
|
|
43
|
+
});
|
|
44
|
+
if (result.empty) {
|
|
45
|
+
throw new Error(`nothing changed since ${result.base.label} (${result.base.reason}) — documenting runs after a build. ` +
|
|
46
|
+
`Build something first, or point --base at the ref the work should be measured from.`);
|
|
47
|
+
}
|
|
48
|
+
return result;
|
|
49
|
+
});
|
|
50
|
+
await run.phase(makePhaseParams({ name: "document", kind: "agent", owner: "documenter", retries: 1, description: "Turn the captured diff into a write-up an engineer can read" }), async (ph) => {
|
|
51
|
+
await ph.call(makeAgentCall({
|
|
52
|
+
output_type: DocumentOutput,
|
|
53
|
+
prompt,
|
|
54
|
+
previous: changes.asEnvelope(changeset, DOCUMENT_NOTES),
|
|
55
|
+
gates: [gates.artifactsExist, gates.filesNonEmpty],
|
|
56
|
+
}));
|
|
57
|
+
});
|
|
58
|
+
return run.finish();
|
|
59
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ADW Plan — one-shot planning workflow.
|
|
3
|
+
*
|
|
4
|
+
* Usage:
|
|
5
|
+
* spf plan "<prompt or path/to/prompt.md>" [--config <path>] [--adw-id a1b2c3d4]
|
|
6
|
+
*
|
|
7
|
+
* Phases: engineer(request) -> planner
|
|
8
|
+
*/
|
|
9
|
+
import type { ChainContext } from "./context.ts";
|
|
10
|
+
export declare const REQUIRED_AGENTS: string[];
|
|
11
|
+
export declare const REQUIRED_SUITES: string[];
|
|
12
|
+
export declare function main(ctx: ChainContext): Promise<number>;
|