aw-spec-cli 0.1.29 → 0.1.30

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 (67) hide show
  1. package/assets/.amp/agent.json +22 -0
  2. package/assets/.amp/settings-edit.json +5 -0
  3. package/assets/.amp/settings-read.json +12 -0
  4. package/assets/.appweaver/DESIGN-FORMAT.md +9 -0
  5. package/assets/.appweaver/STACK-PROFILES.md +102 -15
  6. package/assets/.appweaver/orchestrator/modules/delta-plan.orch.yaml +40 -18
  7. package/assets/.appweaver/orchestrator/modules/delta-spec.orch.yaml +56 -4
  8. package/assets/.appweaver/orchestrator/modules/lane-gate.orch.yaml +17 -1
  9. package/assets/.appweaver/orchestrator/rev-templates/ui-design-template.skeleton.md +12 -3
  10. package/assets/.appweaver/orchestrator/workflows/aw-spec-analyze-plan-delta.orch.yaml +289 -24
  11. package/assets/.appweaver/orchestrator/workflows/aw-spec-analyze-plan.orch.yaml +370 -85
  12. package/assets/.appweaver/orchestrator/workflows/aw-spec-analyze-slice-delta.orch.yaml +349 -65
  13. package/assets/.appweaver/orchestrator/workflows/aw-spec-analyze-slice.orch.yaml +39 -7
  14. package/assets/.appweaver/orchestrator/workflows/aw-spec-build.orch.yaml +63 -4
  15. package/assets/.appweaver/orchestrator/workflows/aw-spec-finalize-delta.orch.yaml +5 -4
  16. package/assets/.appweaver/orchestrator/workflows/aw-spec-implement.orch.yaml +430 -138
  17. package/assets/.appweaver/orchestrator/workflows/aw-spec-plan.orch.yaml +392 -76
  18. package/assets/.appweaver/orchestrator/workflows/aw-spec-select-lane.orch.yaml +104 -0
  19. package/assets/.appweaver/orchestrator/workflows/aw-spec-select-stack.orch.yaml +20 -3
  20. package/assets/.appweaver/orchestrator/workflows/aw-spec-specify.orch.yaml +26 -3
  21. package/assets/.appweaver/scripts/bash/aw-spec-actions.sh +314 -44
  22. package/assets/.appweaver/scripts/powershell/aw-spec-actions.ps1 +249 -20
  23. package/assets/.appweaver/scripts/powershell/aw-xref.ps1 +17 -2
  24. package/assets/.appweaver/scripts/python/aw-plangate.py +109 -19
  25. package/assets/.appweaver/scripts/python/aw-stack.py +1295 -9
  26. package/assets/.appweaver/templates/delta-design-template.md +7 -5
  27. package/assets/.appweaver/templates/delta-spec-template.md +2 -0
  28. package/assets/.appweaver/templates/design-template.md +17 -0
  29. package/assets/.appweaver/templates/ui-design-template.md +19 -4
  30. package/assets/.augment/agent.json +22 -0
  31. package/assets/.augment/commands/appweaver.md +12 -0
  32. package/assets/.cline/agent.json +22 -0
  33. package/assets/.clinerules/workflows/appweaver.md +7 -0
  34. package/assets/.codex/agent.json +22 -0
  35. package/assets/.continue/agent.json +22 -0
  36. package/assets/.continue/prompts/appweaver.md +13 -0
  37. package/assets/.cursor/agent.json +22 -0
  38. package/assets/.cursor/commands/appweaver.md +7 -0
  39. package/assets/.devin/agent.json +22 -0
  40. package/assets/.devin/workflows/appweaver.md +11 -0
  41. package/assets/.factory/agent.json +22 -0
  42. package/assets/.factory/commands/appweaver.md +12 -0
  43. package/assets/.gemini/agent.json +22 -0
  44. package/assets/.github/prompts/appweaver.prompt.md +13 -0
  45. package/assets/.kilo/agent.json +28 -0
  46. package/assets/.kilo/agents/appweaver-edit.md +12 -0
  47. package/assets/.kilo/agents/appweaver-read.md +12 -0
  48. package/assets/.kilo/commands/appweaver.md +11 -0
  49. package/assets/.kiro/agent.json +22 -0
  50. package/assets/.kiro/skills/appweaver/SKILL.md +12 -0
  51. package/assets/.opencode/agent.json +22 -0
  52. package/assets/.opencode/agents/appweaver-edit.md +12 -0
  53. package/assets/.opencode/agents/appweaver-read.md +13 -0
  54. package/assets/.opencode/commands/appweaver.md +11 -0
  55. package/assets/.roo/agent.json +22 -0
  56. package/assets/.roo/commands/appweaver.md +12 -0
  57. package/dist/commands/init.d.ts.map +1 -1
  58. package/dist/commands/init.js +14 -1
  59. package/dist/commands/init.js.map +1 -1
  60. package/dist/utils/config.d.ts.map +1 -1
  61. package/dist/utils/config.js +14 -0
  62. package/dist/utils/config.js.map +1 -1
  63. package/dist/utils/copy-assets.d.ts +16 -3
  64. package/dist/utils/copy-assets.d.ts.map +1 -1
  65. package/dist/utils/copy-assets.js +29 -14
  66. package/dist/utils/copy-assets.js.map +1 -1
  67. package/package.json +1 -1
