aw-spec-cli 0.1.10 → 0.1.12
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 +17 -5
- package/assets/.appweaver/orchestrator/README.md +1 -1
- package/assets/.appweaver/orchestrator/adapters/claude_agent_sdk.py +2 -2
- package/assets/.appweaver/orchestrator/adapters/google_antigravity.py +28 -6
- package/assets/.appweaver/orchestrator/modules/delta-spec.orch.yaml +13 -1
- package/assets/.appweaver/orchestrator/orchestrator.py +126 -39
- package/assets/.appweaver/orchestrator/workflows/aw-spec-adopt.orch.yaml +5 -0
- package/assets/.appweaver/orchestrator/workflows/aw-spec-characterize.orch.yaml +1 -0
- package/assets/.appweaver/orchestrator/workflows/aw-spec-onboard.orch.yaml +1 -0
- package/assets/.appweaver/orchestrator/workflows/aw-spec-specify.orch.yaml +888 -141
- package/assets/.appweaver/scripts/bash/aw-spec-actions.sh +826 -16
- package/assets/.appweaver/scripts/powershell/aw-spec-actions.ps1 +780 -12
- package/assets/.appweaver/stack-profiles/java-spring.json +3 -3
- package/assets/.appweaver/templates/spec-template.md +73 -62
- package/assets/.appweaver/templates/story-template.md +107 -0
- package/assets/guardrails/technology/java-spring/ai-rules.md +505 -0
- package/assets/guardrails/technology/java-spring/api-versioning.md +782 -0
- package/assets/guardrails/technology/java-spring/authorization.md +618 -0
- package/assets/guardrails/technology/java-spring/clean-architecture.md +960 -0
- package/assets/guardrails/technology/java-spring/coding-standards.md +821 -0
- package/assets/guardrails/technology/java-spring/dependency-policy.md +91 -0
- package/assets/guardrails/technology/java-spring/domain-driven-design.md +467 -0
- package/assets/guardrails/technology/java-spring/domain_abstraction.md +424 -0
- package/assets/guardrails/technology/java-spring/folder-structure.md +1537 -0
- package/assets/guardrails/technology/java-spring/global_exception_handler.md +247 -0
- package/assets/guardrails/technology/java-spring/logging.md +215 -0
- package/assets/guardrails/technology/java-spring/modular-monolith.md +77 -0
- package/assets/guardrails/technology/java-spring/persistence.md +316 -0
- package/assets/guardrails/technology/java-spring/problem_details.md +98 -0
- package/assets/guardrails/technology/java-spring/result_mapping.md +48 -0
- package/assets/guardrails/technology/java-spring/technology-constitution.md +168 -0
- package/assets/guardrails/technology/java-spring/testing.md +601 -0
- package/assets/guardrails/technology/java-spring/zero-trust.md +210 -0
- package/dist/index.js +0 -0
- package/dist/utils/config.d.ts +22 -0
- package/dist/utils/config.d.ts.map +1 -0
- package/dist/utils/config.js +76 -0
- package/dist/utils/config.js.map +1 -0
- package/dist/utils/copy-assets.d.ts.map +1 -1
- package/dist/utils/copy-assets.js +4 -11
- package/dist/utils/copy-assets.js.map +1 -1
- package/dist/utils/paths.d.ts.map +1 -1
- package/dist/utils/paths.js +11 -7
- package/dist/utils/paths.js.map +1 -1
- package/package.json +2 -1
- package/assets/guardrails/technology/java-spring/README.md +0 -62
- package/dist/commands/update.d.ts +0 -3
- package/dist/commands/update.d.ts.map +0 -1
- package/dist/commands/update.js +0 -142
- package/dist/commands/update.js.map +0 -1
- package/dist/utils/manifest.d.ts +0 -31
- package/dist/utils/manifest.d.ts.map +0 -1
- package/dist/utils/manifest.js +0 -91
- package/dist/utils/manifest.js.map +0 -1
- package/dist/utils/update-engine.d.ts +0 -52
- package/dist/utils/update-engine.d.ts.map +0 -1
- package/dist/utils/update-engine.js +0 -203
- package/dist/utils/update-engine.js.map +0 -1
|
@@ -35,10 +35,22 @@ transport profile (`agent.json`) and their thin `/appweaver` launcher.
|
|
|
35
35
|
|
|
36
36
|
1. **Context management is the orchestrator's** — as in `workflows/aw-spec-demo.orch.yaml`.
|
|
37
37
|
`build_context` REPLACES the per-request envelope; `load_standards`
|
|
38
|
-
maintains the sticky system prefix.
|
|
38
|
+
maintains the sticky system prefix. Per-step `context:` is THIS request only
|
|
39
|
+
and is never appended onto the envelope. No agent ever assembles its own context.
|
|
40
|
+
`agent_author` / `agent_query` / `agent_review` get **no file tools by
|
|
41
|
+
default**: the inlined payload is the entire world. A step whose task names
|
|
42
|
+
files by PATH rather than inlining them declares `tools:` and gets exactly
|
|
43
|
+
that read-only set — the brownfield reversals do, because a legacy tree is
|
|
44
|
+
handed over as a file LIST and cannot fit a payload. `agent_generate` always
|
|
45
|
+
carries tools (it writes the code), and still receives only the
|
|
46
|
+
interpreter-assembled system + user blocks.
|
|
39
47
|
2. **Execution follows any of the 4 strategies** (`--strategy`, per-step
|
|
40
48
|
`strategy:` override): `sdk`, `sdk-apply`, `spawn`, `agent` — they only
|
|
41
|
-
govern WHO fulfills a generation, never what goes into it.
|
|
49
|
+
govern WHO fulfills a generation, never what goes into it. `agent` isolation
|
|
50
|
+
is best-effort (same conversation); `sdk` / `spawn` / `sdk-apply` generation
|
|
51
|
+
are isolated. Python scripts (`execute_script`) are deterministic only —
|
|
52
|
+
they never MODEL-author spec or code content (a few, like `aw-inventory.py`
|
|
53
|
+
and `aw-rev-constitution.py`, do generate documents — deterministically).
|
|
42
54
|
3. **The checkpoint channel is strategy-independent** (D1): `ask`,
|
|
43
55
|
`user_select` and `dispatch_hooks` suspend via `--checkpoint` under EVERY
|
|
44
56
|
strategy, so the launching agent can relay to the human. Converted-skill
|
|
@@ -83,9 +95,9 @@ the raw object, not its JSON string.
|
|
|
83
95
|
| `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` tolerated; cached across resumes; runs for real even in mock transport |
|
|
84
96
|
| `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 |
|
|
85
97
|
| `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 |
|
|
86
|
-
| `agent_review` | fresh agent |
|
|
87
|
-
| `agent_author` | fresh agent | F5 — **authored artifact** (decl `llm_generate`): the INTERPRETER owns `output_target: {type: file, path}`. Generation is
|
|
88
|
-
| `agent_query` | fresh agent | F6 — generic
|
|
98
|
+
| `agent_review` | fresh agent | payload-only review of `review_of_var`/`review_of` files (inlined into THIS request, never sticky) → `{pass, issues}` |
|
|
99
|
+
| `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) |
|
|
100
|
+
| `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:` |
|
|
89
101
|
| `ask` | human (via launcher) | F7 — question: `prompt`, `options`/`options_from`/`options_from_variable`, `multi`, `answer_key`, `answer_var` (default `answer`). Sources in order: cache → `--answer k=v` → `--non-interactive` (first option; all when multi) → checkpoint suspend `ask` → stdin |
|
|
90
102
|
| `user_select` | human (via launcher) | F7 — pick ONE from `options_from_variable`; `preselect_from` (direct-argument fast path), `single_option_confirm`; `--select stepid=v`; binds `selection` + `outputs[]` |
|
|
91
103
|
| `dispatch_hooks` | invoking agent | F8 — suspend kind `hook` with the `hooks_from_variable` list (slash commands of the LAUNCHING agent); logged-only without a checkpoint; never re-fires across resumes |
|
|
@@ -116,7 +116,7 @@ per step in the workflow YAML (`strategy:` on `agent_generate`/`agent_review`).
|
|
|
116
116
|
| `sdk` | isolated SDK instance | SDK instance (tools) | 1 | guaranteed |
|
|
117
117
|
| `sdk-apply` | isolated SDK instance (read-only) | **invoking agent** (suspend/resume) | 1 + you | generation clean; apply local |
|
|
118
118
|
| `spawn` | fresh CLI process (profile) | same process | N cold | guaranteed |
|
|
119
|
-
| `agent` | invoking agent (suspend/resume) | invoking agent | **0** | best-effort |
|
|
119
|
+
| `agent` | invoking agent (suspend/resume) | invoking agent | **0** | best-effort (payload is interpreter-assembled; the invoking conversation can still add around it) |
|
|
120
120
|
|
|
121
121
|
```bash
|
|
122
122
|
# Strategy 1 — SDK generates + implements (needs: pip install claude-agent-sdk)
|
|
@@ -52,10 +52,10 @@ _PERMISSION_MAP = {"acceptEdits": "acceptEdits",
|
|
|
52
52
|
|
|
53
53
|
|
|
54
54
|
async def generate(request):
|
|
55
|
+
allowed = [t.strip() for t in (request.get("tools") or "").split(",") if t.strip()]
|
|
55
56
|
kwargs = dict(
|
|
56
57
|
system_prompt=request["system"],
|
|
57
|
-
allowed_tools=
|
|
58
|
-
if t.strip()],
|
|
58
|
+
allowed_tools=allowed, # empty = payload-only; do not fall back to the SDK default set
|
|
59
59
|
permission_mode=_PERMISSION_MAP.get(request["permission_mode"], "default"),
|
|
60
60
|
setting_sources=[], # isolation: no CLAUDE.md / settings / hooks
|
|
61
61
|
)
|
|
@@ -10,8 +10,12 @@ Contract (see orchestrator.py `_run_sdk`):
|
|
|
10
10
|
request keys:
|
|
11
11
|
system interpreter-assembled system block (standards + rules)
|
|
12
12
|
user interpreter-assembled user block (context + task)
|
|
13
|
-
tools the workflow's neutral tools list
|
|
14
|
-
|
|
13
|
+
tools the workflow's neutral tools list. Empty selects the
|
|
14
|
+
payload-only posture (deny every file tool, no workspace);
|
|
15
|
+
non-empty selects the permission_mode policy chain plus
|
|
16
|
+
the project workspace. The individual NAMES are neutral
|
|
17
|
+
(Read/Glob/…) and are not mapped one-to-one — Antigravity
|
|
18
|
+
governs capability by policy, so the mode picks the set.
|
|
15
19
|
permission_mode neutral mode: readonly | acceptEdits |
|
|
16
20
|
bypassPermissions | default
|
|
17
21
|
model model name or None
|
|
@@ -49,6 +53,11 @@ from google.antigravity.hooks import policy
|
|
|
49
53
|
_READONLY_TOOLS = ("view_file", "list_directory", "search_directory",
|
|
50
54
|
"find_file", "finish", "ask_question")
|
|
51
55
|
|
|
56
|
+
# the two that touch no file: how the session ENDS its turn and how it asks.
|
|
57
|
+
# A payload-only request still needs these — denying every tool including
|
|
58
|
+
# `finish` leaves the agent no way to say it is done.
|
|
59
|
+
_CONTROL_TOOLS = ("finish", "ask_question")
|
|
60
|
+
|
|
52
61
|
|
|
53
62
|
def _policies(permission_mode):
|
|
54
63
|
"""Neutral permission modes → Antigravity policy chains."""
|
|
@@ -61,12 +70,25 @@ def _policies(permission_mode):
|
|
|
61
70
|
|
|
62
71
|
|
|
63
72
|
async def generate(request):
|
|
73
|
+
tools = [t.strip() for t in (request.get("tools") or "").split(",") if t.strip()]
|
|
74
|
+
# An empty allowlist is a payload-only author/query/review: the interpreter
|
|
75
|
+
# already inlined every file the model may see. Granting the project
|
|
76
|
+
# workspace (or view_file) would let the isolated session wander off the
|
|
77
|
+
# mapped payload. Only the control tools survive the deny — the file tools
|
|
78
|
+
# are the point of the deny, but a session that cannot call `finish` cannot
|
|
79
|
+
# end its turn. A step that DECLARES tools (the brownfield reversals, which
|
|
80
|
+
# are handed the slice's source as a path list) takes the branch below and
|
|
81
|
+
# gets the workspace its readonly policy chain needs.
|
|
82
|
+
if not tools:
|
|
83
|
+
policies = [policy.deny_all()] + [policy.allow(t) for t in _CONTROL_TOOLS]
|
|
84
|
+
workspaces = []
|
|
85
|
+
else:
|
|
86
|
+
policies = _policies(request["permission_mode"])
|
|
87
|
+
workspaces = [os.getcwd()]
|
|
64
88
|
config = LocalAgentConfig(
|
|
65
89
|
system_instructions=request["system"],
|
|
66
|
-
policies=
|
|
67
|
-
|
|
68
|
-
# root so the isolated generation can read/write the files it decides
|
|
69
|
-
workspaces=[os.getcwd()],
|
|
90
|
+
policies=policies,
|
|
91
|
+
workspaces=workspaces,
|
|
70
92
|
model=request.get("model") or None,
|
|
71
93
|
)
|
|
72
94
|
async with Agent(config=config) as agent:
|
|
@@ -1,7 +1,19 @@
|
|
|
1
1
|
# =============================================================================
|
|
2
2
|
# delta-spec (orchestrator module) — author the SPEC-side DELTA of a change
|
|
3
3
|
#
|
|
4
|
-
#
|
|
4
|
+
# ORPHANED — INCLUDED BY NO WORKFLOW. specify v3.0.0 removed the delta branch this module
|
|
5
|
+
# was written for: a later requirement is now an edit to the living spec.md plus a computed
|
|
6
|
+
# Change History row, not a delta document merged back later. Nothing else includes it, so
|
|
7
|
+
# NOTHING AUTHORS A DELTA-SPEC TODAY — the delta-spec.md that cr-setup still scaffolds for
|
|
8
|
+
# every change is an untouched template copy, which is why specify's delta_open guard tests
|
|
9
|
+
# whether one was authored rather than whether a change is open.
|
|
10
|
+
#
|
|
11
|
+
# Kept, not deleted, because the sibling half is still live: /appweaver design includes
|
|
12
|
+
# delta-design on its own delta branch, and finalize-delta / analyze-slice / close-delta
|
|
13
|
+
# still run. Retiring this file is a decision about the delta FAMILY, not about this module.
|
|
14
|
+
#
|
|
15
|
+
# What follows describes it as it was written to be included by specify.
|
|
16
|
+
#
|
|
5
17
|
# This is the SPEC half of the former standalone `cr` capture, relocated here: a
|
|
6
18
|
# later requirement is a new user story on the SAME aggregate. It opens the change
|
|
7
19
|
# (cr-setup — single-active-change guarded; also scaffolds delta-spec AND delta-design)
|
|
@@ -318,7 +318,11 @@ class Orchestrator:
|
|
|
318
318
|
"Perform this task YOURSELF, in this conversation. Read "
|
|
319
319
|
"system_path + user_path and treat them as your ONLY standards "
|
|
320
320
|
"and context (ignore anything else you know about this "
|
|
321
|
-
"session).
|
|
321
|
+
"session). " + (
|
|
322
|
+
"Use the listed tools within the permission mode. "
|
|
323
|
+
if tools else
|
|
324
|
+
"You have NO tools: the payload is the entire world for "
|
|
325
|
+
"this request, so do not open other project files. ") +
|
|
322
326
|
"Write your full final reply — ending with the required "
|
|
323
327
|
"```json fence — to response_path, then re-run the resume "
|
|
324
328
|
"command. Do not edit the checkpoint state file."),
|
|
@@ -515,8 +519,13 @@ class Orchestrator:
|
|
|
515
519
|
argv += [sysmode["flag"], system]
|
|
516
520
|
elif sysmode == "prepend":
|
|
517
521
|
prompt_user = system + "\n\n---\n\n" + user
|
|
518
|
-
|
|
519
|
-
|
|
522
|
+
# Always pass the allowlist when the CLI has a tools flag — omitting it
|
|
523
|
+
# lets the CLI fall back to its default set (Write/Bash/…), which is
|
|
524
|
+
# how a read-only author/query leaked the rest of the repo. An empty
|
|
525
|
+
# string is a real allowlist of nothing: the inlined payload is the
|
|
526
|
+
# entire world.
|
|
527
|
+
if prof.get("tools_flag") is not None:
|
|
528
|
+
argv += [prof["tools_flag"], tools or ""]
|
|
520
529
|
approval = prof.get("approval", {})
|
|
521
530
|
argv += approval.get(permission_mode, approval.get("default", []))
|
|
522
531
|
argv += prof.get("isolation", {}).get(self.opts.isolation, [])
|
|
@@ -686,10 +695,39 @@ class Orchestrator:
|
|
|
686
695
|
sysn = len(_norm_files(self.envelope["system"]))
|
|
687
696
|
self.emit(f"[build_context] system={sysn} file(s) context={len(ctx)} file(s)")
|
|
688
697
|
|
|
698
|
+
def _step_extra_context(self, step):
|
|
699
|
+
"""Per-step `context:` files, THIS request only — never sticky. The
|
|
700
|
+
workflow maps what the model may see; the envelope is the sticky set
|
|
701
|
+
`build_context` replaced, and this list is the extra the step named."""
|
|
702
|
+
p = step.get("parameters") or step
|
|
703
|
+
extra = self.resolve_value(p.get("context")) or []
|
|
704
|
+
return [c for c in extra if isinstance(c, (str, dict))]
|
|
705
|
+
|
|
706
|
+
def _step_read_tools(self, step):
|
|
707
|
+
"""The read tools an `agent_author` / `agent_query` step declares.
|
|
708
|
+
|
|
709
|
+
The default is NONE — those two actions are payload-only, and the
|
|
710
|
+
interpreter inlines every file the workflow mapped. A step whose task
|
|
711
|
+
names files by PATH rather than inlining them declares `tools:` and gets
|
|
712
|
+
exactly that set. The brownfield reversals are the reason it exists:
|
|
713
|
+
adopt hands the model the slice's `source_files` as a LIST and tells it
|
|
714
|
+
to read the code, which cannot be inlined (a legacy tree is far larger
|
|
715
|
+
than any payload) — with no tools those steps authored a spec, a design
|
|
716
|
+
and a source-map from a list of filenames, and the fidelity check that
|
|
717
|
+
exists to catch exactly that fabrication could not read the code either.
|
|
718
|
+
Declaring it in the workflow keeps the dependency where a reader can see
|
|
719
|
+
it; the permission mode stays readonly either way."""
|
|
720
|
+
p = step.get("parameters") or step
|
|
721
|
+
v = p.get("tools", "")
|
|
722
|
+
if isinstance(v, (list, tuple)):
|
|
723
|
+
v = ",".join(str(x) for x in v)
|
|
724
|
+
return (self.resolve(str(v)) or "").strip()
|
|
725
|
+
|
|
689
726
|
def act_agent_generate(self, step):
|
|
690
727
|
tools = step.get("tools", "Read,Write,Edit,Bash")
|
|
691
728
|
permission_mode = step.get("permission_mode", "acceptEdits")
|
|
692
729
|
task = self.resolve(step.get("task", ""))
|
|
730
|
+
extra = self._step_extra_context(step)
|
|
693
731
|
strat = self._effective_strategy(step)
|
|
694
732
|
# checkpoint replay: this step was already fulfilled (any strategy) —
|
|
695
733
|
# bind its cached structured result instead of executing again.
|
|
@@ -724,8 +762,8 @@ class Orchestrator:
|
|
|
724
762
|
'\n\nWhen done, end your reply with a fenced block listing the paths YOU '
|
|
725
763
|
'chose to write:\n```json\n{"status":"ok","files":["<path you wrote>"],'
|
|
726
764
|
'"summary":"<one line>"}\n```')
|
|
727
|
-
system, sys_files = self._assemble_system()
|
|
728
|
-
user, ctx_files = self._assemble_user(instr)
|
|
765
|
+
system, sys_files = self._assemble_system(step.get("include_standards", True))
|
|
766
|
+
user, ctx_files = self._assemble_user(instr, extra=extra)
|
|
729
767
|
self._print_envelope("agent_generate", system, sys_files, user, ctx_files, tools, strat)
|
|
730
768
|
|
|
731
769
|
if self.opts.transport == "mock":
|
|
@@ -760,10 +798,14 @@ class Orchestrator:
|
|
|
760
798
|
instr = task + "\n\nFiles under review:\n" + "\n".join(f"- {r}" for r in review_of)
|
|
761
799
|
instr += ('\n\nEnd your reply with a fenced block:\n'
|
|
762
800
|
'```json\n{"pass":true,"issues":[]}\n```')
|
|
763
|
-
#
|
|
764
|
-
#
|
|
765
|
-
#
|
|
766
|
-
|
|
801
|
+
# Review files are inlined into THIS request only. They used to be
|
|
802
|
+
# appended onto the sticky envelope, so every later generation in the
|
|
803
|
+
# same run inherited them — context the workflow never mapped.
|
|
804
|
+
extra = list(review_of) + self._step_extra_context(step)
|
|
805
|
+
# Payload-only like author/query, and the same opt-in: a review whose
|
|
806
|
+
# files are inlined needs nothing, but the escape hatch has to exist on
|
|
807
|
+
# all three or a `tools:` added here later would be silently ignored.
|
|
808
|
+
tools = self._step_read_tools(step)
|
|
767
809
|
strat = self._effective_strategy(step)
|
|
768
810
|
ck = self._key(step)
|
|
769
811
|
if self.checkpoint and ck in self.cache:
|
|
@@ -772,22 +814,23 @@ class Orchestrator:
|
|
|
772
814
|
self._bind(step, structured)
|
|
773
815
|
self.emit(f"[agent_review] (cached) result={json.dumps(structured)[:200]}")
|
|
774
816
|
return
|
|
775
|
-
system, sys_files = self._assemble_system()
|
|
776
|
-
user, ctx_files = self._assemble_user(instr)
|
|
777
|
-
|
|
817
|
+
system, sys_files = self._assemble_system(step.get("include_standards", True))
|
|
818
|
+
user, ctx_files = self._assemble_user(instr, extra=extra)
|
|
819
|
+
# No file tools by default: the files under review are already in the payload.
|
|
820
|
+
self._print_envelope("agent_review", system, sys_files, user, ctx_files, tools, strat)
|
|
778
821
|
|
|
779
822
|
if self.opts.transport == "mock":
|
|
780
823
|
# mock: pass if every reviewed file exists
|
|
781
824
|
ok = all(os.path.exists(r) for r in review_of)
|
|
782
825
|
structured = {"pass": ok, "issues": ([] if ok else ["missing output file(s)"])}
|
|
783
826
|
elif strat == "agent":
|
|
784
|
-
self._suspend("agent_review", ck, system, user,
|
|
827
|
+
self._suspend("agent_review", ck, system, user, tools, "readonly")
|
|
785
828
|
elif strat in ("sdk", "sdk-apply"):
|
|
786
829
|
# review has no apply phase — sdk-apply degrades to a plain sdk read
|
|
787
|
-
structured, _ = self._run_sdk(system, user,
|
|
830
|
+
structured, _ = self._run_sdk(system, user, tools, "readonly",
|
|
788
831
|
model=step.get("model"))
|
|
789
832
|
else: # spawn
|
|
790
|
-
structured, _ = self._run_fresh(system, user,
|
|
833
|
+
structured, _ = self._run_fresh(system, user, tools, "readonly",
|
|
791
834
|
model=step.get("model"))
|
|
792
835
|
self.last_result = structured
|
|
793
836
|
if self.checkpoint:
|
|
@@ -963,7 +1006,8 @@ class Orchestrator:
|
|
|
963
1006
|
if not target:
|
|
964
1007
|
raise Stop(f"step {step.get('id')}: agent_author needs output_target.path")
|
|
965
1008
|
task = self.resolve(p.get("task") or p.get("prompt") or "")
|
|
966
|
-
extra =
|
|
1009
|
+
extra = self._step_extra_context(step)
|
|
1010
|
+
tools = self._step_read_tools(step)
|
|
967
1011
|
strat = self._effective_strategy(step)
|
|
968
1012
|
ck = self._key(step)
|
|
969
1013
|
if self.checkpoint and ck in self.cache:
|
|
@@ -974,28 +1018,38 @@ class Orchestrator:
|
|
|
974
1018
|
self.emit(f"[agent_author] (cached) rewrote {target}")
|
|
975
1019
|
return
|
|
976
1020
|
instr = task + (
|
|
977
|
-
f"\n\nAuthor the COMPLETE content of the file `{target}`.
|
|
978
|
-
|
|
1021
|
+
f"\n\nAuthor the COMPLETE content of the file `{target}`. " + (
|
|
1022
|
+
f"Your tools are read-only ({tools}), and they are for the files "
|
|
1023
|
+
"this task names by PATH — everything else you need is already in "
|
|
1024
|
+
"the payload. "
|
|
1025
|
+
if tools else
|
|
1026
|
+
"You have no tools — the payload is the entire world for this "
|
|
1027
|
+
"request. ") +
|
|
1028
|
+
"Return the content itself. Reply with ONLY the "
|
|
979
1029
|
"full file content: no commentary, no surrounding code fence.")
|
|
980
1030
|
system, sys_files = self._assemble_system(p.get("include_standards", True))
|
|
981
1031
|
user, ctx_files = self._assemble_user(instr, extra=extra)
|
|
982
|
-
self._print_envelope("agent_author", system, sys_files, user, ctx_files,
|
|
1032
|
+
self._print_envelope("agent_author", system, sys_files, user, ctx_files, tools, strat)
|
|
983
1033
|
if self.opts.transport == "mock":
|
|
984
1034
|
content = self._mock_author(target, task)
|
|
985
1035
|
elif strat == "agent":
|
|
986
|
-
self._suspend("author", ck, system, user,
|
|
1036
|
+
self._suspend("author", ck, system, user, tools, "readonly",
|
|
987
1037
|
instructions=(
|
|
988
1038
|
"Author this artifact YOURSELF, in this conversation. "
|
|
989
1039
|
"Read system_path + user_path as your ONLY standards "
|
|
990
|
-
"and context
|
|
991
|
-
|
|
992
|
-
|
|
993
|
-
|
|
994
|
-
"
|
|
1040
|
+
"and context" + (
|
|
1041
|
+
", and open ONLY the files the task names by path. "
|
|
1042
|
+
if tools else
|
|
1043
|
+
" — do not open other project files. ") +
|
|
1044
|
+
"Write "
|
|
1045
|
+
"the COMPLETE file content — the content ONLY, no "
|
|
1046
|
+
"commentary, no code fence — to response_path, then "
|
|
1047
|
+
"re-run the resume command. Do NOT write the target "
|
|
1048
|
+
"file yourself; the interpreter does that."),
|
|
995
1049
|
extra={"target": target})
|
|
996
1050
|
else:
|
|
997
1051
|
run = self._run_sdk if strat in ("sdk", "sdk-apply") else self._run_fresh
|
|
998
|
-
_, raw = run(system, user,
|
|
1052
|
+
_, raw = run(system, user, tools, "readonly", model=step.get("model"))
|
|
999
1053
|
content = _strip_fence(raw)
|
|
1000
1054
|
if not content.strip():
|
|
1001
1055
|
raise Stop(f"step {step.get('id')}: agent_author returned empty content")
|
|
@@ -1012,16 +1066,16 @@ class Orchestrator:
|
|
|
1012
1066
|
f"({len(content)} chars)")
|
|
1013
1067
|
|
|
1014
1068
|
# -- F6: structured queries --------------------------------------------------
|
|
1015
|
-
def _structured_fresh(self, step, strat, system, user):
|
|
1069
|
+
def _structured_fresh(self, step, strat, system, user, tools=""):
|
|
1016
1070
|
"""spawn/sdk executor for JSON-returning steps, with ONE malformed-JSON
|
|
1017
1071
|
retry (agent strategy instead re-suspends with a note on resume)."""
|
|
1018
1072
|
run = self._run_sdk if strat in ("sdk", "sdk-apply") else self._run_fresh
|
|
1019
|
-
structured, _ = run(system, user,
|
|
1073
|
+
structured, _ = run(system, user, tools, "readonly", model=step.get("model"))
|
|
1020
1074
|
if not structured:
|
|
1021
1075
|
self.emit(f"[retry] {step.get('id')}: no parseable ```json fence — asking once more")
|
|
1022
1076
|
user2 = user + ("\n\nYour previous reply could not be parsed. Return "
|
|
1023
1077
|
"ONLY the required ```json fenced block, nothing else.")
|
|
1024
|
-
structured, _ = run(system, user2,
|
|
1078
|
+
structured, _ = run(system, user2, tools, "readonly", model=step.get("model"))
|
|
1025
1079
|
if not structured:
|
|
1026
1080
|
raise Stop(f"step {step.get('id')}: fresh agent returned no parseable JSON")
|
|
1027
1081
|
return structured
|
|
@@ -1033,7 +1087,8 @@ class Orchestrator:
|
|
|
1033
1087
|
specialization of this. `mock:` supplies the mock-transport result."""
|
|
1034
1088
|
p = step.get("parameters") or step
|
|
1035
1089
|
task = self.resolve(p.get("task") or p.get("prompt") or "")
|
|
1036
|
-
extra =
|
|
1090
|
+
extra = self._step_extra_context(step)
|
|
1091
|
+
tools = self._step_read_tools(step)
|
|
1037
1092
|
strat = self._effective_strategy(step)
|
|
1038
1093
|
ck = self._key(step)
|
|
1039
1094
|
if self.checkpoint and ck in self.cache:
|
|
@@ -1044,18 +1099,24 @@ class Orchestrator:
|
|
|
1044
1099
|
return
|
|
1045
1100
|
schema = p.get("schema")
|
|
1046
1101
|
hint = json.dumps(schema) if isinstance(schema, (dict, list)) else (schema or "{ … }")
|
|
1047
|
-
instr = task + ("\n\nDo not modify any files.
|
|
1102
|
+
instr = task + ("\n\nDo not modify any files. " + (
|
|
1103
|
+
f"Your tools are read-only ({tools}), and they are for the "
|
|
1104
|
+
"files this task names by PATH — everything else you need is "
|
|
1105
|
+
"already in the payload. "
|
|
1106
|
+
if tools else
|
|
1107
|
+
"The payload is the entire world for this request. ") +
|
|
1108
|
+
"End your reply with a "
|
|
1048
1109
|
"fenced block containing ONLY the JSON result, shaped "
|
|
1049
1110
|
"like:\n```json\n" + hint + "\n```")
|
|
1050
1111
|
system, sys_files = self._assemble_system(p.get("include_standards", True))
|
|
1051
1112
|
user, ctx_files = self._assemble_user(instr, extra=extra)
|
|
1052
|
-
self._print_envelope("agent_query", system, sys_files, user, ctx_files,
|
|
1113
|
+
self._print_envelope("agent_query", system, sys_files, user, ctx_files, tools, strat)
|
|
1053
1114
|
if self.opts.transport == "mock":
|
|
1054
1115
|
structured = self.resolve_value(p.get("mock")) or {}
|
|
1055
1116
|
elif strat == "agent":
|
|
1056
|
-
self._suspend("agent_query", ck, system, user,
|
|
1117
|
+
self._suspend("agent_query", ck, system, user, tools, "readonly")
|
|
1057
1118
|
else:
|
|
1058
|
-
structured = self._structured_fresh(step, strat, system, user)
|
|
1119
|
+
structured = self._structured_fresh(step, strat, system, user, tools)
|
|
1059
1120
|
self.last_result = structured
|
|
1060
1121
|
if self.checkpoint:
|
|
1061
1122
|
self.cache[ck] = structured
|
|
@@ -1065,17 +1126,43 @@ class Orchestrator:
|
|
|
1065
1126
|
|
|
1066
1127
|
# -- F7: HITL (D1 — the checkpoint is the interaction channel under EVERY
|
|
1067
1128
|
# strategy; --answer/--select/--non-interactive prefill for headless runs)
|
|
1068
|
-
def _prompt_choice(self, prompt, options):
|
|
1129
|
+
def _prompt_choice(self, prompt, options, multi=False):
|
|
1069
1130
|
print(prompt)
|
|
1070
1131
|
for i, o in enumerate(options, 1):
|
|
1071
1132
|
print(f" {i}) {o}")
|
|
1133
|
+
if multi:
|
|
1134
|
+
print(" (pick any: numbers or names, comma-separated \u2014 "
|
|
1135
|
+
"'all', or blank for none)")
|
|
1072
1136
|
try:
|
|
1073
1137
|
raw = input("> ").strip()
|
|
1074
1138
|
except EOFError:
|
|
1075
1139
|
return None
|
|
1076
|
-
if
|
|
1077
|
-
|
|
1078
|
-
|
|
1140
|
+
if not multi:
|
|
1141
|
+
if raw.isdigit() and 1 <= int(raw) <= len(options):
|
|
1142
|
+
return options[int(raw) - 1]
|
|
1143
|
+
return raw if raw in options else None
|
|
1144
|
+
# A multi-select answer is a SET, and a set has no single-token spelling. Read by
|
|
1145
|
+
# the single-select parser, "1,3" was neither a number in range nor an option, so it
|
|
1146
|
+
# returned None and the run stopped as "question cancelled" \u2014 leaving exactly two
|
|
1147
|
+
# reachable answers, "the first one" and "abort". act_ask splits the string it gets
|
|
1148
|
+
# back on commas, so returning one is all that is needed here.
|
|
1149
|
+
if raw.lower() in ("all", "*"):
|
|
1150
|
+
return ",".join(str(o) for o in options)
|
|
1151
|
+
if raw.lower() in ("", "none"):
|
|
1152
|
+
return ""
|
|
1153
|
+
picked = []
|
|
1154
|
+
for tok in re.split(r"\s*,\s*|\s+", raw):
|
|
1155
|
+
if not tok:
|
|
1156
|
+
continue
|
|
1157
|
+
if tok.isdigit() and 1 <= int(tok) <= len(options):
|
|
1158
|
+
v = options[int(tok) - 1]
|
|
1159
|
+
elif tok in options:
|
|
1160
|
+
v = tok
|
|
1161
|
+
else:
|
|
1162
|
+
return None # an unrecognised token is a typo, not a silent omission
|
|
1163
|
+
if v not in picked:
|
|
1164
|
+
picked.append(v)
|
|
1165
|
+
return ",".join(str(o) for o in picked)
|
|
1079
1166
|
|
|
1080
1167
|
def act_ask(self, step):
|
|
1081
1168
|
"""F7 — single/multi-select question. Answer sources, in order: replay
|
|
@@ -1113,7 +1200,7 @@ class Orchestrator:
|
|
|
1113
1200
|
(" (comma-separated)" if multi else "") +
|
|
1114
1201
|
" to response_path, then re-run the resume command.")})
|
|
1115
1202
|
if raw is None:
|
|
1116
|
-
raw = self._prompt_choice(prompt, options) if options else \
|
|
1203
|
+
raw = self._prompt_choice(prompt, options, multi) if options else \
|
|
1117
1204
|
input(prompt + " ").strip()
|
|
1118
1205
|
if raw is None:
|
|
1119
1206
|
raise Stop("question cancelled")
|
|
@@ -360,6 +360,7 @@ workflow:
|
|
|
360
360
|
- { path: .appweaver/memory/constitution.ui.md, optional: true } # the frontend lane's UX-* base — absent on a backend-only repo, which onboard leaves unwritten by design
|
|
361
361
|
- id: reverse_spec
|
|
362
362
|
action: agent_author
|
|
363
|
+
tools: Read # the code arrives as a PATH LIST, not inlined — a legacy tree does not fit a payload
|
|
363
364
|
include_standards: false # the technical/arch sticky set is for design, never the spec
|
|
364
365
|
output_target: { type: file, path: "{{spec_file}}" }
|
|
365
366
|
task: |
|
|
@@ -435,6 +436,7 @@ workflow:
|
|
|
435
436
|
- { path: .appweaver/memory/constitution.technical.md, optional: true } # technology base ONLY — no domain
|
|
436
437
|
- id: reverse_design
|
|
437
438
|
action: agent_author
|
|
439
|
+
tools: Read # grounded in the CODE, which arrives as {{source_files}} paths
|
|
438
440
|
include_standards: true # the technical/architecture sticky set (reference only — the CODE wins)
|
|
439
441
|
output_target: { type: file, path: "{{design_doc}}" }
|
|
440
442
|
task: |
|
|
@@ -499,6 +501,7 @@ workflow:
|
|
|
499
501
|
- id: reverse_ui_design
|
|
500
502
|
condition: "{{ui_lane}} == true"
|
|
501
503
|
action: agent_author
|
|
504
|
+
tools: Read # same, for the lane's surface ({{ui_source_files}})
|
|
502
505
|
include_standards: true # the lane's sticky technical set (reference only — the CODE wins)
|
|
503
506
|
output_target: { type: file, path: "{{ui_design_doc}}" }
|
|
504
507
|
task: |
|
|
@@ -545,6 +548,7 @@ workflow:
|
|
|
545
548
|
- { path: "{{concept_path}}" } # both file lists to map
|
|
546
549
|
- id: source_map
|
|
547
550
|
action: agent_author
|
|
551
|
+
tools: Read # one row per legacy artefact; the artefacts are paths in the concept
|
|
548
552
|
include_standards: false
|
|
549
553
|
output_target: { type: file, path: "{{feature_dir}}/source-map.md" }
|
|
550
554
|
task: |
|
|
@@ -576,6 +580,7 @@ workflow:
|
|
|
576
580
|
# → written to an Adopt Review report and closed OUT OF BAND (Brownfield B1·R).
|
|
577
581
|
- id: fidelity
|
|
578
582
|
action: agent_query
|
|
583
|
+
tools: Read # the check is spec/design vs CODE; with no code it degrades to spec vs design and certifies fabrication as ok
|
|
579
584
|
context:
|
|
580
585
|
- "{{spec_file}}"
|
|
581
586
|
- "{{design_doc}}"
|
|
@@ -188,6 +188,7 @@ workflow:
|
|
|
188
188
|
- { path: "{{design_file}}" }
|
|
189
189
|
- id: scan_existing_tests
|
|
190
190
|
action: agent_query
|
|
191
|
+
tools: Read,Glob,Grep # no test list is supplied — it globs the tree; empty results make characterize write duplicate golden-masters
|
|
191
192
|
include_standards: false
|
|
192
193
|
schema:
|
|
193
194
|
duplicates: [ { behaviour: "<observable behaviour>", spec_id: "<US-/FR-/BR- id>", existing_test: "<file:test-name>", seam: "<endpoint|handler|domain|persistence>", source_file: "<production file it covers>" } ]
|
|
@@ -844,6 +844,7 @@ workflow:
|
|
|
844
844
|
- { path: "{{bundle_dir}}/architecture/frontend.md", optional: true } # SECONDARY (shallow): the OKF as-built frontend note — corroborate, don't rely on alone
|
|
845
845
|
- id: author_ui
|
|
846
846
|
action: agent_author
|
|
847
|
+
tools: Read # EVIDENCE = THE UI CODE ITSELF; the inventory inlines the file LISTS, not the files
|
|
847
848
|
condition: "{{ui_lane}} == true"
|
|
848
849
|
include_standards: false
|
|
849
850
|
output_target: { type: file, path: "{{constitution_ui_file}}" }
|