@sabaiway/agent-workflow-kit 1.43.0 → 1.45.0
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/CHANGELOG.md +99 -0
- package/README.md +8 -4
- package/SKILL.md +20 -8
- package/bridges/antigravity-cli-bridge/SKILL.md +1 -1
- package/bridges/antigravity-cli-bridge/bin/agy-review.sh +87 -16
- package/bridges/antigravity-cli-bridge/bin/agy-review.test.mjs +3 -0
- package/bridges/antigravity-cli-bridge/bin/agy.test.mjs +4 -2
- package/bridges/antigravity-cli-bridge/capability.json +3 -2
- package/bridges/codex-cli-bridge/SKILL.md +1 -1
- package/bridges/codex-cli-bridge/bin/codex-exec.test.mjs +3 -0
- package/bridges/codex-cli-bridge/bin/codex-review.sh +90 -19
- package/bridges/codex-cli-bridge/bin/codex-review.test.mjs +4 -0
- package/bridges/codex-cli-bridge/capability.json +3 -2
- package/bridges/codex-cli-bridge/references/driving-codex.md +3 -2
- package/bridges/codex-cli-bridge/references/sandbox-and-flags.md +3 -2
- package/capability.json +1 -1
- package/package.json +1 -1
- package/references/modes/autonomy-doctor.md +21 -0
- package/references/modes/bootstrap.md +1 -1
- package/references/modes/doc-parity.md +2 -1
- package/references/modes/grounding.md +8 -7
- package/references/modes/recommendations.md +14 -0
- package/references/modes/review-state.md +1 -1
- package/references/modes/sandbox-masks.md +15 -0
- package/references/modes/upgrade.md +10 -7
- package/references/modes/velocity.md +13 -2
- package/references/shared/composition-handoff.md +21 -16
- package/references/shared/report-footer.md +5 -1
- package/references/templates/AGENTS.md +2 -1
- package/references/templates/autonomy.json +3 -0
- package/tools/autonomy-config.mjs +13 -3
- package/tools/autonomy-doctor.mjs +488 -0
- package/tools/commands.mjs +24 -3
- package/tools/delegation.mjs +9 -5
- package/tools/detect-backends.mjs +2 -2
- package/tools/doc-parity.mjs +31 -2
- package/tools/engine-source.mjs +6 -0
- package/tools/family-registry.mjs +21 -7
- package/tools/fold-completeness-run.mjs +5 -0
- package/tools/grounding.mjs +139 -22
- package/tools/inject-methodology.mjs +117 -28
- package/tools/manifest/schema.md +20 -0
- package/tools/manifest/validate.mjs +26 -0
- package/tools/procedures.mjs +61 -8
- package/tools/recipes.mjs +94 -15
- package/tools/recommendations.mjs +469 -0
- package/tools/review-ledger-write.mjs +14 -0
- package/tools/review-ledger.mjs +3 -2
- package/tools/review-state.mjs +101 -24
- package/tools/run-gates.mjs +3 -0
- package/tools/sandbox-masks.mjs +370 -0
- package/tools/set-recipe.mjs +13 -1
- package/tools/velocity-profile.mjs +231 -24
package/tools/commands.mjs
CHANGED
|
@@ -28,7 +28,7 @@ const invocationOf = (token) => `${BARE_INVOCATION}${token ? ` ${token}` : ''}`;
|
|
|
28
28
|
// ── kinds ────────────────────────────────────────────────────────────────────────
|
|
29
29
|
// read-only — never writes, never commits, never runs a subscription CLI.
|
|
30
30
|
// writer — writes files (a project deployment or a settings/skill placement).
|
|
31
|
-
// guarded —
|
|
31
|
+
// guarded — consent-gated destructive/privileged actions (dry-run-first + explicit consent).
|
|
32
32
|
// project-exec — the kit itself writes nothing, but the mode RUNS the project's own declared
|
|
33
33
|
// commands with the caller's privileges (the `gates` runner) — honest-tagged so a
|
|
34
34
|
// user never reads "read-only" on a surface that executes their gate matrix.
|
|
@@ -98,6 +98,13 @@ const CATALOG = [
|
|
|
98
98
|
kind: READ_ONLY,
|
|
99
99
|
oneLine: 'List every command, grouped, marking each as read-only or as one that makes changes.',
|
|
100
100
|
},
|
|
101
|
+
{
|
|
102
|
+
key: 'recommendations',
|
|
103
|
+
invocation: invocationOf('recommendations'),
|
|
104
|
+
group: 'Inspect',
|
|
105
|
+
kind: READ_ONLY,
|
|
106
|
+
oneLine: 'See what in this deployment is configured sub-optimally — each item with a one-line benefit and the exact consent-gated command that applies it; renders even when everything is optimal.',
|
|
107
|
+
},
|
|
101
108
|
{
|
|
102
109
|
key: 'gates',
|
|
103
110
|
invocation: invocationOf('gates'),
|
|
@@ -140,6 +147,20 @@ const CATALOG = [
|
|
|
140
147
|
kind: GUARDED,
|
|
141
148
|
oneLine: 'Read or change the host-level bridge settings (e.g. the codex Fast tier) — a KEY=VALUE file that survives kit upgrades; previews first, and the Fast tier carries its extra-cost caveat.',
|
|
142
149
|
},
|
|
150
|
+
{
|
|
151
|
+
key: 'autonomy-doctor',
|
|
152
|
+
invocation: invocationOf('autonomy-doctor'),
|
|
153
|
+
group: 'Configure',
|
|
154
|
+
kind: GUARDED,
|
|
155
|
+
oneLine: 'Check whether this machine can run the Claude sandbox and, only with your explicit consent, install what is missing — privileged, preview-first, consent-per-run; it never auto-runs and never commits.',
|
|
156
|
+
},
|
|
157
|
+
{
|
|
158
|
+
key: 'sandbox-masks',
|
|
159
|
+
invocation: invocationOf('sandbox-masks'),
|
|
160
|
+
group: 'Configure',
|
|
161
|
+
kind: GUARDED,
|
|
162
|
+
oneLine: 'Hide the sandbox’s injected device masks from git status via one managed block in the repo’s local exclude file — probe-derived (never a frozen list), preview first, full-block replace on your yes; reviews already ignore the masks by construction.',
|
|
163
|
+
},
|
|
143
164
|
{
|
|
144
165
|
key: 'recipes',
|
|
145
166
|
invocation: invocationOf('recipes'),
|
|
@@ -180,7 +201,7 @@ const CATALOG = [
|
|
|
180
201
|
invocation: invocationOf('grounding'),
|
|
181
202
|
group: 'Orchestrate',
|
|
182
203
|
kind: WRITER,
|
|
183
|
-
oneLine: 'Assemble the verified-facts payload a grounded review runs against — the entry-point Hard Constraints plus a plan’s decision sections; prints it, or writes ONE scratch file with --out.',
|
|
204
|
+
oneLine: 'Assemble the verified-facts payload a grounded review runs against — the entry-point Hard Constraints, the effective autonomy policy, plus a plan’s decision sections; prints it, or writes ONE scratch file with --out.',
|
|
184
205
|
},
|
|
185
206
|
{
|
|
186
207
|
key: 'review-ledger',
|
|
@@ -274,7 +295,7 @@ export const formatHelp = () => {
|
|
|
274
295
|
const lines = [
|
|
275
296
|
`${SKILL_NAME} — command index (this list is read-only)`,
|
|
276
297
|
'',
|
|
277
|
-
'Each command is tagged read-only · writer (makes changes) · guarded (destructive, previews first) · runs project cmds (executes your own declared commands).',
|
|
298
|
+
'Each command is tagged read-only · writer (makes changes) · guarded (consent-gated destructive/privileged actions, previews first) · runs project cmds (executes your own declared commands).',
|
|
278
299
|
];
|
|
279
300
|
for (const group of GROUP_ORDER) {
|
|
280
301
|
const inGroup = COMMANDS.filter((c) => c.group === group);
|
package/tools/delegation.mjs
CHANGED
|
@@ -27,6 +27,10 @@ export const EXPECTED_MEMORY_NAME = 'agent-workflow-memory';
|
|
|
27
27
|
// DOES seed orchestration.json (Mode: upgrade step 3). This closes the stale-memory trap that the
|
|
28
28
|
// read-only family-registry note (MEMORY_ORCH_TEMPLATE_REL) only INFORMS about — the gate ACTS. The
|
|
29
29
|
// two key on the same asset; a cross-tool parity test pins them in lockstep.
|
|
30
|
+
// The autonomy seed (AD-044 Plan 4) is deliberately NOT here: the Plan-3 decision stands — no
|
|
31
|
+
// detectMemory autonomy-marker gate. A memory too old to ship references/templates/autonomy.json
|
|
32
|
+
// stays delegable; the kit-side upgrade ensure seeds the policy from the kit's own template twin,
|
|
33
|
+
// and the family-registry caveat INFORMS about the stale memory (inform, never gate).
|
|
30
34
|
export const REQUIRED_MEMORY_ASSETS = [
|
|
31
35
|
{ path: 'references/templates', type: 'dir' },
|
|
32
36
|
{ path: 'references/templates/orchestration.json', type: 'file' },
|
|
@@ -85,7 +89,7 @@ export const handoffPlan = (delegate) =>
|
|
|
85
89
|
memoryWrites: ['docs/ai/', 'AGENTS.md', 'docs/ai/.memory-version'],
|
|
86
90
|
// The lens region runs AFTER the substrate deploy (its own precondition: the file exists)
|
|
87
91
|
// — it converges a stale-memory seed to the installed engine's canon (AD-041).
|
|
88
|
-
kitWrites: ['AGENTS.md methodology
|
|
92
|
+
kitWrites: ['AGENTS.md pointer slots (methodology / orchestration / autonomy)', 'docs/ai/agent_rules.md lens region', 'docs/ai/.workflow-version'],
|
|
89
93
|
stampsPresent: ['.memory-version', '.workflow-version'],
|
|
90
94
|
memoryRaisesCommitGate: false,
|
|
91
95
|
commitGate: 'kit-only-after-injection',
|
|
@@ -93,11 +97,11 @@ export const handoffPlan = (delegate) =>
|
|
|
93
97
|
: {
|
|
94
98
|
mode: 'fallback',
|
|
95
99
|
memoryWrites: [],
|
|
96
|
-
// Fallback now ships the kit's OWN AGENTS.md carrying the EMPTY
|
|
97
|
-
// the kit reconciles
|
|
98
|
-
// path — so both paths end with
|
|
100
|
+
// Fallback now ships the kit's OWN AGENTS.md carrying the EMPTY pointer slots (Plan 2);
|
|
101
|
+
// the kit reconciles them (ensure-slot + inject-because-empty) exactly like the delegate
|
|
102
|
+
// path — so both paths end with FILLED slots, not inline methodology. The lens region
|
|
99
103
|
// runs after the fallback-template copy of docs/ai (same reconcile, both paths — AD-041).
|
|
100
|
-
kitWrites: ['docs/ai/', 'AGENTS.md', 'AGENTS.md methodology
|
|
104
|
+
kitWrites: ['docs/ai/', 'AGENTS.md', 'AGENTS.md pointer slots (methodology / orchestration / autonomy)', 'docs/ai/agent_rules.md lens region', 'docs/ai/.workflow-version'],
|
|
101
105
|
stampsPresent: ['.workflow-version'],
|
|
102
106
|
memoryRaisesCommitGate: false,
|
|
103
107
|
commitGate: 'kit-only-after-injection',
|
|
@@ -88,7 +88,7 @@ const RAW_BACKENDS = [
|
|
|
88
88
|
],
|
|
89
89
|
grounding: 'automatic — the wrapper precomputes the full working-tree change set (repo map, status, diffs, untracked contents) and codex auto-merges the root AGENTS.md; no grounding flags',
|
|
90
90
|
continue: [],
|
|
91
|
-
receipt: 'side effect — a successful review appends one JSON receipt line to <git dir>/agent-workflow-review-receipts.jsonl (AW_REVIEW_RECEIPTS overrides): fingerprint = sha256 over the canonical uncommitted-state payload (staged diff + unstaged diff + untracked-not-ignored contents — the review-payload domain) in code mode, the artifact-file sha256 in plan mode; verdict parsed from the mandated literal verdict line (schema mode: the verdict field); always fresh:true (one-shot) + grounded:true (native AGENTS.md auto-merge, factsHash null); a write failure warns, never fails the review',
|
|
91
|
+
receipt: 'side effect — a successful review appends one JSON receipt line to <git dir>/agent-workflow-review-receipts.jsonl (AW_REVIEW_RECEIPTS overrides): fingerprint = sha256 over the canonical uncommitted-state payload (staged diff + unstaged diff + untracked-not-ignored contents — the review-payload domain; never-committable untracked paths — character/block devices, FIFOs, sockets — are excluded from the domain entirely, untracked symlinks/directories ride as name-only notes) in code mode, the artifact-file sha256 in plan mode; verdict parsed from the mandated literal verdict line (schema mode: the verdict field); always fresh:true (one-shot) + grounded:true (native AGENTS.md auto-merge, factsHash null); a write failure warns, never fails the review',
|
|
92
92
|
},
|
|
93
93
|
},
|
|
94
94
|
bin: 'codex',
|
|
@@ -121,7 +121,7 @@ const RAW_BACKENDS = [
|
|
|
121
121
|
'agy-review --continue [--decided @f] [--focus "…"]',
|
|
122
122
|
'agy-review --conversation <id> [--decided @f] [--focus "…"]',
|
|
123
123
|
],
|
|
124
|
-
receipt: "side effect — a successful review appends one JSON receipt line to <git dir>/agent-workflow-review-receipts.jsonl (AW_REVIEW_RECEIPTS overrides; plan/diff outside a git tree: warn + skip unless overridden): fingerprint = sha256 over the canonical uncommitted-state payload (staged diff + unstaged diff + untracked-not-ignored contents — the review-payload domain) in code mode, the artifact-file sha256 in plan/diff mode; verdict recorded verbatim from the mandated '### Verdict' section (SHIP / SHIP WITH NITS / REWORK); grounded = whether a NON-EMPTY --facts payload was supplied (an empty payload records grounded:false — fail-closed, the state gate rejects it), factsHash = sha256 of the facts payload; a continuation receipt is fresh:false (informational-only — it cannot attest the folded tree); a write failure warns, never fails the review",
|
|
124
|
+
receipt: "side effect — a successful review appends one JSON receipt line to <git dir>/agent-workflow-review-receipts.jsonl (AW_REVIEW_RECEIPTS overrides; plan/diff outside a git tree: warn + skip unless overridden): fingerprint = sha256 over the canonical uncommitted-state payload (staged diff + unstaged diff + untracked-not-ignored contents — the review-payload domain; never-committable untracked paths — character/block devices, FIFOs, sockets — are excluded from the domain entirely, untracked symlinks/directories ride as name-only notes) in code mode, the artifact-file sha256 in plan/diff mode; verdict recorded verbatim from the mandated '### Verdict' section (SHIP / SHIP WITH NITS / REWORK); grounded = whether a NON-EMPTY --facts payload was supplied (an empty payload records grounded:false — fail-closed, the state gate rejects it), factsHash = sha256 of the facts payload; a continuation receipt is fresh:false (informational-only — it cannot attest the folded tree); a write failure warns, never fails the review",
|
|
125
125
|
},
|
|
126
126
|
},
|
|
127
127
|
bin: 'agy',
|
package/tools/doc-parity.mjs
CHANGED
|
@@ -26,11 +26,16 @@ import { fileURLToPath, pathToFileURL } from 'node:url';
|
|
|
26
26
|
import { SCHEMA_VERSION, REVIEW_CAP, V4_CLASSES, V4_OVERRIDE_SCOPES } from './review-ledger.mjs';
|
|
27
27
|
import { HARD_MAX, DEFAULT_DIFF_CAP } from './review-ledger-write.mjs';
|
|
28
28
|
import { RESULT_SCHEMA_VERSION } from './fold-completeness.mjs';
|
|
29
|
+
import { EXIT as DOCTOR_EXIT, STATUS as DOCTOR_STATUS, TRUSTED_DIRS as DOCTOR_TRUSTED_DIRS } from './autonomy-doctor.mjs';
|
|
30
|
+
import { RECOMMENDATIONS_SECTION_HEADER, RECOMMENDATIONS_EMPTY_LINE } from './recommendations.mjs';
|
|
29
31
|
|
|
30
32
|
const KIT_ROOT = resolve(dirname(fileURLToPath(import.meta.url)), '..');
|
|
31
33
|
|
|
32
34
|
const REVIEW_LEDGER_DOC = 'references/modes/review-ledger.md';
|
|
33
35
|
const FOLD_DOC = 'references/modes/fold-completeness.md';
|
|
36
|
+
const AUTONOMY_DOCTOR_DOC = 'references/modes/autonomy-doctor.md';
|
|
37
|
+
const RECOMMENDATIONS_DOC = 'references/modes/recommendations.md';
|
|
38
|
+
const UPGRADE_DOC = 'references/modes/upgrade.md';
|
|
34
39
|
|
|
35
40
|
// A typed usage failure (exit 2) for the CLI parser — the codebase's typed-error idiom (no classes).
|
|
36
41
|
const usageFail = (message) => Object.assign(new Error(message), { exitCode: 2 });
|
|
@@ -45,6 +50,19 @@ const valueBinding = (constant, value, phrase, files) => ({ constant, value, tok
|
|
|
45
50
|
// so the closed set can never disagree with the code. Every word must appear in the ledger contract.
|
|
46
51
|
const LEDGER_VOCAB = [...V4_CLASSES, ...V4_OVERRIDE_SCOPES, 'gate-run'];
|
|
47
52
|
|
|
53
|
+
// The autonomy-doctor D7 contract (AD-044 Plan 2): the live EXIT table + every status token must
|
|
54
|
+
// render into the mode's contract doc. `usage` is skipped as a bare-word token (trivially present
|
|
55
|
+
// everywhere) — its exit-code phrase below pins that outcome instead.
|
|
56
|
+
const DOCTOR_EXIT_PHRASES = [
|
|
57
|
+
['ready', `\`${DOCTOR_EXIT.ready}\` ready`],
|
|
58
|
+
['stop', `\`${DOCTOR_EXIT.stop}\` precondition STOP`],
|
|
59
|
+
['usage', `\`${DOCTOR_EXIT.usage}\` usage`],
|
|
60
|
+
['notReady', `\`${DOCTOR_EXIT.notReady}\` not-ready diagnosis`],
|
|
61
|
+
['installFailed', `\`${DOCTOR_EXIT.installFailed}\` install failed`],
|
|
62
|
+
['verifyFailed', `\`${DOCTOR_EXIT.verifyFailed}\` verify failed`],
|
|
63
|
+
['unsupported', `\`${DOCTOR_EXIT.unsupported}\` unsupported / untrusted`],
|
|
64
|
+
];
|
|
65
|
+
|
|
48
66
|
export const BINDINGS = Object.freeze([
|
|
49
67
|
valueBinding('SCHEMA_VERSION', SCHEMA_VERSION, `schema v${SCHEMA_VERSION}`, [REVIEW_LEDGER_DOC]),
|
|
50
68
|
valueBinding('HARD_MAX', HARD_MAX, `hard-max ceiling of ${HARD_MAX}`, [REVIEW_LEDGER_DOC]),
|
|
@@ -52,6 +70,16 @@ export const BINDINGS = Object.freeze([
|
|
|
52
70
|
valueBinding('REVIEW_CAP', REVIEW_CAP, `cap ≤${REVIEW_CAP}`, [REVIEW_LEDGER_DOC]),
|
|
53
71
|
valueBinding('RESULT_SCHEMA_VERSION', RESULT_SCHEMA_VERSION, `schema v${RESULT_SCHEMA_VERSION}`, [FOLD_DOC]),
|
|
54
72
|
...LEDGER_VOCAB.map((word) => valueBinding(`vocab:${word}`, word, word, [REVIEW_LEDGER_DOC])),
|
|
73
|
+
...DOCTOR_EXIT_PHRASES.map(([key, phrase]) => valueBinding(`doctor-exit:${key}`, DOCTOR_EXIT[key], phrase, [AUTONOMY_DOCTOR_DOC])),
|
|
74
|
+
...Object.values(DOCTOR_STATUS)
|
|
75
|
+
.filter((token) => token !== DOCTOR_STATUS.usage)
|
|
76
|
+
.map((token) => valueBinding(`doctor-status:${token}`, token, token, [AUTONOMY_DOCTOR_DOC])),
|
|
77
|
+
valueBinding('doctor-trusted-dirs', DOCTOR_TRUSTED_DIRS.join(':'), DOCTOR_TRUSTED_DIRS.join(':'), [AUTONOMY_DOCTOR_DOC]),
|
|
78
|
+
// The upgrade Recommendations section contract (AD-044 Plan 4): the section header and the
|
|
79
|
+
// exact empty-state line must render in BOTH the mode doc and upgrade.md (both exits reference
|
|
80
|
+
// them) — a reworded doc would silently break the paste-verbatim contract.
|
|
81
|
+
valueBinding('recommendations-header', RECOMMENDATIONS_SECTION_HEADER, RECOMMENDATIONS_SECTION_HEADER, [RECOMMENDATIONS_DOC, UPGRADE_DOC]),
|
|
82
|
+
valueBinding('recommendations-empty-line', RECOMMENDATIONS_EMPTY_LINE, RECOMMENDATIONS_EMPTY_LINE, [RECOMMENDATIONS_DOC, UPGRADE_DOC]),
|
|
55
83
|
].map((b) => Object.freeze(b)));
|
|
56
84
|
|
|
57
85
|
// ── the pure checker (readText is injectable for hermetic tests) ────────────────────────
|
|
@@ -96,8 +124,9 @@ Usage:
|
|
|
96
124
|
node doc-parity.mjs [--check | --json]
|
|
97
125
|
|
|
98
126
|
A CLOSED, exported registry binds each live code constant (review-ledger SCHEMA_VERSION / REVIEW_CAP,
|
|
99
|
-
review-ledger-write HARD_MAX / DEFAULT_DIFF_CAP, fold-completeness RESULT_SCHEMA_VERSION)
|
|
100
|
-
ledger vocabulary (V4_CLASSES / V4_OVERRIDE_SCOPES + gate-run)
|
|
127
|
+
review-ledger-write HARD_MAX / DEFAULT_DIFF_CAP, fold-completeness RESULT_SCHEMA_VERSION), the
|
|
128
|
+
ledger vocabulary (V4_CLASSES / V4_OVERRIDE_SCOPES + gate-run), and the autonomy-doctor contract
|
|
129
|
+
(the EXIT table, the status tokens, the trusted-dir allowlist) to the exact token its
|
|
101
130
|
references/modes/*.md contract must carry, and asserts the CURRENT value renders into every bound
|
|
102
131
|
file. A drifted doc, an unreadable bound file, or an absent token FAILS CLOSED.
|
|
103
132
|
|
package/tools/engine-source.mjs
CHANGED
|
@@ -31,6 +31,12 @@ export const ORCHESTRATION_FRAGMENT_REL = 'references/orchestration-slot.md';
|
|
|
31
31
|
// CLI fail loudly on — an engine too old to ship it. readEngineFragment accepts an arbitrary `rel`
|
|
32
32
|
// (no whitelist), so no further plumbing is needed beyond this constant.
|
|
33
33
|
export const PROCEDURES_FRAGMENT_REL = 'references/procedures.md';
|
|
34
|
+
// The autonomy-policy slot fragment — the THIRD bounded fragment the kit injects (AD-044 Plan 3),
|
|
35
|
+
// the cross-agent read contract for docs/ai/autonomy.json. An engine released before it does not
|
|
36
|
+
// ship it; detectEngine({ rel }) lets the chained reconcile soft-skip that too-old engine (and
|
|
37
|
+
// `status` caveat it) instead of discarding the methodology/orchestration fills. readEngineFragment
|
|
38
|
+
// accepts an arbitrary `rel`, so no further plumbing is needed beyond this constant.
|
|
39
|
+
export const AUTONOMY_FRAGMENT_REL = 'references/autonomy-slot.md';
|
|
34
40
|
// The agent-rules lens pair — the canonical lens block + its append-only prior store (engine
|
|
35
41
|
// >= 1.13.0; an older engine ships neither). The kit's lens-region reconcile live-reads BOTH via
|
|
36
42
|
// readEngineFragment({ rel }); detectEngine({ rel }) lets it soft-skip a too-old engine (and
|
|
@@ -26,7 +26,7 @@ import { resolveDir, detectBackends, findOnPath } from './detect-backends.mjs';
|
|
|
26
26
|
import { parseSemver, compareSemver } from './semver-lite.mjs';
|
|
27
27
|
import { validateManifest, readAuthoritativeVersion, UNSUPPORTED, INVALID } from './manifest/validate.mjs';
|
|
28
28
|
import { START_MARKER, excludePath, inferVisibility } from './hide-footprint.mjs';
|
|
29
|
-
import { readEngineFragment, ORCHESTRATION_FRAGMENT_REL, PROCEDURES_FRAGMENT_REL, LENS_FRAGMENT_REL, LENS_PRIORS_REL } from './engine-source.mjs';
|
|
29
|
+
import { readEngineFragment, ORCHESTRATION_FRAGMENT_REL, PROCEDURES_FRAGMENT_REL, AUTONOMY_FRAGMENT_REL, LENS_FRAGMENT_REL, LENS_PRIORS_REL } from './engine-source.mjs';
|
|
30
30
|
import { ACTIVITIES, resolveActivityRecipe } from './recipes.mjs';
|
|
31
31
|
// The config reader lives in orchestration-config.mjs (the single config contract). The read-only status
|
|
32
32
|
// settings-survey reuses THIS reader (one strict-JSON + loud-on-malformed contract), not a second copy.
|
|
@@ -182,6 +182,7 @@ export const classifyMember = (member, deps = {}) => {
|
|
|
182
182
|
// caveat keys on both — an engine missing only the prior store must never report healthy.
|
|
183
183
|
const ENGINE_FRAGMENT_CAVEATS = [
|
|
184
184
|
{ rels: [ORCHESTRATION_FRAGMENT_REL], caveat: 'engine present but does not supply the recipes pointer (too old / incomplete) — run `npx @sabaiway/agent-workflow-engine@latest init`' },
|
|
185
|
+
{ rels: [AUTONOMY_FRAGMENT_REL], caveat: 'engine present but does not supply the autonomy pointer (too old / incomplete) — run `npx @sabaiway/agent-workflow-engine@latest init`' },
|
|
185
186
|
{ rels: [PROCEDURES_FRAGMENT_REL], caveat: 'engine present but does not ship the activity-procedures canon (too old / incomplete) — run `npx @sabaiway/agent-workflow-engine@latest init`' },
|
|
186
187
|
{ rels: [LENS_FRAGMENT_REL, LENS_PRIORS_REL], caveat: 'engine present but does not ship the agent-rules lens canon (the fragment + its prior store; too old / incomplete — engine >= 1.13.0) — run `npx @sabaiway/agent-workflow-engine@latest init`' },
|
|
187
188
|
];
|
|
@@ -192,10 +193,16 @@ const ENGINE_FRAGMENT_CAVEATS = [
|
|
|
192
193
|
// adds a parity drift-guard tying this path to that required-asset set, so the note and the gate can
|
|
193
194
|
// never key on different files (until then they are kept in lockstep by review).
|
|
194
195
|
export const MEMORY_ORCH_TEMPLATE_REL = 'references/templates/orchestration.json';
|
|
196
|
+
// The autonomy-policy seed (AD-044 Plan 4) gets the SAME inform-never-gate treatment: the
|
|
197
|
+
// delegation gate deliberately does NOT key on it (the Plan-3 decision), so this caveat is the
|
|
198
|
+
// discovery mechanism — the recommendations advisor surfaces it via surveyFamily.
|
|
199
|
+
export const MEMORY_AUTONOMY_TEMPLATE_REL = 'references/templates/autonomy.json';
|
|
195
200
|
// Worded as an honest OBSERVATION, not a diagnosis (absence can mean old OR incomplete), and it makes
|
|
196
201
|
// NO claim about an orchestration.json seeding outcome (that depends on delegate-vs-fallback).
|
|
197
202
|
const MEMORY_BEHIND_NOTE =
|
|
198
203
|
"the memory installed here doesn't include the current orchestration template — refresh it with `npx @sabaiway/agent-workflow-memory@latest init`, then restart the session.";
|
|
204
|
+
const MEMORY_AUTONOMY_BEHIND_NOTE =
|
|
205
|
+
"the memory installed here doesn't include the current autonomy-policy template — refresh it with `npx @sabaiway/agent-workflow-memory@latest init`, then restart the session.";
|
|
199
206
|
|
|
200
207
|
// ── the bridge freshness probe (deterministic-first — INV-A / INV-B) ─────────────
|
|
201
208
|
// The bridges are not npm packages: their ONLY delivery channel is the copy bundled inside this kit
|
|
@@ -261,12 +268,19 @@ export const surveyFamily = (deps = {}) =>
|
|
|
261
268
|
// Only attach when it is provably ABSENT (a non-ENOENT probe error → 'unknown' → skip, never a
|
|
262
269
|
// false "missing" claim). Mirrors the engine-caveat SHAPE; keyed on the Step-2.4 required asset.
|
|
263
270
|
if (row.kind === 'memory-substrate' && row.manifestState === OK && row.skillDir) {
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
271
|
+
// Two required-template probes, same shape: orchestration (Step 2.4) + autonomy (AD-044
|
|
272
|
+
// Plan 4 — inform, never gate). Each attaches its own caveat on provable absence only.
|
|
273
|
+
for (const [rel, note] of [
|
|
274
|
+
[MEMORY_ORCH_TEMPLATE_REL, MEMORY_BEHIND_NOTE],
|
|
275
|
+
[MEMORY_AUTONOMY_TEMPLATE_REL, MEMORY_AUTONOMY_BEHIND_NOTE],
|
|
276
|
+
]) {
|
|
277
|
+
const templateProbe = probeMarker(join(row.skillDir, rel), deps);
|
|
278
|
+
if (templateProbe === 'absent') {
|
|
279
|
+
row.caveats = [...(row.caveats ?? []), note];
|
|
280
|
+
} else if (templateProbe === 'unknown') {
|
|
281
|
+
// Could not verify → this row must not be counted "checked, current" by the verdict (INV-B).
|
|
282
|
+
row.freshness = FRESH_UNKNOWN;
|
|
283
|
+
}
|
|
270
284
|
}
|
|
271
285
|
}
|
|
272
286
|
// Bridge freshness probe (INV-A / INV-B): only for a provably-OURS placed bridge (manifestState
|
|
@@ -1029,6 +1029,11 @@ loud advisory failure) but leaves --check unaffected.
|
|
|
1029
1029
|
|
|
1030
1030
|
The read-only gate is a SEPARATE tool: node fold-completeness.mjs --check / --status / --json.
|
|
1031
1031
|
|
|
1032
|
+
Sandbox-safe: the RUNNER itself needs no network and writes only repo-local state (the D4 sandbox
|
|
1033
|
+
lane). The spawned suite/bound commands are COMMAND-SHAPE dependent — --suite / AW_FOLD_SUITE_CMD /
|
|
1034
|
+
AW_FOLD_BOUND_CMD / the verification profile can name project-defined commands; the default
|
|
1035
|
+
(node --test on stdout TAP) is plain and no-network — keep overrides sandbox-safe by shape.
|
|
1036
|
+
|
|
1032
1037
|
Exit codes: 0 written / advisory printed; 1 a typed STOP (loop derivation / suite discovery / a --red
|
|
1033
1038
|
or --reattest refusal / a malformed SARIF on --findings / malformed record / fs error); 2 usage.`;
|
|
1034
1039
|
|
package/tools/grounding.mjs
CHANGED
|
@@ -19,14 +19,15 @@
|
|
|
19
19
|
// in-band marker + stderr report (never a silent cut).
|
|
20
20
|
//
|
|
21
21
|
// Catalog honesty: this is a WRITER — `--out <path>` writes a file. Invariant: `--out` accepts
|
|
22
|
-
// only gitignored
|
|
22
|
+
// only system-temp destinations or a FRESH gitignored in-repo path and REFUSES a tracked/in-repo
|
|
23
23
|
// not-ignored path (a new untracked file would itself move the review fingerprint the facts are
|
|
24
24
|
// about to ground). stdout is the default. It never commits and never runs a subscription CLI.
|
|
25
25
|
//
|
|
26
26
|
// Dependency-free, Node >= 18. No side effects on import (the isDirectRun idiom).
|
|
27
27
|
|
|
28
28
|
import { readFileSync, writeFileSync, lstatSync, realpathSync } from 'node:fs';
|
|
29
|
-
import { resolve, join, relative, isAbsolute, dirname, basename } from 'node:path';
|
|
29
|
+
import { resolve, join, relative, isAbsolute, dirname, basename, sep } from 'node:path';
|
|
30
|
+
import { tmpdir } from 'node:os';
|
|
30
31
|
import { pathToFileURL } from 'node:url';
|
|
31
32
|
import { spawnSync } from 'node:child_process';
|
|
32
33
|
import { fail } from './orchestration-config.mjs';
|
|
@@ -35,6 +36,9 @@ import { fail } from './orchestration-config.mjs';
|
|
|
35
36
|
// computeTelemetry give the counts; the segment filter gives the scope.
|
|
36
37
|
import { ORIGINS, computeTelemetry, filterSegmentRecords, readLedger, resolveLedgerPath, resolveBase } from './review-ledger.mjs';
|
|
37
38
|
import { plansInFlight } from './review-state.mjs';
|
|
39
|
+
// (f) --autonomy (AD-044 Plan 3): the effective per-project autonomy policy for the facts payload.
|
|
40
|
+
// READ core only — never autonomy-write.mjs (the import-split invariant).
|
|
41
|
+
import { AUTONOMY_REL, loadAutonomy, resolveAutonomy, isSparseSeedConfig } from './autonomy-config.mjs';
|
|
38
42
|
|
|
39
43
|
const PLAN_EXECUTION = 'plan-execution';
|
|
40
44
|
|
|
@@ -82,11 +86,15 @@ export const sliceSection = (text, heading, { optional = false, label = 'documen
|
|
|
82
86
|
|
|
83
87
|
// ── assembly ───────────────────────────────────────────────────────────────────────
|
|
84
88
|
|
|
85
|
-
export const assembleGrounding = ({ constraintsText = null, planText = null, planLabel = 'plan' } = {}) => {
|
|
89
|
+
export const assembleGrounding = ({ constraintsText = null, autonomyText = null, planText = null, planLabel = 'plan' } = {}) => {
|
|
86
90
|
const parts = [];
|
|
87
91
|
if (constraintsText != null) {
|
|
88
92
|
parts.push(sliceSection(constraintsText, CONSTRAINTS_HEADING, { label: 'AGENTS.md' }));
|
|
89
93
|
}
|
|
94
|
+
// The autonomy block is COMPUTED (resolveAutonomy over the policy file), not sliced — it rides
|
|
95
|
+
// between the constraints and the plan sections: the natural read order (what always binds →
|
|
96
|
+
// how autonomous this session is → what this plan decides).
|
|
97
|
+
if (autonomyText != null) parts.push(autonomyText);
|
|
90
98
|
if (planText != null) {
|
|
91
99
|
for (const { heading, optional } of PLAN_SECTIONS) {
|
|
92
100
|
const section = sliceSection(planText, heading, { optional, label: planLabel });
|
|
@@ -178,6 +186,38 @@ export const resolveLedgerSummary = ({ cwd, env }) => {
|
|
|
178
186
|
return renderLedgerSummary(records, { loop, base });
|
|
179
187
|
};
|
|
180
188
|
|
|
189
|
+
// ── (f) --autonomy: the effective autonomy policy as a computed facts block (AD-044 Plan 3) ─────
|
|
190
|
+
// The ONE shared resolver (resolveAutonomy) renders the FULL effective policy — every red-line and
|
|
191
|
+
// every activity resolved, no key undefined — with a stated source line. The policy file resolves
|
|
192
|
+
// from the GIT TOP of cwd (the --ledger-summary gitTop precedent): a cwd-relative read from a
|
|
193
|
+
// subdir would silently emit computed defaults for a policy-carrying repo, the exact silent failure
|
|
194
|
+
// grounding exists to kill. Malformed/unreadable policy fails CLOSED (loadAutonomy throws exit 1);
|
|
195
|
+
// an ABSENT file is NOT an error — the computed defaults ARE the effective policy (deliberately
|
|
196
|
+
// diverging from `velocity-profile --autonomy`'s refuse-on-absent: that surface WRITES settings and
|
|
197
|
+
// demands a declared policy; this one emits informational facts). Enforcement honesty: these facts
|
|
198
|
+
// INFORM a review; enforcement stays the OS sandbox + the orchestrator process (AD-044 D1).
|
|
199
|
+
|
|
200
|
+
export const renderAutonomyFacts = (config, source) => {
|
|
201
|
+
const resolved = resolveAutonomy(config);
|
|
202
|
+
// The STRUCTURAL seed (_README only) reads as computed defaults; an EXPLICIT declared-defaults
|
|
203
|
+
// policy reads as a declared policy (the shared predicate — same contract on every surface).
|
|
204
|
+
const defaultsEquivalent = source !== 'none' && isSparseSeedConfig(config);
|
|
205
|
+
const head =
|
|
206
|
+
source === 'none'
|
|
207
|
+
? `## Autonomy policy — ${AUTONOMY_REL} absent; the computed defaults ARE the effective policy`
|
|
208
|
+
: defaultsEquivalent
|
|
209
|
+
? `## Autonomy policy — ${AUTONOMY_REL} present but defaults-equivalent (the sparse seed); the computed defaults ARE the effective policy`
|
|
210
|
+
: `## Autonomy policy — ${AUTONOMY_REL}`;
|
|
211
|
+
const redlines = Object.entries(resolved.redlines).map(([k, v]) => `${k}:${v}`).join(' ');
|
|
212
|
+
const activities = Object.entries(resolved.activities).map(([a, v]) => `${a}:${v.autonomy}`).join(' ');
|
|
213
|
+
return `${head}\n\nred-lines — ${redlines}\nactivities — ${activities}\n`;
|
|
214
|
+
};
|
|
215
|
+
|
|
216
|
+
const resolveAutonomyFacts = ({ cwd }) => {
|
|
217
|
+
const { config, source } = loadAutonomy(gitTop(cwd));
|
|
218
|
+
return renderAutonomyFacts(config, source);
|
|
219
|
+
};
|
|
220
|
+
|
|
181
221
|
// ── the --out destination guard (gitignored / out-of-repo scratch ONLY) ────────────────
|
|
182
222
|
|
|
183
223
|
const gitLine = (args, cwd) => {
|
|
@@ -185,24 +225,35 @@ const gitLine = (args, cwd) => {
|
|
|
185
225
|
return r.error || r.status == null ? null : { status: r.status, stdout: r.stdout ?? '' };
|
|
186
226
|
};
|
|
187
227
|
|
|
188
|
-
// STOP unless `outPath` is a safe scratch destination
|
|
189
|
-
//
|
|
190
|
-
//
|
|
191
|
-
//
|
|
192
|
-
//
|
|
193
|
-
//
|
|
194
|
-
//
|
|
228
|
+
// STOP unless `outPath` is a safe scratch destination — the CLOSED surface (this writer is
|
|
229
|
+
// bridge-tier auto-allowable): (a) under a SYSTEM TEMP root outside the repo ($TMPDIR /
|
|
230
|
+
// os.tmpdir() / /tmp — rewritable), or (b) a FRESH gitignored in-repo path (create-only; an
|
|
231
|
+
// EXISTING in-repo file, even gitignored, is a project file — the .env clobber class). Refused:
|
|
232
|
+
// tracked paths, in-repo not-ignored paths (a new untracked file would move the fingerprint),
|
|
233
|
+
// every other outside-repo destination, symlink and existing non-regular leaves, and any
|
|
234
|
+
// unverifiable lstat. The check runs on the REAL destination, never the lexical path (codex R1):
|
|
235
|
+
// the parent directory is realpath-resolved first, so a symlink can never route the write onto a
|
|
236
|
+
// tracked/in-repo file. Returns { path, kind: 'temp' | 'repo-fresh' } — a repo-fresh caller MUST
|
|
237
|
+
// write with the exclusive flag ('wx'), sealing the guard→write race (codex R11).
|
|
195
238
|
export const assertScratchDestination = (outPath, cwd) => {
|
|
196
239
|
const lexical = isAbsolute(outPath) ? outPath : resolve(cwd, outPath);
|
|
197
240
|
let leaf = null;
|
|
198
241
|
try {
|
|
199
242
|
leaf = lstatSync(lexical);
|
|
200
|
-
} catch {
|
|
243
|
+
} catch (err) {
|
|
244
|
+
// ONLY an absent leaf is a fresh file; any other lstat failure fails CLOSED (codex R9 — this
|
|
245
|
+
// writer is bridge-tier auto-allowable, so an unverifiable leaf must never be written).
|
|
246
|
+
if (err?.code !== 'ENOENT') {
|
|
247
|
+
throw fail(1, `--out cannot inspect the destination (${outPath}: ${err?.code ?? err?.message ?? err}) — refusing to write an unverifiable leaf`);
|
|
248
|
+
}
|
|
201
249
|
leaf = null; // absent → a fresh file; the parent still gets realpath-checked below
|
|
202
250
|
}
|
|
203
251
|
if (leaf?.isSymbolicLink()) {
|
|
204
252
|
throw fail(1, `--out refuses a symlink destination (${outPath}) — the write would follow it onto another file; name the real scratch path`);
|
|
205
253
|
}
|
|
254
|
+
if (leaf != null && !leaf.isFile()) {
|
|
255
|
+
throw fail(1, `--out refuses an existing non-regular destination (${outPath}) — a FIFO/device/directory write would hang or land on a non-file (codex R9); name a fresh or regular scratch path`);
|
|
256
|
+
}
|
|
206
257
|
let realParent;
|
|
207
258
|
try {
|
|
208
259
|
realParent = realpathSync(dirname(lexical));
|
|
@@ -210,11 +261,31 @@ export const assertScratchDestination = (outPath, cwd) => {
|
|
|
210
261
|
throw fail(1, `--out parent directory does not exist (${dirname(lexical)}) — create the scratch dir first`);
|
|
211
262
|
}
|
|
212
263
|
const full = join(realParent, basename(lexical));
|
|
264
|
+
// An out-of-repo (or no-repo) destination is scratch ONLY under a system temp root (codex R8
|
|
265
|
+
// blocker): the bridge tier auto-allows this tool with an args wildcard, so "anything outside
|
|
266
|
+
// the repo is scratch" would let an unattended run overwrite e.g. ~/.bashrc promptless.
|
|
267
|
+
// $TMPDIR / os.tmpdir() / /tmp are the scratch surface; everything else refuses loudly.
|
|
268
|
+
const assertTempScratch = () => {
|
|
269
|
+
const tempRoots = [...new Set([tmpdir(), process.env.TMPDIR, '/tmp'].filter(Boolean).map((p) => {
|
|
270
|
+
try {
|
|
271
|
+
return realpathSync(p);
|
|
272
|
+
} catch {
|
|
273
|
+
return null;
|
|
274
|
+
}
|
|
275
|
+
}).filter(Boolean))];
|
|
276
|
+
if (!tempRoots.some((t) => full === t || full.startsWith(`${t}${sep}`))) {
|
|
277
|
+
throw fail(1, `--out refuses an outside-repo destination that is not under a system temp root (${full}) — grounding output is scratch: use $TMPDIR//tmp, or a fresh gitignored in-repo path (temp roots checked: ${tempRoots.join(', ')})`);
|
|
278
|
+
}
|
|
279
|
+
return { path: full, kind: 'temp' };
|
|
280
|
+
};
|
|
213
281
|
const top = gitLine(['rev-parse', '--show-toplevel'], cwd);
|
|
214
|
-
if (top == null || top.status !== 0) return
|
|
282
|
+
if (top == null || top.status !== 0) return { path: assertTempScratch().path, kind: 'temp' }; // no git tree → temp-only scratch
|
|
215
283
|
const root = top.stdout.replace(/\r?\n$/, '');
|
|
216
284
|
const rel = relative(root, full);
|
|
217
|
-
|
|
285
|
+
// Segment-safe outside test (the Issue-004 class, codex R5): an in-repo file literally named
|
|
286
|
+
// `..facts` has rel `..facts` — `startsWith('..')` would misread it as outside and BYPASS the
|
|
287
|
+
// tracked/ignored refusals below. Only `..` exactly or a `..${sep}`-prefixed rel is outside.
|
|
288
|
+
if (rel === '..' || rel.startsWith(`..${sep}`) || isAbsolute(rel)) return { path: assertTempScratch().path, kind: 'temp' };
|
|
218
289
|
const tracked = gitLine(['ls-files', '--error-unmatch', '--', rel], root);
|
|
219
290
|
if (tracked != null && tracked.status === 0) {
|
|
220
291
|
throw fail(1, `--out refuses a TRACKED path (${rel}) — grounding output is scratch; write it to a gitignored path or outside the repo`);
|
|
@@ -223,7 +294,15 @@ export const assertScratchDestination = (outPath, cwd) => {
|
|
|
223
294
|
if (ignored == null || ignored.status !== 0) {
|
|
224
295
|
throw fail(1, `--out refuses an in-repo path that is not gitignored (${rel}) — a new untracked file would move the review fingerprint; use a gitignored path or a location outside the repo`);
|
|
225
296
|
}
|
|
226
|
-
|
|
297
|
+
// An in-repo destination must be a FRESH file (codex R10): even a gitignored existing file is a
|
|
298
|
+
// project file (.env is the canonical victim) — an auto-allowable writer must never clobber one.
|
|
299
|
+
// The rewritable scratch surface is the system temp; in-repo gitignored writes are create-only —
|
|
300
|
+
// the CALLER must open with the exclusive flag ('wx'): the kind seals the pre-check against the
|
|
301
|
+
// guard→write race (codex R11).
|
|
302
|
+
if (leaf != null) {
|
|
303
|
+
throw fail(1, `--out refuses to OVERWRITE an existing in-repo file (${rel}) — even gitignored, it is a project file (the .env class); write rewritable scratch under $TMPDIR//tmp, or remove the file first`);
|
|
304
|
+
}
|
|
305
|
+
return { path: full, kind: 'repo-fresh' };
|
|
227
306
|
};
|
|
228
307
|
|
|
229
308
|
// ── CLI ────────────────────────────────────────────────────────────────────────────
|
|
@@ -231,10 +310,15 @@ export const assertScratchDestination = (outPath, cwd) => {
|
|
|
231
310
|
const HELP = `grounding — grounded-review facts assembler for the agent-workflow family (AD-038).
|
|
232
311
|
|
|
233
312
|
Usage:
|
|
234
|
-
node grounding.mjs [--constraints] [--plan <path>] [--ledger-summary] [--reserve-bytes <n>] [--out <path>]
|
|
313
|
+
node grounding.mjs [--constraints] [--autonomy] [--plan <path>] [--ledger-summary] [--reserve-bytes <n>] [--out <path>]
|
|
235
314
|
|
|
236
315
|
--constraints slice the root AGENTS.md "Hard Constraints" section verbatim
|
|
237
316
|
(exactly one matching heading, else a loud STOP)
|
|
317
|
+
--autonomy append the COMPUTED effective autonomy policy (resolveAutonomy over the
|
|
318
|
+
git-top docs/ai/autonomy.json): every red-line + per-activity level, with a
|
|
319
|
+
stated source line; absent file → the computed defaults ARE the policy
|
|
320
|
+
(exit 0); malformed/unreadable → fail-closed STOP (exit 1); informational —
|
|
321
|
+
enforcement stays the sandbox + the orchestrator
|
|
238
322
|
--plan <path> extract the plan's decision-bearing sections verbatim + whole:
|
|
239
323
|
"## Approach" + "## Verification" (REQUIRED — STOP if missing),
|
|
240
324
|
"## Decisions (locked)" when present; a duplicate heading is a STOP
|
|
@@ -244,7 +328,8 @@ Usage:
|
|
|
244
328
|
loud STOP unless exactly one plan is in flight
|
|
245
329
|
--reserve-bytes <n> the artifact share agy-review will add around these facts — the output
|
|
246
330
|
budget becomes AGY_MAX_PROMPT_BYTES − n (loud tail-trim on overflow)
|
|
247
|
-
--out <path> write instead of stdout —
|
|
331
|
+
--out <path> write instead of stdout — system-temp scratch (rewritable), or a FRESH
|
|
332
|
+
gitignored in-repo path (create-only); everything else refuses
|
|
248
333
|
(a tracked or in-repo not-ignored path is refused)
|
|
249
334
|
|
|
250
335
|
Feed the output to the review wrapper: agy-review code --facts @<out>. AGY_MAX_PROMPT_BYTES
|
|
@@ -256,6 +341,7 @@ Exit codes: 0 success; 2 usage; 1 STOP (missing/duplicate section, unreadable fi
|
|
|
256
341
|
|
|
257
342
|
const parseArgs = (argv) => {
|
|
258
343
|
let constraints = false;
|
|
344
|
+
let autonomy = false;
|
|
259
345
|
let plan = null;
|
|
260
346
|
let out = null;
|
|
261
347
|
let reserve = 0;
|
|
@@ -263,6 +349,7 @@ const parseArgs = (argv) => {
|
|
|
263
349
|
for (let i = 0; i < argv.length; i += 1) {
|
|
264
350
|
const a = argv[i];
|
|
265
351
|
if (a === '--constraints') constraints = true;
|
|
352
|
+
else if (a === '--autonomy') autonomy = true;
|
|
266
353
|
else if (a === '--ledger-summary') ledgerSummary = true;
|
|
267
354
|
else if (a === '--plan') {
|
|
268
355
|
plan = argv[i + 1];
|
|
@@ -279,8 +366,10 @@ const parseArgs = (argv) => {
|
|
|
279
366
|
i += 1;
|
|
280
367
|
} else throw fail(2, `unknown argument: ${a}`);
|
|
281
368
|
}
|
|
282
|
-
if (!constraints && plan == null && !ledgerSummary)
|
|
283
|
-
|
|
369
|
+
if (!constraints && !autonomy && plan == null && !ledgerSummary) {
|
|
370
|
+
throw fail(2, 'nothing to assemble — pass --constraints, --autonomy, --plan <path>, and/or --ledger-summary');
|
|
371
|
+
}
|
|
372
|
+
return { constraints, autonomy, plan, out, reserve, ledgerSummary };
|
|
284
373
|
};
|
|
285
374
|
|
|
286
375
|
const resolveBudget = (env, reserve) => {
|
|
@@ -300,7 +389,7 @@ export const main = (argv, ctx = {}) => {
|
|
|
300
389
|
const env = ctx.env ?? process.env;
|
|
301
390
|
try {
|
|
302
391
|
if (argv.includes('--help') || argv.includes('-h')) return { code: 0, stdout: HELP, stderr: '' };
|
|
303
|
-
const { constraints, plan, out, reserve, ledgerSummary } = parseArgs(argv);
|
|
392
|
+
const { constraints, autonomy, plan, out, reserve, ledgerSummary } = parseArgs(argv);
|
|
304
393
|
const budget = resolveBudget(env, reserve);
|
|
305
394
|
|
|
306
395
|
const readOrStop = (path, label) => {
|
|
@@ -311,10 +400,29 @@ export const main = (argv, ctx = {}) => {
|
|
|
311
400
|
}
|
|
312
401
|
};
|
|
313
402
|
const constraintsText = constraints ? readOrStop('AGENTS.md', 'root AGENTS.md') : null;
|
|
403
|
+
const autonomyText = autonomy ? resolveAutonomyFacts({ cwd }) : null;
|
|
404
|
+
// --plan is CONFINED to the git work tree (codex R4): the bridge tier auto-allows this tool
|
|
405
|
+
// with an args wildcard, so an unattended invocation could otherwise point --plan at ANY
|
|
406
|
+
// readable file and ship it into the facts payload. Plans are in-repo by contract
|
|
407
|
+
// (docs/plans); an outside-tree path is a loud refusal, never a silent read.
|
|
408
|
+
if (plan != null) {
|
|
409
|
+
const top = gitTop(cwd);
|
|
410
|
+
const planReal = (() => {
|
|
411
|
+
try {
|
|
412
|
+
return realpathSync(resolve(cwd, plan));
|
|
413
|
+
} catch (err) {
|
|
414
|
+
throw fail(1, `plan file '${plan}' is unreadable (${(err && err.code) || err}) — STOP`);
|
|
415
|
+
}
|
|
416
|
+
})();
|
|
417
|
+
const topReal = realpathSync(top);
|
|
418
|
+
if (planReal !== topReal && !planReal.startsWith(`${topReal}${sep}`)) {
|
|
419
|
+
throw fail(1, `--plan '${plan}' resolves outside the git work tree (${planReal}) — plans live in-repo (docs/plans); refusing to read outside-tree content into the facts payload`);
|
|
420
|
+
}
|
|
421
|
+
}
|
|
314
422
|
const planText = plan != null ? readOrStop(plan, 'plan file') : null;
|
|
315
423
|
|
|
316
424
|
const parts = [];
|
|
317
|
-
const assembled = assembleGrounding({ constraintsText, planText, planLabel: plan ?? 'plan' });
|
|
425
|
+
const assembled = assembleGrounding({ constraintsText, autonomyText, planText, planLabel: plan ?? 'plan' });
|
|
318
426
|
if (assembled) parts.push(assembled);
|
|
319
427
|
if (ledgerSummary) {
|
|
320
428
|
const summary = resolveLedgerSummary({ cwd, env });
|
|
@@ -327,8 +435,17 @@ export const main = (argv, ctx = {}) => {
|
|
|
327
435
|
: '';
|
|
328
436
|
|
|
329
437
|
if (out != null) {
|
|
330
|
-
const
|
|
331
|
-
|
|
438
|
+
const dest = assertScratchDestination(out, cwd);
|
|
439
|
+
// repo-fresh writes are EXCLUSIVE ('wx'): the create-only pre-check would otherwise race a
|
|
440
|
+
// file created between the guard and this write (codex R11); temp scratch stays rewritable.
|
|
441
|
+
// ctx.writeFile is a TEST seam (the EEXIST race arm is not constructible without it).
|
|
442
|
+
const writeFile = ctx.writeFile ?? writeFileSync;
|
|
443
|
+
try {
|
|
444
|
+
writeFile(dest.path, text, dest.kind === 'repo-fresh' ? { flag: 'wx' } : {});
|
|
445
|
+
} catch (err) {
|
|
446
|
+
if (err?.code === 'EEXIST') throw fail(1, `--out lost the create-only race: ${out} appeared between the guard and the write — an in-repo destination is never overwritten; re-run with a fresh path or temp scratch`);
|
|
447
|
+
throw err;
|
|
448
|
+
}
|
|
332
449
|
return { code: 0, stdout: `[grounding] wrote ${Buffer.byteLength(text, 'utf8')} bytes to ${out} — pass it as: agy-review code --facts @${out}`, stderr };
|
|
333
450
|
}
|
|
334
451
|
return { code: 0, stdout: text, stderr };
|