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.
- package/assets/.amp/agent.json +22 -0
- package/assets/.amp/settings-edit.json +5 -0
- package/assets/.amp/settings-read.json +12 -0
- package/assets/.appweaver/DESIGN-FORMAT.md +9 -0
- package/assets/.appweaver/STACK-PROFILES.md +102 -15
- package/assets/.appweaver/orchestrator/modules/delta-plan.orch.yaml +40 -18
- package/assets/.appweaver/orchestrator/modules/delta-spec.orch.yaml +56 -4
- package/assets/.appweaver/orchestrator/modules/lane-gate.orch.yaml +17 -1
- package/assets/.appweaver/orchestrator/rev-templates/ui-design-template.skeleton.md +12 -3
- package/assets/.appweaver/orchestrator/workflows/aw-spec-analyze-plan-delta.orch.yaml +289 -24
- package/assets/.appweaver/orchestrator/workflows/aw-spec-analyze-plan.orch.yaml +370 -85
- package/assets/.appweaver/orchestrator/workflows/aw-spec-analyze-slice-delta.orch.yaml +349 -65
- package/assets/.appweaver/orchestrator/workflows/aw-spec-analyze-slice.orch.yaml +39 -7
- package/assets/.appweaver/orchestrator/workflows/aw-spec-build.orch.yaml +63 -4
- package/assets/.appweaver/orchestrator/workflows/aw-spec-finalize-delta.orch.yaml +5 -4
- package/assets/.appweaver/orchestrator/workflows/aw-spec-implement.orch.yaml +430 -138
- package/assets/.appweaver/orchestrator/workflows/aw-spec-plan.orch.yaml +392 -76
- package/assets/.appweaver/orchestrator/workflows/aw-spec-select-lane.orch.yaml +104 -0
- package/assets/.appweaver/orchestrator/workflows/aw-spec-select-stack.orch.yaml +20 -3
- package/assets/.appweaver/orchestrator/workflows/aw-spec-specify.orch.yaml +26 -3
- package/assets/.appweaver/scripts/bash/aw-spec-actions.sh +314 -44
- package/assets/.appweaver/scripts/powershell/aw-spec-actions.ps1 +249 -20
- package/assets/.appweaver/scripts/powershell/aw-xref.ps1 +17 -2
- package/assets/.appweaver/scripts/python/aw-plangate.py +109 -19
- package/assets/.appweaver/scripts/python/aw-stack.py +1295 -9
- package/assets/.appweaver/templates/delta-design-template.md +7 -5
- package/assets/.appweaver/templates/delta-spec-template.md +2 -0
- package/assets/.appweaver/templates/design-template.md +17 -0
- package/assets/.appweaver/templates/ui-design-template.md +19 -4
- package/assets/.augment/agent.json +22 -0
- package/assets/.augment/commands/appweaver.md +12 -0
- package/assets/.cline/agent.json +22 -0
- package/assets/.clinerules/workflows/appweaver.md +7 -0
- package/assets/.codex/agent.json +22 -0
- package/assets/.continue/agent.json +22 -0
- package/assets/.continue/prompts/appweaver.md +13 -0
- package/assets/.cursor/agent.json +22 -0
- package/assets/.cursor/commands/appweaver.md +7 -0
- package/assets/.devin/agent.json +22 -0
- package/assets/.devin/workflows/appweaver.md +11 -0
- package/assets/.factory/agent.json +22 -0
- package/assets/.factory/commands/appweaver.md +12 -0
- package/assets/.gemini/agent.json +22 -0
- package/assets/.github/prompts/appweaver.prompt.md +13 -0
- package/assets/.kilo/agent.json +28 -0
- package/assets/.kilo/agents/appweaver-edit.md +12 -0
- package/assets/.kilo/agents/appweaver-read.md +12 -0
- package/assets/.kilo/commands/appweaver.md +11 -0
- package/assets/.kiro/agent.json +22 -0
- package/assets/.kiro/skills/appweaver/SKILL.md +12 -0
- package/assets/.opencode/agent.json +22 -0
- package/assets/.opencode/agents/appweaver-edit.md +12 -0
- package/assets/.opencode/agents/appweaver-read.md +13 -0
- package/assets/.opencode/commands/appweaver.md +11 -0
- package/assets/.roo/agent.json +22 -0
- package/assets/.roo/commands/appweaver.md +12 -0
- package/dist/commands/init.d.ts.map +1 -1
- package/dist/commands/init.js +14 -1
- package/dist/commands/init.js.map +1 -1
- package/dist/utils/config.d.ts.map +1 -1
- package/dist/utils/config.js +14 -0
- package/dist/utils/config.js.map +1 -1
- package/dist/utils/copy-assets.d.ts +16 -3
- package/dist/utils/copy-assets.d.ts.map +1 -1
- package/dist/utils/copy-assets.js +29 -14
- package/dist/utils/copy-assets.js.map +1 -1
- 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
|
-
|
|
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
|
|
112
|
-
path = os.path.join(fd, RECEIPT)
|
|
173
|
+
def _load(path):
|
|
113
174
|
try:
|
|
114
|
-
|
|
175
|
+
return json.load(open(path, encoding="utf-8"))
|
|
115
176
|
except Exception:
|
|
116
|
-
|
|
117
|
-
|
|
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": "", "
|
|
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
|
|
125
|
-
"
|
|
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
|
-
|
|
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
|
|