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.
Files changed (48) hide show
  1. package/assets/.appweaver/ORCH-DIALECT.md +3 -3
  2. package/assets/.appweaver/RULE-PARTITION.md +41 -28
  3. package/assets/.appweaver/orchestrator/orchestrator.py +139 -17
  4. package/assets/.appweaver/orchestrator/rev-templates/ai-rules.reference.md +3 -3
  5. package/assets/.appweaver/orchestrator/rev-templates/delta-design-template.skeleton.md +19 -12
  6. package/assets/.appweaver/orchestrator/rev-templates/design-template.skeleton.md +25 -17
  7. package/assets/.appweaver/orchestrator/rev-templates/domain-requirements.skeleton.md +2 -2
  8. package/assets/.appweaver/orchestrator/rev-templates/domain-rule-bindings.skeleton.md +10 -10
  9. package/assets/.appweaver/orchestrator/rev-templates/tech-rule-bindings.skeleton.md +28 -17
  10. package/assets/.appweaver/orchestrator/rev-templates/technical-constitution.skeleton.md +22 -24
  11. package/assets/.appweaver/orchestrator/rev-templates/technical-rule-file.skeleton.md +1 -1
  12. package/assets/.appweaver/orchestrator/rev-templates/ui-delta-design-template.skeleton.md +29 -19
  13. package/assets/.appweaver/orchestrator/rev-templates/ui-design-template.skeleton.md +27 -20
  14. package/assets/.appweaver/orchestrator/workflows/aw-spec-adopt.orch.yaml +184 -37
  15. package/assets/.appweaver/orchestrator/workflows/aw-spec-analyze-plan-delta.orch.yaml +18 -0
  16. package/assets/.appweaver/orchestrator/workflows/aw-spec-analyze-plan.orch.yaml +19 -0
  17. package/assets/.appweaver/orchestrator/workflows/aw-spec-analyze-slice-delta.orch.yaml +18 -0
  18. package/assets/.appweaver/orchestrator/workflows/aw-spec-constitution.orch.yaml +1 -1
  19. package/assets/.appweaver/orchestrator/workflows/aw-spec-dependency-tree.orch.yaml +4 -1
  20. package/assets/.appweaver/orchestrator/workflows/aw-spec-foundation.orch.yaml +5 -1
  21. package/assets/.appweaver/orchestrator/workflows/aw-spec-inventory.orch.yaml +58 -43
  22. package/assets/.appweaver/orchestrator/workflows/aw-spec-onboard.orch.yaml +650 -205
  23. package/assets/.appweaver/orchestrator/workflows/aw-spec-plan.orch.yaml +19 -0
  24. package/assets/.appweaver/orchestrator/workflows/aw-spec-select-domain.orch.yaml +3 -1
  25. package/assets/.appweaver/orchestrator/workflows/aw-spec-select-feature.orch.yaml +5 -5
  26. package/assets/.appweaver/orchestrator/workflows/aw-spec-select-stack.orch.yaml +2 -2
  27. package/assets/.appweaver/orchestrator/workflows/aw-spec-specify.orch.yaml +4 -1
  28. package/assets/.appweaver/orchestrator/workflows/aw-spec-ui-model.orch.yaml +34 -6
  29. package/assets/.appweaver/orchestrator/workflows/aw-spec-ui-prototype.orch.yaml +40 -1
  30. package/assets/.appweaver/orchestrator/workflows/aw-spec-workflow-model.orch.yaml +23 -4
  31. package/assets/.appweaver/scripts/bash/aw-rev-constitution.sh +1 -1
  32. package/assets/.appweaver/scripts/bash/aw-spec-actions.sh +82 -10
  33. package/assets/.appweaver/scripts/powershell/aw-rev-constitution.ps1 +1 -1
  34. package/assets/.appweaver/scripts/powershell/aw-spec-actions.ps1 +67 -2
  35. package/assets/.appweaver/scripts/python/aw-inventory.py +1809 -156
  36. package/assets/.appweaver/scripts/python/aw-rev-constitution.py +106 -145
  37. package/assets/.appweaver/scripts/python/aw-stack.py +45 -1
  38. package/assets/.appweaver/scripts/python/aw-xref.py +73 -2
  39. package/assets/.appweaver/scripts/python/validate_okf.py +10 -5
  40. package/assets/.appweaver/templates/okf-concept-template.md +10 -2
  41. package/assets/.appweaver/templates/proposed-inventory-template.md +100 -48
  42. package/assets/.appweaver/templates/source-map-template.md +14 -6
  43. package/assets/foundations/dotnet/ModularMonolith/ModularMonolith.pack.txt +3 -3
  44. package/assets/foundations/dotnet/VerticalSlice/VerticalSlice.pack.txt +5 -123
  45. package/assets/foundations/java/Hexagonal/Hexagonal.pack.txt +784 -0
  46. package/assets/foundations/java/Hexagonal/pack-foundation.sh +357 -0
  47. package/assets/foundations/java/Hexagonal/unpack-foundation.sh +399 -0
  48. 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 generation reported, + `vars:`) |
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 design
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
- > design or into code.
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
- **design + implement**. Domain-agnostic.
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 design or implement.
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
- ### `design` — *architecture; domain input = the spec only*
78
- - **Domain rule files: NEVER.** Domain reaches design solely as spec ids.
79
- - **Technical rule files: yes** — architecture and cross-cutting rules the design mediates the
80
- spec against.
81
- - Constitution: the **technical** half.
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 design intent, which the domain never reaches.
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
- - The **UI half** is authored where the **scan** finds a UI — `frontend_signals` reaching lane
125
- strength — not where a map heading exists, because at onboard there is no map to read. The
126
- order is therefore inverted: the half is authored from the code, then the `frontend/` rule set is
127
- generated as its detail layer, then `## /aw-spec-ui-constitution` is appended to the technical
128
- map. That heading is what makes every LATER `constitution` run behave like the greenfield case
129
- above.
130
- - A frontend lane declared in `.appweaver/stacks.json` corroborates rather than decides: with no
131
- UI source there is nothing to reverse-engineer, so nothing is authored.
132
- - `onboard` also **records** the lanes it reverse-engineered in `.appweaver/stacks.json`, through
133
- `lane-declare` — still the sole writer, now with two callers. A derived lane carries
134
- `origin: reverse-engineered` and an existing declaration is never overwritten, so a lane a
135
- person confirmed always outranks one a scan proposed. Confirming or correcting a lane stays
136
- `select-stack`'s job.
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 service design + delta-design pair is authored against `constitution.technical.md`, 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 **design** is authored with the technical set only, so design stays domain-free.
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 `design`, `implement` or `build`** — on either
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 design and code only through the SPEC TIER** — as spec ids, and as the
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 or rp in seen:
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(rp)
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(cf) or [])
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
- f"{proc.stderr.strip()[:200]}")
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}): {proc.stderr.strip()[:500]}")
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(wf, os.path.dirname(os.path.abspath(opts.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 / design.md / source-map.md + the
16
- OKF bundle are ground truth.** Author only what they evidence; they override any pre-trained
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, the merge
10
- contract — and NOTHING ELSE.
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 amends
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, and it never edits the master itself.
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, and never the
58
- master itself: finalize-delta merges it later in the chain.
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 — folds into the master's DA-n / WA-n ledgers
149
- on merge. -->
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 this record into the master backend-plan.md alongside the
157
- spec and domain-model merges and leaves the change OPEN; `close-delta` bumps the
158
- version, writes the Change History rows and archives the change under
159
- `changes/archive/`.
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 design — it is the project's design TEMPLATE, which the forward
20
- `design` phase and `adopt` then fill once per feature. So the generated file
21
- keeps [PLACEHOLDER] tokens and authoring comments; what onboard replaces is
22
- the STRUCTURE (which sections exist, what they are called), not the content.
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: the forward `design` bootstrap_bf branch
35
- names those sections in its prompt.
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
- # Design: [AGGREGATE]
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/[Aggregate]/spec.md`
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 aggregate — the MEDIATOR between
70
- spec.md (WHAT/WHY) and the generated code (HOW). It carries structure,
71
- contracts, behaviour and decisions; it is not code.
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 aggregate and the 3–5 decisions that
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 aggregate in 3–6 sentences.]
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
- <!-- In lockstep with spec.md. finalize-delta appends the row when it merges a
249
- delta; close-delta bumps the version and stamps it into the header. A
250
- change is a DELTA on this document, never a new design doc. Newest last. -->
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-driven-design.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-driven-design.domain]] · [[<generated siblings>]]
48
+ - [[domain-model.domain]] · [[<generated siblings>]]