@@ -16,6 +16,25 @@ model sha does, and an unrecognised verdict records as `gaps`. That rule changed
16
16
  and kept in step by eye. One module both shells invoke removes the question: there is
17
17
  nothing left to keep in step. Same reason aw-crstate.py owns "has this change merged".
18
18
 
19
+ ONE RECEIPT PER LANE. With --lane, the receipt is `.aw-plan-gate.<lane>.json` and it
20
+ hashes the spec, the model and THAT lane's plan only, with that lane's verdict. A backend
21
+ developer and a frontend developer gate their own plans at different times, so a frontend
22
+ plan edited, or left with gaps, never refuses the backend's implement, and the reverse.
23
+ A feature gated before the per-lane receipts existed still has the shared
24
+ `.aw-plan-gate.json`: `check --lane` falls back to it and compares only that lane's plan
25
+ names in it, so the other lane's document moving does not read as this lane's drift.
26
+ Without --lane both commands keep the shared receipt.
27
+
28
+ THE FRONTEND RECEIPT ALSO HASHES THE BACKEND'S PUBLISHED SURFACE. A frontend plan binds to
29
+ the operations the backend plan's API section publishes, and the backend developer can
30
+ change that section after the frontend was gated — rename an operation, change a field,
31
+ answer a contract request. So the frontend receipt records `surface_sha`, the hash of that
32
+ section (aw-stack's api_section; the whole backend plan when it has none, "" when there is
33
+ no backend plan), and `check --lane frontend` reads a different surface as moved bytes. The
34
+ backend plan's other sections are not in it: a change to the domain or infrastructure
35
+ layers leaves the frontend receipt fresh. A receipt written before the field existed has no
36
+ baseline for it and is not refused over it.
37
+
19
38
  NO BASELINE IS NOT "NOTHING MOVED". `spec_moved` / `model_moved` report "" where there is
20
39
  nothing to compare against — no receipt, or a receipt that hashed nothing for that
21
40
  document. "" is falsy in Python, in PowerShell and to the engine's require_true, so every
@@ -23,14 +42,14 @@ flag computed from them reads exactly as a false did; what it buys is that a rea
23
42
  tell "unchanged" from "unknown", which a bare false cannot express.
24
43
 
25
44
  Usage:
26
- aw-plangate.py stamp --feature-dir <p> --context-dir <p> --root <p>
45
+ aw-plangate.py stamp --feature-dir <p> --context-dir <p> --root <p> [--lane <k>]
27
46
  [--spec-version <v>] [--mode <text>]
28
47
  [--verdict conformant|routed-gaps|gaps] [--plan-gaps <n>]
29
48
  [--routed <json list>]
30
49
  -> {file, gated, spec_version, verdict, plan_gaps, routed_gaps, hashed[], count}
