aw-spec-cli 0.1.5 → 0.1.7
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/.agents/agent.json +1 -1
- package/assets/.agents/skills/{aw-spec-orch → appweaver}/SKILL.md +10 -10
- package/assets/.agents/skills/{aw-spec-orch-demo → appweaver-demo}/SKILL.md +9 -9
- package/assets/.claude/agent.json +1 -1
- package/assets/.claude/skills/{aw-spec-orch → appweaver}/SKILL.md +10 -10
- package/assets/.claude/skills/{aw-spec-orch-demo → appweaver-demo}/SKILL.md +9 -9
- package/assets/.kilocode/agent.json +1 -1
- package/assets/.kilocode/workflows/{aw-spec-orch-demo.md → appweaver-demo.md} +9 -9
- package/assets/.kilocode/workflows/{aw-spec-orch.md → appweaver.md} +10 -10
- package/assets/docs/pharma-constitution.my-ref.md +86 -0
- package/dist/utils/paths.js +1 -1
- package/dist/utils/paths.js.map +1 -1
- package/package.json +1 -1
- package/assets/.aw-spec/DESIGN-FORMAT.md +0 -151
- package/assets/.aw-spec/ORCH-DIALECT.md +0 -156
- package/assets/.aw-spec/RULE-PARTITION.md +0 -126
- package/assets/.aw-spec/domain.json +0 -3
- package/assets/.aw-spec/extensions/.registry +0 -19
- package/assets/.aw-spec/extensions/agent-context/README.md +0 -57
- package/assets/.aw-spec/extensions/agent-context/agent-context-config.yml +0 -4
- package/assets/.aw-spec/extensions/agent-context/commands/aw-spec.agent-context.update.md +0 -26
- package/assets/.aw-spec/extensions/agent-context/extension.yml +0 -37
- package/assets/.aw-spec/extensions/agent-context/scripts/bash/update-agent-context.sh +0 -201
- package/assets/.aw-spec/extensions/agent-context/scripts/powershell/update-agent-context.ps1 +0 -237
- package/assets/.aw-spec/extensions.yml +0 -16
- package/assets/.aw-spec/feature.json +0 -3
- package/assets/.aw-spec/init-options.json +0 -9
- package/assets/.aw-spec/integration.json +0 -15
- package/assets/.aw-spec/integrations/aw-spec.manifest.json +0 -9
- package/assets/.aw-spec/interpreter/run-skill.py +0 -1158
- package/assets/.aw-spec/memory/constitution.md +0 -50
- package/assets/.aw-spec/orchestrator/AGENT-PROFILE.md +0 -94
- package/assets/.aw-spec/orchestrator/README.md +0 -300
- package/assets/.aw-spec/orchestrator/adapters/claude_agent_sdk.py +0 -74
- package/assets/.aw-spec/orchestrator/adapters/google_antigravity.py +0 -74
- package/assets/.aw-spec/orchestrator/modules/delta-design.orch.yaml +0 -105
- package/assets/.aw-spec/orchestrator/modules/delta-spec.orch.yaml +0 -79
- package/assets/.aw-spec/orchestrator/modules/domain-select.orch.yaml +0 -81
- package/assets/.aw-spec/orchestrator/modules/tech-select.orch.yaml +0 -75
- package/assets/.aw-spec/orchestrator/orchestrator.py +0 -1721
- package/assets/.aw-spec/orchestrator/rev-templates/ai-rules.reference.md +0 -40
- package/assets/.aw-spec/orchestrator/rev-templates/comandrulemap.skeleton.md +0 -59
- package/assets/.aw-spec/orchestrator/rev-templates/commandtechrulemap.skeleton.md +0 -97
- package/assets/.aw-spec/orchestrator/rev-templates/domain-constitution.skeleton.md +0 -61
- package/assets/.aw-spec/orchestrator/rev-templates/domain-requirements.skeleton.md +0 -48
- package/assets/.aw-spec/orchestrator/rev-templates/domain-rule-file.skeleton.md +0 -39
- package/assets/.aw-spec/orchestrator/rev-templates/technical-constitution.skeleton.md +0 -106
- package/assets/.aw-spec/orchestrator/rev-templates/technical-rule-file.skeleton.md +0 -54
- package/assets/.aw-spec/orchestrator/workflows/_smoke.orch.yaml +0 -160
- package/assets/.aw-spec/orchestrator/workflows/aw-spec-add-component.orch.yaml +0 -244
- package/assets/.aw-spec/orchestrator/workflows/aw-spec-adopt.orch.yaml +0 -775
- package/assets/.aw-spec/orchestrator/workflows/aw-spec-analyze-delta-design.orch.yaml +0 -248
- package/assets/.aw-spec/orchestrator/workflows/aw-spec-analyze-design.orch.yaml +0 -261
- package/assets/.aw-spec/orchestrator/workflows/aw-spec-analyze-slice.orch.yaml +0 -425
- package/assets/.aw-spec/orchestrator/workflows/aw-spec-build.orch.yaml +0 -188
- package/assets/.aw-spec/orchestrator/workflows/aw-spec-characterize.orch.yaml +0 -474
- package/assets/.aw-spec/orchestrator/workflows/aw-spec-close-delta.orch.yaml +0 -181
- package/assets/.aw-spec/orchestrator/workflows/aw-spec-constitution.orch.yaml +0 -284
- package/assets/.aw-spec/orchestrator/workflows/aw-spec-demo.orch.yaml +0 -103
- package/assets/.aw-spec/orchestrator/workflows/aw-spec-dependency-tree.orch.yaml +0 -69
- package/assets/.aw-spec/orchestrator/workflows/aw-spec-deploy.orch.yaml +0 -85
- package/assets/.aw-spec/orchestrator/workflows/aw-spec-design.orch.yaml +0 -382
- package/assets/.aw-spec/orchestrator/workflows/aw-spec-finalize-delta.orch.yaml +0 -382
- package/assets/.aw-spec/orchestrator/workflows/aw-spec-foundation.orch.yaml +0 -246
- package/assets/.aw-spec/orchestrator/workflows/aw-spec-implement-delta.orch.yaml +0 -231
- package/assets/.aw-spec/orchestrator/workflows/aw-spec-implement.orch.yaml +0 -258
- package/assets/.aw-spec/orchestrator/workflows/aw-spec-inventory.orch.yaml +0 -236
- package/assets/.aw-spec/orchestrator/workflows/aw-spec-onboard.orch.yaml +0 -949
- package/assets/.aw-spec/orchestrator/workflows/aw-spec-parity.orch.yaml +0 -293
- package/assets/.aw-spec/orchestrator/workflows/aw-spec-run.orch.yaml +0 -177
- package/assets/.aw-spec/orchestrator/workflows/aw-spec-select-domain.orch.yaml +0 -79
- package/assets/.aw-spec/orchestrator/workflows/aw-spec-select-feature.orch.yaml +0 -128
- package/assets/.aw-spec/orchestrator/workflows/aw-spec-specify.orch.yaml +0 -412
- package/assets/.aw-spec/scripts/bash/aw-inventory.sh +0 -14
- package/assets/.aw-spec/scripts/bash/aw-parity.sh +0 -13
- package/assets/.aw-spec/scripts/bash/aw-rev-constitution.sh +0 -13
- package/assets/.aw-spec/scripts/bash/aw-spec-actions.sh +0 -1899
- package/assets/.aw-spec/scripts/bash/aw-xref.sh +0 -15
- package/assets/.aw-spec/scripts/bash/common.sh +0 -533
- package/assets/.aw-spec/scripts/powershell/aw-inventory.ps1 +0 -14
- package/assets/.aw-spec/scripts/powershell/aw-parity.ps1 +0 -13
- package/assets/.aw-spec/scripts/powershell/aw-rev-constitution.ps1 +0 -13
- package/assets/.aw-spec/scripts/powershell/aw-spec-actions.ps1 +0 -1563
- package/assets/.aw-spec/scripts/powershell/aw-xref.ps1 +0 -15
- package/assets/.aw-spec/scripts/python/aw-inventory.py +0 -683
- package/assets/.aw-spec/scripts/python/aw-parity.py +0 -380
- package/assets/.aw-spec/scripts/python/aw-rev-constitution.py +0 -312
- package/assets/.aw-spec/scripts/python/aw-xref.py +0 -1330
- package/assets/.aw-spec/scripts/python/validate_okf.py +0 -349
- package/assets/.aw-spec/templates/constitution-domain-template.md +0 -75
- package/assets/.aw-spec/templates/constitution-technical-template.md +0 -70
- package/assets/.aw-spec/templates/delta-design-template.md +0 -69
- package/assets/.aw-spec/templates/delta-spec-template.md +0 -60
- package/assets/.aw-spec/templates/design-template.md +0 -383
- package/assets/.aw-spec/templates/okf-concept-template.md +0 -103
- package/assets/.aw-spec/templates/parity-manifest-template.json +0 -49
- package/assets/.aw-spec/templates/source-map-template.md +0 -85
- package/assets/.aw-spec/templates/spec-template.md +0 -252
- package/assets/docs/ModularMonolith/ModularMonolith.Reference.pack.txt +0 -25248
- package/assets/docs/ModularMonolith/ModularMonolith.pack.txt +0 -5726
- package/assets/docs/ModularMonolith/pack-foundation.ps1 +0 -145
- package/assets/docs/ModularMonolith/pack-foundation.sh +0 -196
- package/assets/docs/ModularMonolith/pack-reference-foundation.ps1 +0 -157
- package/assets/docs/ModularMonolith/pack-reference-foundation.sh +0 -209
- package/assets/docs/ModularMonolith/unpack-foundation.ps1 +0 -126
- package/assets/docs/ModularMonolith/unpack-foundation.sh +0 -217
- package/assets/docs/ModularMonolith/unpack-reference-foundation.ps1 +0 -129
- package/assets/docs/ModularMonolith/unpack-reference-foundation.sh +0 -218
- package/assets/docs/README.md +0 -106
- package/assets/docs/VerticalSlice/PACKAGING.md +0 -185
- package/assets/docs/VerticalSlice/VerticalSlice.Reference.pack.txt +0 -19318
- package/assets/docs/VerticalSlice/VerticalSlice.pack.txt +0 -2833
- package/assets/docs/VerticalSlice/pack-foundation.ps1 +0 -145
- package/assets/docs/VerticalSlice/pack-foundation.sh +0 -196
- package/assets/docs/VerticalSlice/pack-reference-foundation.ps1 +0 -157
- package/assets/docs/VerticalSlice/pack-reference-foundation.sh +0 -209
- package/assets/docs/VerticalSlice/unpack-foundation.ps1 +0 -126
- package/assets/docs/VerticalSlice/unpack-foundation.sh +0 -217
- package/assets/docs/VerticalSlice/unpack-reference-foundation.ps1 +0 -129
- package/assets/docs/VerticalSlice/unpack-reference-foundation.sh +0 -218
- package/assets/docs/architecture/clean-architecture.md +0 -71
- package/assets/docs/architecture/cqrs.md +0 -141
- package/assets/docs/architecture/eventing.md +0 -83
- package/assets/docs/architecture/folder-structure.md +0 -61
- package/assets/docs/architecture/modular-monolith.md +0 -47
- package/assets/docs/architecture/persistence.md +0 -269
- package/assets/docs/architecture/testing.md +0 -55
- package/assets/docs/architecture/vertical-slices.md +0 -201
- package/assets/docs/components/Email/README.md +0 -119
- package/assets/docs/components/Email/ReferenceEndpoints/SendEmailEndpoint.cs +0 -84
- package/assets/docs/components/Email/integration.md +0 -300
- package/assets/docs/components/Jobs/README.md +0 -113
- package/assets/docs/components/Jobs/integration.md +0 -259
- package/assets/docs/components/QrGenerator/README.md +0 -85
- package/assets/docs/components/QrGenerator/ReferenceEndpoints/GenerateQrEndpoint.cs +0 -57
- package/assets/docs/components/QrGenerator/integration.md +0 -230
- package/assets/docs/components/README.md +0 -74
- package/assets/docs/domain/domain-driven-design.md +0 -212
- package/assets/docs/domainspecs/healthcare/ai-rules.domain.md +0 -34
- package/assets/docs/domainspecs/healthcare/api-versioning.domain.md +0 -17
- package/assets/docs/domainspecs/healthcare/auditing.domain.md +0 -97
- package/assets/docs/domainspecs/healthcare/authorization.domain.md +0 -26
- package/assets/docs/domainspecs/healthcare/clinical-terms.md +0 -20
- package/assets/docs/domainspecs/healthcare/coding-standards.domain.md +0 -26
- package/assets/docs/domainspecs/healthcare/comandrulemap.md +0 -100
- package/assets/docs/domainspecs/healthcare/commandtechrulemap.md +0 -119
- package/assets/docs/domainspecs/healthcare/consent.md +0 -19
- package/assets/docs/domainspecs/healthcare/cqrs.domain.md +0 -12
- package/assets/docs/domainspecs/healthcare/data-classification.domain.md +0 -28
- package/assets/docs/domainspecs/healthcare/dependency-policy.domain.md +0 -11
- package/assets/docs/domainspecs/healthcare/dicom.md +0 -15
- package/assets/docs/domainspecs/healthcare/domain-driven-design.domain.md +0 -28
- package/assets/docs/domainspecs/healthcare/domain-requirements.md +0 -55
- package/assets/docs/domainspecs/healthcare/eventing.domain.md +0 -18
- package/assets/docs/domainspecs/healthcare/fhir.md +0 -36
- package/assets/docs/domainspecs/healthcare/folder-structure.domain.md +0 -23
- package/assets/docs/domainspecs/healthcare/hipaa.md +0 -34
- package/assets/docs/domainspecs/healthcare/hl7.md +0 -15
- package/assets/docs/domainspecs/healthcare/integration-terms.md +0 -21
- package/assets/docs/domainspecs/healthcare/persistence.domain.md +0 -20
- package/assets/docs/domainspecs/healthcare/terminology.md +0 -30
- package/assets/docs/domainspecs/healthcare/testing.domain.md +0 -15
- package/assets/docs/domainspecs/healthcare/vertical-slices.domain.md +0 -18
- package/assets/docs/domainspecs/healthcare/zero-trust.domain.md +0 -12
- package/assets/docs/domainspecs/pharma/ai-rules.domain.md +0 -32
- package/assets/docs/domainspecs/pharma/api-versioning.domain.md +0 -20
- package/assets/docs/domainspecs/pharma/atc.md +0 -22
- package/assets/docs/domainspecs/pharma/auditing.domain.md +0 -90
- package/assets/docs/domainspecs/pharma/authorization.domain.md +0 -25
- package/assets/docs/domainspecs/pharma/coding-standards.domain.md +0 -28
- package/assets/docs/domainspecs/pharma/comandrulemap.md +0 -108
- package/assets/docs/domainspecs/pharma/commandtechrulemap.md +0 -119
- package/assets/docs/domainspecs/pharma/cqrs.domain.md +0 -16
- package/assets/docs/domainspecs/pharma/data-classification.domain.md +0 -24
- package/assets/docs/domainspecs/pharma/data-provenance.md +0 -21
- package/assets/docs/domainspecs/pharma/dependency-policy.domain.md +0 -11
- package/assets/docs/domainspecs/pharma/domain-driven-design.domain.md +0 -36
- package/assets/docs/domainspecs/pharma/domain-requirements.md +0 -99
- package/assets/docs/domainspecs/pharma/eventing.domain.md +0 -18
- package/assets/docs/domainspecs/pharma/fhir.md +0 -31
- package/assets/docs/domainspecs/pharma/folder-structure.domain.md +0 -24
- package/assets/docs/domainspecs/pharma/hl7.md +0 -17
- package/assets/docs/domainspecs/pharma/idmp.md +0 -26
- package/assets/docs/domainspecs/pharma/integration-terms.md +0 -21
- package/assets/docs/domainspecs/pharma/ndc.md +0 -28
- package/assets/docs/domainspecs/pharma/persistence.domain.md +0 -23
- package/assets/docs/domainspecs/pharma/pharma-terms.md +0 -27
- package/assets/docs/domainspecs/pharma/product-lifecycle.domain.md +0 -30
- package/assets/docs/domainspecs/pharma/regulatory-compliance.md +0 -19
- package/assets/docs/domainspecs/pharma/terminology.md +0 -32
- package/assets/docs/domainspecs/pharma/testing.domain.md +0 -19
- package/assets/docs/domainspecs/pharma/vertical-slices.domain.md +0 -17
- package/assets/docs/domainspecs/pharma/zero-trust.domain.md +0 -12
- package/assets/docs/glossary/ai-rules.md +0 -59
- package/assets/docs/governance/api-versioning.md +0 -99
- package/assets/docs/governance/coding-standards.md +0 -87
- package/assets/docs/governance/dependency-policy.md +0 -40
- package/assets/docs/interop/ihe-conformance.md +0 -31
- package/assets/docs/requirement-spec.txt +0 -27
- package/assets/docs/security/auditing.md +0 -160
- package/assets/docs/security/authorization-matrix.md +0 -14
- package/assets/docs/security/authorization.md +0 -87
- package/assets/docs/security/data-classification.md +0 -38
- package/assets/docs/security/zero-trust.md +0 -76
- package/assets/docs/technology-constitution.md +0 -172
|
@@ -1,1721 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env python3
|
|
2
|
-
"""
|
|
3
|
-
aw-orchestrator — reference runtime for the "interpreter owns context,
|
|
4
|
-
a FRESH Claude executes" architecture.
|
|
5
|
-
|
|
6
|
-
User
|
|
7
|
-
│
|
|
8
|
-
▼ (a Claude Code skill launches this runtime)
|
|
9
|
-
Python Workflow / Interpreter Runtime ── THIS FILE
|
|
10
|
-
│
|
|
11
|
-
├─ build_context Builds the COMPLETE context for the next request
|
|
12
|
-
│ (reads standards + rule files + prior outputs and
|
|
13
|
-
│ assembles a self-contained system + user payload).
|
|
14
|
-
▼
|
|
15
|
-
Invoke Claude Code (FRESH generation)
|
|
16
|
-
│ `claude -p --bare --system-prompt-file … --tools …`
|
|
17
|
-
│ • -p → a brand-new conversation (the calling
|
|
18
|
-
│ agent's transcript is NOT inherited)
|
|
19
|
-
│ • --bare → no CLAUDE.md / auto-memory / hooks;
|
|
20
|
-
│ the ONLY context is what we pass
|
|
21
|
-
│ • system → the interpreter-authored standards
|
|
22
|
-
│ • stdin → the interpreter-authored task
|
|
23
|
-
├─ Claude uses tools (Edit / Write / Bash …) to do the actual work
|
|
24
|
-
▼
|
|
25
|
-
Return structured result (a trailing ```json {…} ``` block → dict)
|
|
26
|
-
│
|
|
27
|
-
▼
|
|
28
|
-
Interpreter continues to the next workflow step
|
|
29
|
-
│
|
|
30
|
-
├─ agent_review Review the current output (another FRESH generation)
|
|
31
|
-
▼
|
|
32
|
-
build_context → … continues
|
|
33
|
-
|
|
34
|
-
Key property (your R6): every model request sees ONLY the payload the
|
|
35
|
-
interpreter assembled. The orchestrating Claude Code's own context is never
|
|
36
|
-
part of a generation — because generation happens in a separate `--bare`
|
|
37
|
-
process, not in this conversation.
|
|
38
|
-
|
|
39
|
-
Stdlib only. Reuses the production YAML loader from ../interpreter/run-skill.py
|
|
40
|
-
so skills stay in the same dialect family.
|
|
41
|
-
|
|
42
|
-
Workflow dialect: aw-spec.orchestrator/v1 (see ORCH-DIALECT.md). On top of the
|
|
43
|
-
demo actions (set / build_context / agent_generate / agent_review / loop /
|
|
44
|
-
break) the engine provides the full vocabulary the aw-spec-* conversions need:
|
|
45
|
-
execute_script run a shared deterministic script (decl port; --os sh/ps1)
|
|
46
|
-
verify_context the four decl gate forms (files / non-empty / true / value)
|
|
47
|
-
load_standards sticky SYSTEM standards set, prepended to every payload
|
|
48
|
-
agent_author authored artifact: interpreter-owned output path, read-only
|
|
49
|
-
generation under EVERY strategy, the ENGINE writes the file
|
|
50
|
-
agent_query generic read-only structured JSON query (outputs[] binding)
|
|
51
|
-
ask/user_select HITL — suspends via the checkpoint channel under ANY
|
|
52
|
-
strategy (the launcher relays to the user), or --answer /
|
|
53
|
-
--select / --non-interactive prefills
|
|
54
|
-
dispatch_hooks hands resolved hooks to the INVOKING agent (suspend 'hook')
|
|
55
|
-
include run a module .orch.yaml inline (shared vars/scripts/cache)
|
|
56
|
-
loop over:/as: foreach over a list var (plus the existing while: form)
|
|
57
|
-
|
|
58
|
-
Usage:
|
|
59
|
-
orchestrator.py <workflow.yaml> [--transport mock|real]
|
|
60
|
-
[--model opus] [--budget 0.50] [--var k=v ...] [--input "<text>"]
|
|
61
|
-
[--answer k=v ...] [--select stepid=v ...] [--non-interactive]
|
|
62
|
-
[--os posix|windows] [--show-full] [--json]
|
|
63
|
-
orchestrator.py <workflow.yaml> --validate [--json] # lint, don't execute
|
|
64
|
-
|
|
65
|
-
Execution strategies (--strategy, overridable per step via `strategy:`):
|
|
66
|
-
The interpreter assembles 100% of every payload in ALL strategies; they only
|
|
67
|
-
differ in WHO executes it and WHO applies the file changes.
|
|
68
|
-
|
|
69
|
-
sdk Strategy 1: an isolated Agent SDK instance generates AND
|
|
70
|
-
implements with its own tools. Fresh context per step; the
|
|
71
|
-
invoking agent's conversation never enters a request. EVERYTHING
|
|
72
|
-
SDK-specific is declared by the agent profile's `sdk` block
|
|
73
|
-
(--agent-profile, e.g. agent.json) — the package/module to import
|
|
74
|
-
(pip-installed on demand with --sdk-install or sdk.auto_install)
|
|
75
|
-
and the `adapter` (a Python file exporting generate(request);
|
|
76
|
-
shared per-SDK adapters live in .aw-spec/orchestrator/adapters/)
|
|
77
|
-
that owns that SDK's calling convention — so the engine names no
|
|
78
|
-
agent, has no default, and stays agent-agnostic. Imported
|
|
79
|
-
lazily; needs API auth.
|
|
80
|
-
sdk-apply Strategy 2: the SDK instance (same profile-declared sdk block)
|
|
81
|
-
generates read-only (full intended file contents as an artifact);
|
|
82
|
-
the interpreter then SUSPENDS (checkpoint) and the INVOKING agent
|
|
83
|
-
applies the artifact with its tools — visible diffs, native
|
|
84
|
-
permission prompts. Generation is isolated; only the apply phase
|
|
85
|
-
runs in the invoker. Needs --checkpoint.
|
|
86
|
-
spawn Strategy 3 (the original transport): a fresh CLI process per
|
|
87
|
-
generation, described by --agent-profile (claude -p --bare /
|
|
88
|
-
gemini -p). Isolated, but cold-starts every step.
|
|
89
|
-
agent Strategy 4: zero new instances — the run-skill.py suspend/resume
|
|
90
|
-
method. The interpreter exits (code 2) at each generation and the
|
|
91
|
-
INVOKING agent fulfills it in its own conversation (payload
|
|
92
|
-
ownership kept; context isolation is best-effort only). Needs
|
|
93
|
-
--checkpoint.
|
|
94
|
-
|
|
95
|
-
Legacy inference when --strategy is omitted: --agent-profile → spawn,
|
|
96
|
-
--checkpoint → agent, neither → mock transport (no model call; prints the
|
|
97
|
-
exact payloads and simulates the writes).
|
|
98
|
-
|
|
99
|
-
Loop + checkpoint machinery is strategy-independent: completed generations
|
|
100
|
-
are written to the replay cache, so any mix of strategies survives
|
|
101
|
-
suspend/resume.
|
|
102
|
-
"""
|
|
103
|
-
import argparse
|
|
104
|
-
import importlib.util
|
|
105
|
-
import json
|
|
106
|
-
import os
|
|
107
|
-
import re
|
|
108
|
-
import subprocess
|
|
109
|
-
import sys
|
|
110
|
-
|
|
111
|
-
# ── reuse the production YAML-subset loader ──────────────────────────────────
|
|
112
|
-
_INTERP = os.path.join(os.path.dirname(os.path.abspath(__file__)), "..", "interpreter", "run-skill.py")
|
|
113
|
-
_spec = importlib.util.spec_from_file_location("aw_run_skill", _INTERP)
|
|
114
|
-
_run_skill = importlib.util.module_from_spec(_spec)
|
|
115
|
-
_spec.loader.exec_module(_run_skill)
|
|
116
|
-
load_yaml = _run_skill.load_yaml
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
BANNER = "─" * 70
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
class Stop(Exception):
|
|
123
|
-
pass
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
class Suspend(Exception):
|
|
127
|
-
"""Raised in checkpoint mode when a generation is needed: the interpreter
|
|
128
|
-
persists state and exits so the INVOKING agent can fulfill the request
|
|
129
|
-
(run-skill.py's suspend/resume method — no new agent instance)."""
|
|
130
|
-
def __init__(self, request):
|
|
131
|
-
self.request = request
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
def _norm_files(entries):
|
|
135
|
-
"""Normalize a system:/context: list into [(path, optional)]."""
|
|
136
|
-
out = []
|
|
137
|
-
for e in entries or []:
|
|
138
|
-
if isinstance(e, dict):
|
|
139
|
-
out.append((e.get("path"), bool(e.get("optional"))))
|
|
140
|
-
else:
|
|
141
|
-
out.append((e, False))
|
|
142
|
-
return out
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
def _extract_json(text):
|
|
146
|
-
"""Pull a structured result out of a free-form model reply: the last
|
|
147
|
-
```json … ``` fence, else the last bare {...}."""
|
|
148
|
-
if not text:
|
|
149
|
-
return {}
|
|
150
|
-
fences = re.findall(r"```(?:json)?\s*(\{.*?\})\s*```", text, re.DOTALL)
|
|
151
|
-
for blob in reversed(fences):
|
|
152
|
-
try:
|
|
153
|
-
return json.loads(blob)
|
|
154
|
-
except Exception:
|
|
155
|
-
continue
|
|
156
|
-
i, j = text.rfind("{"), text.rfind("}")
|
|
157
|
-
if 0 <= i < j:
|
|
158
|
-
try:
|
|
159
|
-
return json.loads(text[i:j + 1])
|
|
160
|
-
except Exception:
|
|
161
|
-
pass
|
|
162
|
-
return {}
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
def _strip_fence(text):
|
|
166
|
-
"""If the ENTIRE reply is one fenced code block, unwrap it (models often
|
|
167
|
-
fence authored file content despite being told not to)."""
|
|
168
|
-
t = (text or "").strip()
|
|
169
|
-
m = re.fullmatch(r"```[A-Za-z0-9_+-]*\n(.*?)\n?```", t, re.DOTALL)
|
|
170
|
-
return m.group(1) + "\n" if m else (text or "")
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
class Orchestrator:
|
|
174
|
-
def __init__(self, wf, opts):
|
|
175
|
-
self.wf = wf
|
|
176
|
-
self.opts = opts
|
|
177
|
-
self.vars = dict(wf.get("vars", {}) or {})
|
|
178
|
-
self.vars.update(opts.vars)
|
|
179
|
-
# the CURRENT context envelope, produced by build_context and consumed
|
|
180
|
-
# by the next agent_generate / agent_review.
|
|
181
|
-
self.envelope = {"system": [], "context": []}
|
|
182
|
-
# F4: sticky standards — _assemble_system always prepends these, so the
|
|
183
|
-
# system prefix stays byte-stable across steps (prompt-cache lever)
|
|
184
|
-
# while build_context keeps declaring only the per-request delta.
|
|
185
|
-
self.standards = list(wf.get("standards", []) or [])
|
|
186
|
-
# F1: top-level scripts ref map {ref: {sh: path, ps1: path}} (decl shape)
|
|
187
|
-
self.scripts = dict(wf.get("scripts", {}) or {})
|
|
188
|
-
# every file that has entered any payload (verify_context required_files)
|
|
189
|
-
self.loaded = []
|
|
190
|
-
# every REQUIRED system:/context: file that was declared but absent at
|
|
191
|
-
# assembly time (item H). Distinct, run-cumulative, surfaced in the run
|
|
192
|
-
# result + the context_missing/_count vars so a workflow can gate on it.
|
|
193
|
-
self.missing_required = []
|
|
194
|
-
self.last_result = {}
|
|
195
|
-
# agent-specific transport profile (None in mock mode). This engine is
|
|
196
|
-
# general — the profile (loaded from a .claude/.agents/.kilocode file)
|
|
197
|
-
# describes HOW to spawn that agent's fresh generation CLI.
|
|
198
|
-
self.profile = getattr(opts, "profile", None)
|
|
199
|
-
# checkpoint transport (run-skill.py method): replay + response cache
|
|
200
|
-
# keyed by a loop-aware invocation path. No new agent instance.
|
|
201
|
-
self.checkpoint = getattr(opts, "checkpoint", None)
|
|
202
|
-
self.cache = getattr(opts, "_cache", {}) or {}
|
|
203
|
-
self.key_stack = []
|
|
204
|
-
|
|
205
|
-
# -- variable resolution --------------------------------------------------
|
|
206
|
-
def _lookup(self, key):
|
|
207
|
-
"""Dotted/indexed lookup into vars ('q.text', 'items.0'). (found, raw)."""
|
|
208
|
-
parts = key.split(".")
|
|
209
|
-
if parts[0] not in self.vars:
|
|
210
|
-
return (False, None)
|
|
211
|
-
cur = self.vars[parts[0]]
|
|
212
|
-
for p in parts[1:]:
|
|
213
|
-
if isinstance(cur, dict) and p in cur:
|
|
214
|
-
cur = cur[p]
|
|
215
|
-
elif isinstance(cur, list) and p.isdigit() and int(p) < len(cur):
|
|
216
|
-
cur = cur[int(p)]
|
|
217
|
-
else:
|
|
218
|
-
return (False, None)
|
|
219
|
-
return (True, cur)
|
|
220
|
-
|
|
221
|
-
def _get(self, key):
|
|
222
|
-
found, cur = self._lookup(key)
|
|
223
|
-
if not found or cur is None:
|
|
224
|
-
return ""
|
|
225
|
-
return cur if isinstance(cur, str) else json.dumps(cur)
|
|
226
|
-
|
|
227
|
-
def resolve(self, val):
|
|
228
|
-
if isinstance(val, str):
|
|
229
|
-
return re.sub(r"\{\{\s*([^}]+?)\s*\}\}", lambda m: str(self._get(m.group(1).strip())), val)
|
|
230
|
-
if isinstance(val, list):
|
|
231
|
-
return [self.resolve(x) for x in val]
|
|
232
|
-
if isinstance(val, dict):
|
|
233
|
-
return {k: self.resolve(v) for k, v in val.items()}
|
|
234
|
-
return val
|
|
235
|
-
|
|
236
|
-
def resolve_value(self, x):
|
|
237
|
-
"""resolve(), except a value that is EXACTLY one {{ref}} returns the raw
|
|
238
|
-
object (list/dict/bool) instead of its JSON string (run-skill parity —
|
|
239
|
-
needed for loop over:, ask options_from, set, include with:)."""
|
|
240
|
-
if isinstance(x, str):
|
|
241
|
-
m = re.fullmatch(r"\{\{\s*([^}]+?)\s*\}\}", x.strip())
|
|
242
|
-
if m:
|
|
243
|
-
found, v = self._lookup(m.group(1).strip())
|
|
244
|
-
return v if found else x
|
|
245
|
-
return self.resolve(x)
|
|
246
|
-
if isinstance(x, list):
|
|
247
|
-
return [self.resolve_value(i) for i in x]
|
|
248
|
-
return x
|
|
249
|
-
|
|
250
|
-
def _json_get(self, obj, path):
|
|
251
|
-
if not isinstance(obj, dict):
|
|
252
|
-
return None
|
|
253
|
-
if not path.startswith("$."):
|
|
254
|
-
return obj.get(path)
|
|
255
|
-
cur = obj
|
|
256
|
-
for part in path[2:].split("."):
|
|
257
|
-
cur = cur.get(part) if isinstance(cur, dict) else None
|
|
258
|
-
return cur
|
|
259
|
-
|
|
260
|
-
def _bind(self, step, data):
|
|
261
|
-
for out in step.get("outputs", []) or []:
|
|
262
|
-
self.vars[out["name"]] = self._json_get(data, out["from"])
|
|
263
|
-
|
|
264
|
-
def _cond(self, expr):
|
|
265
|
-
expr = self.resolve(expr).strip()
|
|
266
|
-
for op in ("==", "!="):
|
|
267
|
-
if op in expr:
|
|
268
|
-
lhs, rhs = [s.strip().strip("'\"") for s in expr.split(op, 1)]
|
|
269
|
-
return (lhs == rhs) if op == "==" else (lhs != rhs)
|
|
270
|
-
return expr not in ("", "false", "False", "None", "null", "[]")
|
|
271
|
-
|
|
272
|
-
def emit(self, msg):
|
|
273
|
-
if not self.opts.json:
|
|
274
|
-
print(msg)
|
|
275
|
-
|
|
276
|
-
# -- strategy selection -----------------------------------------------------
|
|
277
|
-
def _effective_strategy(self, step):
|
|
278
|
-
"""Per-step `strategy:` overrides the run default (--strategy). In mock
|
|
279
|
-
transport the strategy is informational only — nothing executes."""
|
|
280
|
-
s = step.get("strategy") or self.opts.strategy
|
|
281
|
-
if self.opts.transport == "mock":
|
|
282
|
-
return s
|
|
283
|
-
if s == "spawn" and not self.profile:
|
|
284
|
-
raise Stop(f"step {step.get('id')}: strategy 'spawn' needs --agent-profile")
|
|
285
|
-
if s in ("sdk", "sdk-apply") and not self.profile:
|
|
286
|
-
raise Stop(f"step {step.get('id')}: strategy '{s}' needs --agent-profile "
|
|
287
|
-
"(its sdk block declares the SDK + adapter; no built-in default)")
|
|
288
|
-
if s in ("agent", "sdk-apply") and not self.checkpoint:
|
|
289
|
-
raise Stop(f"step {step.get('id')}: strategy '{s}' needs --checkpoint <statefile>")
|
|
290
|
-
return s
|
|
291
|
-
|
|
292
|
-
# -- checkpoint machinery (run-skill.py suspend/resume method) --------------
|
|
293
|
-
def _key(self, step):
|
|
294
|
-
"""Stable, loop-aware invocation key for a step (for the replay cache)."""
|
|
295
|
-
return "/".join(self.key_stack + [step.get("id", "?")])
|
|
296
|
-
|
|
297
|
-
def _suspend(self, kind, step_key, system, user, tools, permission_mode,
|
|
298
|
-
instructions=None, extra=None):
|
|
299
|
-
"""Write the fully-assembled payload to sidecar files next to the
|
|
300
|
-
checkpoint, then raise Suspend so main() persists state and exits.
|
|
301
|
-
The INVOKING agent fulfills the request — no new instance is spawned.
|
|
302
|
-
The interpreter keeps 100% ownership of what goes INTO the payload;
|
|
303
|
-
it cannot strip what the invoking agent's own conversation adds AROUND
|
|
304
|
-
it (that is the isolation trade-off of this transport)."""
|
|
305
|
-
base = self.checkpoint
|
|
306
|
-
os.makedirs(os.path.dirname(base) or ".", exist_ok=True)
|
|
307
|
-
with open(base + ".system.md", "w", encoding="utf-8") as fh:
|
|
308
|
-
fh.write(system)
|
|
309
|
-
with open(base + ".user.md", "w", encoding="utf-8") as fh:
|
|
310
|
-
fh.write(user)
|
|
311
|
-
req = {
|
|
312
|
-
"kind": kind, "step": step_key,
|
|
313
|
-
"tools": tools, "permission_mode": permission_mode,
|
|
314
|
-
"system_path": base + ".system.md",
|
|
315
|
-
"user_path": base + ".user.md",
|
|
316
|
-
"response_path": base + ".response.md",
|
|
317
|
-
"instructions": instructions or (
|
|
318
|
-
"Perform this task YOURSELF, in this conversation. Read "
|
|
319
|
-
"system_path + user_path and treat them as your ONLY standards "
|
|
320
|
-
"and context (ignore anything else you know about this "
|
|
321
|
-
"session). Use the listed tools within the permission mode. "
|
|
322
|
-
"Write your full final reply — ending with the required "
|
|
323
|
-
"```json fence — to response_path, then re-run the resume "
|
|
324
|
-
"command. Do not edit the checkpoint state file."),
|
|
325
|
-
"resume": self._resume_cmd(),
|
|
326
|
-
}
|
|
327
|
-
req.update(extra or {})
|
|
328
|
-
raise Suspend(req)
|
|
329
|
-
|
|
330
|
-
def _suspend_simple(self, request):
|
|
331
|
-
"""Suspend WITHOUT payload sidecars (kinds ask/select/hook): the
|
|
332
|
-
launcher needs only the request itself. This is the D1 interaction
|
|
333
|
-
channel — it works under EVERY strategy (strategies govern who
|
|
334
|
-
GENERATES; the checkpoint carries everything human-facing)."""
|
|
335
|
-
base = self.checkpoint
|
|
336
|
-
os.makedirs(os.path.dirname(base) or ".", exist_ok=True)
|
|
337
|
-
request.setdefault("response_path", base + ".response.md")
|
|
338
|
-
request.setdefault("resume", self._resume_cmd())
|
|
339
|
-
raise Suspend(request)
|
|
340
|
-
|
|
341
|
-
def _resume_cmd(self):
|
|
342
|
-
cmd = f"python3 {sys.argv[0]} {self.opts.workflow} --checkpoint {self.checkpoint}"
|
|
343
|
-
return cmd + (" --json" if self.opts.json else "")
|
|
344
|
-
|
|
345
|
-
def _suspend_apply(self, step_key, artifact, tools, permission_mode):
|
|
346
|
-
"""Strategy 2 (sdk-apply) handoff: the isolated SDK generation already
|
|
347
|
-
DECIDED the changes (full intended file contents, in `artifact`); the
|
|
348
|
-
invoking agent only applies them. Same suspend/resume channel."""
|
|
349
|
-
base = self.checkpoint
|
|
350
|
-
os.makedirs(os.path.dirname(base) or ".", exist_ok=True)
|
|
351
|
-
with open(base + ".artifact.md", "w", encoding="utf-8") as fh:
|
|
352
|
-
fh.write(artifact or "")
|
|
353
|
-
raise Suspend({
|
|
354
|
-
"kind": "apply", "step": step_key,
|
|
355
|
-
"tools": tools, "permission_mode": permission_mode,
|
|
356
|
-
"artifact_path": base + ".artifact.md",
|
|
357
|
-
"response_path": base + ".response.md",
|
|
358
|
-
"instructions": (
|
|
359
|
-
"An ISOLATED generation already decided these changes — apply "
|
|
360
|
-
"them, do not redesign them. Read artifact_path and write its "
|
|
361
|
-
"file contents EXACTLY to the paths it names, using the listed "
|
|
362
|
-
"tools within the permission mode. Then write your full final "
|
|
363
|
-
'reply — ending with a fenced block ```json {"status":"ok",'
|
|
364
|
-
'"files":["<path you wrote>"],"summary":"<one line>"} ``` — to '
|
|
365
|
-
"response_path, and re-run the resume command."),
|
|
366
|
-
"resume": self._resume_cmd(),
|
|
367
|
-
})
|
|
368
|
-
|
|
369
|
-
# -- context assembly (the "Builds Complete Context" box) -----------------
|
|
370
|
-
def _read(self, path):
|
|
371
|
-
try:
|
|
372
|
-
with open(path, encoding="utf-8") as fh:
|
|
373
|
-
return fh.read()
|
|
374
|
-
except OSError:
|
|
375
|
-
return ""
|
|
376
|
-
|
|
377
|
-
def _collect_files(self, entries, kind):
|
|
378
|
-
"""Resolve a normalized [(path, optional)] list to the files that exist,
|
|
379
|
-
HONOURING the `optional:` flag (item H).
|
|
380
|
-
|
|
381
|
-
Both assembly paths used to do `for p, _ in entries` and filter on
|
|
382
|
-
os.path.exists alone — the flag was parsed by _norm_files and then
|
|
383
|
-
discarded, so a missing REQUIRED standard/context file was dropped
|
|
384
|
-
exactly as silently as an optional one. The payload simply came up
|
|
385
|
-
short (an empty '# ENGINEERING STANDARDS & RULES' heading, a domain
|
|
386
|
-
constitution the task text still claimed was 'in context') and every
|
|
387
|
-
workflow went on to report OK. Required now ANNOUNCES itself.
|
|
388
|
-
|
|
389
|
-
It announces rather than stops: a declared context path is often
|
|
390
|
-
legitimately conditional (a delta doc on the bootstrap path, an
|
|
391
|
-
overlay before a domain is selected), so a hard stop here would fail
|
|
392
|
-
green flows. Workflows that genuinely require a file gate on it
|
|
393
|
-
deliberately — verify_context `required_files`, or `context_missing_count`
|
|
394
|
-
against the run-cumulative set (cf. concept-paths' `missing`).
|
|
395
|
-
|
|
396
|
-
A path that resolves to "" is an unbound {{var}} on this branch, not a
|
|
397
|
-
missing file — skipped silently, as before."""
|
|
398
|
-
files, seen = [], set()
|
|
399
|
-
for p, optional in entries:
|
|
400
|
-
rp = self.resolve(p)
|
|
401
|
-
if not rp or rp in seen:
|
|
402
|
-
continue
|
|
403
|
-
if os.path.exists(rp):
|
|
404
|
-
seen.add(rp)
|
|
405
|
-
files.append(rp)
|
|
406
|
-
elif optional:
|
|
407
|
-
self.emit(f"[{kind}] optional missing, skip {rp}")
|
|
408
|
-
else:
|
|
409
|
-
self.emit(f"[{kind}] MISSING (required): {rp}")
|
|
410
|
-
if rp not in self.missing_required:
|
|
411
|
-
self.missing_required.append(rp)
|
|
412
|
-
self.vars["context_missing"] = ",".join(self.missing_required)
|
|
413
|
-
self.vars["context_missing_count"] = str(len(self.missing_required))
|
|
414
|
-
return files
|
|
415
|
-
|
|
416
|
-
def _assemble_system(self, include_standards=True):
|
|
417
|
-
"""The interpreter-authored SYSTEM block: the sticky standards set (F4)
|
|
418
|
-
+ the envelope's per-request system files. The standards prefix is
|
|
419
|
-
byte-stable across steps — the prompt-cache lever in a real deployment.
|
|
420
|
-
include_standards=False is the decl `include_loaded_context: false`
|
|
421
|
-
analog for narrow judgments (e.g. a constitution gate)."""
|
|
422
|
-
entries = (_norm_files(self.standards) if include_standards else [])
|
|
423
|
-
entries += _norm_files(self.envelope["system"])
|
|
424
|
-
files = self._collect_files(entries, "system")
|
|
425
|
-
self._track_loaded(files)
|
|
426
|
-
parts = [
|
|
427
|
-
"You are a fresh generation agent invoked by the AppWeaver spec "
|
|
428
|
-
"interpreter. Follow ONLY the standards and the task provided. Do "
|
|
429
|
-
"not assume any prior conversation.\n",
|
|
430
|
-
"\n# ENGINEERING STANDARDS & RULES (authoritative)\n",
|
|
431
|
-
]
|
|
432
|
-
for f in files:
|
|
433
|
-
parts.append(f"\n===== RULE FILE: {f} =====\n{self._read(f)}\n")
|
|
434
|
-
return "".join(parts), files
|
|
435
|
-
|
|
436
|
-
def _assemble_user(self, task, extra=None):
|
|
437
|
-
"""The interpreter-authored USER block: prior artifacts + the task.
|
|
438
|
-
`extra` = per-step context paths appended for THIS request only. Both
|
|
439
|
-
lists go through _norm_files, so a step-level `context:` entry may use
|
|
440
|
-
the { path:, optional: } form too — it used to be filtered out for not
|
|
441
|
-
being a str, i.e. dropped even harder than a missing file."""
|
|
442
|
-
entries = _norm_files(self.envelope["context"]) + _norm_files(extra or [])
|
|
443
|
-
files = self._collect_files(entries, "context")
|
|
444
|
-
self._track_loaded(files)
|
|
445
|
-
parts = []
|
|
446
|
-
if files:
|
|
447
|
-
parts.append("# CONTEXT ARTIFACTS\n")
|
|
448
|
-
for f in files:
|
|
449
|
-
parts.append(f"\n===== FILE: {f} =====\n{self._read(f)}\n")
|
|
450
|
-
parts.append("\n# TASK\n" + task)
|
|
451
|
-
return "".join(parts), files
|
|
452
|
-
|
|
453
|
-
def _track_loaded(self, files):
|
|
454
|
-
for f in files:
|
|
455
|
-
if f not in self.loaded:
|
|
456
|
-
self.loaded.append(f)
|
|
457
|
-
|
|
458
|
-
# -- the fresh-generation transport ---------------------------------------
|
|
459
|
-
def _print_envelope(self, kind, system, sys_files, user, ctx_files, tools, strat=None):
|
|
460
|
-
if self.opts.transport == "mock":
|
|
461
|
-
tag = "transport=mock" + (f" strategy={strat}" if strat else "")
|
|
462
|
-
elif strat == "agent":
|
|
463
|
-
tag = "strategy=agent (INVOKING agent fulfills — no new instance)"
|
|
464
|
-
elif strat == "sdk":
|
|
465
|
-
tag = "strategy=sdk (isolated SDK instance generates + implements)"
|
|
466
|
-
elif strat == "sdk-apply":
|
|
467
|
-
tag = "strategy=sdk-apply (isolated SDK generates; invoking agent applies)"
|
|
468
|
-
else:
|
|
469
|
-
tag = (f"strategy=spawn agent={(self.profile or {}).get('name', '?')}"
|
|
470
|
-
f" isolation={self.opts.isolation}")
|
|
471
|
-
head = "AGENT REQUEST" if strat == "agent" else "FRESH AGENT INVOCATION"
|
|
472
|
-
self.emit(f"\n{BANNER}\n{head} ({kind}) {tag}")
|
|
473
|
-
self.emit(f" tools : {tools or '(none / read-only)'}")
|
|
474
|
-
self.emit(f" system: {len(system)} chars files={sys_files}")
|
|
475
|
-
self.emit(f" user : {len(user)} chars context={ctx_files}")
|
|
476
|
-
if strat == "agent":
|
|
477
|
-
self.emit(" isolation: BEST-EFFORT — the payload is interpreter-assembled, "
|
|
478
|
-
"but it runs in the invoking agent's own conversation.")
|
|
479
|
-
elif strat == "sdk-apply":
|
|
480
|
-
self.emit(" isolation: generation is fully isolated; only the APPLY phase "
|
|
481
|
-
"runs in the invoking agent.")
|
|
482
|
-
else:
|
|
483
|
-
self.emit(" isolation: the fresh agent sees ONLY the two blocks below — "
|
|
484
|
-
"NOT this orchestrator's conversation.")
|
|
485
|
-
cut = None if self.opts.show_full else 1400
|
|
486
|
-
self.emit("\n── SYSTEM (--system-prompt) ──")
|
|
487
|
-
self.emit(system if cut is None else system[:cut] + ("\n…[truncated; --show-full]" if len(system) > cut else ""))
|
|
488
|
-
self.emit("\n── USER (stdin) ──")
|
|
489
|
-
self.emit(user if cut is None else user[:cut] + ("\n…[truncated; --show-full]" if len(user) > cut else ""))
|
|
490
|
-
self.emit(BANNER)
|
|
491
|
-
|
|
492
|
-
def _run_fresh(self, system, user, tools, permission_mode, model=None):
|
|
493
|
-
"""Invoke a fresh, isolated generation using the loaded agent PROFILE.
|
|
494
|
-
Returns (structured, raw).
|
|
495
|
-
|
|
496
|
-
The engine is agent-agnostic: the profile (agent-specific, from a
|
|
497
|
-
.claude/.agents/.kilocode file) declares the CLI to spawn. A SEPARATE
|
|
498
|
-
process → a brand-new conversation that does NOT inherit this
|
|
499
|
-
orchestrator's transcript. The profile maps our neutral knobs
|
|
500
|
-
(system / user / tools / permission_mode / isolation / model / budget)
|
|
501
|
-
onto that CLI's actual flags."""
|
|
502
|
-
if self.opts.transport == "mock":
|
|
503
|
-
# No model call. The banner (already printed) is the demonstration.
|
|
504
|
-
return None, None
|
|
505
|
-
prof = self.profile or {}
|
|
506
|
-
argv = [prof["bin"]] + list(prof.get("args", []))
|
|
507
|
-
model = model or self.opts.model # per-step `model:` beats --model
|
|
508
|
-
if model and prof.get("model_flag"):
|
|
509
|
-
argv += [prof["model_flag"], model]
|
|
510
|
-
# system: either injected as a CLI flag, or folded into the prompt when
|
|
511
|
-
# the CLI has no system-prompt flag (e.g. gemini).
|
|
512
|
-
sysmode = prof.get("system", "prepend")
|
|
513
|
-
prompt_user = user
|
|
514
|
-
if isinstance(sysmode, dict) and sysmode.get("flag"):
|
|
515
|
-
argv += [sysmode["flag"], system]
|
|
516
|
-
elif sysmode == "prepend":
|
|
517
|
-
prompt_user = system + "\n\n---\n\n" + user
|
|
518
|
-
if tools and prof.get("tools_flag"):
|
|
519
|
-
argv += [prof["tools_flag"], tools]
|
|
520
|
-
approval = prof.get("approval", {})
|
|
521
|
-
argv += approval.get(permission_mode, approval.get("default", []))
|
|
522
|
-
argv += prof.get("isolation", {}).get(self.opts.isolation, [])
|
|
523
|
-
if self.opts.budget and prof.get("budget_flag"):
|
|
524
|
-
argv += [prof["budget_flag"], str(self.opts.budget)]
|
|
525
|
-
umode = prof.get("user", {"mode": "stdin"})
|
|
526
|
-
stdin_data = None
|
|
527
|
-
if umode.get("mode") == "arg":
|
|
528
|
-
argv += [umode.get("flag", "-p"), prompt_user]
|
|
529
|
-
else:
|
|
530
|
-
stdin_data = prompt_user
|
|
531
|
-
|
|
532
|
-
if self.opts.dry_run:
|
|
533
|
-
shown = [a if len(a) < 120 else f"<{len(a)} chars>" for a in argv]
|
|
534
|
-
self.emit(f"[dry-run] {prof.get('name', '?')} would run:\n " + " ".join(shown))
|
|
535
|
-
if stdin_data is not None:
|
|
536
|
-
self.emit(f"[dry-run] + stdin: {len(stdin_data)} chars")
|
|
537
|
-
raise Stop("dry-run: command shown above (nothing executed)")
|
|
538
|
-
|
|
539
|
-
proc = subprocess.run(argv, input=stdin_data, capture_output=True, text=True)
|
|
540
|
-
if proc.returncode != 0:
|
|
541
|
-
raise Stop(f"fresh agent failed ({proc.returncode}): {proc.stderr.strip()[:500]}")
|
|
542
|
-
try:
|
|
543
|
-
obj = json.loads(proc.stdout)
|
|
544
|
-
except Exception:
|
|
545
|
-
return _extract_json(proc.stdout), proc.stdout # text-output fallback
|
|
546
|
-
raw = ""
|
|
547
|
-
for k in prof.get("result_keys", ["result"]):
|
|
548
|
-
if isinstance(obj, dict) and obj.get(k):
|
|
549
|
-
raw = obj[k]
|
|
550
|
-
break
|
|
551
|
-
if not raw:
|
|
552
|
-
raw = proc.stdout
|
|
553
|
-
return _extract_json(raw), raw
|
|
554
|
-
|
|
555
|
-
def _ensure_sdk_module(self, module_name, package_name):
|
|
556
|
-
"""Import the profile-declared sdk module; if missing, optionally pip
|
|
557
|
-
install the profile-declared package (--sdk-install or the sdk block's
|
|
558
|
-
`auto_install: true`), then import again."""
|
|
559
|
-
try:
|
|
560
|
-
return importlib.import_module(module_name)
|
|
561
|
-
except ImportError:
|
|
562
|
-
allowed = (getattr(self.opts, "sdk_install", False)
|
|
563
|
-
or bool(((self.profile or {}).get("sdk") or {}).get("auto_install")))
|
|
564
|
-
if not allowed:
|
|
565
|
-
raise Stop(f"strategy 'sdk'/'sdk-apply' needs the '{package_name}' "
|
|
566
|
-
f"package (module '{module_name}', declared in the agent "
|
|
567
|
-
f"profile's sdk block): pip install {package_name}, or "
|
|
568
|
-
"re-run with --sdk-install to install it automatically")
|
|
569
|
-
self.emit(f"[sdk] module '{module_name}' not importable — installing "
|
|
570
|
-
f"'{package_name}' (declared in the agent profile) …")
|
|
571
|
-
cmd = [sys.executable, "-m", "pip", "install", package_name]
|
|
572
|
-
proc = subprocess.run(cmd, capture_output=True, text=True)
|
|
573
|
-
if proc.returncode != 0 and "externally-managed-environment" in (proc.stderr or ""):
|
|
574
|
-
# PEP 668 python (e.g. Homebrew): fall back to a user-site install
|
|
575
|
-
self.emit("[sdk] environment is externally managed — retrying with "
|
|
576
|
-
"--user --break-system-packages")
|
|
577
|
-
proc = subprocess.run(cmd + ["--user", "--break-system-packages"],
|
|
578
|
-
capture_output=True, text=True)
|
|
579
|
-
if proc.returncode != 0:
|
|
580
|
-
raise Stop(f"pip install {package_name} failed: {proc.stderr.strip()[:400]}")
|
|
581
|
-
importlib.invalidate_caches()
|
|
582
|
-
try:
|
|
583
|
-
return importlib.import_module(module_name)
|
|
584
|
-
except ImportError as e:
|
|
585
|
-
raise Stop(f"installed '{package_name}' but module '{module_name}' "
|
|
586
|
-
f"still won't import: {e}")
|
|
587
|
-
|
|
588
|
-
def _load_sdk_adapter(self, adapter_rel):
|
|
589
|
-
"""Load the profile-declared adapter file (the Python module that owns
|
|
590
|
-
that SDK's calling convention; shared per-SDK adapters live in
|
|
591
|
-
.aw-spec/orchestrator/adapters/) and return its generate(request)
|
|
592
|
-
callable. Paths are relative to the profile file."""
|
|
593
|
-
base = getattr(self.opts, "profile_dir", "") or "."
|
|
594
|
-
path = adapter_rel if os.path.isabs(adapter_rel) else os.path.join(base, adapter_rel)
|
|
595
|
-
if not os.path.exists(path):
|
|
596
|
-
raise Stop(f"sdk adapter not found: {path} (sdk.adapter in the agent "
|
|
597
|
-
"profile) — point it at an existing adapter (shared ones "
|
|
598
|
-
"live in .aw-spec/orchestrator/adapters/)")
|
|
599
|
-
try:
|
|
600
|
-
spec = importlib.util.spec_from_file_location("aw_sdk_adapter", path)
|
|
601
|
-
mod = importlib.util.module_from_spec(spec)
|
|
602
|
-
spec.loader.exec_module(mod)
|
|
603
|
-
except Exception as e:
|
|
604
|
-
raise Stop(f"sdk adapter {path} failed to load: {e}")
|
|
605
|
-
fn = getattr(mod, "generate", None)
|
|
606
|
-
if not callable(fn):
|
|
607
|
-
raise Stop(f"sdk adapter {path} must export generate(request)")
|
|
608
|
-
return fn
|
|
609
|
-
|
|
610
|
-
def _run_sdk(self, system, user, tools, permission_mode, model=None):
|
|
611
|
-
"""Strategy 1/2 executor: an isolated generation via the agent's SDK.
|
|
612
|
-
Each call is a NEW session — the only context is the payload we pass
|
|
613
|
-
(system + user). Returns (structured, raw).
|
|
614
|
-
|
|
615
|
-
Agent-agnostic, like `spawn`: the engine has NO built-in SDK and no
|
|
616
|
-
default agent — EVERYTHING SDK-specific comes from the loaded
|
|
617
|
-
profile's `sdk` block (agent.json):
|
|
618
|
-
module / package what to import; installed on demand when missing
|
|
619
|
-
(--sdk-install or sdk.auto_install)
|
|
620
|
-
adapter REQUIRED — a Python file (path relative to the
|
|
621
|
-
profile; shared per-SDK adapters live in
|
|
622
|
-
.aw-spec/orchestrator/adapters/) exporting
|
|
623
|
-
generate(request) -> reply text; it owns that
|
|
624
|
-
SDK's calling convention (sync or async)
|
|
625
|
-
Everything is imported lazily so the engine stays stdlib-only
|
|
626
|
-
otherwise."""
|
|
627
|
-
if not self.profile:
|
|
628
|
-
raise Stop("strategy 'sdk'/'sdk-apply' needs --agent-profile — the "
|
|
629
|
-
"profile's sdk block declares the SDK to use (the engine "
|
|
630
|
-
"has no default agent)")
|
|
631
|
-
sdk_cfg = self.profile.get("sdk") or {}
|
|
632
|
-
module_name = sdk_cfg.get("module")
|
|
633
|
-
package_name = sdk_cfg.get("package")
|
|
634
|
-
adapter_rel = sdk_cfg.get("adapter")
|
|
635
|
-
missing = [k for k, v in (("module", module_name), ("package", package_name),
|
|
636
|
-
("adapter", adapter_rel)) if not v]
|
|
637
|
-
if missing:
|
|
638
|
-
raise Stop(f"agent profile '{self.profile.get('name', '?')}': sdk block "
|
|
639
|
-
f"is missing {', '.join(missing)} — strategy 'sdk'/'sdk-apply' "
|
|
640
|
-
"is fully profile-driven (no built-in default); declare the "
|
|
641
|
-
"sdk module/package to import and the adapter file that "
|
|
642
|
-
"drives it")
|
|
643
|
-
|
|
644
|
-
if self.opts.dry_run:
|
|
645
|
-
self.emit(f"[dry-run] sdk would run: module={module_name} "
|
|
646
|
-
f"package={package_name} adapter={adapter_rel} "
|
|
647
|
-
f"system={len(system)} chars user={len(user)} chars "
|
|
648
|
-
f"tools={tools or '(none)'} permission_mode={permission_mode}")
|
|
649
|
-
raise Stop("dry-run: sdk request shown above (nothing executed)")
|
|
650
|
-
|
|
651
|
-
self._ensure_sdk_module(module_name, package_name)
|
|
652
|
-
generate = self._load_sdk_adapter(adapter_rel)
|
|
653
|
-
import asyncio
|
|
654
|
-
request = {"system": system, "user": user, "tools": tools,
|
|
655
|
-
"permission_mode": permission_mode,
|
|
656
|
-
"model": model or self.opts.model or None, "sdk": sdk_cfg}
|
|
657
|
-
try:
|
|
658
|
-
out = generate(request)
|
|
659
|
-
raw = asyncio.run(out) if asyncio.iscoroutine(out) else out
|
|
660
|
-
except Stop:
|
|
661
|
-
raise
|
|
662
|
-
except Exception as e:
|
|
663
|
-
raise Stop(f"sdk adapter '{adapter_rel}' generation failed: {e}")
|
|
664
|
-
if not isinstance(raw, str):
|
|
665
|
-
raise Stop(f"sdk adapter '{adapter_rel}' generate(request) must return "
|
|
666
|
-
"the reply text (str)")
|
|
667
|
-
return _extract_json(raw), raw
|
|
668
|
-
|
|
669
|
-
# -- actions --------------------------------------------------------------
|
|
670
|
-
def act_set(self, step):
|
|
671
|
-
assigns = step.get("vars") or step.get("parameters") or {}
|
|
672
|
-
for k, v in assigns.items():
|
|
673
|
-
self.vars[k] = self.resolve_value(v)
|
|
674
|
-
self.emit(f"[set] {list(assigns.keys())}")
|
|
675
|
-
|
|
676
|
-
def act_build_context(self, step):
|
|
677
|
-
ctx = list(step.get("context", []) or [])
|
|
678
|
-
# dynamic context: append a var holding a list of paths — e.g. the files a
|
|
679
|
-
# prior agent_generate reported it wrote (since IT, not we, chose them).
|
|
680
|
-
cf = step.get("context_from")
|
|
681
|
-
if cf:
|
|
682
|
-
ctx += list(self.vars.get(cf) or [])
|
|
683
|
-
self.envelope = {"system": step.get("system", []) or [], "context": ctx}
|
|
684
|
-
for k, v in (step.get("vars", {}) or {}).items():
|
|
685
|
-
self.vars[k] = self.resolve(v)
|
|
686
|
-
sysn = len(_norm_files(self.envelope["system"]))
|
|
687
|
-
self.emit(f"[build_context] system={sysn} file(s) context={len(ctx)} file(s)")
|
|
688
|
-
|
|
689
|
-
def act_agent_generate(self, step):
|
|
690
|
-
tools = step.get("tools", "Read,Write,Edit,Bash")
|
|
691
|
-
permission_mode = step.get("permission_mode", "acceptEdits")
|
|
692
|
-
task = self.resolve(step.get("task", ""))
|
|
693
|
-
strat = self._effective_strategy(step)
|
|
694
|
-
# checkpoint replay: this step was already fulfilled (any strategy) —
|
|
695
|
-
# bind its cached structured result instead of executing again.
|
|
696
|
-
ck = self._key(step)
|
|
697
|
-
if self.checkpoint and ck in self.cache:
|
|
698
|
-
structured = self.cache[ck]
|
|
699
|
-
self.last_result = structured
|
|
700
|
-
self._bind(step, structured)
|
|
701
|
-
self.emit(f"[agent_generate] (cached) result={json.dumps(structured)[:200]}")
|
|
702
|
-
return
|
|
703
|
-
# The interpreter does NOT dictate output paths. The generating runtime +
|
|
704
|
-
# LLM decide where files go (per the loaded folder-structure/architecture
|
|
705
|
-
# standards) and report back the paths.
|
|
706
|
-
if strat == "sdk-apply" and self.opts.transport != "mock":
|
|
707
|
-
# Strategy 2: generation is read-only — it DECIDES paths + content;
|
|
708
|
-
# the invoking agent applies them afterwards (via suspend "apply").
|
|
709
|
-
instr = task + (
|
|
710
|
-
"\n\nDecide the correct file path(s) yourself by following the "
|
|
711
|
-
"loaded standards (folder-structure / architecture rules) and "
|
|
712
|
-
"the project's existing layout, but DO NOT write any files — "
|
|
713
|
-
"you have no write tools. Output the COMPLETE intended content "
|
|
714
|
-
"of every file in your reply: one fenced code block per file, "
|
|
715
|
-
"each immediately preceded by its path, so another process can "
|
|
716
|
-
"apply it verbatim."
|
|
717
|
-
'\n\nEnd your reply with a fenced block:\n```json\n{"status":"ok",'
|
|
718
|
-
'"planned_files":["<path>"],"summary":"<one line>"}\n```')
|
|
719
|
-
else:
|
|
720
|
-
instr = task + (
|
|
721
|
-
"\n\nDecide the correct file path(s) yourself by following the loaded "
|
|
722
|
-
"standards (folder-structure / architecture rules) and the project's "
|
|
723
|
-
"existing layout. Create or edit them with your Write/Edit tools."
|
|
724
|
-
'\n\nWhen done, end your reply with a fenced block listing the paths YOU '
|
|
725
|
-
'chose to write:\n```json\n{"status":"ok","files":["<path you wrote>"],'
|
|
726
|
-
'"summary":"<one line>"}\n```')
|
|
727
|
-
system, sys_files = self._assemble_system()
|
|
728
|
-
user, ctx_files = self._assemble_user(instr)
|
|
729
|
-
self._print_envelope("agent_generate", system, sys_files, user, ctx_files, tools, strat)
|
|
730
|
-
|
|
731
|
-
if self.opts.transport == "mock":
|
|
732
|
-
structured = self._mock_generate(task)
|
|
733
|
-
elif strat == "agent":
|
|
734
|
-
self._suspend("agent_generate", ck, system, user, tools, permission_mode)
|
|
735
|
-
elif strat == "sdk":
|
|
736
|
-
structured, _ = self._run_sdk(system, user, tools, permission_mode,
|
|
737
|
-
model=step.get("model"))
|
|
738
|
-
elif strat == "sdk-apply":
|
|
739
|
-
_, raw = self._run_sdk(system, user, "Read", "readonly",
|
|
740
|
-
model=step.get("model"))
|
|
741
|
-
self._suspend_apply(ck, raw, tools, permission_mode)
|
|
742
|
-
else: # spawn
|
|
743
|
-
structured, _ = self._run_fresh(system, user, tools, permission_mode,
|
|
744
|
-
model=step.get("model"))
|
|
745
|
-
self.last_result = structured
|
|
746
|
-
if self.checkpoint:
|
|
747
|
-
self.cache[ck] = structured
|
|
748
|
-
self._bind(step, structured)
|
|
749
|
-
self.emit(f"[agent_generate] strategy={strat or 'mock'} "
|
|
750
|
-
f"result={json.dumps(structured)[:200]}")
|
|
751
|
-
|
|
752
|
-
def act_agent_review(self, step):
|
|
753
|
-
# Review the files the generation actually produced. Prefer a var holding
|
|
754
|
-
# the reported paths (review_of_var) over any statically-listed review_of.
|
|
755
|
-
if step.get("review_of_var"):
|
|
756
|
-
review_of = list(self.vars.get(step["review_of_var"]) or [])
|
|
757
|
-
else:
|
|
758
|
-
review_of = [self.resolve(r) for r in (step.get("review_of", []) or [])]
|
|
759
|
-
task = self.resolve(step.get("task", ""))
|
|
760
|
-
instr = task + "\n\nFiles under review:\n" + "\n".join(f"- {r}" for r in review_of)
|
|
761
|
-
instr += ('\n\nEnd your reply with a fenced block:\n'
|
|
762
|
-
'```json\n{"pass":true,"issues":[]}\n```')
|
|
763
|
-
# review is read-only: put the files to review INTO context, tools=Read only
|
|
764
|
-
# (mutate BEFORE the checkpoint-cache branch so a replayed run rebuilds
|
|
765
|
-
# the exact same envelope state for the steps that follow)
|
|
766
|
-
self.envelope["context"] = list(self.envelope["context"]) + review_of
|
|
767
|
-
strat = self._effective_strategy(step)
|
|
768
|
-
ck = self._key(step)
|
|
769
|
-
if self.checkpoint and ck in self.cache:
|
|
770
|
-
structured = self.cache[ck]
|
|
771
|
-
self.last_result = structured
|
|
772
|
-
self._bind(step, structured)
|
|
773
|
-
self.emit(f"[agent_review] (cached) result={json.dumps(structured)[:200]}")
|
|
774
|
-
return
|
|
775
|
-
system, sys_files = self._assemble_system()
|
|
776
|
-
user, ctx_files = self._assemble_user(instr)
|
|
777
|
-
self._print_envelope("agent_review", system, sys_files, user, ctx_files, "Read", strat)
|
|
778
|
-
|
|
779
|
-
if self.opts.transport == "mock":
|
|
780
|
-
# mock: pass if every reviewed file exists
|
|
781
|
-
ok = all(os.path.exists(r) for r in review_of)
|
|
782
|
-
structured = {"pass": ok, "issues": ([] if ok else ["missing output file(s)"])}
|
|
783
|
-
elif strat == "agent":
|
|
784
|
-
self._suspend("agent_review", ck, system, user, "Read", "readonly")
|
|
785
|
-
elif strat in ("sdk", "sdk-apply"):
|
|
786
|
-
# review has no apply phase — sdk-apply degrades to a plain sdk read
|
|
787
|
-
structured, _ = self._run_sdk(system, user, "Read", "readonly",
|
|
788
|
-
model=step.get("model"))
|
|
789
|
-
else: # spawn
|
|
790
|
-
structured, _ = self._run_fresh(system, user, "Read", "acceptEdits",
|
|
791
|
-
model=step.get("model"))
|
|
792
|
-
self.last_result = structured
|
|
793
|
-
if self.checkpoint:
|
|
794
|
-
self.cache[ck] = structured
|
|
795
|
-
self._bind(step, structured)
|
|
796
|
-
self.emit(f"[agent_review] strategy={strat or 'mock'} "
|
|
797
|
-
f"result={json.dumps(structured)[:200]}")
|
|
798
|
-
|
|
799
|
-
def _mock_generate(self, task):
|
|
800
|
-
# No output path was specified — the mock stands in for the runtime/LLM
|
|
801
|
-
# DECIDING one. Here it lifts a name from the first `backtick` token in the
|
|
802
|
-
# task and places it under the out_dir workspace hint. A real fresh agent
|
|
803
|
-
# would instead derive the path from the loaded folder-structure standard.
|
|
804
|
-
first = task.strip().splitlines()[0] if task.strip() else ""
|
|
805
|
-
m = re.search(r"`([A-Za-z_][A-Za-z0-9_]*)`", task)
|
|
806
|
-
name = m.group(1) if m else "GeneratedComponent"
|
|
807
|
-
out_dir = self.vars.get("out_dir") or ".aw-spec/orchestrator/.demo-out"
|
|
808
|
-
path = os.path.join(out_dir, name + ".cs")
|
|
809
|
-
os.makedirs(os.path.dirname(path) or ".", exist_ok=True)
|
|
810
|
-
with open(path, "w", encoding="utf-8") as fh:
|
|
811
|
-
fh.write(
|
|
812
|
-
"// [mock fresh-agent output]\n"
|
|
813
|
-
"// The PATH was decided by the fresh runtime (mock stand-in), NOT\n"
|
|
814
|
-
"// specified by the interpreter.\n"
|
|
815
|
-
f"// saw only: system={[self.resolve(p) for p,_ in _norm_files(self.envelope['system'])]}\n"
|
|
816
|
-
f"// context={[self.resolve(p) for p,_ in _norm_files(self.envelope['context'])]}\n"
|
|
817
|
-
f"// task: {first}\n")
|
|
818
|
-
return {"status": "ok", "files": [path], "summary": f"mock implemented: {first}"}
|
|
819
|
-
|
|
820
|
-
# -- F1: deterministic scripts ---------------------------------------------
|
|
821
|
-
def _script_path(self, ref):
|
|
822
|
-
variants = self.scripts.get(ref) or {}
|
|
823
|
-
return variants.get("ps1" if self.opts.os == "windows" else "sh")
|
|
824
|
-
|
|
825
|
-
def act_execute_script(self, step):
|
|
826
|
-
"""F1 — run a shared deterministic script (decl `execute_script` port).
|
|
827
|
-
Top-level `scripts:` declares {ref: {sh:…, ps1:…}}; --os picks the
|
|
828
|
-
variant. stdout's LAST line must be JSON, bound via outputs[]. Results
|
|
829
|
-
enter the replay cache, so scripts never re-fire across resumes.
|
|
830
|
-
Scripts run for real even under the mock transport (they are the
|
|
831
|
-
deterministic, model-free part of every skill)."""
|
|
832
|
-
p = step.get("parameters") or step
|
|
833
|
-
ref = (p.get("script") or {}).get("ref")
|
|
834
|
-
script = self._script_path(ref)
|
|
835
|
-
if not script or not os.path.exists(script):
|
|
836
|
-
raise Stop(f"step {step.get('id')}: script not found for ref '{ref}': {script}")
|
|
837
|
-
sub = p.get("subcommand")
|
|
838
|
-
args = [self.resolve(a) for a in (p.get("args") or [])]
|
|
839
|
-
ck = self._key(step)
|
|
840
|
-
if self.checkpoint and ck in self.cache:
|
|
841
|
-
self._bind(step, self.cache[ck])
|
|
842
|
-
self.emit(f"[execute_script] {sub} (cached)")
|
|
843
|
-
return
|
|
844
|
-
runner = (["pwsh", "-NoProfile", "-File", script]
|
|
845
|
-
if self.opts.os == "windows" else ["bash", script])
|
|
846
|
-
self.emit(f"[execute_script] {sub} {' '.join(args)}")
|
|
847
|
-
if self.opts.dry_run:
|
|
848
|
-
self._bind(step, {})
|
|
849
|
-
return
|
|
850
|
-
proc = subprocess.run(runner + [sub] + args, capture_output=True, text=True)
|
|
851
|
-
if proc.returncode != 0:
|
|
852
|
-
if p.get("on_error") == "continue":
|
|
853
|
-
self.emit(f"[execute_script] {sub} failed (continuing): "
|
|
854
|
-
f"{proc.stderr.strip()[:200]}")
|
|
855
|
-
self._bind(step, {})
|
|
856
|
-
return
|
|
857
|
-
raise Stop(f"script failed ({sub}): {proc.stderr.strip()[:500]}")
|
|
858
|
-
try:
|
|
859
|
-
data = json.loads(proc.stdout.strip().splitlines()[-1])
|
|
860
|
-
except Exception as e:
|
|
861
|
-
raise Stop(f"script did not return JSON ({sub}): {proc.stdout!r} ({e})")
|
|
862
|
-
self._bind(step, data)
|
|
863
|
-
if self.checkpoint:
|
|
864
|
-
self.cache[ck] = data
|
|
865
|
-
self.emit(f"[execute_script] -> {json.dumps(data)[:200]}")
|
|
866
|
-
|
|
867
|
-
# -- F2: gates ---------------------------------------------------------------
|
|
868
|
-
def act_verify_context(self, step):
|
|
869
|
-
"""F2 — the four decl gate forms: required_files (+allow_missing),
|
|
870
|
-
require_non_empty (+on_empty), require_true (+on_false),
|
|
871
|
-
require_value/equals (+on_mismatch). Messages interpolate {{vars}}."""
|
|
872
|
-
p = step.get("parameters") or step
|
|
873
|
-
if p.get("require_non_empty"):
|
|
874
|
-
var = p["require_non_empty"]
|
|
875
|
-
val = self.vars.get(var)
|
|
876
|
-
if not val:
|
|
877
|
-
msg = self.resolve(p.get("on_empty_message", f"{var} is empty"))
|
|
878
|
-
self.emit(f"[verify_context] {var} is empty: {msg}")
|
|
879
|
-
if p.get("on_empty") == "stop":
|
|
880
|
-
raise Stop(msg)
|
|
881
|
-
else:
|
|
882
|
-
self.emit(f"[verify_context] {var} non-empty")
|
|
883
|
-
if p.get("require_true"):
|
|
884
|
-
var = p["require_true"]
|
|
885
|
-
v = self.vars.get(var)
|
|
886
|
-
truthy = (v.strip() not in ("", "false", "False", "None", "null", "[]")
|
|
887
|
-
if isinstance(v, str) else bool(v))
|
|
888
|
-
if not truthy:
|
|
889
|
-
msg = self.resolve(p.get("on_false_message", f"{var} is not satisfied"))
|
|
890
|
-
self.emit(f"[verify_context] gate failed ({var}): {msg}")
|
|
891
|
-
if p.get("on_false", "stop") == "stop":
|
|
892
|
-
raise Stop(msg)
|
|
893
|
-
else:
|
|
894
|
-
self.emit(f"[verify_context] gate ok ({var})")
|
|
895
|
-
if p.get("require_value"):
|
|
896
|
-
var = p["require_value"]
|
|
897
|
-
actual = str(self.vars.get(var, ""))
|
|
898
|
-
expected = str(self.resolve(p.get("equals", "")))
|
|
899
|
-
if actual != expected:
|
|
900
|
-
msg = self.resolve(p.get("on_mismatch_message", f"{var} != {expected}"))
|
|
901
|
-
self.emit(f"[verify_context] value gate failed ({var}={actual!r}): {msg}")
|
|
902
|
-
if p.get("on_mismatch", "stop") == "stop":
|
|
903
|
-
raise Stop(msg)
|
|
904
|
-
else:
|
|
905
|
-
self.emit(f"[verify_context] value gate ok ({var}=={expected})")
|
|
906
|
-
for rf in p.get("required_files", []) or []:
|
|
907
|
-
rf = self.resolve(rf)
|
|
908
|
-
if rf in self.loaded or os.path.exists(rf):
|
|
909
|
-
self.emit(f"[verify_context] present {rf}")
|
|
910
|
-
elif p.get("allow_missing"):
|
|
911
|
-
self.emit(f"[verify_context] missing (allowed) {rf}")
|
|
912
|
-
else:
|
|
913
|
-
raise Stop(f"required file not in context: {rf}")
|
|
914
|
-
|
|
915
|
-
# -- F4: sticky standards ------------------------------------------------------
|
|
916
|
-
def act_load_standards(self, step):
|
|
917
|
-
"""F4 — append files to the sticky standards set (dedup; `optional:`
|
|
918
|
-
tolerated). _assemble_system prepends the set to EVERY payload, so a
|
|
919
|
-
skill loads its standards once (decl load_context → system per D3)."""
|
|
920
|
-
p = step.get("parameters") or step
|
|
921
|
-
files = p.get("files") or []
|
|
922
|
-
if p.get("files_from_variable"):
|
|
923
|
-
files = [{"path": x} for x in (self.vars.get(p["files_from_variable"]) or [])]
|
|
924
|
-
step_optional = p.get("optional", False)
|
|
925
|
-
have = {self.resolve(pp) for pp, _ in _norm_files(self.standards)}
|
|
926
|
-
for f in files:
|
|
927
|
-
path = self.resolve(f["path"] if isinstance(f, dict) else f)
|
|
928
|
-
optional = f.get("optional", step_optional) if isinstance(f, dict) else step_optional
|
|
929
|
-
if path in have:
|
|
930
|
-
self.emit(f"[load_standards] dedup skip {path}")
|
|
931
|
-
continue
|
|
932
|
-
if os.path.exists(path):
|
|
933
|
-
self.standards.append({"path": path})
|
|
934
|
-
have.add(path)
|
|
935
|
-
self.emit(f"[load_standards] loaded {path}")
|
|
936
|
-
elif optional:
|
|
937
|
-
self.emit(f"[load_standards] optional missing, skip {path}")
|
|
938
|
-
else:
|
|
939
|
-
raise Stop(f"required standards file missing: {path}")
|
|
940
|
-
|
|
941
|
-
# -- F5: authored artifacts ------------------------------------------------
|
|
942
|
-
def _write_target(self, target, content):
|
|
943
|
-
os.makedirs(os.path.dirname(target) or ".", exist_ok=True)
|
|
944
|
-
with open(target, "w", encoding="utf-8") as fh:
|
|
945
|
-
fh.write(content)
|
|
946
|
-
|
|
947
|
-
def _mock_author(self, target, task):
|
|
948
|
-
sysf = [self.resolve(p) for p, _ in _norm_files(self.envelope["system"])]
|
|
949
|
-
ctxf = [self.resolve(p) for p, _ in _norm_files(self.envelope["context"])]
|
|
950
|
-
return ("<!-- authored by aw-orchestrator MOCK transport -->\n"
|
|
951
|
-
f"<!-- target: {target} -->\n"
|
|
952
|
-
f"<!-- envelope: system={sysf} context={ctxf} -->\n"
|
|
953
|
-
f"<!-- task chars: {len(task)} -->\n\n# (mock content)\n")
|
|
954
|
-
|
|
955
|
-
def act_agent_author(self, step):
|
|
956
|
-
"""F5 — authored artifact (decl `llm_generate` port): the INTERPRETER
|
|
957
|
-
owns output_target.path (scaffold scripts created it). Generation is
|
|
958
|
-
READ-ONLY under EVERY strategy — the returned content is written by
|
|
959
|
-
the ENGINE, so sdk-apply needs no apply phase and degrades to sdk.
|
|
960
|
-
The replay cache stores the raw content (idempotent rewrites)."""
|
|
961
|
-
p = step.get("parameters") or step
|
|
962
|
-
target = self.resolve((p.get("output_target") or {}).get("path", ""))
|
|
963
|
-
if not target:
|
|
964
|
-
raise Stop(f"step {step.get('id')}: agent_author needs output_target.path")
|
|
965
|
-
task = self.resolve(p.get("task") or p.get("prompt") or "")
|
|
966
|
-
extra = [c for c in (self.resolve_value(p.get("context")) or []) if isinstance(c, (str, dict))]
|
|
967
|
-
strat = self._effective_strategy(step)
|
|
968
|
-
ck = self._key(step)
|
|
969
|
-
if self.checkpoint and ck in self.cache:
|
|
970
|
-
self._write_target(target, self.cache[ck])
|
|
971
|
-
structured = {"status": "ok", "files": [target], "summary": f"authored {target}"}
|
|
972
|
-
self.last_result = structured
|
|
973
|
-
self._bind(step, structured)
|
|
974
|
-
self.emit(f"[agent_author] (cached) rewrote {target}")
|
|
975
|
-
return
|
|
976
|
-
instr = task + (
|
|
977
|
-
f"\n\nAuthor the COMPLETE content of the file `{target}`. You have "
|
|
978
|
-
"no write tools — return the content itself. Reply with ONLY the "
|
|
979
|
-
"full file content: no commentary, no surrounding code fence.")
|
|
980
|
-
system, sys_files = self._assemble_system(p.get("include_standards", True))
|
|
981
|
-
user, ctx_files = self._assemble_user(instr, extra=extra)
|
|
982
|
-
self._print_envelope("agent_author", system, sys_files, user, ctx_files, "Read", strat)
|
|
983
|
-
if self.opts.transport == "mock":
|
|
984
|
-
content = self._mock_author(target, task)
|
|
985
|
-
elif strat == "agent":
|
|
986
|
-
self._suspend("author", ck, system, user, "Read", "readonly",
|
|
987
|
-
instructions=(
|
|
988
|
-
"Author this artifact YOURSELF, in this conversation. "
|
|
989
|
-
"Read system_path + user_path as your ONLY standards "
|
|
990
|
-
"and context. Write the COMPLETE file content — the "
|
|
991
|
-
"content ONLY, no commentary, no code fence — to "
|
|
992
|
-
"response_path, then re-run the resume command. Do "
|
|
993
|
-
"NOT write the target file yourself; the interpreter "
|
|
994
|
-
"does that."),
|
|
995
|
-
extra={"target": target})
|
|
996
|
-
else:
|
|
997
|
-
run = self._run_sdk if strat in ("sdk", "sdk-apply") else self._run_fresh
|
|
998
|
-
_, raw = run(system, user, "Read", "readonly", model=step.get("model"))
|
|
999
|
-
content = _strip_fence(raw)
|
|
1000
|
-
if not content.strip():
|
|
1001
|
-
raise Stop(f"step {step.get('id')}: agent_author returned empty content")
|
|
1002
|
-
if self.opts.dry_run:
|
|
1003
|
-
self.emit(f"[agent_author] dry-run: not writing {target}")
|
|
1004
|
-
return
|
|
1005
|
-
self._write_target(target, content)
|
|
1006
|
-
structured = {"status": "ok", "files": [target], "summary": f"authored {target}"}
|
|
1007
|
-
self.last_result = structured
|
|
1008
|
-
if self.checkpoint:
|
|
1009
|
-
self.cache[ck] = content
|
|
1010
|
-
self._bind(step, structured)
|
|
1011
|
-
self.emit(f"[agent_author] strategy={strat or 'mock'} wrote {target} "
|
|
1012
|
-
f"({len(content)} chars)")
|
|
1013
|
-
|
|
1014
|
-
# -- F6: structured queries --------------------------------------------------
|
|
1015
|
-
def _structured_fresh(self, step, strat, system, user):
|
|
1016
|
-
"""spawn/sdk executor for JSON-returning steps, with ONE malformed-JSON
|
|
1017
|
-
retry (agent strategy instead re-suspends with a note on resume)."""
|
|
1018
|
-
run = self._run_sdk if strat in ("sdk", "sdk-apply") else self._run_fresh
|
|
1019
|
-
structured, _ = run(system, user, "Read", "readonly", model=step.get("model"))
|
|
1020
|
-
if not structured:
|
|
1021
|
-
self.emit(f"[retry] {step.get('id')}: no parseable ```json fence — asking once more")
|
|
1022
|
-
user2 = user + ("\n\nYour previous reply could not be parsed. Return "
|
|
1023
|
-
"ONLY the required ```json fenced block, nothing else.")
|
|
1024
|
-
structured, _ = run(system, user2, "Read", "readonly", model=step.get("model"))
|
|
1025
|
-
if not structured:
|
|
1026
|
-
raise Stop(f"step {step.get('id')}: fresh agent returned no parseable JSON")
|
|
1027
|
-
return structured
|
|
1028
|
-
|
|
1029
|
-
def act_agent_query(self, step):
|
|
1030
|
-
"""F6 — generic READ-ONLY structured query (decl `llm_query` port): the
|
|
1031
|
-
caller states the JSON shape (schema:), the reply's trailing ```json
|
|
1032
|
-
fence is bound via outputs[]. agent_review stays the {pass,issues}
|
|
1033
|
-
specialization of this. `mock:` supplies the mock-transport result."""
|
|
1034
|
-
p = step.get("parameters") or step
|
|
1035
|
-
task = self.resolve(p.get("task") or p.get("prompt") or "")
|
|
1036
|
-
extra = [c for c in (self.resolve_value(p.get("context")) or []) if isinstance(c, (str, dict))]
|
|
1037
|
-
strat = self._effective_strategy(step)
|
|
1038
|
-
ck = self._key(step)
|
|
1039
|
-
if self.checkpoint and ck in self.cache:
|
|
1040
|
-
structured = self.cache[ck]
|
|
1041
|
-
self.last_result = structured
|
|
1042
|
-
self._bind(step, structured)
|
|
1043
|
-
self.emit(f"[agent_query] (cached) result={json.dumps(structured)[:200]}")
|
|
1044
|
-
return
|
|
1045
|
-
schema = p.get("schema")
|
|
1046
|
-
hint = json.dumps(schema) if isinstance(schema, (dict, list)) else (schema or "{ … }")
|
|
1047
|
-
instr = task + ("\n\nDo not modify any files. End your reply with a "
|
|
1048
|
-
"fenced block containing ONLY the JSON result, shaped "
|
|
1049
|
-
"like:\n```json\n" + hint + "\n```")
|
|
1050
|
-
system, sys_files = self._assemble_system(p.get("include_standards", True))
|
|
1051
|
-
user, ctx_files = self._assemble_user(instr, extra=extra)
|
|
1052
|
-
self._print_envelope("agent_query", system, sys_files, user, ctx_files, "Read", strat)
|
|
1053
|
-
if self.opts.transport == "mock":
|
|
1054
|
-
structured = self.resolve_value(p.get("mock")) or {}
|
|
1055
|
-
elif strat == "agent":
|
|
1056
|
-
self._suspend("agent_query", ck, system, user, "Read", "readonly")
|
|
1057
|
-
else:
|
|
1058
|
-
structured = self._structured_fresh(step, strat, system, user)
|
|
1059
|
-
self.last_result = structured
|
|
1060
|
-
if self.checkpoint:
|
|
1061
|
-
self.cache[ck] = structured
|
|
1062
|
-
self._bind(step, structured)
|
|
1063
|
-
self.emit(f"[agent_query] strategy={strat or 'mock'} "
|
|
1064
|
-
f"result={json.dumps(structured)[:200]}")
|
|
1065
|
-
|
|
1066
|
-
# -- F7: HITL (D1 — the checkpoint is the interaction channel under EVERY
|
|
1067
|
-
# strategy; --answer/--select/--non-interactive prefill for headless runs)
|
|
1068
|
-
def _prompt_choice(self, prompt, options):
|
|
1069
|
-
print(prompt)
|
|
1070
|
-
for i, o in enumerate(options, 1):
|
|
1071
|
-
print(f" {i}) {o}")
|
|
1072
|
-
try:
|
|
1073
|
-
raw = input("> ").strip()
|
|
1074
|
-
except EOFError:
|
|
1075
|
-
return None
|
|
1076
|
-
if raw.isdigit() and 1 <= int(raw) <= len(options):
|
|
1077
|
-
return options[int(raw) - 1]
|
|
1078
|
-
return raw if raw in options else None
|
|
1079
|
-
|
|
1080
|
-
def act_ask(self, step):
|
|
1081
|
-
"""F7 — single/multi-select question. Answer sources, in order: replay
|
|
1082
|
-
cache → --answer key=value → --non-interactive default (first option;
|
|
1083
|
-
all for multi) → checkpoint suspend kind 'ask' (the launcher asks the
|
|
1084
|
-
USER) → interactive stdin. Binds answer_var (default 'answer')."""
|
|
1085
|
-
p = step.get("parameters") or step
|
|
1086
|
-
prompt = self.resolve(p.get("prompt", "?"))
|
|
1087
|
-
if p.get("options_from"):
|
|
1088
|
-
options = self.resolve_value(p["options_from"]) or []
|
|
1089
|
-
elif p.get("options_from_variable"):
|
|
1090
|
-
options = self.vars.get(p["options_from_variable"]) or []
|
|
1091
|
-
else:
|
|
1092
|
-
options = [self.resolve(o) for o in (p.get("options") or [])]
|
|
1093
|
-
multi = bool(p.get("multi"))
|
|
1094
|
-
key = self.resolve(p["answer_key"]) if p.get("answer_key") else step.get("id", "")
|
|
1095
|
-
var = p.get("answer_var", "answer")
|
|
1096
|
-
ck = self._key(step)
|
|
1097
|
-
if self.checkpoint and ck in self.cache:
|
|
1098
|
-
self.vars[var] = self.cache[ck]
|
|
1099
|
-
self.emit(f"[ask] {key} -> {self.cache[ck]} (cached)")
|
|
1100
|
-
return
|
|
1101
|
-
raw = self.opts.answers.get(key)
|
|
1102
|
-
if raw is None and self.opts.non_interactive:
|
|
1103
|
-
raw = ",".join(str(o) for o in options) if multi else \
|
|
1104
|
-
(options[0] if options else "")
|
|
1105
|
-
if raw is None and self.checkpoint:
|
|
1106
|
-
self._suspend_simple({
|
|
1107
|
-
"kind": "ask", "key": key, "step": ck,
|
|
1108
|
-
"prompt": prompt, "options": options, "multi": multi,
|
|
1109
|
-
"instructions": (
|
|
1110
|
-
"Ask the USER this question (the options are the allowed "
|
|
1111
|
-
"choices" + ("; several may be picked" if multi else "") +
|
|
1112
|
-
"). Write the answer as PLAIN TEXT" +
|
|
1113
|
-
(" (comma-separated)" if multi else "") +
|
|
1114
|
-
" to response_path, then re-run the resume command.")})
|
|
1115
|
-
if raw is None:
|
|
1116
|
-
raw = self._prompt_choice(prompt, options) if options else \
|
|
1117
|
-
input(prompt + " ").strip()
|
|
1118
|
-
if raw is None:
|
|
1119
|
-
raise Stop("question cancelled")
|
|
1120
|
-
answer = [s.strip() for s in str(raw).split(",") if s.strip()] if multi else raw
|
|
1121
|
-
self.vars[var] = answer
|
|
1122
|
-
if self.checkpoint:
|
|
1123
|
-
self.cache[ck] = answer
|
|
1124
|
-
self.emit(f"[ask] {key} -> {answer}")
|
|
1125
|
-
|
|
1126
|
-
def act_user_select(self, step):
|
|
1127
|
-
"""F7 — pick ONE option from a variable. Order: replay cache →
|
|
1128
|
-
preselect_from (direct-argument fast path) → --select stepid=value →
|
|
1129
|
-
single_option_confirm → --non-interactive (first) → checkpoint suspend
|
|
1130
|
-
kind 'select' → interactive stdin. Binds `selection` + outputs[]."""
|
|
1131
|
-
p = step.get("parameters") or step
|
|
1132
|
-
options = self.vars.get(p.get("options_from_variable")) or []
|
|
1133
|
-
pre = p.get("preselect_from")
|
|
1134
|
-
pre_val = self.vars.get(pre) if pre else None
|
|
1135
|
-
ck = self._key(step)
|
|
1136
|
-
if self.checkpoint and ck in self.cache:
|
|
1137
|
-
chosen = self.cache[ck]
|
|
1138
|
-
elif pre_val and pre_val in options:
|
|
1139
|
-
chosen = pre_val
|
|
1140
|
-
elif self.opts.selects.get(step.get("id")):
|
|
1141
|
-
chosen = self.opts.selects[step["id"]]
|
|
1142
|
-
elif len(options) == 1 and p.get("single_option_confirm"):
|
|
1143
|
-
chosen = options[0]
|
|
1144
|
-
elif self.opts.non_interactive:
|
|
1145
|
-
chosen = options[0] if options else None
|
|
1146
|
-
elif self.checkpoint:
|
|
1147
|
-
self._suspend_simple({
|
|
1148
|
-
"kind": "select", "step": ck,
|
|
1149
|
-
"prompt": self.resolve(p.get("prompt", "Select:")),
|
|
1150
|
-
"options": options,
|
|
1151
|
-
"instructions": (
|
|
1152
|
-
"Ask the USER to pick ONE of the options. Write the chosen "
|
|
1153
|
-
"value as plain text to response_path, then re-run the "
|
|
1154
|
-
"resume command.")})
|
|
1155
|
-
else:
|
|
1156
|
-
chosen = self._prompt_choice(self.resolve(p.get("prompt", "Select:")), options)
|
|
1157
|
-
if not chosen:
|
|
1158
|
-
raise Stop("selection cancelled")
|
|
1159
|
-
self.vars["selection"] = chosen
|
|
1160
|
-
for out in step.get("outputs", []) or []:
|
|
1161
|
-
src = out["from"]
|
|
1162
|
-
self.vars[out["name"]] = chosen if src == "selection" else self.vars.get(src)
|
|
1163
|
-
if self.checkpoint:
|
|
1164
|
-
self.cache[ck] = chosen
|
|
1165
|
-
self.emit(f"[user_select] chosen: {chosen}")
|
|
1166
|
-
|
|
1167
|
-
# -- F8: hooks ----------------------------------------------------------------
|
|
1168
|
-
def act_dispatch_hooks(self, step):
|
|
1169
|
-
"""F8 — hand the resolved hooks (slash commands of the INVOKING agent)
|
|
1170
|
-
to the launcher via suspend kind 'hook' (D4 phase 1). Without a
|
|
1171
|
-
checkpoint (mock/CI) the intent is only logged, like run-skill. Cached
|
|
1172
|
-
so hooks never re-fire across resumes."""
|
|
1173
|
-
p = step.get("parameters") or step
|
|
1174
|
-
ck = self._key(step)
|
|
1175
|
-
if self.checkpoint and ck in self.cache:
|
|
1176
|
-
self.emit("[dispatch_hooks] (cached)")
|
|
1177
|
-
return
|
|
1178
|
-
hooks = self.vars.get(p.get("hooks_from_variable")) or []
|
|
1179
|
-
if not hooks:
|
|
1180
|
-
self.emit("[dispatch_hooks] none")
|
|
1181
|
-
if self.checkpoint:
|
|
1182
|
-
self.cache[ck] = True
|
|
1183
|
-
return
|
|
1184
|
-
for h in hooks:
|
|
1185
|
-
kind = "optional" if h.get("optional") else "mandatory"
|
|
1186
|
-
self.emit(f"[dispatch_hooks] {kind}: {h.get('slash')} ({h.get('extension')})")
|
|
1187
|
-
if self.checkpoint and self.opts.transport != "mock":
|
|
1188
|
-
self._suspend_simple({
|
|
1189
|
-
"kind": "hook", "step": ck, "hooks": hooks,
|
|
1190
|
-
"instructions": (
|
|
1191
|
-
"Run each MANDATORY hook (a slash command of YOUR agent) in "
|
|
1192
|
-
"this conversation; optional hooks are at your discretion. "
|
|
1193
|
-
"Then write 'done' to response_path and re-run the resume "
|
|
1194
|
-
"command.")})
|
|
1195
|
-
if self.checkpoint:
|
|
1196
|
-
self.cache[ck] = True
|
|
1197
|
-
|
|
1198
|
-
# -- F9: modules --------------------------------------------------------------
|
|
1199
|
-
def act_include(self, step):
|
|
1200
|
-
"""F9 — run a module .orch.yaml inline: shares vars, envelope, sticky
|
|
1201
|
-
standards, the caller's scripts map and the replay cache. `with:` seeds
|
|
1202
|
-
module inputs; cache keys are namespaced under the include id. Stop
|
|
1203
|
-
propagates to the caller (module gates stop the whole skill)."""
|
|
1204
|
-
module_path = self.resolve(step.get("module", ""))
|
|
1205
|
-
for k, v in (step.get("with", {}) or {}).items():
|
|
1206
|
-
self.vars[k] = self.resolve_value(v)
|
|
1207
|
-
try:
|
|
1208
|
-
with open(module_path, encoding="utf-8") as fh:
|
|
1209
|
-
mod = load_yaml(fh.read())
|
|
1210
|
-
except OSError as e:
|
|
1211
|
-
raise Stop(f"include: module not found: {module_path} ({e})")
|
|
1212
|
-
# module-declared scripts extend the caller's map (caller wins on clash)
|
|
1213
|
-
for ref, variants in (mod.get("scripts", {}) or {}).items():
|
|
1214
|
-
self.scripts.setdefault(ref, variants)
|
|
1215
|
-
self.standards += list(mod.get("standards", []) or [])
|
|
1216
|
-
self.emit(f"[include] {module_path}")
|
|
1217
|
-
self.key_stack.append("inc:" + step.get("id", "include"))
|
|
1218
|
-
try:
|
|
1219
|
-
self._run_steps(mod.get("workflow", []) or [])
|
|
1220
|
-
finally:
|
|
1221
|
-
self.key_stack.pop()
|
|
1222
|
-
|
|
1223
|
-
# -- runner ---------------------------------------------------------------
|
|
1224
|
-
def _run_steps(self, steps):
|
|
1225
|
-
for step in steps:
|
|
1226
|
-
cond = step.get("condition")
|
|
1227
|
-
if cond is not None and not self._cond(cond):
|
|
1228
|
-
self.emit(f"[skip] {step.get('id')} (condition false)")
|
|
1229
|
-
continue
|
|
1230
|
-
action = step.get("action")
|
|
1231
|
-
if action == "break":
|
|
1232
|
-
return "break"
|
|
1233
|
-
if action == "loop":
|
|
1234
|
-
self._run_loop(step)
|
|
1235
|
-
continue
|
|
1236
|
-
fn = getattr(self, f"act_{action}", None)
|
|
1237
|
-
if fn is None:
|
|
1238
|
-
raise Stop(f"unknown action: {action}")
|
|
1239
|
-
fn(step)
|
|
1240
|
-
return None
|
|
1241
|
-
|
|
1242
|
-
def _run_loop(self, step):
|
|
1243
|
-
"""foreach (`over:` a list var, binding `as:`/`<as>_index`) or while
|
|
1244
|
-
(`while:` + `max_iterations`). F3: the foreach items come through
|
|
1245
|
-
resolve_value, so `over: "{{list}}"` yields the RAW list."""
|
|
1246
|
-
substeps = step.get("steps", []) or []
|
|
1247
|
-
loop_id = step.get("id", "loop")
|
|
1248
|
-
as_name = step.get("as", "item")
|
|
1249
|
-
max_it = step.get("max_iterations")
|
|
1250
|
-
if "over" in step:
|
|
1251
|
-
items = self.resolve_value(step["over"]) or []
|
|
1252
|
-
if isinstance(items, str):
|
|
1253
|
-
try:
|
|
1254
|
-
items = json.loads(items)
|
|
1255
|
-
except Exception:
|
|
1256
|
-
pass
|
|
1257
|
-
if not isinstance(items, list):
|
|
1258
|
-
raise Stop(f"loop '{loop_id}': 'over' is not a list: {step.get('over')}")
|
|
1259
|
-
for idx, item in enumerate(items):
|
|
1260
|
-
if max_it is not None and idx >= max_it:
|
|
1261
|
-
break
|
|
1262
|
-
self.vars[as_name] = item
|
|
1263
|
-
self.vars[as_name + "_index"] = idx
|
|
1264
|
-
self.vars["iteration"] = idx
|
|
1265
|
-
self.emit(f"[loop] {loop_id} item {idx}")
|
|
1266
|
-
# iteration-scoped key prefix: each pass gets its own cache entries
|
|
1267
|
-
self.key_stack.append(f"{loop_id}#{idx}")
|
|
1268
|
-
try:
|
|
1269
|
-
broke = self._run_steps(substeps) == "break"
|
|
1270
|
-
finally:
|
|
1271
|
-
self.key_stack.pop()
|
|
1272
|
-
if broke:
|
|
1273
|
-
break
|
|
1274
|
-
return
|
|
1275
|
-
max_it = 5 if max_it is None else max_it
|
|
1276
|
-
i = 0
|
|
1277
|
-
while self._cond(step.get("while", "true")):
|
|
1278
|
-
if i >= max_it:
|
|
1279
|
-
self.emit(f"[loop] {loop_id} hit max_iterations={max_it}")
|
|
1280
|
-
break
|
|
1281
|
-
self.vars["iteration"] = i
|
|
1282
|
-
self.vars[as_name + "_index"] = i
|
|
1283
|
-
self.emit(f"[loop] {loop_id} iteration {i}")
|
|
1284
|
-
# iteration-scoped key prefix: each pass gets its own cache entries
|
|
1285
|
-
self.key_stack.append(f"{loop_id}#{i}")
|
|
1286
|
-
try:
|
|
1287
|
-
broke = self._run_steps(substeps) == "break"
|
|
1288
|
-
finally:
|
|
1289
|
-
self.key_stack.pop()
|
|
1290
|
-
if broke:
|
|
1291
|
-
break
|
|
1292
|
-
i += 1
|
|
1293
|
-
|
|
1294
|
-
def _bind_inputs(self):
|
|
1295
|
-
"""F10 — bind the workflow's declared `inputs:` to --input (decl
|
|
1296
|
-
USER_INPUT shape). The bound value is persisted in the checkpoint
|
|
1297
|
-
state, so resumes keep it without re-passing --input."""
|
|
1298
|
-
for name, spec in (self.wf.get("inputs") or {}).items():
|
|
1299
|
-
if not isinstance(spec, dict) or not spec.get("bind"):
|
|
1300
|
-
continue
|
|
1301
|
-
bind = spec["bind"]
|
|
1302
|
-
if getattr(self.opts, "input", ""):
|
|
1303
|
-
self.vars[bind] = self.opts.input
|
|
1304
|
-
else:
|
|
1305
|
-
self.vars.setdefault(bind, "")
|
|
1306
|
-
if spec.get("required") and not self.vars.get(bind):
|
|
1307
|
-
raise Stop(f"input '{name}' is required — pass --input \"<text>\"")
|
|
1308
|
-
|
|
1309
|
-
def run(self):
|
|
1310
|
-
self._bind_inputs()
|
|
1311
|
-
self._run_steps(self.wf.get("workflow", []) or [])
|
|
1312
|
-
return {"status": "ok", "outputs": self.resolve(self.wf.get("outputs", {}) or {}),
|
|
1313
|
-
"vars": self.vars, "missing_required": list(self.missing_required)}
|
|
1314
|
-
|
|
1315
|
-
|
|
1316
|
-
# ─────────────────────────────────────────────────────────────────────────────
|
|
1317
|
-
# F11 — static validator (--validate): lints a workflow without executing it.
|
|
1318
|
-
# ─────────────────────────────────────────────────────────────────────────────
|
|
1319
|
-
DIALECT = "aw-spec.orchestrator/v1"
|
|
1320
|
-
KNOWN_ACTIONS = {
|
|
1321
|
-
"set", "build_context", "load_standards", "agent_generate", "agent_review",
|
|
1322
|
-
"agent_author", "agent_query", "execute_script", "verify_context",
|
|
1323
|
-
"dispatch_hooks", "user_select", "ask", "loop", "break", "include",
|
|
1324
|
-
}
|
|
1325
|
-
STRATEGIES = {"sdk", "sdk-apply", "spawn", "agent"}
|
|
1326
|
-
# engine-produced vars a workflow may reference without declaring: the loop/ask
|
|
1327
|
-
# bindings, plus the context-assembly miss report (_collect_files, item H).
|
|
1328
|
-
_SPECIAL_VARS = {"selection", "answer", "iteration",
|
|
1329
|
-
"context_missing", "context_missing_count"}
|
|
1330
|
-
|
|
1331
|
-
|
|
1332
|
-
def _refs_in(v):
|
|
1333
|
-
found = set()
|
|
1334
|
-
if isinstance(v, str):
|
|
1335
|
-
for m in re.findall(r"\{\{\s*([^}]+?)\s*\}\}", v):
|
|
1336
|
-
found.add(m.strip())
|
|
1337
|
-
elif isinstance(v, list):
|
|
1338
|
-
for x in v:
|
|
1339
|
-
found |= _refs_in(x)
|
|
1340
|
-
elif isinstance(v, dict):
|
|
1341
|
-
for x in v.values():
|
|
1342
|
-
found |= _refs_in(x)
|
|
1343
|
-
return found
|
|
1344
|
-
|
|
1345
|
-
|
|
1346
|
-
def validate_workflow(wf, base_dir):
|
|
1347
|
-
errors, warnings = [], []
|
|
1348
|
-
if not isinstance(wf, dict):
|
|
1349
|
-
return (["top-level YAML is not a mapping"], [])
|
|
1350
|
-
steps = wf.get("workflow")
|
|
1351
|
-
if not isinstance(steps, list) or not steps:
|
|
1352
|
-
errors.append("missing or empty 'workflow' list")
|
|
1353
|
-
steps = steps if isinstance(steps, list) else []
|
|
1354
|
-
if wf.get("dialect") not in (None, DIALECT):
|
|
1355
|
-
warnings.append(f"dialect is '{wf.get('dialect')}' — this engine implements {DIALECT}")
|
|
1356
|
-
elif not wf.get("dialect"):
|
|
1357
|
-
warnings.append(f"no 'dialect:' header (expected {DIALECT})")
|
|
1358
|
-
scripts = wf.get("scripts", {}) or {}
|
|
1359
|
-
|
|
1360
|
-
def walk(ss):
|
|
1361
|
-
for s in ss or []:
|
|
1362
|
-
if isinstance(s, dict):
|
|
1363
|
-
yield s
|
|
1364
|
-
if s.get("action") == "loop":
|
|
1365
|
-
yield from walk(s.get("steps"))
|
|
1366
|
-
|
|
1367
|
-
def _params(s):
|
|
1368
|
-
return s.get("parameters") or s
|
|
1369
|
-
|
|
1370
|
-
# pre-scan everything that produces variables, so ordering never false-alarms
|
|
1371
|
-
produced = set(_SPECIAL_VARS) | set((wf.get("vars") or {}).keys())
|
|
1372
|
-
for _, spec in (wf.get("inputs") or {}).items():
|
|
1373
|
-
if isinstance(spec, dict) and spec.get("bind"):
|
|
1374
|
-
produced.add(spec["bind"])
|
|
1375
|
-
|
|
1376
|
-
def scan_produced(ss):
|
|
1377
|
-
for s in walk(ss):
|
|
1378
|
-
for o in s.get("outputs") or []:
|
|
1379
|
-
if isinstance(o, dict) and o.get("name"):
|
|
1380
|
-
produced.add(o["name"])
|
|
1381
|
-
p = _params(s)
|
|
1382
|
-
act = s.get("action")
|
|
1383
|
-
if act == "ask":
|
|
1384
|
-
produced.add(p.get("answer_var", "answer"))
|
|
1385
|
-
elif act == "set":
|
|
1386
|
-
produced.update((s.get("vars") or s.get("parameters") or {}).keys())
|
|
1387
|
-
elif act == "build_context":
|
|
1388
|
-
produced.update((s.get("vars") or {}).keys())
|
|
1389
|
-
elif act == "loop":
|
|
1390
|
-
produced.add(s.get("as", "item"))
|
|
1391
|
-
produced.add(s.get("as", "item") + "_index")
|
|
1392
|
-
elif act == "include":
|
|
1393
|
-
produced.update((s.get("with") or {}).keys())
|
|
1394
|
-
mpath = s.get("module", "")
|
|
1395
|
-
for cand in (mpath, os.path.join(base_dir, mpath)):
|
|
1396
|
-
if mpath and os.path.exists(cand):
|
|
1397
|
-
try:
|
|
1398
|
-
mwf = load_yaml(open(cand, encoding="utf-8").read())
|
|
1399
|
-
scan_produced(mwf.get("workflow", []))
|
|
1400
|
-
except Exception:
|
|
1401
|
-
pass
|
|
1402
|
-
break
|
|
1403
|
-
|
|
1404
|
-
scan_produced(steps)
|
|
1405
|
-
|
|
1406
|
-
seen_ids = set()
|
|
1407
|
-
for step in walk(steps):
|
|
1408
|
-
loc = step.get("id") or "step"
|
|
1409
|
-
sid = step.get("id")
|
|
1410
|
-
if not sid:
|
|
1411
|
-
warnings.append(f"{loc}: missing id")
|
|
1412
|
-
elif sid in seen_ids:
|
|
1413
|
-
errors.append(f"duplicate step id: {sid}")
|
|
1414
|
-
else:
|
|
1415
|
-
seen_ids.add(sid)
|
|
1416
|
-
|
|
1417
|
-
action = step.get("action")
|
|
1418
|
-
if action not in KNOWN_ACTIONS:
|
|
1419
|
-
errors.append(f"{loc}: unknown action '{action}' "
|
|
1420
|
-
f"(known: {', '.join(sorted(KNOWN_ACTIONS))})")
|
|
1421
|
-
if step.get("strategy") and step["strategy"] not in STRATEGIES:
|
|
1422
|
-
errors.append(f"{loc}: unknown strategy '{step['strategy']}' "
|
|
1423
|
-
f"(known: {', '.join(sorted(STRATEGIES))})")
|
|
1424
|
-
p = _params(step)
|
|
1425
|
-
|
|
1426
|
-
if action == "loop" and "over" not in step and "while" not in step:
|
|
1427
|
-
errors.append(f"{loc}: loop needs 'over:' or 'while:'")
|
|
1428
|
-
if action == "ask" and not (p.get("options_from") or p.get("options_from_variable")
|
|
1429
|
-
or p.get("options")):
|
|
1430
|
-
warnings.append(f"{loc}: ask has no options source (free-text input)")
|
|
1431
|
-
if action == "user_select" and not p.get("options_from_variable"):
|
|
1432
|
-
errors.append(f"{loc}: user_select needs options_from_variable")
|
|
1433
|
-
if action == "include":
|
|
1434
|
-
m = step.get("module")
|
|
1435
|
-
if not m:
|
|
1436
|
-
errors.append(f"{loc}: include needs 'module'")
|
|
1437
|
-
elif not (os.path.exists(m) or os.path.exists(os.path.join(base_dir, m))):
|
|
1438
|
-
warnings.append(f"{loc}: include module not found on disk: {m}")
|
|
1439
|
-
if action == "agent_query" and not step.get("outputs"):
|
|
1440
|
-
warnings.append(f"{loc}: agent_query has no outputs[] to bind")
|
|
1441
|
-
if action == "agent_author" and not (p.get("output_target") or {}).get("path"):
|
|
1442
|
-
errors.append(f"{loc}: agent_author needs output_target.path")
|
|
1443
|
-
if action in ("agent_generate", "agent_author", "agent_query") \
|
|
1444
|
-
and not (p.get("task") or p.get("prompt")):
|
|
1445
|
-
warnings.append(f"{loc}: {action} has an empty task")
|
|
1446
|
-
if action == "execute_script":
|
|
1447
|
-
ref = (p.get("script") or {}).get("ref")
|
|
1448
|
-
if not ref:
|
|
1449
|
-
errors.append(f"{loc}: execute_script needs script.ref")
|
|
1450
|
-
elif ref not in scripts:
|
|
1451
|
-
# a module declares no scripts: map — the ref resolves against
|
|
1452
|
-
# the INCLUDING caller's map, so standalone it's only a warning
|
|
1453
|
-
(warnings if not scripts else errors).append(
|
|
1454
|
-
f"{loc}: script ref '{ref}' not declared in top-level scripts:"
|
|
1455
|
-
+ ("" if scripts else " (module? the including caller must declare it)"))
|
|
1456
|
-
else:
|
|
1457
|
-
variants = scripts.get(ref) or {}
|
|
1458
|
-
if not any(variants.get(v) and (os.path.exists(variants[v]) or
|
|
1459
|
-
os.path.exists(os.path.join(base_dir, variants[v])))
|
|
1460
|
-
for v in ("sh", "ps1")):
|
|
1461
|
-
warnings.append(f"{loc}: no script variant file found on disk for ref '{ref}'")
|
|
1462
|
-
if not p.get("subcommand"):
|
|
1463
|
-
errors.append(f"{loc}: execute_script needs subcommand")
|
|
1464
|
-
|
|
1465
|
-
used = _refs_in(steps) | _refs_in(wf.get("outputs", {}))
|
|
1466
|
-
for name in sorted(used):
|
|
1467
|
-
if name.split(".")[0] not in produced:
|
|
1468
|
-
warnings.append(f"variable {{{{{name}}}}} referenced but never produced")
|
|
1469
|
-
return errors, warnings
|
|
1470
|
-
|
|
1471
|
-
|
|
1472
|
-
def _print_suspended(pending, opts):
|
|
1473
|
-
"""Checkpoint transport: hand the pending request to the INVOKING agent."""
|
|
1474
|
-
if opts.json:
|
|
1475
|
-
print(json.dumps({"status": "suspended", "pending": pending}, indent=2))
|
|
1476
|
-
else:
|
|
1477
|
-
print("\n=== SUSPENDED (invoking agent: fulfill this, then resume) ===")
|
|
1478
|
-
print(json.dumps(pending, indent=2))
|
|
1479
|
-
|
|
1480
|
-
|
|
1481
|
-
def _clear_checkpoint(opts):
|
|
1482
|
-
"""Terminal end (ok/stopped): remove the state file and its sidecars."""
|
|
1483
|
-
if not opts.checkpoint:
|
|
1484
|
-
return
|
|
1485
|
-
for p in (opts.checkpoint, opts.checkpoint + ".system.md",
|
|
1486
|
-
opts.checkpoint + ".user.md", opts.checkpoint + ".artifact.md",
|
|
1487
|
-
opts.checkpoint + ".response.md"):
|
|
1488
|
-
if os.path.exists(p):
|
|
1489
|
-
os.remove(p)
|
|
1490
|
-
|
|
1491
|
-
|
|
1492
|
-
def main(argv=None):
|
|
1493
|
-
ap = argparse.ArgumentParser(description="aw-orchestrator — interpreter-owned context, fresh-Claude execution")
|
|
1494
|
-
ap.add_argument("workflow")
|
|
1495
|
-
ap.add_argument("--transport", choices=["mock", "real"], default="mock",
|
|
1496
|
-
help="mock = no model call (prints the payloads); "
|
|
1497
|
-
"real = spawn the agent profile's CLI per generation")
|
|
1498
|
-
ap.add_argument("--agent-profile", default="", dest="agent_profile",
|
|
1499
|
-
help="path to an agent transport profile JSON "
|
|
1500
|
-
"(.claude/.agents/.kilocode/...); implies --transport real")
|
|
1501
|
-
ap.add_argument("--isolation", choices=["bare", "fresh"], default="bare",
|
|
1502
|
-
help="selects the profile's isolation.<mode> args (bare = strongest)")
|
|
1503
|
-
ap.add_argument("--model", default="", help="model alias/name for the fresh agent")
|
|
1504
|
-
ap.add_argument("--budget", default="", help="value for the profile's budget_flag, if it declares one")
|
|
1505
|
-
ap.add_argument("--dry-run", action="store_true", dest="dry_run",
|
|
1506
|
-
help="print the exact command the profile would run, without executing")
|
|
1507
|
-
ap.add_argument("--sdk-install", action="store_true", dest="sdk_install",
|
|
1508
|
-
help="if the profile's sdk module isn't importable, pip install "
|
|
1509
|
-
"the package its sdk block declares, then continue")
|
|
1510
|
-
ap.add_argument("--strategy", choices=["sdk", "sdk-apply", "spawn", "agent"],
|
|
1511
|
-
default=None,
|
|
1512
|
-
help="execution strategy for generations (per-step `strategy:` "
|
|
1513
|
-
"overrides): sdk = isolated SDK instance generates + implements; "
|
|
1514
|
-
"sdk-apply = isolated SDK generates, INVOKING agent applies "
|
|
1515
|
-
"(needs --checkpoint); spawn = fresh CLI process per generation "
|
|
1516
|
-
"(needs --agent-profile); agent = invoking agent fulfills via "
|
|
1517
|
-
"suspend/resume, zero new instances (needs --checkpoint). "
|
|
1518
|
-
"Omitted: inferred from --agent-profile/--checkpoint, else mock")
|
|
1519
|
-
ap.add_argument("--checkpoint", metavar="STATEFILE",
|
|
1520
|
-
help="suspend/resume state file (run-skill.py method). Required by "
|
|
1521
|
-
"strategies 'agent' and 'sdk-apply'; at each handoff the "
|
|
1522
|
-
"interpreter writes the payload/artifact sidecars next to "
|
|
1523
|
-
"STATEFILE and exits 2 so the INVOKING agent fulfills the "
|
|
1524
|
-
"pending request (writes STATEFILE.response.md) and re-runs "
|
|
1525
|
-
"the same command")
|
|
1526
|
-
ap.add_argument("--var", action="append", default=[], help="k=value (seed vars)")
|
|
1527
|
-
ap.add_argument("--input", default="",
|
|
1528
|
-
help="positional user input, bound via the workflow's inputs: block")
|
|
1529
|
-
ap.add_argument("--answer", action="append", default=[],
|
|
1530
|
-
help="key=value prefill for ask steps (key = answer_key, else step id)")
|
|
1531
|
-
ap.add_argument("--select", action="append", default=[],
|
|
1532
|
-
help="stepid=value prefill for user_select steps")
|
|
1533
|
-
ap.add_argument("--non-interactive", action="store_true", dest="non_interactive",
|
|
1534
|
-
help="asks answer with the first option (all options for multi), "
|
|
1535
|
-
"selects take the first option — CI/smoke mode")
|
|
1536
|
-
default_os = "windows" if os.name == "nt" else "posix"
|
|
1537
|
-
ap.add_argument("--os", choices=["posix", "windows"], default=default_os,
|
|
1538
|
-
help="script variant for execute_script (sh vs ps1)")
|
|
1539
|
-
ap.add_argument("--validate", action="store_true",
|
|
1540
|
-
help="lint the workflow (static checks) without executing")
|
|
1541
|
-
ap.add_argument("--show-full", action="store_true", help="print entire payload, no truncation")
|
|
1542
|
-
ap.add_argument("--json", action="store_true")
|
|
1543
|
-
opts = ap.parse_args(argv)
|
|
1544
|
-
opts.vars = dict(v.split("=", 1) for v in opts.var if "=" in v)
|
|
1545
|
-
opts.answers = dict(s.split("=", 1) for s in opts.answer if "=" in s)
|
|
1546
|
-
opts.selects = dict(s.split("=", 1) for s in opts.select if "=" in s)
|
|
1547
|
-
|
|
1548
|
-
# ---- F11: --validate lints and exits (never executes) --------------------
|
|
1549
|
-
if opts.validate:
|
|
1550
|
-
try:
|
|
1551
|
-
with open(opts.workflow, encoding="utf-8") as fh:
|
|
1552
|
-
wf = load_yaml(fh.read())
|
|
1553
|
-
except Exception as e:
|
|
1554
|
-
print(json.dumps({"status": "invalid", "errors": [f"parse: {e}"]})
|
|
1555
|
-
if opts.json else f"ERROR: parse: {e}")
|
|
1556
|
-
return 1
|
|
1557
|
-
errors, warnings = validate_workflow(
|
|
1558
|
-
wf, os.path.dirname(os.path.abspath(opts.workflow)))
|
|
1559
|
-
if opts.json:
|
|
1560
|
-
print(json.dumps({"status": "valid" if not errors else "invalid",
|
|
1561
|
-
"errors": errors, "warnings": warnings}, indent=2))
|
|
1562
|
-
else:
|
|
1563
|
-
for w in warnings:
|
|
1564
|
-
print("WARN: ", w)
|
|
1565
|
-
for e in errors:
|
|
1566
|
-
print("ERROR:", e)
|
|
1567
|
-
print(f"\n{opts.workflow}: {len(errors)} error(s), {len(warnings)} warning(s)")
|
|
1568
|
-
return 1 if errors else 0
|
|
1569
|
-
|
|
1570
|
-
# ---- checkpoint mode: load prior state + ingest the agent's response ----
|
|
1571
|
-
# (before profile/strategy resolution, so a bare `--checkpoint` resume
|
|
1572
|
-
# restores the original run's profile/strategy/model/os and prefills)
|
|
1573
|
-
opts._cache = {}
|
|
1574
|
-
if opts.checkpoint and os.path.exists(opts.checkpoint):
|
|
1575
|
-
with open(opts.checkpoint, encoding="utf-8") as fh:
|
|
1576
|
-
state = json.load(fh)
|
|
1577
|
-
opts._cache = state.get("cache", {})
|
|
1578
|
-
# replay with the original seeds; freshly passed flags override/extend
|
|
1579
|
-
merged = dict(state.get("vars", {}))
|
|
1580
|
-
merged.update(opts.vars)
|
|
1581
|
-
opts.vars = merged
|
|
1582
|
-
if not opts.strategy and state.get("strategy"):
|
|
1583
|
-
opts.strategy = state["strategy"]
|
|
1584
|
-
if not opts.model and state.get("model"):
|
|
1585
|
-
opts.model = state["model"]
|
|
1586
|
-
if not opts.agent_profile and state.get("agent_profile"):
|
|
1587
|
-
opts.agent_profile = state["agent_profile"]
|
|
1588
|
-
if opts.isolation == "bare" and state.get("isolation"):
|
|
1589
|
-
opts.isolation = state["isolation"]
|
|
1590
|
-
if opts.os == default_os and state.get("os"):
|
|
1591
|
-
opts.os = state["os"]
|
|
1592
|
-
if not opts.input:
|
|
1593
|
-
opts.input = state.get("input", "")
|
|
1594
|
-
merged = dict(state.get("answers", {}))
|
|
1595
|
-
merged.update(opts.answers)
|
|
1596
|
-
opts.answers = merged
|
|
1597
|
-
merged = dict(state.get("selects", {}))
|
|
1598
|
-
merged.update(opts.selects)
|
|
1599
|
-
opts.selects = merged
|
|
1600
|
-
pending = state.get("pending")
|
|
1601
|
-
if pending:
|
|
1602
|
-
rp = pending.get("response_path", "")
|
|
1603
|
-
if rp and os.path.exists(rp):
|
|
1604
|
-
with open(rp, encoding="utf-8") as fh:
|
|
1605
|
-
raw = fh.read()
|
|
1606
|
-
os.remove(rp)
|
|
1607
|
-
# ingest by suspend kind (the pending.kind protocol)
|
|
1608
|
-
kind = pending.get("kind", "")
|
|
1609
|
-
note = None
|
|
1610
|
-
if kind == "ask":
|
|
1611
|
-
val = raw.strip()
|
|
1612
|
-
parsed = ([s.strip() for s in val.split(",") if s.strip()]
|
|
1613
|
-
if pending.get("multi") else val)
|
|
1614
|
-
elif kind == "select":
|
|
1615
|
-
parsed = raw.strip()
|
|
1616
|
-
if not parsed:
|
|
1617
|
-
note = "response_path was empty — write the chosen option's value"
|
|
1618
|
-
elif kind == "author":
|
|
1619
|
-
parsed = raw
|
|
1620
|
-
if not raw.strip():
|
|
1621
|
-
note = ("response_path was empty — write the complete "
|
|
1622
|
-
"file content, then re-run the resume command")
|
|
1623
|
-
elif kind == "hook":
|
|
1624
|
-
parsed = True
|
|
1625
|
-
else: # agent_generate / agent_review / agent_query / apply
|
|
1626
|
-
parsed = _extract_json(raw)
|
|
1627
|
-
if not parsed:
|
|
1628
|
-
note = ("response_path had no parseable ```json fence — "
|
|
1629
|
-
"rewrite it ending with the required fenced block, "
|
|
1630
|
-
"then re-run the resume command")
|
|
1631
|
-
if note:
|
|
1632
|
-
pending["note"] = note
|
|
1633
|
-
_print_suspended(pending, opts)
|
|
1634
|
-
return 2
|
|
1635
|
-
opts._cache[pending["step"]] = parsed
|
|
1636
|
-
else:
|
|
1637
|
-
# no response yet — re-emit the same pending request
|
|
1638
|
-
_print_suspended(pending, opts)
|
|
1639
|
-
return 2
|
|
1640
|
-
|
|
1641
|
-
opts.profile = None
|
|
1642
|
-
opts.profile_dir = ""
|
|
1643
|
-
if opts.agent_profile:
|
|
1644
|
-
with open(opts.agent_profile, encoding="utf-8") as fh:
|
|
1645
|
-
opts.profile = json.load(fh)
|
|
1646
|
-
# sdk adapters are declared relative to the profile file
|
|
1647
|
-
opts.profile_dir = os.path.dirname(os.path.abspath(opts.agent_profile))
|
|
1648
|
-
if opts.transport == "mock":
|
|
1649
|
-
opts.transport = "real"
|
|
1650
|
-
|
|
1651
|
-
# ---- strategy resolution -------------------------------------------------
|
|
1652
|
-
# explicit --strategy wins; else legacy inference from the flags given.
|
|
1653
|
-
if not opts.strategy:
|
|
1654
|
-
if opts.profile:
|
|
1655
|
-
opts.strategy = "spawn"
|
|
1656
|
-
elif opts.checkpoint:
|
|
1657
|
-
opts.strategy = "agent"
|
|
1658
|
-
if opts.strategy and opts.transport == "mock":
|
|
1659
|
-
opts.transport = "real"
|
|
1660
|
-
if opts.strategy == "spawn" and not opts.profile:
|
|
1661
|
-
print("ERROR: strategy 'spawn' requires --agent-profile <profile.json>")
|
|
1662
|
-
return 2
|
|
1663
|
-
if opts.strategy in ("sdk", "sdk-apply") and not opts.profile:
|
|
1664
|
-
print(f"ERROR: strategy '{opts.strategy}' requires --agent-profile "
|
|
1665
|
-
"<profile.json> — its sdk block declares the SDK module/package "
|
|
1666
|
-
"and adapter (the engine has no default agent)")
|
|
1667
|
-
return 2
|
|
1668
|
-
if opts.strategy in ("agent", "sdk-apply") and not opts.checkpoint:
|
|
1669
|
-
print(f"ERROR: strategy '{opts.strategy}' requires --checkpoint <statefile>")
|
|
1670
|
-
return 2
|
|
1671
|
-
if opts.transport == "real" and not opts.strategy:
|
|
1672
|
-
print("ERROR: --transport real needs --strategy (or --agent-profile / --checkpoint)")
|
|
1673
|
-
return 2
|
|
1674
|
-
|
|
1675
|
-
with open(opts.workflow, encoding="utf-8") as fh:
|
|
1676
|
-
wf = load_yaml(fh.read())
|
|
1677
|
-
|
|
1678
|
-
orch = Orchestrator(wf, opts)
|
|
1679
|
-
try:
|
|
1680
|
-
result = orch.run()
|
|
1681
|
-
_clear_checkpoint(opts)
|
|
1682
|
-
except Suspend as e:
|
|
1683
|
-
# persist state (vars seeds + strategy/model + replay cache + the
|
|
1684
|
-
# pending request) and hand control back to the INVOKING agent.
|
|
1685
|
-
# Exit 2 = "suspended".
|
|
1686
|
-
with open(opts.checkpoint, "w", encoding="utf-8") as fh:
|
|
1687
|
-
json.dump({"vars": opts.vars, "strategy": opts.strategy,
|
|
1688
|
-
"model": opts.model, "input": opts.input,
|
|
1689
|
-
"answers": opts.answers, "selects": opts.selects,
|
|
1690
|
-
"agent_profile": opts.agent_profile,
|
|
1691
|
-
"isolation": opts.isolation, "os": opts.os,
|
|
1692
|
-
"cache": orch.cache, "pending": e.request}, fh, indent=2)
|
|
1693
|
-
_print_suspended(e.request, opts)
|
|
1694
|
-
return 2
|
|
1695
|
-
except Stop as e:
|
|
1696
|
-
result = {"status": "stopped", "reason": str(e), "vars": orch.vars,
|
|
1697
|
-
"missing_required": list(orch.missing_required)}
|
|
1698
|
-
_clear_checkpoint(opts)
|
|
1699
|
-
|
|
1700
|
-
if opts.json:
|
|
1701
|
-
print(json.dumps(result, indent=2))
|
|
1702
|
-
else:
|
|
1703
|
-
print(f"\n=== {result['status'].upper()} ===")
|
|
1704
|
-
if result.get("reason"):
|
|
1705
|
-
print(result["reason"])
|
|
1706
|
-
# item H: a required system:/context: file that never made it into a
|
|
1707
|
-
# payload is a silent quality hole — the model was told it had rules it
|
|
1708
|
-
# never received. Repeat it at the end, where the operator will see it.
|
|
1709
|
-
if result.get("missing_required"):
|
|
1710
|
-
print(f"\nWARNING: {len(result['missing_required'])} required context "
|
|
1711
|
-
"file(s) were declared but MISSING — every payload that named "
|
|
1712
|
-
"them was assembled without them:")
|
|
1713
|
-
for m in result["missing_required"]:
|
|
1714
|
-
print(f" - {m}")
|
|
1715
|
-
if result.get("outputs"):
|
|
1716
|
-
print(json.dumps(result["outputs"], indent=2))
|
|
1717
|
-
return 0 if result["status"] == "ok" else 1
|
|
1718
|
-
|
|
1719
|
-
|
|
1720
|
-
if __name__ == "__main__":
|
|
1721
|
-
sys.exit(main())
|