aw-spec-cli 0.1.22 → 0.1.24
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/assets/.appweaver/ORCH-DIALECT.md +3 -3
- package/assets/.appweaver/RULE-PARTITION.md +41 -28
- package/assets/.appweaver/orchestrator/orchestrator.py +139 -17
- package/assets/.appweaver/orchestrator/rev-templates/ai-rules.reference.md +3 -3
- package/assets/.appweaver/orchestrator/rev-templates/delta-design-template.skeleton.md +19 -12
- package/assets/.appweaver/orchestrator/rev-templates/design-template.skeleton.md +25 -17
- package/assets/.appweaver/orchestrator/rev-templates/domain-requirements.skeleton.md +2 -2
- package/assets/.appweaver/orchestrator/rev-templates/domain-rule-bindings.skeleton.md +10 -10
- package/assets/.appweaver/orchestrator/rev-templates/tech-rule-bindings.skeleton.md +28 -17
- package/assets/.appweaver/orchestrator/rev-templates/technical-constitution.skeleton.md +22 -24
- package/assets/.appweaver/orchestrator/rev-templates/technical-rule-file.skeleton.md +1 -1
- package/assets/.appweaver/orchestrator/rev-templates/ui-delta-design-template.skeleton.md +29 -19
- package/assets/.appweaver/orchestrator/rev-templates/ui-design-template.skeleton.md +27 -20
- package/assets/.appweaver/orchestrator/workflows/aw-spec-adopt.orch.yaml +184 -37
- package/assets/.appweaver/orchestrator/workflows/aw-spec-analyze-plan-delta.orch.yaml +18 -0
- package/assets/.appweaver/orchestrator/workflows/aw-spec-analyze-plan.orch.yaml +19 -0
- package/assets/.appweaver/orchestrator/workflows/aw-spec-analyze-slice-delta.orch.yaml +18 -0
- package/assets/.appweaver/orchestrator/workflows/aw-spec-constitution.orch.yaml +1 -1
- package/assets/.appweaver/orchestrator/workflows/aw-spec-dependency-tree.orch.yaml +4 -1
- package/assets/.appweaver/orchestrator/workflows/aw-spec-foundation.orch.yaml +5 -1
- package/assets/.appweaver/orchestrator/workflows/aw-spec-inventory.orch.yaml +58 -43
- package/assets/.appweaver/orchestrator/workflows/aw-spec-onboard.orch.yaml +650 -205
- package/assets/.appweaver/orchestrator/workflows/aw-spec-plan.orch.yaml +19 -0
- package/assets/.appweaver/orchestrator/workflows/aw-spec-select-domain.orch.yaml +3 -1
- package/assets/.appweaver/orchestrator/workflows/aw-spec-select-feature.orch.yaml +5 -5
- package/assets/.appweaver/orchestrator/workflows/aw-spec-select-stack.orch.yaml +2 -2
- package/assets/.appweaver/orchestrator/workflows/aw-spec-specify.orch.yaml +4 -1
- package/assets/.appweaver/orchestrator/workflows/aw-spec-ui-model.orch.yaml +34 -6
- package/assets/.appweaver/orchestrator/workflows/aw-spec-ui-prototype.orch.yaml +40 -1
- package/assets/.appweaver/orchestrator/workflows/aw-spec-workflow-model.orch.yaml +23 -4
- package/assets/.appweaver/scripts/bash/aw-rev-constitution.sh +1 -1
- package/assets/.appweaver/scripts/bash/aw-spec-actions.sh +82 -10
- package/assets/.appweaver/scripts/powershell/aw-rev-constitution.ps1 +1 -1
- package/assets/.appweaver/scripts/powershell/aw-spec-actions.ps1 +67 -2
- package/assets/.appweaver/scripts/python/aw-inventory.py +1809 -156
- package/assets/.appweaver/scripts/python/aw-rev-constitution.py +106 -145
- package/assets/.appweaver/scripts/python/aw-stack.py +45 -1
- package/assets/.appweaver/scripts/python/aw-xref.py +73 -2
- package/assets/.appweaver/scripts/python/validate_okf.py +10 -5
- package/assets/.appweaver/templates/okf-concept-template.md +10 -2
- package/assets/.appweaver/templates/proposed-inventory-template.md +100 -48
- package/assets/.appweaver/templates/source-map-template.md +14 -6
- package/assets/foundations/dotnet/ModularMonolith/ModularMonolith.pack.txt +3 -3
- package/assets/foundations/dotnet/VerticalSlice/VerticalSlice.pack.txt +5 -123
- package/assets/foundations/java/Hexagonal/Hexagonal.pack.txt +784 -0
- package/assets/foundations/java/Hexagonal/pack-foundation.sh +357 -0
- package/assets/foundations/java/Hexagonal/unpack-foundation.sh +399 -0
- package/package.json +1 -1
|
@@ -88,11 +88,11 @@ the raw object, not its JSON string.
|
|
|
88
88
|
| action | who runs | what it does |
|
|
89
89
|
|---|---|---|
|
|
90
90
|
| `set` | engine | assign vars (`vars:` or decl-style `parameters:`); raw-object aware |
|
|
91
|
-
| `build_context` | engine | REPLACE the per-request envelope: `system:`/`context:` file lists (+ `context_from: <var>` holding paths a prior
|
|
91
|
+
| `build_context` | engine | REPLACE the per-request envelope: `system:`/`context:` file lists (+ `context_from: <var>` — or a list of vars, appended in order — holding paths a prior step produced, + `vars:`) |
|
|
92
92
|
| `load_standards` | engine | F4 — append to the **sticky standards set** prepended to every system block (dedup, `optional:`, `files:`/`files_from_variable`). This is the byte-stable, prompt-cacheable prefix |
|
|
93
|
-
| `execute_script` | engine | F1 — run a `scripts:` ref (`subcommand:` + `args:`; sh/ps1 via `--os`); stdout's last line = JSON, bound via `outputs[]`; `on_error: continue` tolerates a script that ran and exited non-zero (its outputs bind empty) — never an undeclared ref, which the pre-flight refuses before step 1; cached across resumes; runs for real even in mock transport |
|
|
93
|
+
| `execute_script` | engine | F1 — run a `scripts:` ref (`subcommand:` + `args:`; sh/ps1 via `--os`); stdout's last line = JSON, bound via `outputs[]`; `on_error: continue` tolerates a script that ran and exited non-zero (a JSON object on its last stdout line is still bound, so a gate can show the script's own reason; with none, the outputs bind empty) — never an undeclared ref, which the pre-flight refuses before step 1; cached across resumes; runs for real even in mock transport |
|
|
94
94
|
| `verify_context` | engine | F2 — gates: `required_files` (+`allow_missing`), `require_non_empty` (+`on_empty[_message]`), `require_true` (+`on_false[_message]`), `require_value`+`equals` (+`on_mismatch[_message]`). `stop` raises a terminal stop |
|
|
95
|
-
| `agent_generate` | fresh agent | demo contract (code work): agent DECIDES file paths per the loaded standards, writes with its tools, reports `files[]` in the trailing ```json fence. `sdk-apply` = read-only generation + invoking agent applies |
|
|
95
|
+
| `agent_generate` | fresh agent | demo contract (code work): agent DECIDES file paths per the loaded standards, writes with its tools, reports `files[]` in the trailing ```json fence; a reported path not on disk is dropped from `files` (and listed in `files_not_written`) before outputs bind, so a `require_non_empty` gate on it proves something was written. `sdk-apply` = read-only generation + invoking agent applies |
|
|
96
96
|
| `agent_review` | fresh agent | payload-only review of `review_of_var`/`review_of` files (inlined into THIS request, never sticky) → `{pass, issues}` |
|
|
97
97
|
| `agent_author` | fresh agent | F5 — **authored artifact** (decl `llm_generate`): the INTERPRETER owns `output_target: {type: file, path}`. Generation is payload-only under every strategy (mapped `context:` + sticky envelope + standards are the entire world) and never writes — the ENGINE writes the returned content (sdk-apply degrades to sdk). Optional `context:` (per-request extras), `include_standards: false` (narrow payload), `tools:` (read-only opt-in for a task that names files by path) |
|
|
98
98
|
| `agent_query` | fresh agent | F6 — generic payload-only structured JSON (decl `llm_query`): `schema:` shapes the required fence, `outputs[]` binds it, `mock:` supplies the mock-transport result; one malformed-JSON retry on fresh strategies. Same tools contract as `agent_author` — none unless the step declares `tools:` |
|
|
@@ -21,9 +21,9 @@ governs. It also stops domain detail leaking straight into code authoring withou
|
|
|
21
21
|
pinned in a reviewable requirement.
|
|
22
22
|
|
|
23
23
|
> **Lift, then gate.** If a domain constraint is needed downstream, it must first be *lifted*
|
|
24
|
-
> into the spec as an id. A gate that finds a constraint in neither the spec nor the
|
|
24
|
+
> into the spec as an id. A gate that finds a constraint in neither the spec nor the plans
|
|
25
25
|
> reports `LIFT-FAILURE:` and routes back to `specify` — it must never invent the rule into a
|
|
26
|
-
>
|
|
26
|
+
> plan or into code.
|
|
27
27
|
|
|
28
28
|
---
|
|
29
29
|
|
|
@@ -42,13 +42,13 @@ A command with **no heading** in a map loads nothing mapped for that class and
|
|
|
42
42
|
### The constitution halves are not mapped
|
|
43
43
|
|
|
44
44
|
- `.appweaver/memory/constitution.technical.md` — the technology base. Standing input to
|
|
45
|
-
**
|
|
45
|
+
the **service lane of `plan` and of `implement`**. Domain-agnostic.
|
|
46
46
|
- `.appweaver/memory/constitution.ui.md` — the **frontend lane's** base, a UX-* principle series.
|
|
47
47
|
Standing input to the **FRONTEND LANE of `plan` and of `implement`**. Domain-agnostic for the same reason the
|
|
48
48
|
technical base is, and it exists only where a frontend lane does — a backend-only project has
|
|
49
49
|
no such file.
|
|
50
50
|
- `.appweaver/memory/constitution.domain.md` — the active domain's addenda. Standing input to
|
|
51
|
-
**specify** (and the gate checklist). Deliberately **not** loaded by
|
|
51
|
+
**specify** (and the gate checklist). Deliberately **not** loaded by plan or implement.
|
|
52
52
|
- `.appweaver/memory/constitution.md` — the composed human/compatibility view. The pipeline loads
|
|
53
53
|
the halves, not this file.
|
|
54
54
|
|
|
@@ -74,11 +74,13 @@ lane loads the technical base, the frontend lane loads the UI base. What they sh
|
|
|
74
74
|
**HOW to model**; **WHAT** the model is stays in the domain tier.
|
|
75
75
|
- Constitution: the **domain** half.
|
|
76
76
|
|
|
77
|
-
### `
|
|
78
|
-
- **Domain rule files: NEVER.** Domain reaches
|
|
79
|
-
-
|
|
80
|
-
|
|
81
|
-
|
|
77
|
+
### `plan` — *architecture, both lanes; domain input = the spec tier only*
|
|
78
|
+
- **Domain rule files: NEVER.** Domain reaches plan solely as spec ids and the context's
|
|
79
|
+
`domain-model.md`.
|
|
80
|
+
- **Technical rule files: yes** — the architecture and cross-cutting rules each lane's plan
|
|
81
|
+
mediates the spec against: `## /aw-spec-plan` for the service lane, `## /aw-spec-plan-ui`
|
|
82
|
+
for the frontend lane.
|
|
83
|
+
- Constitution: the **technical** half in the service lane, the **UI** half in the frontend one.
|
|
82
84
|
|
|
83
85
|
### `implement` — *code generation, both lanes, both paths*
|
|
84
86
|
- **Domain rule files: NEVER.** The lane's plan is the only input describing what to BUILD.
|
|
@@ -97,7 +99,7 @@ lane loads the technical base, the frontend lane loads the UI base. What they sh
|
|
|
97
99
|
|
|
98
100
|
### `build` — *compile + repair*
|
|
99
101
|
- **Technical rule files: yes** — the layering and convention rules the compile-fixer respects.
|
|
100
|
-
- No domain files: build repairs against
|
|
102
|
+
- No domain files: build repairs against the plans' intent, which the domain never reaches.
|
|
101
103
|
|
|
102
104
|
### `run` — *runtime*
|
|
103
105
|
- **Technical rule files:** only if the project evidences a runtime/hosting concern.
|
|
@@ -121,23 +123,24 @@ lane loads the technical base, the frontend lane loads the UI base. What they sh
|
|
|
121
123
|
- Authors all three halves itself, and generates the rule set each one ratifies. The rule maps do
|
|
122
124
|
not exist yet when it starts, so its authority is the **code**: every principle and every
|
|
123
125
|
generated rule file is evidenced from source, and a greenfield rule file is never an input.
|
|
124
|
-
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
126
|
+
- **Which lanes exist is the declaration's answer.** `select-stack` runs first and writes
|
|
127
|
+
`.appweaver/stacks.json`; onboard refuses to start without it. A lane declared there gets its
|
|
128
|
+
half and its rule set, a lane declared `(none)` gets neither, and the scan — pointed at the
|
|
129
|
+
declared source path — decides what the governance SAYS, never whether the lane exists. A
|
|
130
|
+
declared UI lane whose path holds no recognisable UI source is reported, and its governance is
|
|
131
|
+
authored thin rather than skipped.
|
|
132
|
+
- The **UI half** is authored from the lane's code, not from a map heading, because at onboard
|
|
133
|
+
there is no map to read. The order is therefore inverted: the half is authored from the code,
|
|
134
|
+
then the `frontend/` rule set is generated as its detail layer, then the lane's three headings
|
|
135
|
+
are appended to the technical map — `## /aw-spec-ui-constitution` (what a LATER `constitution`
|
|
136
|
+
run re-ratifies from, so it behaves like the greenfield case above), `## /aw-spec-plan-ui` and
|
|
137
|
+
`## /aw-spec-implement-ui` (what the frontend lane of `plan` and `implement` load).
|
|
138
|
+
- `onboard` **never writes** `.appweaver/stacks.json`. `lane-declare` is its sole writer and
|
|
139
|
+
`select-stack` its only caller, so every lane is one a person confirmed.
|
|
137
140
|
- The **document templates** it derives obey the same partition as the halves that govern them:
|
|
138
|
-
the
|
|
141
|
+
the backend plan + delta pair is authored against `constitution.technical.md`, the frontend
|
|
139
142
|
lane's pair against `constitution.ui.md` and the generated `frontend/` rules, and neither
|
|
140
|
-
reads a domain file. This is invariant 7 applied one level up — a lane's template describes
|
|
143
|
+
reads a domain file. The domain-model template is lane-neutral. This is invariant 7 applied one level up — a lane's template describes
|
|
141
144
|
that lane's code and no other's.
|
|
142
145
|
- **No greenfield artifact is ever in context.** Where a step needs a shape reference it reads a
|
|
143
146
|
neutral skeleton from `.appweaver/orchestrator/rev-templates/`, which carries the format and no
|
|
@@ -145,19 +148,29 @@ lane loads the technical base, the frontend lane loads the UI base. What they sh
|
|
|
145
148
|
so an instruction not to copy an exemplar would be the only control — absence is the stronger
|
|
146
149
|
one.
|
|
147
150
|
- Ordering is preserved for the same reason as above: technical half → UI half → domain half.
|
|
151
|
+
- **Per bounded context.** The first context to onboard authors every tier above; each later
|
|
152
|
+
context amends the tiers that exist, additively and from its own code, under the same
|
|
153
|
+
partition — the service tier's step sees no domain file, the frontend tier's step no service
|
|
154
|
+
rule file, and the domain tier's step reads the technical half only to continue its
|
|
155
|
+
numbering. A tier that does not exist yet (a lane declared after an earlier onboard) is
|
|
156
|
+
authored in full, in the order above. The document templates are written once per lane and
|
|
157
|
+
never amended.
|
|
148
158
|
|
|
149
159
|
### `adopt` (brownfield)
|
|
150
160
|
- Same partition, reversed: the reversed **spec** is authored with domain rules in context; the
|
|
151
|
-
reversed **
|
|
161
|
+
reversed **plan** is authored with the technical set only, so the plan stays domain-free.
|
|
162
|
+
- Each lane's reversed plan is authored under its own lane's rules, as in `plan`: the backend
|
|
163
|
+
plan under `## /aw-spec-adopt`, and the frontend plan under `## /aw-spec-plan-ui`, which
|
|
164
|
+
REPLACES the backend set before that step rather than stacking on it.
|
|
152
165
|
- Technical files here are **reference, not authority — the CODE wins.**
|
|
153
166
|
|
|
154
167
|
---
|
|
155
168
|
|
|
156
169
|
## Invariants
|
|
157
170
|
|
|
158
|
-
1. **No domain rule file is ever loaded by `
|
|
171
|
+
1. **No domain rule file is ever loaded by `plan`, `implement` or `build`** — on either
|
|
159
172
|
lane and on either path.
|
|
160
|
-
2. **Domain reaches
|
|
173
|
+
2. **Domain reaches the plans and code only through the SPEC TIER** — as spec ids, and as the
|
|
161
174
|
bounded context's `domain-model.md` (plus a change's pending model delta) that those ids
|
|
162
175
|
point into. Never as a domain rule file: the rules were resolved into the model and the
|
|
163
176
|
spec at `specify`, and a phase downstream of that reads the resolution, never re-derives
|
|
@@ -101,6 +101,7 @@ Execution strategies (--strategy, overridable per step via `strategy:`):
|
|
|
101
101
|
suspend/resume.
|
|
102
102
|
"""
|
|
103
103
|
import argparse
|
|
104
|
+
import glob
|
|
104
105
|
import importlib.util
|
|
105
106
|
import json
|
|
106
107
|
import os
|
|
@@ -162,6 +163,34 @@ def _extract_json(text):
|
|
|
162
163
|
return {}
|
|
163
164
|
|
|
164
165
|
|
|
166
|
+
def _last_json_line(text):
|
|
167
|
+
"""The object on stdout's LAST line — the script contract — or None when that
|
|
168
|
+
line is missing or is not a JSON object."""
|
|
169
|
+
lines = (text or "").strip().splitlines()
|
|
170
|
+
if not lines:
|
|
171
|
+
return None
|
|
172
|
+
try:
|
|
173
|
+
data = json.loads(lines[-1])
|
|
174
|
+
except Exception:
|
|
175
|
+
return None
|
|
176
|
+
return data if isinstance(data, dict) else None
|
|
177
|
+
|
|
178
|
+
|
|
179
|
+
def _script_reason(data, stderr, returncode):
|
|
180
|
+
"""Why a script failed, in its own words where it gave any. A script that
|
|
181
|
+
prints `{"ok": "false", "error": …}` and exits non-zero has already said why;
|
|
182
|
+
stderr is often only its progress log, so the JSON is read first."""
|
|
183
|
+
data = data or {}
|
|
184
|
+
for key in ("error", "problems", "message", "reason"):
|
|
185
|
+
v = data.get(key)
|
|
186
|
+
if isinstance(v, str) and v.strip():
|
|
187
|
+
return v.strip()
|
|
188
|
+
errs = data.get("errors")
|
|
189
|
+
if isinstance(errs, list) and errs:
|
|
190
|
+
return "; ".join(str(e) for e in errs)
|
|
191
|
+
return (stderr or "").strip() or f"exit code {returncode}, no output"
|
|
192
|
+
|
|
193
|
+
|
|
165
194
|
def _strip_fence(text):
|
|
166
195
|
"""If the ENTIRE reply is one fenced code block, unwrap it (models often
|
|
167
196
|
fence authored file content despite being told not to)."""
|
|
@@ -199,6 +228,9 @@ class Orchestrator:
|
|
|
199
228
|
# assembly time (item H). Distinct, run-cumulative, surfaced in the run
|
|
200
229
|
# result + the context_missing/_count vars so a workflow can gate on it.
|
|
201
230
|
self.missing_required = []
|
|
231
|
+
# paths an agent_generate reply named as written that were not on disk —
|
|
232
|
+
# run-cumulative, reported at the end beside missing_required.
|
|
233
|
+
self.reported_missing = []
|
|
202
234
|
self.last_result = {}
|
|
203
235
|
# agent-specific transport profile (None in mock mode). This engine is
|
|
204
236
|
# general — the profile (loaded from a .claude/.agents/.kilocode file)
|
|
@@ -406,14 +438,21 @@ class Orchestrator:
|
|
|
406
438
|
against the run-cumulative set (cf. concept-paths' `missing`).
|
|
407
439
|
|
|
408
440
|
A path that resolves to "" is an unbound {{var}} on this branch, not a
|
|
409
|
-
missing file — skipped silently, as before.
|
|
441
|
+
missing file — skipped silently, as before.
|
|
442
|
+
|
|
443
|
+
A file named twice is sent once, compared by the file it resolves to rather
|
|
444
|
+
than by spelling: a script returns `knowledge/x.md` where a workflow writes
|
|
445
|
+
`{{bundle_dir}}/x.md` with an absolute bundle_dir, and both are one file."""
|
|
410
446
|
files, seen = [], set()
|
|
411
447
|
for p, optional in entries:
|
|
412
448
|
rp = self.resolve(p)
|
|
413
|
-
if not rp
|
|
449
|
+
if not rp:
|
|
450
|
+
continue
|
|
451
|
+
key = os.path.normcase(os.path.realpath(rp))
|
|
452
|
+
if key in seen:
|
|
414
453
|
continue
|
|
415
454
|
if os.path.exists(rp):
|
|
416
|
-
seen.add(
|
|
455
|
+
seen.add(key)
|
|
417
456
|
files.append(rp)
|
|
418
457
|
elif optional:
|
|
419
458
|
self.emit(f"[{kind}] optional missing, skip {rp}")
|
|
@@ -693,10 +732,11 @@ class Orchestrator:
|
|
|
693
732
|
def act_build_context(self, step):
|
|
694
733
|
ctx = list(step.get("context", []) or [])
|
|
695
734
|
# dynamic context: append a var holding a list of paths — e.g. the files a
|
|
696
|
-
# prior agent_generate reported it wrote (since IT, not we, chose them).
|
|
735
|
+
# prior agent_generate reported it wrote (since IT, not we, chose them). A list
|
|
736
|
+
# of var names appends each in order.
|
|
697
737
|
cf = step.get("context_from")
|
|
698
|
-
if cf:
|
|
699
|
-
ctx += list(self.vars.get(
|
|
738
|
+
for name in (cf if isinstance(cf, list) else [cf] if cf else []):
|
|
739
|
+
ctx += list(self.vars.get(name) or [])
|
|
700
740
|
self.envelope = {"system": step.get("system", []) or [], "context": ctx}
|
|
701
741
|
for k, v in (step.get("vars", {}) or {}).items():
|
|
702
742
|
self.vars[k] = self.resolve(v)
|
|
@@ -732,6 +772,30 @@ class Orchestrator:
|
|
|
732
772
|
v = ",".join(str(x) for x in v)
|
|
733
773
|
return (self.resolve(str(v)) or "").strip()
|
|
734
774
|
|
|
775
|
+
def _written_only(self, step, structured):
|
|
776
|
+
"""The generation result with `files` narrowed to paths that exist on disk.
|
|
777
|
+
|
|
778
|
+
`files` is the paths the agent SAYS it wrote. Every workflow gates on it with
|
|
779
|
+
`require_non_empty`, which proved only that the reply named something: a reply
|
|
780
|
+
listing a file whose write had failed passed the gate, and the next step read
|
|
781
|
+
a template that was never written. A path that is not on disk is dropped here
|
|
782
|
+
and announced, so an existing gate stops when nothing real was written, and a
|
|
783
|
+
step that builds context from the list gets only real files. The unfiltered
|
|
784
|
+
list stays in the replay cache, so every replay re-checks the disk."""
|
|
785
|
+
if not isinstance(structured, dict) or not isinstance(structured.get("files"), list):
|
|
786
|
+
return structured
|
|
787
|
+
kept, missing = [], []
|
|
788
|
+
for f in structured["files"]:
|
|
789
|
+
(kept if isinstance(f, str) and f.strip() and os.path.exists(f) else missing).append(f)
|
|
790
|
+
if not missing:
|
|
791
|
+
return structured
|
|
792
|
+
self.emit(f"[agent_generate] {step.get('id')}: {len(missing)} reported file(s) "
|
|
793
|
+
f"not on disk, dropped: {', '.join(str(m) for m in missing)}")
|
|
794
|
+
for m in missing:
|
|
795
|
+
if m not in self.reported_missing:
|
|
796
|
+
self.reported_missing.append(m)
|
|
797
|
+
return dict(structured, files=kept, files_not_written=missing)
|
|
798
|
+
|
|
735
799
|
def act_agent_generate(self, step):
|
|
736
800
|
tools = step.get("tools", "Read,Write,Edit,Bash")
|
|
737
801
|
permission_mode = step.get("permission_mode", "acceptEdits")
|
|
@@ -742,7 +806,7 @@ class Orchestrator:
|
|
|
742
806
|
# bind its cached structured result instead of executing again.
|
|
743
807
|
ck = self._key(step)
|
|
744
808
|
if self.checkpoint and ck in self.cache:
|
|
745
|
-
structured = self.cache[ck]
|
|
809
|
+
structured = self._written_only(step, self.cache[ck])
|
|
746
810
|
self.last_result = structured
|
|
747
811
|
self._bind(step, structured)
|
|
748
812
|
self.emit(f"[agent_generate] (cached) result={json.dumps(structured)[:200]}")
|
|
@@ -789,9 +853,10 @@ class Orchestrator:
|
|
|
789
853
|
else: # spawn
|
|
790
854
|
structured, _ = self._run_fresh(system, user, tools, permission_mode,
|
|
791
855
|
model=step.get("model"))
|
|
792
|
-
self.last_result = structured
|
|
793
856
|
if self.checkpoint:
|
|
794
857
|
self.cache[ck] = structured
|
|
858
|
+
structured = self._written_only(step, structured)
|
|
859
|
+
self.last_result = structured
|
|
795
860
|
self._bind(step, structured)
|
|
796
861
|
self.emit(f"[agent_generate] strategy={strat or 'mock'} "
|
|
797
862
|
f"result={json.dumps(structured)[:200]}")
|
|
@@ -901,12 +966,18 @@ class Orchestrator:
|
|
|
901
966
|
return
|
|
902
967
|
proc = subprocess.run(runner + [sub] + args, capture_output=True, text=True)
|
|
903
968
|
if proc.returncode != 0:
|
|
969
|
+
# A script that fails can still have answered: many print their verdict as
|
|
970
|
+
# JSON (`ok: false` plus the reason) and exit non-zero. Under `on_error:
|
|
971
|
+
# continue` that object is bound like a success's, so the gate after the
|
|
972
|
+
# step reads the script's own reason instead of a blank. A failure with no
|
|
973
|
+
# JSON binds nothing, as before.
|
|
974
|
+
data = _last_json_line(proc.stdout)
|
|
975
|
+
reason = _script_reason(data, proc.stderr, proc.returncode)
|
|
904
976
|
if p.get("on_error") == "continue":
|
|
905
|
-
self.emit(f"[execute_script] {sub} failed (continuing): "
|
|
906
|
-
|
|
907
|
-
self._bind(step, {})
|
|
977
|
+
self.emit(f"[execute_script] {sub} failed (continuing): {reason[:200]}")
|
|
978
|
+
self._bind(step, data or {})
|
|
908
979
|
return
|
|
909
|
-
raise Stop(f"script failed ({sub}): {
|
|
980
|
+
raise Stop(f"script failed ({sub}): {reason[:500]}")
|
|
910
981
|
try:
|
|
911
982
|
data = json.loads(proc.stdout.strip().splitlines()[-1])
|
|
912
983
|
except Exception as e:
|
|
@@ -1425,7 +1496,8 @@ class Orchestrator:
|
|
|
1425
1496
|
self._bind_inputs()
|
|
1426
1497
|
self._run_steps(self.wf.get("workflow", []) or [])
|
|
1427
1498
|
return {"status": "ok", "outputs": self.resolve(self.wf.get("outputs", {}) or {}),
|
|
1428
|
-
"vars": self.vars, "missing_required": list(self.missing_required)
|
|
1499
|
+
"vars": self.vars, "missing_required": list(self.missing_required),
|
|
1500
|
+
"reported_not_written": list(self.reported_missing)}
|
|
1429
1501
|
|
|
1430
1502
|
|
|
1431
1503
|
# ─────────────────────────────────────────────────────────────────────────────
|
|
@@ -1491,7 +1563,7 @@ def _module_script_refs(mpath, base_dir, seen):
|
|
|
1491
1563
|
return out
|
|
1492
1564
|
|
|
1493
1565
|
|
|
1494
|
-
def validate_workflow(wf, base_dir):
|
|
1566
|
+
def validate_workflow(wf, base_dir, path=""):
|
|
1495
1567
|
errors, warnings = [], []
|
|
1496
1568
|
if not isinstance(wf, dict):
|
|
1497
1569
|
return (["top-level YAML is not a mapping"], [])
|
|
@@ -1621,9 +1693,51 @@ def validate_workflow(wf, base_dir):
|
|
|
1621
1693
|
for name in sorted(used):
|
|
1622
1694
|
if name.split(".")[0] not in produced:
|
|
1623
1695
|
warnings.append(f"variable {{{{{name}}}}} referenced but never produced")
|
|
1696
|
+
errors_n, warnings_n = _validate_next(wf, base_dir, path)
|
|
1697
|
+
return errors + errors_n, warnings + warnings_n
|
|
1698
|
+
|
|
1699
|
+
|
|
1700
|
+
# outputs.next is the ONLY channel a finished run has for telling the operator what
|
|
1701
|
+
# to run next: the engine returns wf["outputs"] and the launching agent relays it.
|
|
1702
|
+
# Both faults it guards are silent — a next: indented out of the outputs map is
|
|
1703
|
+
# parsed and dropped, and a retired command name still reads as a valid sentence —
|
|
1704
|
+
# so the run ends looking successful and the agent invents a successor to fill the
|
|
1705
|
+
# gap. Warnings, not errors: a workflow may legitimately name a command that is
|
|
1706
|
+
# designed but not yet built, and pre-flight refuses to start on errors alone.
|
|
1707
|
+
def _validate_next(wf, base_dir, path=""):
|
|
1708
|
+
errors, warnings = [], []
|
|
1709
|
+
outputs = wf.get("outputs") or {}
|
|
1710
|
+
known = {os.path.basename(p)[len("aw-spec-"):-len(".orch.yaml")]
|
|
1711
|
+
for p in glob.glob(os.path.join(base_dir, "aw-spec-*.orch.yaml"))}
|
|
1712
|
+
# A module has no successor to name: it is included INTO a caller, and the caller's
|
|
1713
|
+
# own next covers the run. Only a launchable aw-spec-<name> workflow is asked for one.
|
|
1714
|
+
launchable = os.path.basename(path).startswith("aw-spec-") if path else bool(known)
|
|
1715
|
+
if launchable and "next" not in outputs:
|
|
1716
|
+
where = " (it is at the document's top level, OUTSIDE outputs:, so the " \
|
|
1717
|
+
"engine parses it and then drops it)" if "next" in wf else ""
|
|
1718
|
+
warnings.append(f"outputs: declares no 'next' — the run ends with no "
|
|
1719
|
+
f"suggested successor{where}")
|
|
1720
|
+
if not known: # validating a module, or a workflow read from elsewhere
|
|
1721
|
+
return errors, warnings
|
|
1722
|
+
for text in _output_strings(outputs):
|
|
1723
|
+
for cmd in re.findall(r"/appweaver\s+([a-z][a-z0-9-]*)", text):
|
|
1724
|
+
if cmd not in known:
|
|
1725
|
+
warnings.append(f"outputs names '/appweaver {cmd}', which resolves "
|
|
1726
|
+
f"to no aw-spec-{cmd}.orch.yaml")
|
|
1624
1727
|
return errors, warnings
|
|
1625
1728
|
|
|
1626
1729
|
|
|
1730
|
+
def _output_strings(v):
|
|
1731
|
+
if isinstance(v, str):
|
|
1732
|
+
yield v
|
|
1733
|
+
elif isinstance(v, list):
|
|
1734
|
+
for x in v:
|
|
1735
|
+
yield from _output_strings(x)
|
|
1736
|
+
elif isinstance(v, dict):
|
|
1737
|
+
for x in v.values():
|
|
1738
|
+
yield from _output_strings(x)
|
|
1739
|
+
|
|
1740
|
+
|
|
1627
1741
|
def _print_suspended(pending, opts):
|
|
1628
1742
|
"""Checkpoint transport: hand the pending request to the INVOKING agent."""
|
|
1629
1743
|
if opts.json:
|
|
@@ -1710,7 +1824,7 @@ def main(argv=None):
|
|
|
1710
1824
|
if opts.json else f"ERROR: parse: {e}")
|
|
1711
1825
|
return 1
|
|
1712
1826
|
errors, warnings = validate_workflow(
|
|
1713
|
-
wf, os.path.dirname(os.path.abspath(opts.workflow)))
|
|
1827
|
+
wf, os.path.dirname(os.path.abspath(opts.workflow)), opts.workflow)
|
|
1714
1828
|
if opts.json:
|
|
1715
1829
|
print(json.dumps({"status": "valid" if not errors else "invalid",
|
|
1716
1830
|
"errors": errors, "warnings": warnings}, indent=2))
|
|
@@ -1732,7 +1846,8 @@ def main(argv=None):
|
|
|
1732
1846
|
try:
|
|
1733
1847
|
with open(opts.workflow, encoding="utf-8") as fh:
|
|
1734
1848
|
wf = load_yaml(fh.read())
|
|
1735
|
-
errors, _ = validate_workflow(
|
|
1849
|
+
errors, _ = validate_workflow(
|
|
1850
|
+
wf, os.path.dirname(os.path.abspath(opts.workflow)), opts.workflow)
|
|
1736
1851
|
except Exception as e:
|
|
1737
1852
|
wf, errors = None, [f"parse: {e}"]
|
|
1738
1853
|
if errors:
|
|
@@ -1871,7 +1986,8 @@ def main(argv=None):
|
|
|
1871
1986
|
return 2
|
|
1872
1987
|
except Stop as e:
|
|
1873
1988
|
result = {"status": "stopped", "reason": str(e), "vars": orch.vars,
|
|
1874
|
-
"missing_required": list(orch.missing_required)
|
|
1989
|
+
"missing_required": list(orch.missing_required),
|
|
1990
|
+
"reported_not_written": list(orch.reported_missing)}
|
|
1875
1991
|
_clear_checkpoint(opts)
|
|
1876
1992
|
|
|
1877
1993
|
if opts.json:
|
|
@@ -1889,6 +2005,12 @@ def main(argv=None):
|
|
|
1889
2005
|
"them was assembled without them:")
|
|
1890
2006
|
for m in result["missing_required"]:
|
|
1891
2007
|
print(f" - {m}")
|
|
2008
|
+
if result.get("reported_not_written"):
|
|
2009
|
+
print(f"\nWARNING: {len(result['reported_not_written'])} file(s) a generation "
|
|
2010
|
+
"reported as written are not on disk — dropped from its result, so no "
|
|
2011
|
+
"later step read them:")
|
|
2012
|
+
for m in result["reported_not_written"]:
|
|
2013
|
+
print(f" - {m}")
|
|
1892
2014
|
if result.get("outputs"):
|
|
1893
2015
|
print(json.dumps(result["outputs"], indent=2))
|
|
1894
2016
|
return 0 if result["status"] == "ok" else 1
|
|
@@ -12,9 +12,9 @@
|
|
|
12
12
|
> Pure operating rules — domain-agnostic, stack-agnostic. Applies to every brownfield
|
|
13
13
|
> reverse-engineering authoring step (onboard's constitution + rule-set authors, adopt).
|
|
14
14
|
|
|
15
|
-
The **code, the deterministic scan, and any adopted spec.md /
|
|
16
|
-
OKF bundle are ground truth.** Author only what they
|
|
17
|
-
habit and any greenfield template ideal.
|
|
15
|
+
The **code, the deterministic scan, and any adopted spec.md / backend-plan.md /
|
|
16
|
+
frontend-plan.md / source-map.md + the OKF bundle are ground truth.** Author only what they
|
|
17
|
+
evidence; they override any pre-trained habit and any greenfield template ideal.
|
|
18
18
|
|
|
19
19
|
## Evidence discipline
|
|
20
20
|
|
|
@@ -6,8 +6,8 @@
|
|
|
6
6
|
TEMPLATE that onboard installs at
|
|
7
7
|
.appweaver/templates/overrides/delta-design-template.md. It carries the neutral
|
|
8
8
|
bones of a change record — the classification triple, the model bridge, the
|
|
9
|
-
wave discipline, the impact and regression sections, the two ledgers,
|
|
10
|
-
|
|
9
|
+
wave discipline, the impact and regression sections, the two ledgers, what
|
|
10
|
+
happens to the record at merge — and NOTHING ELSE.
|
|
11
11
|
|
|
12
12
|
IT NAMES NO LAYERING AND NO TECHNOLOGY. The "Section" column below is a SLOT:
|
|
13
13
|
it is filled with the ACTUAL sections of the master plan template generated
|
|
@@ -23,9 +23,10 @@
|
|
|
23
23
|
|
|
24
24
|
DISCIPLINE THIS SKELETON ENFORCES (structurally — there is no project content
|
|
25
25
|
here to leak):
|
|
26
|
-
• ONE MASTER PER LANE — a change
|
|
26
|
+
• ONE MASTER PER LANE — a change is planned against
|
|
27
27
|
specs/<Context>/<Feature>/backend-plan.md; it never opens a second plan
|
|
28
|
-
document
|
|
28
|
+
document and never edits the master. Neither does finalize-delta: the
|
|
29
|
+
master is re-derived from the merged spec and model by `plan main`.
|
|
29
30
|
• ADDED / MODIFIED / REMOVED — the complete classification. A shared element
|
|
30
31
|
is MODIFIED in place, never duplicated into the change.
|
|
31
32
|
• EVERY ROW SERVES A SPEC ID — the delta reads the delta-spec and the domain
|
|
@@ -54,8 +55,11 @@
|
|
|
54
55
|
|
|
55
56
|
<!--
|
|
56
57
|
This delta applies to the SINGLE master plan at
|
|
57
|
-
specs/[Context]/[Feature]/backend-plan.md — never a new plan doc
|
|
58
|
-
master
|
|
58
|
+
specs/[Context]/[Feature]/backend-plan.md — never a new plan doc. It does not
|
|
59
|
+
touch the master, and neither does finalize-delta: the master is DERIVED from
|
|
60
|
+
spec.md and domain-model.md, the two documents finalize-delta merges, so
|
|
61
|
+
`plan main` re-derives it rather than patching it from this file. close-delta
|
|
62
|
+
archives this document as the record of how the change was built.
|
|
59
63
|
|
|
60
64
|
IT PLANS AGAINST THE EFFECTIVE MODEL — the master domain-model.md, this change's
|
|
61
65
|
own model delta, and the PREDECESSOR deltas (the open ones in a LOWER wave,
|
|
@@ -145,15 +149,18 @@ changed.]
|
|
|
145
149
|
|
|
146
150
|
## New assumptions / deviations (from this change)
|
|
147
151
|
|
|
148
|
-
<!-- Anything this change defers
|
|
149
|
-
|
|
152
|
+
<!-- Anything this change defers. It stays this document's record — the master's
|
|
153
|
+
DA-n / WA-n ledgers are re-derived with the rest of the master plan, not
|
|
154
|
+
patched from here. -->
|
|
150
155
|
|
|
151
156
|
- **DA-[n]**: [assumption to clarify later] — or `None`
|
|
152
157
|
- **WA-[n]**: [deviation to clear later] — or `None`
|
|
153
158
|
|
|
154
159
|
## On merge
|
|
155
160
|
|
|
156
|
-
`finalize-delta` merges
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
`
|
|
161
|
+
`finalize-delta` merges the change's SPEC half into `spec.md` and its MODEL half into the
|
|
162
|
+
context's `domain-model.md`, in one transaction, and leaves the change OPEN. It does NOT
|
|
163
|
+
merge this document: the master plan is derived from those two, so `plan main` re-derives
|
|
164
|
+
it rather than patching it. `close-delta` archives this file with the change, bumps the
|
|
165
|
+
minor version and writes the Change History rows. None of it is this document's job — it
|
|
166
|
+
records the change, it does not apply it.
|
|
@@ -16,10 +16,11 @@
|
|
|
16
16
|
CODE, never copied.
|
|
17
17
|
|
|
18
18
|
THIS IS A TEMPLATE OF A TEMPLATE. What onboard generates from it is not a
|
|
19
|
-
filled
|
|
20
|
-
`
|
|
21
|
-
|
|
22
|
-
|
|
19
|
+
filled plan — it is the project's backend plan TEMPLATE, which the service lane
|
|
20
|
+
of `plan` and `adopt` then fill once per feature into
|
|
21
|
+
specs/<Context>/<Feature>/backend-plan.md. So the generated file keeps
|
|
22
|
+
[PLACEHOLDER] tokens and authoring comments; what onboard replaces is the
|
|
23
|
+
STRUCTURE (which sections exist, what they are called), not the content.
|
|
23
24
|
|
|
24
25
|
WHAT IS PINNED, AND WHY (DESIGN-FORMAT.md § Brownfield variant is the
|
|
25
26
|
authority — these are the only fixed points, and each earns it by being
|
|
@@ -31,8 +32,8 @@
|
|
|
31
32
|
lets a derived layout and the greenfield layout be read by one tool.
|
|
32
33
|
• The traceability spine (§4) and both ledgers (§5 DA-n, §6 WA-n) survive
|
|
33
34
|
every layout — the analyze gates read them.
|
|
34
|
-
• The §0–§6 numbering itself:
|
|
35
|
-
|
|
35
|
+
• The §0–§6 numbering itself: `plan`'s bootstrap_bf branch names those
|
|
36
|
+
sections in its prompt.
|
|
36
37
|
Everything else — how many §2 axes there are, what they are called, what
|
|
37
38
|
lives in each — is the code's answer, not the kit's.
|
|
38
39
|
|
|
@@ -51,13 +52,15 @@
|
|
|
51
52
|
raises, contracts as field shapes. The stack binds at `implement`.
|
|
52
53
|
═══════════════════════════════════════════════════════════════════════════
|
|
53
54
|
-->
|
|
54
|
-
#
|
|
55
|
+
# Backend Plan: [FEATURE]
|
|
55
56
|
|
|
56
|
-
**Feature**: `[Feature]`
|
|
57
|
+
**Feature**: `[Feature]` <!-- the capability scope the developer chose; the aggregate(s) it acts on are the spec's "Acts on aggregate(s)" line -->
|
|
57
58
|
|
|
58
59
|
**Plan home**: `specs/[Context]/[Feature]/backend-plan.md`
|
|
59
60
|
|
|
60
|
-
**Source spec**: `specs/[
|
|
61
|
+
**Source spec**: `specs/[Context]/[Feature]/spec.md`
|
|
62
|
+
|
|
63
|
+
**Source model**: `specs/[Context]/domain-model.md` <!-- where the context has one -->
|
|
61
64
|
|
|
62
65
|
**Version**: v1.0
|
|
63
66
|
|
|
@@ -66,9 +69,10 @@
|
|
|
66
69
|
<!--
|
|
67
70
|
WHAT THIS DOCUMENT IS
|
|
68
71
|
=====================
|
|
69
|
-
The single, living High-Level Design for ONE
|
|
70
|
-
|
|
71
|
-
|
|
72
|
+
The single, living High-Level Design for ONE feature — the capability scope the
|
|
73
|
+
developer chose, over the aggregate(s) its spec says it acts on — and the
|
|
74
|
+
MEDIATOR between spec.md (WHAT/WHY) and the generated code (HOW). It carries
|
|
75
|
+
structure, contracts, behaviour and decisions; it is not code.
|
|
72
76
|
|
|
73
77
|
ANCHORING RULES
|
|
74
78
|
- Every design element traces UP to an id (FR-/BR-/CN-/DC-/IC-/US-/SC-) and the
|
|
@@ -110,11 +114,11 @@
|
|
|
110
114
|
|
|
111
115
|
## 0. Design Overview
|
|
112
116
|
|
|
113
|
-
<!-- One paragraph: the shape of this
|
|
117
|
+
<!-- One paragraph: the shape of this feature and the 3–5 decisions that
|
|
114
118
|
matter most. The project's standing structural convention is NOT restated
|
|
115
119
|
here — it is §1.1. -->
|
|
116
120
|
|
|
117
|
-
[The architectural essence of this
|
|
121
|
+
[The architectural essence of this feature in 3–6 sentences.]
|
|
118
122
|
|
|
119
123
|
### Ubiquitous Language (design delta)
|
|
120
124
|
|
|
@@ -245,9 +249,13 @@
|
|
|
245
249
|
|
|
246
250
|
## Change History
|
|
247
251
|
|
|
248
|
-
<!--
|
|
249
|
-
|
|
250
|
-
|
|
252
|
+
<!-- A row is appended when this plan is RE-DERIVED to carry a change: the plan
|
|
253
|
+
is derived from spec.md and the context's domain-model.md, and finalize-delta
|
|
254
|
+
merges a change into those two, never into this one (`plan main` re-derives
|
|
255
|
+
it). close-delta bumps the version and stamps it into the header, so a header
|
|
256
|
+
ahead of the newest row means the plan has not been re-derived since. A
|
|
257
|
+
change is planned as a DELTA in the change's own directory, never as a new
|
|
258
|
+
plan document. Newest last. -->
|
|
251
259
|
|
|
252
260
|
| Version | Date | Change | User stories | CR |
|
|
253
261
|
|---------|------|--------|--------------|-----|
|
|
@@ -20,7 +20,7 @@ generated_by: aw-spec-onboard/[VERSION]
|
|
|
20
20
|
[DOMAIN: Bounded Contexts / Modules]
|
|
21
21
|
=====================================================================
|
|
22
22
|
- [Context / module] — aggregate root: **[Aggregate]** (adopted — current evidence); owns
|
|
23
|
-
[entities] and the value objects ([named VOs]). See [[domain-
|
|
23
|
+
[entities] and the value objects ([named VOs]). See [[domain-model.domain]].
|
|
24
24
|
- [Named external reference / port] — [role]; not adopted as an aggregate.
|
|
25
25
|
- [… list each bounded context observed in the OKF overview / aggregate index …]
|
|
26
26
|
|
|
@@ -45,4 +45,4 @@ generated_by: aw-spec-onboard/[VERSION]
|
|
|
45
45
|
|
|
46
46
|
## Related
|
|
47
47
|
|
|
48
|
-
- [[domain-
|
|
48
|
+
- [[domain-model.domain]] · [[<generated siblings>]]
|