31
- aw-plangate.py check --feature-dir <p> --context-dir <p> --root <p>
32
- -> {file, tracked, gate_fresh, verdict, may_generate, plan_gaps, routed_gaps[],
33
- routed_csv, spec_moved, model_moved, plans_changed[], plans_changed_csv,
50
+ aw-plangate.py check --feature-dir <p> --context-dir <p> --root <p> [--lane <k>]
51
+ -> {file, lane, shared, tracked, gate_fresh, verdict, may_generate, plan_gaps, routed_gaps[],
52
+ routed_csv, spec_moved, model_moved, surface_moved, plans_changed[], plans_changed_csv,
34
53
  gated, spec_version, reason}
35
54
  """
36
55
 
@@ -56,6 +75,16 @@ VERDICTS = ("conformant", "routed-gaps", "gaps")
56
75
  # implement actually generates from is unwatched.
57
76
  PLAN_NAMES = ("backend-plan.md", "frontend-plan.md", "design.md", "design.ui.md")
58
77
 
78
+ # Each lane's plan, under its current name and its pre-lane-split one.
79
+ LANE_PLAN_NAMES = {
80
+ "backend": ("backend-plan.md", "design.md"),
81
+ "frontend": ("frontend-plan.md", "design.ui.md"),
82
+ }
83
+
84
+
85
+ def receipt_name(lane=""):
86
+ return ".aw-plan-gate.%s.json" % lane if lane in LANE_PLAN_NAMES else RECEIPT
87
+
59
88
  NOTE = ("Written by every master-mode analyze-plan run. It records the bytes that were "
60
89
  "checked against each other and the verdict on them, not a version — the plans' "
61
90
  "**Version**: headers are stamped in lockstep by close-delta and cannot answer "
@@ -71,10 +100,39 @@ def sha(path):
71
100
  return ""
72
101
 
73
102
 
103
+ def _api_section(text):
104
+ """aw-stack's api_section, loaded by path because its filename is not an importable
105
+ module name; the whole text when it cannot be loaded."""
106
+ try:
107
+ import importlib.util
108
+ path = os.path.join(os.path.dirname(os.path.abspath(__file__)), "aw-stack.py")
109
+ spec = importlib.util.spec_from_file_location("aw_stack", path)
110
+ mod = importlib.util.module_from_spec(spec)
111
+ spec.loader.exec_module(mod)
112
+ return mod.api_section(text)
113
+ except Exception:
114
+ return None
115
+
116
+
117
+ def surface_sha(fd):
118
+ """The hash of the backend plan's published surface — its API section, or the whole plan
119
+ when it has none; "" when the feature has no backend plan."""
120
+ for name in LANE_PLAN_NAMES["backend"]:
121
+ path = os.path.join(fd, name)
122
+ if os.path.isfile(path):
123
+ with open(path, encoding="utf-8", errors="replace") as fh:
124
+ text = fh.read()
125
+ section = _api_section(text)
126
+ return hashlib.sha256((section if section is not None else text)
127
+ .encode("utf-8")).hexdigest()
128
+ return ""
129
+
130
+
74
131
  def stamp(fd, ctx, root, spec_version="", mode="master", verdict="conformant",
75
- plan_gaps=0, routed=None):
132
+ plan_gaps=0, routed=None, lane=""):
133
+ lane = lane if lane in LANE_PLAN_NAMES else ""
76
134
  plans = {}
77
- for name in PLAN_NAMES:
135
+ for name in (LANE_PLAN_NAMES[lane] if lane else PLAN_NAMES):
78
136
  h = sha(os.path.join(fd, name))
79
137
  if h:
80
138
  plans[name] = h
@@ -99,30 +157,50 @@ def stamp(fd, ctx, root, spec_version="", mode="master", verdict="conformant",
99
157
  "plans": plans,
100
158
  "note": NOTE,
101
159
  }
102
- path = os.path.join(fd, RECEIPT)
160
+ if lane:
161
+ rec["lane"] = lane
162
+ if lane == "frontend":
163
+ rec["surface_sha"] = surface_sha(fd)
164
+ path = os.path.join(fd, receipt_name(lane))
103
165
  with open(path, "w", encoding="utf-8") as fh:
104
166
  fh.write(json.dumps(rec, indent=2) + "\n")
105
- return {"file": os.path.relpath(path, root), "gated": rec["gated"],
167
+ return {"file": os.path.relpath(path, root), "lane": lane, "gated": rec["gated"],
106
168
  "spec_version": rec["spec_version"], "verdict": verdict,
107
169
  "plan_gaps": plan_gaps, "routed_gaps": len(routed),
108
170
  "hashed": sorted(plans.keys()), "count": len(plans), "ok": True}
109
171
 
110
172
 
111
- def check(fd, ctx, root):
112
- path = os.path.join(fd, RECEIPT)
173
+ def _load(path):
113
174
  try:
114
- rec = json.load(open(path, encoding="utf-8"))
175
+ return json.load(open(path, encoding="utf-8"))
115
176
  except Exception:
116
- rec = None
117
- out = {"file": os.path.relpath(path, root), "tracked": bool(rec), "gate_fresh": False,
177
+ return None
178
+
179
+
180
+ def check(fd, ctx, root, lane=""):
181
+ lane = lane if lane in LANE_PLAN_NAMES else ""
182
+ path = os.path.join(fd, receipt_name(lane))
183
+ rec = _load(path)
184
+ shared = False
185
+ if lane and rec is None:
186
+ # A feature gated before the per-lane receipts: read the shared one, narrowed to
187
+ # this lane's plan names below.
188
+ legacy = os.path.join(fd, RECEIPT)
189
+ rec = _load(legacy)
190
+ if rec is not None:
191
+ path, shared = legacy, True
192
+ out = {"file": os.path.relpath(path, root), "lane": lane, "shared": shared,
193
+ "tracked": bool(rec), "gate_fresh": False,
118
194
  "verdict": "", "may_generate": True, "plan_gaps": 0, "routed_gaps": [],
119
- "routed_csv": "", "spec_moved": "", "model_moved": "", "plans_changed": [],
195
+ "routed_csv": "", "spec_moved": "", "model_moved": "", "surface_moved": "",
196
+ "plans_changed": [],
120
197
  "plans_changed_csv": "", "gated": "", "spec_version": "", "reason": "", "ok": True}
121
198
  if not rec:
122
199
  # Never gated. Not a failure and not a stale plan — an unknown, and the only honest
123
200
  # thing to report about a tree that has not run analyze-plan since this op existed.
124
- out["reason"] = ("no .aw-plan-gate.json — this feature has not been through "
125
- "analyze-plan in master mode since the gate began leaving a receipt")
201
+ out["reason"] = ("no %s — this feature's %s has not been through analyze-plan in "
202
+ "master mode since the gate began leaving a receipt"
203
+ % (receipt_name(lane), ("%s plan" % lane) if lane else "plans"))
126
204
  return out
127
205
 
128
206
  out["gated"] = rec.get("gated", "")
@@ -149,11 +227,18 @@ def check(fd, ctx, root):
149
227
 
150
228
  changed = []
151
229
  for name, was in sorted((rec.get("plans") or {}).items()):
230
+ if lane and name not in LANE_PLAN_NAMES[lane]:
231
+ continue
152
232
  if sha(os.path.join(fd, name)) != was:
153
233
  changed.append(name)
154
234
  out["plans_changed"] = changed
155
235
  out["plans_changed_csv"] = ", ".join(changed)
156
- out["gate_fresh"] = not (out["spec_moved"] or out["model_moved"] or changed)
236
+ # The backend's published surface, for the frontend lane only. "" recorded means the
237
+ # frontend was gated with no backend plan, so a backend plan written since has moved it.
238
+ if lane == "frontend" and "surface_sha" in rec:
239
+ out["surface_moved"] = (rec.get("surface_sha") or "") != surface_sha(fd)
240
+ out["gate_fresh"] = not (out["spec_moved"] or out["model_moved"] or out["surface_moved"]
241
+ or changed)
157
242
  # Moved bytes refuse whatever the verdict was; unmoved bytes refuse only on `gaps`.
158
243
  out["may_generate"] = out["gate_fresh"] and out["verdict"] != "gaps"
159
244
 
@@ -163,6 +248,9 @@ def check(fd, ctx, root):
163
248
  bits.append("spec.md has changed")
164
249
  if out["model_moved"]:
165
250
  bits.append("domain-model.md has changed")
251
+ if out["surface_moved"]:
252
+ bits.append("the backend plan's published surface (its API section) has changed — "
253
+ "re-gate the frontend plan against it")
166
254
  if changed:
167
255
  bits.append("%s changed (a re-derive by `plan main`, or a hand edit)"
168
256
  % ", ".join(changed))
@@ -190,6 +278,8 @@ def main():
190
278
  p.add_argument("--feature-dir", required=True)
191
279
  p.add_argument("--context-dir", required=True)
192
280
  p.add_argument("--root", required=True)
281
+ p.add_argument("--lane", default="", choices=["", "backend", "frontend"],
282
+ help="the lane's own receipt (.aw-plan-gate.<lane>.json)")
193
283
  if name == "stamp":
194
284
  p.add_argument("--spec-version", default="")
195
285
  p.add_argument("--mode", default="master")
@@ -206,9 +296,9 @@ def main():
206
296
  if not isinstance(routed, list):
207
297
  routed = []
208
298
  out = stamp(args.feature_dir, args.context_dir, args.root, args.spec_version,
209
- args.mode, args.verdict, args.plan_gaps, routed)
299
+ args.mode, args.verdict, args.plan_gaps, routed, args.lane)
210
300
  else:
211
- out = check(args.feature_dir, args.context_dir, args.root)
301
+ out = check(args.feature_dir, args.context_dir, args.root, args.lane)
212
302
  sys.stdout.write(json.dumps(out) + "\n")
213
303
 
214
304