@codedology/code-forge 0.15.2 → 0.15.4
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 +81 -0
- package/README.md +5 -1
- package/docs/README.md +2 -0
- package/docs/reference/blocks.json +162 -1
- package/docs/reference/config.md +38 -15
- package/native/README.md +10 -0
- package/native/bin/darwin-arm64/code-forge-native +0 -0
- package/native/bin/darwin-x64/code-forge-native +0 -0
- package/native/bin/linux-arm64/code-forge-native +0 -0
- package/native/bin/linux-x64/code-forge-native +0 -0
- package/package.json +4 -1
- package/schema/code-forge.schema.json +83 -3
- package/schema/outcome.schema.json +45 -0
- package/schema/pipeline.schema.json +30 -0
- package/scripts/first-use.mjs +8 -0
- package/scripts/gen-config-doc.mjs +1 -1
- package/skill/references/autopilot.md +6 -1
- package/skill/references/code.md +52 -0
- package/skill/references/ledger.md +13 -1
- package/skill/references/plan.md +5 -1
- package/skill/references/review.md +46 -8
- package/skill/scripts/forge +1 -1
- package/src/cli/block.mjs +427 -38
- package/src/cli/context.mjs +17 -7
- package/src/cli/doctor.mjs +32 -1
- package/src/cli/gates.mjs +76 -10
- package/src/cli/help.mjs +5 -0
- package/src/cli/native-status.mjs +13 -0
- package/src/cli/outcome.mjs +87 -0
- package/src/cli/pipeline.mjs +61 -0
- package/src/cli/proof.mjs +10 -7
- package/src/cli/report.mjs +53 -6
- package/src/cli/review-file.mjs +278 -41
- package/src/cli/run.mjs +3 -2
- package/src/cli/status.mjs +43 -9
- package/src/cli/sweep.mjs +28 -0
- package/src/cli/validate.mjs +6 -0
- package/src/config/validate.mjs +83 -1
- package/src/context/pack.mjs +53 -3
- package/src/doctor/index.mjs +11 -0
- package/src/doctor/local.mjs +22 -4
- package/src/doctor/worker-probe.mjs +1 -1
- package/src/engines/builders/codex.mjs +27 -7
- package/src/engines/builders/grok.mjs +11 -5
- package/src/engines/codex-home.mjs +18 -15
- package/src/engines/sentinel.mjs +11 -0
- package/src/gates/detect.mjs +6 -3
- package/src/gates/run.mjs +295 -27
- package/src/ledger/export.mjs +1 -1
- package/src/ledger/report.mjs +143 -5
- package/src/outcome/outcome.mjs +108 -0
- package/src/pipeline/pipeline.mjs +112 -0
- package/src/proof/red-green.mjs +41 -13
- package/src/proof/runners.mjs +28 -4
- package/src/proof/worktree.mjs +25 -15
- package/src/review/agent-waiver.mjs +113 -12
- package/src/review/engine.mjs +25 -5
- package/src/review/gate-check.mjs +52 -10
- package/src/review/lenses/full.md +1 -0
- package/src/review/lenses/quick.md +1 -0
- package/src/review/model-stats.mjs +125 -0
- package/src/review/packet.mjs +88 -9
- package/src/review/related.mjs +112 -0
- package/src/review/reopen.mjs +42 -16
- package/src/review/route.mjs +58 -4
- package/src/review/session-retry.mjs +59 -0
- package/src/session/facts.mjs +19 -7
- package/src/session/plan-check.mjs +4 -1
- package/src/session/spawn.mjs +54 -3
- package/src/state/block.mjs +139 -49
- package/src/state/config-snapshot.mjs +13 -3
- package/src/state/run.mjs +3 -2
- package/src/util/exec.mjs +48 -31
- package/src/util/native.mjs +131 -0
- package/src/util/reaper.mjs +238 -8
- package/src/worker/engine.mjs +7 -3
- package/src/worker/loop.mjs +211 -30
- package/src/worker/queue.mjs +107 -24
- package/src/worker/watch.mjs +18 -7
|
@@ -9,6 +9,13 @@
|
|
|
9
9
|
"$comment": "version/provider/levels are REQUIRED with NO schema-level default (a config omitting them is refused here) — `migrate.mjs` fills a MISSING `version` to CURRENT_VERSION before this schema ever runs (so an on-disk file may omit `version`), but `provider` has no such auto-fill anywhere and must be present in the loaded document.",
|
|
10
10
|
"properties": {
|
|
11
11
|
"version": { "type": "integer", "const": 1 },
|
|
12
|
+
"native": {
|
|
13
|
+
"type": "object",
|
|
14
|
+
"additionalProperties": false,
|
|
15
|
+
"properties": {
|
|
16
|
+
"daemon": { "type": "boolean", "default": true, "description": "Use the optional native daemon for run status and block waits. False uses Node only; CODE_FORGE_NATIVE=0 also disables it." }
|
|
17
|
+
}
|
|
18
|
+
},
|
|
12
19
|
"project": {
|
|
13
20
|
"type": "object",
|
|
14
21
|
"additionalProperties": false,
|
|
@@ -110,6 +117,18 @@
|
|
|
110
117
|
"type": "object",
|
|
111
118
|
"additionalProperties": false,
|
|
112
119
|
"properties": {
|
|
120
|
+
"exclude": {
|
|
121
|
+
"type": "array",
|
|
122
|
+
"items": { "type": "string", "minLength": 1 },
|
|
123
|
+
"default": ["**/package-lock.json", "**/npm-shrinkwrap.json", "**/yarn.lock", "**/pnpm-lock.yaml", "**/bun.lock", "**/bun.lockb", "**/composer.lock", "**/Cargo.lock", "**/Gemfile.lock", "**/poetry.lock", "**/uv.lock", "**/go.sum", "**/Package.resolved", "**/Podfile.lock", "**/pubspec.lock", "**/mix.lock", "**/flake.lock"],
|
|
124
|
+
"description": "Repo-relative globs excluded from model code review, including scaffolding archives. Uses the ownership glob syntax (*, ?, ** and braces). A project list replaces the defaults; [] disables exclusion. Ownership and all other close checks still apply. A passing close signs gate.review_excluded per file."
|
|
125
|
+
},
|
|
126
|
+
"lockfile_registries": {
|
|
127
|
+
"type": "array",
|
|
128
|
+
"items": { "type": "string", "minLength": 1 },
|
|
129
|
+
"default": ["registry.npmjs.org"],
|
|
130
|
+
"description": "Allowed URL hosts for excluded package-lock.json and npm-shrinkwrap.json packages.*.resolved entries. Close requires HTTPS and integrity for each resolved package; link:true and entries without resolved are allowed. Other lock formats receive no safety check."
|
|
131
|
+
},
|
|
113
132
|
"multimodel": { "type": "boolean", "default": false },
|
|
114
133
|
"multimodel_for_docs": { "type": "boolean", "default": false },
|
|
115
134
|
"single_reviewer_max_risk": { "type": "number", "minimum": 0, "maximum": 3, "default": 1 },
|
|
@@ -154,6 +173,17 @@
|
|
|
154
173
|
"min_tokens_out": { "type": "integer", "minimum": 1, "default": 120, "description": "The stub guard: a review answer with fewer output tokens is unavailable (too_short). A clean pass (passed: true, no findings, a non-blank summary) needs only min(this, 40), whatever the diff size (B53)." },
|
|
155
174
|
"batch_small_files": { "type": "boolean", "default": true },
|
|
156
175
|
"session_timeout_s": { "type": "integer", "minimum": 1, "default": 300 },
|
|
176
|
+
"adaptive": {
|
|
177
|
+
"type": "object",
|
|
178
|
+
"additionalProperties": false,
|
|
179
|
+
"description": "Choose the primary reviewer by availability × (1 − waived share) ÷ median session seconds from this project's ledger. No history keeps configured order; a matching explicit route has configured priority but competes on performance unless pinned. Disabling restores legacy routing exactly.",
|
|
180
|
+
"properties": {
|
|
181
|
+
"enabled": { "type": "boolean", "default": true },
|
|
182
|
+
"window_days": { "type": "integer", "minimum": 1, "default": 14 },
|
|
183
|
+
"min_sessions": { "type": "integer", "minimum": 1, "default": 10 },
|
|
184
|
+
"explore": { "type": "number", "minimum": 0, "maximum": 1, "default": 0.1, "description": "Share of tickets given to a less-measured candidate, deterministically hashed from the ticket id." }
|
|
185
|
+
}
|
|
186
|
+
},
|
|
157
187
|
"routes": {
|
|
158
188
|
"type": "array",
|
|
159
189
|
"description": "B90: size-based review routing. min_lines and model are required. A rule's level decides which sessions it changes: an L2 rule (level: L2, the default) changes only the reviewers' model, and in consensus mode only slot A (slot B stays review.second_levels.L2); an L3 rule changes only the judge's model. The count is the file's lines at the reviewed state (the bytes in the review packet): newline bytes, plus one for a last line without one; a deleted file is never routed. Rules compete only within their level: among the rules of one level, the largest min_lines at or under the count wins; the same level and min_lines twice is refused. Left out, an optional key gets: provider the top-level provider (the rule replaces the level entry, so the level's own provider is not inherited; validation checks the rule with that same provider); effort none; fallback none; session_timeout_s nothing (the rule does not count in the timeout); second review.second_levels for the rule's level. The timeout is one value for every session of the ticket: the largest session_timeout_s among the matched rules that set one (a matched rule without one is left out of that max), else review.session_timeout_s. The second-level try uses the rule's second, except for an L2 rule in consensus mode. The worker writes one signed review.route row per routed level before the first session, never twice for one ticket; its second_model is null when the rule's second is not used, which includes an L2 rule in consensus mode, because slot B is not the rule's. run reload accepts a change.",
|
|
@@ -164,6 +194,7 @@
|
|
|
164
194
|
"properties": {
|
|
165
195
|
"min_lines": { "type": "integer", "minimum": 0, "description": "B90: the rule applies to a file with at least this many lines (0: every file that is not deleted)." },
|
|
166
196
|
"level": { "type": "string", "enum": ["L2", "L3"], "default": "L2", "description": "B90: the level the rule replaces, L2 (the reviewers) or L3 (the judge)." },
|
|
197
|
+
"pin": { "type": "boolean", "default": false, "description": "Force this matching route's model instead of adaptive primary-reviewer selection." },
|
|
167
198
|
"model": { "type": "string", "minLength": 1 },
|
|
168
199
|
"effort": { "type": "string" },
|
|
169
200
|
"provider": { "$ref": "#/$defs/provider" },
|
|
@@ -200,7 +231,8 @@
|
|
|
200
231
|
"xai": { "type": "integer", "minimum": 1, "default": 2 }
|
|
201
232
|
}
|
|
202
233
|
},
|
|
203
|
-
"parallel_tickets": { "type": "integer", "minimum": 1, "maximum": 16, "default":
|
|
234
|
+
"parallel_tickets": { "type": "integer", "minimum": 1, "maximum": 16, "default": 6, "description": "B42: how many review tickets the worker runs at once (oldest first); 1 = one at a time. Tickets for the same file never overlap." },
|
|
235
|
+
"provider_parallel": { "type": "integer", "minimum": 1, "maximum": 16, "default": 4, "description": "Maximum simultaneous review sessions per provider in the worker. Tickets waiting for this cap still count toward parallel_tickets; stricter provider_concurrency limits still apply." },
|
|
204
236
|
"fair_share": { "type": "boolean", "default": false, "description": "Give each free ticket slot to the block with the fewest running tickets; ties stay oldest first." },
|
|
205
237
|
"file_loop": { "type": "string", "enum": ["wait", "overlap", "watch"], "default": "wait", "description": "After each green file, enqueue its review. wait (default): wait for its ticket before the next file. overlap: continue coding, then run review-file --wait-block <id> --run <run> before the completion sentinel; inspect findings with --wait <ticket>. watch: commit each green file instead of running review-file. A file's first commit is queued at once. Later commits of a file that already has a ticket of this block are held (a watch.held row) until --wait-block, which queues the latest content once before the sentinel. Older pending tickets are still superseded." },
|
|
206
238
|
"agent_waivers": {
|
|
@@ -247,12 +279,48 @@
|
|
|
247
279
|
},
|
|
248
280
|
"gates": {
|
|
249
281
|
"type": "object",
|
|
282
|
+
"description": "Every fixed gate, custom gate and repeat inherits CI=1 and npm_config_yes=false; a gate's explicit env wins. These injected defaults are omitted from env_keys. A gate exiting 0 fails with reason prompt if the last non-empty line of either stdout or stderr ends with ?, (y), (y/n), [y/N], [Y/n] or Yes/No (case-insensitive; trailing whitespace ignored). Earlier prompt-like lines followed by normal output do not count. The failure says: gate ended on an interactive prompt; install the tool or make the command non-interactive.",
|
|
250
283
|
"additionalProperties": false,
|
|
251
284
|
"properties": {
|
|
252
285
|
"test": { "$ref": "#/$defs/argvOrNull" },
|
|
253
286
|
"lint": { "$ref": "#/$defs/argvOrNull" },
|
|
254
287
|
"types": { "$ref": "#/$defs/argvOrNull" },
|
|
255
288
|
"format": { "$ref": "#/$defs/argvOrNull" },
|
|
289
|
+
"merge_gate": {
|
|
290
|
+
"type": "boolean",
|
|
291
|
+
"default": true,
|
|
292
|
+
"description": "With block land --merge, run the configured full suite on a trial merge in a temporary detached worktree under the run worktree root before merging into main. Failure or conflict leaves main unchanged and removes the temporary tree. False or --no-merge-gate skips this check with a warning. Without --merge the gate does not run."
|
|
293
|
+
},
|
|
294
|
+
"kinds": {
|
|
295
|
+
"type": "object",
|
|
296
|
+
"additionalProperties": false,
|
|
297
|
+
"properties": {
|
|
298
|
+
"code": { "type": "array", "items": { "type": "string", "minLength": 1 } },
|
|
299
|
+
"docs": { "type": "array", "items": { "type": "string", "minLength": 1 } },
|
|
300
|
+
"contract": { "type": "array", "items": { "type": "string", "minLength": 1 } }
|
|
301
|
+
},
|
|
302
|
+
"description": "Optional gate sets for code, docs and contract blocks. Each value is an array of fixed gate names (test, lint, types, format) or gates.custom[].name values. Unknown names and explicitly null fixed gates are refused with the kind and name. With gates run --run R --block B, the declared block kind wins; otherwise the kind is inferred from owned files using block open's rules. An entry runs only its listed gates; every other configured gate is reported skipped with reason not in gates.kinds.<kind>. A missing entry runs all gates as usual. An empty array runs no commands, reports 0 gates for kind <kind>, and writes gate.done with all_ok: true so block close can proceed to its remaining checks. Light blocks keep their light rules and ignore kinds. A gates run without --block ignores kinds. Example: gates: {types: [npm, run, typecheck], kinds: {docs: [types], contract: []}}."
|
|
303
|
+
},
|
|
304
|
+
"custom": {
|
|
305
|
+
"type": "array",
|
|
306
|
+
"maxItems": 20,
|
|
307
|
+
"description": "Named gates run after test, lint, types and format in list order, independently even after a failure, with the same timeout and env rules. At most 20; names are unique and cannot be fixed gate names (gates-not-argv-array error rule). Custom argv refuses {test_files} and {changed_files}; --changed and light-block narrowing do not apply. Evidence uses safe relative paths or simple globs: * within a segment, ** across segments; absolute paths, backslashes, empty strings and .. segments are refused. After pass or fail, regular files under cwd are hashed (no symlinks followed), sorted by path, deduplicated and capped at 200 per gate as {path, bytes, sha256}; unmatched patterns appear in missing and do not fail the gate. Files are never copied or stored as content. gate.red includes gate and custom: true; gate.done includes custom summaries {name, ok, evidence_count, missing?}, and with --run/--block an evidence map by name. Website example: gates: {custom: [{name: build, argv: [npm, run, build]}, {name: lighthouse, argv: [npm, run, lighthouse], evidence: ['reports/**/*.json']}, {name: links, argv: [npm, run, links]}]}.",
|
|
308
|
+
"items": {
|
|
309
|
+
"type": "object",
|
|
310
|
+
"additionalProperties": false,
|
|
311
|
+
"required": ["name", "argv"],
|
|
312
|
+
"properties": {
|
|
313
|
+
"name": { "type": "string", "pattern": "^[a-z][a-z0-9_-]{0,31}$", "not": { "enum": ["test", "lint", "types", "format"] }, "description": "Matches ^[a-z][a-z0-9_-]{0,31}$, unique in the list, and not test, lint, types or format." },
|
|
314
|
+
"argv": {
|
|
315
|
+
"type": "array", "minItems": 1,
|
|
316
|
+
"items": { "type": "string", "minLength": 1 },
|
|
317
|
+
"not": { "contains": { "type": "string", "pattern": "\\{(?:test_files|changed_files)\\}" } }
|
|
318
|
+
},
|
|
319
|
+
"env": { "type": "object", "additionalProperties": false, "patternProperties": { "^[A-Za-z_][A-Za-z0-9_]*$": { "type": "string" } } },
|
|
320
|
+
"evidence": { "type": "array", "items": { "type": "string", "minLength": 1, "pattern": "^(?!/)(?!.*(?:^|/)(?:[A-Za-z]:|\\.\\.(?:/|$)))[^\\\\\\u0000]+$" } }
|
|
321
|
+
}
|
|
322
|
+
}
|
|
323
|
+
},
|
|
256
324
|
"extra": {
|
|
257
325
|
"type": "object",
|
|
258
326
|
"additionalProperties": false,
|
|
@@ -268,6 +336,13 @@
|
|
|
268
336
|
"maximum": 10,
|
|
269
337
|
"description": "After a passing test gate with --run and --block, run its command this many additional times for only tests added or changed against the block base; absent means off. Requires a whole {test_files} argv item; otherwise prints repeat_new_tests: skipped (gates.test has no {test_files}). A failed repeat makes the gate red with reason flaky-test; output and signed gate rows carry repeats {n, files, failed}, and report shows the count."
|
|
270
338
|
},
|
|
339
|
+
"timeout_s": {
|
|
340
|
+
"type": "integer",
|
|
341
|
+
"minimum": 30,
|
|
342
|
+
"maximum": 7200,
|
|
343
|
+
"default": 300,
|
|
344
|
+
"description": "Whole seconds, 30..7200, for each fixed gate, custom gate and test repeat. gates run --timeout-ms overrides this value. A timed-out gate fails with reason timeout and stderr naming the timeout in seconds and gates.timeout_s to raise."
|
|
345
|
+
},
|
|
271
346
|
"full_suite_threshold_files": {
|
|
272
347
|
"type": "integer",
|
|
273
348
|
"minimum": 1,
|
|
@@ -303,7 +378,12 @@
|
|
|
303
378
|
"properties": {
|
|
304
379
|
"dir": { "type": "string", "minLength": 1, "default": "../<repo-name>-worktrees", "description": "Where the trees go, relative to the repository top level (not the workspace). In the default, <repo-name> is replaced with the basename of the repository top level; the default text is never used as is. The default sits beside the repository, so the main tree never lists the trees as untracked." },
|
|
305
380
|
"branch": { "type": "string", "minLength": 1, "default": "forge/{run}/{block}", "description": "The branch template: only {run} and {block} are substituted; the result must pass git check-ref-format --branch." },
|
|
306
|
-
"base": { "type": "string", "minLength": 1, "description": "The commit-ish the tree starts from; default the block's base (block open --base, else HEAD)." }
|
|
381
|
+
"base": { "type": "string", "minLength": 1, "description": "The commit-ish the tree starts from; default the block's base (block open --base, else HEAD)." },
|
|
382
|
+
"setup": {
|
|
383
|
+
"type": "array",
|
|
384
|
+
"description": "Argv commands run sequentially in the proof export after export and before RED. A command failure stops proof with reason setup and its output tail. Never run during block worktree creation or under proof lock.",
|
|
385
|
+
"items": { "type": "array", "minItems": 1, "items": { "type": "string", "minLength": 1, "pattern": "^[^\\u0000]*$" } }
|
|
386
|
+
}
|
|
307
387
|
}
|
|
308
388
|
},
|
|
309
389
|
"export": {
|
|
@@ -325,7 +405,7 @@
|
|
|
325
405
|
"runners": {
|
|
326
406
|
"type": "array",
|
|
327
407
|
"minItems": 1,
|
|
328
|
-
"description": "
|
|
408
|
+
"description": "How proof red-green runs tests. The first matching entry wins. With no match, Node files use node:test and PHP tests in Pest/Laravel projects use a default JUnit runner. run reload may append runners but cannot change, reorder or remove existing entries. With runners set no block may own .code-forge.yml. An empty list is refused: leave the key out for defaults.",
|
|
329
409
|
"items": {
|
|
330
410
|
"type": "object",
|
|
331
411
|
"additionalProperties": false,
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
|
3
|
+
"$id": "https://code-forge.dev/schema/outcome.schema.json",
|
|
4
|
+
"title": "Specialist skill outcome",
|
|
5
|
+
"type": "object",
|
|
6
|
+
"properties": {
|
|
7
|
+
"skill": { "type": "string" },
|
|
8
|
+
"version": { "type": "string" },
|
|
9
|
+
"request": { "type": "string" },
|
|
10
|
+
"status": { "enum": ["draft", "approved", "blocked", "failed"] },
|
|
11
|
+
"approved_by": { "type": ["string", "null"] },
|
|
12
|
+
"artifact": { "type": ["string", "null"] },
|
|
13
|
+
"files": {
|
|
14
|
+
"type": "array",
|
|
15
|
+
"items": {
|
|
16
|
+
"type": "object",
|
|
17
|
+
"properties": {
|
|
18
|
+
"path": { "type": "string" },
|
|
19
|
+
"sha256": { "type": "string" }
|
|
20
|
+
},
|
|
21
|
+
"required": ["path", "sha256"],
|
|
22
|
+
"additionalProperties": false
|
|
23
|
+
}
|
|
24
|
+
},
|
|
25
|
+
"decisions": { "type": "array", "items": { "type": "string" } },
|
|
26
|
+
"checks": {
|
|
27
|
+
"type": "array",
|
|
28
|
+
"items": {
|
|
29
|
+
"type": "object",
|
|
30
|
+
"properties": {
|
|
31
|
+
"name": { "type": "string" },
|
|
32
|
+
"result": { "enum": ["pass", "warn", "fail"] },
|
|
33
|
+
"detail": { "type": "array", "items": { "type": "string" } },
|
|
34
|
+
"evidence": { "type": "string" }
|
|
35
|
+
},
|
|
36
|
+
"required": ["name", "result"],
|
|
37
|
+
"additionalProperties": false
|
|
38
|
+
}
|
|
39
|
+
},
|
|
40
|
+
"cost_usd": { "type": ["number", "null"] },
|
|
41
|
+
"minutes": { "type": ["number", "null"] }
|
|
42
|
+
},
|
|
43
|
+
"required": ["skill", "status", "files"],
|
|
44
|
+
"additionalProperties": false
|
|
45
|
+
}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
|
3
|
+
"$id": "https://code-forge.dev/schema/pipeline.schema.json",
|
|
4
|
+
"title": "Specialist skill pipeline",
|
|
5
|
+
"type": "object",
|
|
6
|
+
"properties": {
|
|
7
|
+
"root": { "type": "string" },
|
|
8
|
+
"steps": {
|
|
9
|
+
"type": "array",
|
|
10
|
+
"minItems": 1,
|
|
11
|
+
"items": {
|
|
12
|
+
"type": "object",
|
|
13
|
+
"properties": {
|
|
14
|
+
"id": { "type": "string", "pattern": "^[a-z0-9][a-z0-9-]*$" },
|
|
15
|
+
"skill": { "type": "string" },
|
|
16
|
+
"needs": { "type": "array", "items": { "type": "string" }, "default": [] },
|
|
17
|
+
"outcome": {
|
|
18
|
+
"type": "string",
|
|
19
|
+
"pattern": "^(?![\\\\/])(?![A-Za-z]:)(?!.*(?:^|[\\\\/])\\.\\.(?:[\\\\/]|$)).+$"
|
|
20
|
+
},
|
|
21
|
+
"approval": { "enum": ["human", "none"] }
|
|
22
|
+
},
|
|
23
|
+
"required": ["id", "skill", "outcome", "approval"],
|
|
24
|
+
"additionalProperties": false
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
},
|
|
28
|
+
"required": ["root", "steps"],
|
|
29
|
+
"additionalProperties": false
|
|
30
|
+
}
|
package/scripts/first-use.mjs
CHANGED
|
@@ -50,7 +50,9 @@ import { fileURLToPath } from 'node:url';
|
|
|
50
50
|
import { parse as parseYAML, parseDocument } from 'yaml';
|
|
51
51
|
import { readAllRows } from '../src/ledger/write.mjs';
|
|
52
52
|
import { estimateCostUsd } from '../src/ledger/prices.mjs';
|
|
53
|
+
import { readRun } from '../src/state/run.mjs';
|
|
53
54
|
import { exec } from '../src/util/exec.mjs';
|
|
55
|
+
import { stopMembers } from '../src/util/reaper.mjs';
|
|
54
56
|
import { runRoot, setRunRoot, tmpBase } from '../src/util/tmp.mjs';
|
|
55
57
|
|
|
56
58
|
const PKG = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..');
|
|
@@ -435,6 +437,12 @@ export async function main(argv, io = {}) {
|
|
|
435
437
|
const ended = await exec([process.execPath, BIN, 'run', 'end', '--run', run], { cwd: copy, env: scrubbedEnv(), timeoutMs: 60000 });
|
|
436
438
|
say(`run end · exit ${ended.code} · ${ended.stdout.trimEnd() || ended.stderr.trimEnd()}`);
|
|
437
439
|
summary.run_ended = ended.code === 0;
|
|
440
|
+
if (summary.run_ended) {
|
|
441
|
+
// `run end` sends SIGTERM; delivery is not exit. Wait for the pinned worker's
|
|
442
|
+
// cleanup before this script reports completion, rechecking identity before signals.
|
|
443
|
+
const record = await readRun(run);
|
|
444
|
+
await stopMembers(new Map((record.workers ?? (record.worker ? [record.worker] : [])).map((worker) => [worker.pid, worker.started_at])));
|
|
445
|
+
}
|
|
438
446
|
}
|
|
439
447
|
process.off('SIGINT', onSignal);
|
|
440
448
|
process.off('SIGTERM', onSignal);
|
|
@@ -64,7 +64,7 @@ function notesLabel(nodeIn, defs) {
|
|
|
64
64
|
if (node.exclusiveMinimum !== undefined) parts.push(`> ${node.exclusiveMinimum}`);
|
|
65
65
|
if (node.minLength !== undefined) parts.push(`minLength ${node.minLength}`);
|
|
66
66
|
if (node.minItems !== undefined) parts.push(`minItems ${node.minItems}`);
|
|
67
|
-
if (typeof node.description === 'string'
|
|
67
|
+
if (typeof node.description === 'string') parts.push(node.description);
|
|
68
68
|
return parts.join('; ');
|
|
69
69
|
}
|
|
70
70
|
|
|
@@ -13,6 +13,11 @@ Autopilot lets the owner leave for a set time. A delegate (a fresh L2 or L3 sess
|
|
|
13
13
|
|
|
14
14
|
## §2 During the window
|
|
15
15
|
|
|
16
|
+
`block waive --yes` may waive a critical only while the run's autopilot grant is active; otherwise `waive-yes-critical`.
|
|
17
|
+
Cite a test file named in the block's acceptance `tests:` and passed in its latest signed gate record for the current content; otherwise `waive-yes-critical-no-test`.
|
|
18
|
+
Also cite the finding's path with `:line` or `:start-end`, and obtain agreement from the fresh L3 judge. Refusals write nothing.
|
|
19
|
+
The signed waiver records `by: agent`, `autopilot: true` and the cited `test` path; the human can undo it with `block unwaive`. Warning/nit waivers are unchanged.
|
|
20
|
+
|
|
16
21
|
The allowable scope `close:routine` means “close a routine block.” It lets plain `block close` pass `close.require_human` without a terminal when every changed file has a signed approval, a signed green gate row exists, and the block has no waiver rows, transcript hits or accepted hits. Waived or answered-only reviews do not qualify. Refusal is `close-not-routine: <reason>` (exit 1); success signs `autopilot.close {block, grant}` alongside the normal close. It never permits `block close --decide` or critical waivers.
|
|
17
22
|
|
|
18
23
|
- **Every owner-level question goes to the delegate first**: `forge autopilot ask --run <r> --scope <scope> --question <text> --options <words> [--block <id>] [--file <path>] [--finding <id>]`. Use the fixed option words, or the action will refuse the answer: a waiver `waive,fix` · an extra round `allow,deny` · a coder level the candidate levels, e.g. `L1,L2`.
|
|
@@ -44,7 +49,7 @@ These never go to the delegate, whatever the grant says (the CLI's fixed deny li
|
|
|
44
49
|
- plan approval (`plan.md` §4.1) and design approval;
|
|
45
50
|
- a budget raise (`code.md` §6, `review.md` §7);
|
|
46
51
|
- a threshold edit or any other limit or rule change — only the owner's own `forge autopilot approve` at a terminal;
|
|
47
|
-
- a critical waiver, a proof waiver, or closing a block without its reviews;
|
|
52
|
+
- a critical waiver through the delegate's `autopilot waive` action (the evidence-backed agent exception is in §2), a proof waiver, or closing a block without its reviews;
|
|
48
53
|
- merges and destructive actions.
|
|
49
54
|
|
|
50
55
|
When one of these comes up during the window: stop that line of work, add it to Actions only you can take, and wait. The delegate never relaxes a rule or a limit to keep work moving (`rules-core.md` R17).
|
package/skill/references/code.md
CHANGED
|
@@ -2,6 +2,8 @@
|
|
|
2
2
|
|
|
3
3
|
Small changes may use `block open <id> --kind light`. Close allows at most `light.max_files` owned changed files (default 5) and `light.max_lines` added plus removed lines (default 300), including new files. Auto ticket lines are unchanged. `gates run --block` runs the block's tests through `{test_files}`, ignoring the full-suite threshold; supply block tests and a test command containing that item. A close refusal `light-limits <files> files, <lines> lines` means the block outgrew this kind: the orchestrator stops the open attempt, then opens the same id with `block open <id> --kind code --attempt <n+1>` and the original base, ownership and acceptance.
|
|
4
4
|
|
|
5
|
+
For website checks, configure `gates.custom` with up to 20 unique named `{name, argv, env?, evidence?}` gates, such as build, lighthouse and links. They run independently after the four fixed gates, including on light blocks and with `--changed`; custom argv refuses `{test_files}` and `{changed_files}`. The `gates-not-argv-array` error rule validates entries. Evidence paths/globs stay under cwd; after pass or fail, up to 200 sorted regular files are recorded as path, bytes and SHA-256 without copying content. Missing patterns are listed without failing the gate. `gate.done` carries custom summaries and, with `--run/--block`, the evidence map; red custom gates carry their name and `custom: true`.
|
|
6
|
+
|
|
5
7
|
## §1 The coder's first deliverable is a facts diff
|
|
6
8
|
|
|
7
9
|
Before any forecast or code, the coder reads the code it will call and reports **every contradiction between the plan and the code** in one message — a signature, a flag, a key, a version. One stop for facts, never one per discovery. Contract corrections (a key, a rule, a signature) are the architect's alone and are recorded, not asked; a ruling reaches the human only when it changes user-visible behaviour, money semantics or an approved decision. A coder that stops three times for three true facts did nothing wrong; the protocol did.
|
|
@@ -14,6 +16,19 @@ The coder then forecasts **cases** (tests) and **lines**. Compute the floor your
|
|
|
14
16
|
|
|
15
17
|
Acceptance (`block open --acceptance`) is a YAML/JSON list. Code clauses need `tests: [<test ids>]`. A device check instead uses `- clause: "Check the layout on a phone"` with `kind: human_check` on the next indented line and no `tests` key (even an empty list is refused). These texts are recorded as the block's `human_checks` and skipped by test-coverage checks; every other clause still needs tests.
|
|
16
18
|
|
|
19
|
+
For design first, put `{clause: "Owner approves the design", kind: human_check}` in `design.acc.yml`, then follow this order:
|
|
20
|
+
|
|
21
|
+
```sh
|
|
22
|
+
code-forge block open DESIGN --run <r> --level L1 --kind docs --owned docs/design.md --acceptance design.acc.yml --brief <file> --lines <n>
|
|
23
|
+
# Finish the design and pass its code gates, then:
|
|
24
|
+
code-forge block close DESIGN --run <r>
|
|
25
|
+
# The owner approves the design in their terminal:
|
|
26
|
+
code-forge block human-pass DESIGN --run <r> --note "Design approved"
|
|
27
|
+
code-forge block open CODE --run <r> --level L2 --owned src/feature.mjs --acceptance code.acc.yml --after DESIGN --brief <file> --lines <n>
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
`--after <id> [<id> ...]` collects ids like `--owned` and removes duplicates. Every dependency must be `closed` in this run, checked under the same run lock as the other open checks. Open refuses with `after-not-done`, naming every blocker and its status (`open`, `awaiting_human_check`, `stopped`, or `not in run`); no rows or worktree are created. With `--json`, the refusal also prints `{block, event: "refused", code: "after-not-done", message}` (self-dependency uses `bad-after`). Only a supplied flag adds `after: [ids]` to the entry and `dispatch` row. Supply it again on each later attempt that must wait for the design; an attempt without it does not recheck dependencies.
|
|
31
|
+
|
|
17
32
|
Run a Codex coder with `codex exec --json` in its worktree, save stdout to a log, and pass that
|
|
18
33
|
log to `block close --transcript <file>`. A Codex JSON log (every non-empty line is an object
|
|
19
34
|
with a string `type`, first event `thread.started`) is checked by its commands: command items'
|
|
@@ -72,6 +87,43 @@ Each `review-file` ticket is served by the worker `forge run start` launched: to
|
|
|
72
87
|
|
|
73
88
|
## §5 The block gate is a re-measurement — `forge block close <id> --run <r>`
|
|
74
89
|
|
|
90
|
+
Paths added by `block claim` survive retries: `block open <id> --attempt N` (N > 1)
|
|
91
|
+
restores earlier signed claims alongside `--owned`, and `block attempt` keeps them too.
|
|
92
|
+
Both print `kept claims: <n>`. Do not claim the same paths again after a retry.
|
|
93
|
+
|
|
94
|
+
The orchestrator can use `code-forge block land <id> --run <r> --report <file> --transcript <file> [--max <dur>] [--merge] [--no-merge-gate] [--json]`
|
|
95
|
+
to join reviews, run changed gates in the recorded block worktree, then run the normal close checks.
|
|
96
|
+
The first failure exits 1 and names the step, findings, gate output or close refusal, and the next
|
|
97
|
+
command. Human checks must finish before a merge. With `--merge`, a closed block's branch is merged
|
|
98
|
+
with `--no-ff` in the clean main checkout on the recorded base branch; the block worktree must also
|
|
99
|
+
be clean. Before the real merge, land merges the current base branch HEAD and block branch in a
|
|
100
|
+
temporary detached worktree under the run's worktree root and runs the configured full gates
|
|
101
|
+
without block scope or `--changed`, using the shared gate runner. A green suite removes the
|
|
102
|
+
temporary tree, prints `merge gate: full suite passed (<n>s)`, and permits the real merge.
|
|
103
|
+
A red suite or conflict stops at `merge-gate` with the first ten failing TAP test names (or gate
|
|
104
|
+
diagnostics) or conflict files and a `next:` action. Every outcome removes the temporary tree;
|
|
105
|
+
a refusal leaves main clean and its HEAD unchanged. `gates.merge_gate` defaults to `true` in
|
|
106
|
+
the run config; `false` or `--no-merge-gate` skips the check with a warning.
|
|
107
|
+
It never forces or switches branches. Success prints `block <id> landed`; without
|
|
108
|
+
`--merge`, it prints `block <id> closed; merge: <cmd>` and a hint that the merge gate runs only
|
|
109
|
+
with `--merge`, and leaves the base unchanged. A merge refusal
|
|
110
|
+
leaves the block closed; use the displayed next command after correcting the checkout. `--json`
|
|
111
|
+
prints one object with `block`, `event` and `merge`, or a refusal with `step`, `reason` and `next`.
|
|
112
|
+
|
|
113
|
+
`--max` defaults to `30m`; durations also accept seconds (`90s`) and milliseconds (`1500ms`).
|
|
114
|
+
Land makes up to three join attempts while a held findings round remains open, all within one
|
|
115
|
+
wait budget. Completed findings stop it. An exhausted wait stops at `join` with `wait_timeout`
|
|
116
|
+
and prints a `next:` land command with a larger `--max`, preserving the report and transcript.
|
|
117
|
+
|
|
118
|
+
Land automatically claims changed files outside the owned set before joining reviews.
|
|
119
|
+
`block close --auto-claim` also claims, queues reviews in the block worktree, waits, and
|
|
120
|
+
continues only after approval. Each file prints an auto-claim line and gets the same signed
|
|
121
|
+
`block claim` row with `auto: true` and reason `auto-claim: changed outside owned files`.
|
|
122
|
+
Findings stop the operation. A file owned or claimed by another open block still refuses
|
|
123
|
+
`unowned_change`, naming that block, including separate worktrees. `.git/`, `.code-forge/`
|
|
124
|
+
and paths outside the repository are never auto-claimed. Plain close keeps refusing
|
|
125
|
+
unowned files; manual `block claim` remains available.
|
|
126
|
+
|
|
75
127
|
To correct an open block's snapshotted clauses, the human uses `code-forge block amend-acceptance <id> --run <r> --acceptance <file> --reason <text>` with a nonblank reason and the same acceptance format as `block open`. Both terminals and confirmation are required, with no `--yes` and no active autopilot grant. Invalid or unchanged lists write nothing; a successful amendment updates the human checks and signs one `block.acceptance_amended` row with both clause-list hashes. Earlier reviews no longer count at close; review requests, block waits and watch ticks queue a fresh review with the new clauses. Coders must never run this human verb.
|
|
76
128
|
|
|
77
129
|
The human can use `block close <id> --run <r> --decide --reason <text>` to see all waivable findings, unacknowledged agent waivers and acceptable transcript hits in one numbered list, then confirm once. Yes writes the same signed human rows as the individual verbs, with the shared reason for findings and hits, then runs normal close. No writes nothing. Unreviewed or stopped content needing a new review, failed gates, unowned changes and missing proof print `cannot decide here:` and write nothing. Both terminal streams are required; no `--yes`; an active autopilot grant refuses decision close. Coders must leave this step to the human.
|
|
@@ -10,7 +10,19 @@ Events you will read most: `run.start`, `run.mode {mode: brief, plan: none}` (a
|
|
|
10
10
|
|
|
11
11
|
`review.unwaived {block, file, finding, reason, by: human}` is a signed human undo of earlier agent waivers. `agent_waivers.acked {block, digest, count, by: human}` is a signed human acknowledgement of the current counted set. The gate verifies both rows. The digest is SHA-256 of the sorted file, finding and row signature set. New or removed waivers need a new acknowledgement when the run snapshot requires one.
|
|
12
12
|
|
|
13
|
-
Terminal output in this version. Sections: cost per block — completed and still open, with `completed` and the count of unknown-price sessions (per level and lane, split review / coder / S1+S2 / facts / proof time) · lane distribution · escalations per block with `trigger` · S1 calls per block · S2 rate and overrule rate per question · the review budget table with `missed_after_degrade` per degrade step · review depth and context-mode distribution, findings by category and severity · fix rounds per file (one block round is one round for each file it touched, `review.md` §5), rounds that stalled, files stopped at `review_cap` · `review.unavailable` by reason · proof time per block · forecast vs actual lines and cases with the measured lines-per-case · blocks stopped at L3 · `overspend` and `l3_fallback` counts · runs stopped for `no_engine` · `outside_review` (14th): per PR sorted by number, `{pr, rounds, findings, link, blocks, code_forge_usd, unknown_usd_sessions}` — `code_forge_usd` sums the `cost_per_block` totals of the PR's blocks in its run, rounded as `cost_per_block` rounds; unknown-price sessions count 0 and are counted; a PR with no blocks shows 0 and 0 unknown; `[]` with no row; in the terminal the lines print under `== outside_review ==`: `outside review: none`, or one line per PR (`PR #12 · 3 outside rounds · 7 findings · code-forge $3.75 (+1 unknown) · B1, B2`). `agent_waivers` is the 15th
|
|
13
|
+
Terminal output in this version. Sections: cost per block — completed and still open, with `completed` and the count of unknown-price sessions (per level and lane, split review / coder / S1+S2 / facts / proof time) · lane distribution · escalations per block with `trigger` · S1 calls per block · S2 rate and overrule rate per question · the review budget table with `missed_after_degrade` per degrade step · review depth and context-mode distribution, findings by category and severity · fix rounds per file (one block round is one round for each file it touched, `review.md` §5), rounds that stalled, files stopped at `review_cap` · `review.unavailable` by reason · proof time per block · forecast vs actual lines and cases with the measured lines-per-case · blocks stopped at L3 · `overspend` and `l3_fallback` counts · runs stopped for `no_engine` · `outside_review` (14th): per PR sorted by number, `{pr, rounds, findings, link, blocks, code_forge_usd, unknown_usd_sessions}` — `code_forge_usd` sums the `cost_per_block` totals of the PR's blocks in its run, rounded as `cost_per_block` rounds; unknown-price sessions count 0 and are counted; a PR with no blocks shows 0 and 0 unknown; `[]` with no row; in the terminal the lines print under `== outside_review ==`: `outside review: none`, or one line per PR (`PR #12 · 3 outside rounds · 7 findings · code-forge $3.75 (+1 unknown) · B1, B2`). `agent_waivers` is the 15th section. It lists every agent waiver as `{block, file, finding, severity, reason, verdict, unwaived}`. A later human unwaive marks earlier matching rows as `unwaived: true`. A later agent waiver is a separate entry. An empty run gives `[]`. `run_time` is appended as the 16th section (shape below). `--export <dir>` writes one JSON file per section — the durable dataset a future UI reads. After the sections the terminal output prints the spend per run (open blocks included) and the total. `report` checks no row's signature in any section (an unsigned row counts like a signed one); shadow rows (`source: shadow`) are excluded from every section.
|
|
14
|
+
|
|
15
|
+
Each `cost_per_block` entry also carries `wallMs` and `humanWaitMs` in milliseconds. `wallMs` spans the first valid `dispatch.ts` through the final completion: `block.close` without a pending human check, or `block.human_pass` after `block.awaiting_human_check`. An entry whose last lifecycle row is `block.stop` has `stopped: true` and `wallMs` through the stop timestamp, with `humanWaitMs: null`; `stopped` is omitted otherwise. Stopped blocks are listed even without spend or a close. Attempts share the first dispatch; redispatching clears `stopped` and makes the block open again until its final completion, whose wall time still starts at the first dispatch. `humanWaitMs` spans the final check's `block.awaiting_human_check.ts` through `block.human_pass.ts`. Both fields are null while the block is open or awaiting a human check; `humanWaitMs` is also null with no human check. Missing or invalid endpoints give null, as do reversed endpoints. Existing grouping by run and block still applies.
|
|
16
|
+
|
|
17
|
+
In text output, a `cost_per_block` entry with no run uses `outside runs` in the run column, and spend with no run prints as ` outside runs: $<usd> · <n> session(s)` (with the same unknown-price suffix) after the real runs and before the spend total; `--json` and `--export` keep `run: null` unchanged.
|
|
18
|
+
|
|
19
|
+
`run_time` has the shape `{runs: [...], outside_runs?: {usd, rows}, total: {...}}` in `--json` and `run_time.json` from `--export`:
|
|
20
|
+
|
|
21
|
+
- `runs` lists only runs with a string run id, in first-seen ledger order. Each entry is `{run, started, ended, wallMs, usd, blocks_closed, blocks_stopped}`, with `open: true` added only when there is no valid `run.end` timestamp. `started` is the first valid `run.start.ts`, falling back to the run's first valid row timestamp, or null. `ended` is the final valid `run.end.ts`, or null. These timestamp strings preserve the row's ISO representation. `wallMs` is elapsed milliseconds to `ended`, or to the last valid row timestamp for an open run; it is null when endpoints are unavailable or reversed. `usd` reuses the existing per-run spend total, including open blocks and counting unknown-price sessions as 0. `blocks_closed` counts distinct completed blocks (pending human checks do not count); `blocks_stopped` counts distinct blocks with a `block.stop` row, even across attempts.
|
|
22
|
+
- `outside_runs` counts rows without a string run id as `{usd, rows}`: their spend from the same spend helpers and their row count. They contribute no run or wall time. The key is omitted when there are no such rows, including an empty ledger.
|
|
23
|
+
- `total` is `{runs, wallMs, usd, blocks_closed}`: the number of listed real runs, the sum of their known durations (null contributes 0), the existing whole-ledger spend total **including `outside_runs.usd`**, and the sum of their completed-block counts. With no rows it is `{runs: 0, wallMs: 0, usd: 0, blocks_closed: 0}` and the list is `[]`. One slug is one repo/site, so this is the site's cost and cumulative run time.
|
|
24
|
+
|
|
25
|
+
Only full ISO timestamps with a timezone are used for timing; missing or invalid `ts` values are ignored without dropping the row's spend or lifecycle information. Shadow rows never affect times, counts or spend. The terminal shows block dollars beside readable times (for example `12m 30s`) and a status of `completed`, `stopped` or `open`, retains the cost breakdown and proof time, and prints a short run table with its total line; `-` means unavailable time. When `outside_runs` exists, `outside runs: $<usd> (<rows> rows, no wall time)` prints immediately before the total, with no `(none)` run in the table.
|
|
14
26
|
|
|
15
27
|
## §3 Live views
|
|
16
28
|
|
package/skill/references/plan.md
CHANGED
|
@@ -2,7 +2,11 @@
|
|
|
2
2
|
|
|
3
3
|
## §0 Facts before design (step 0 of every plan)
|
|
4
4
|
|
|
5
|
-
Run `forge facts --brief <brief> [--sources <path>...] --out <plans_dir>/<slug>.facts.md` before anything else.
|
|
5
|
+
Run `forge facts --brief <brief> [--sources <path>...] --out <plans_dir>/<slug>.facts.md` before anything else.
|
|
6
|
+
|
|
7
|
+
`--sources <path>` adds the top-level .md/.txt files of a folder as more CLAIM TEXT; it is not a list of folders the delegate may read.
|
|
8
|
+
|
|
9
|
+
What it does:
|
|
6
10
|
|
|
7
11
|
1. Deterministic: the CLI extracts every *claim token* from the brief and the sources it names — CLI flags, `<cli> <verb>` commands, paths, versions, endpoints, package names, environment variable names — and writes a claim list. A `<cli> <verb>` written without its CLI (`run reload`) whose first word is not one of the project's CLIs, not a well-known CLI (`git gh npm npx node pnpm yarn bun deno ls cat grep which find test make docker curl claude codex python python3 pip go cargo op`), and that the brief never uses as a CLI of its own (`` `gh` `` or `` `gh --repo x` `` in a code span) — decided from the brief and `package.json` alone, never PATH — becomes `<project CLI> <verb>` (`code-forge run reload`) when the brief itself (not a source) names one of the project's CLIs (its `package.json` `bin` names) as a whole word — `.code-forge.yml` or `code-forge.config` do not count; `plan check` then counts a clause citing `run reload` as backed by that VERIFIED fact, and only when the extra word is one of the project's own CLIs.
|
|
8
12
|
2. One L0 session whose cwd is a **read-only snapshot of the repository at HEAD plus the `--sources` files**, built under the run's temp root and removed at session end — so `./path` claims are checkable, while **uncommitted files are not visible unless named as sources** (the snapshot is HEAD, not the working tree) — and whose packet is the claim list and the rule "run the cheapest read-only check, quote the command and the output line, tag VERIFIED / NOT-FOUND / UNVERIFIABLE, never infer". Two lines of defence, exactly as built. **First, at spawn:** the session gets the shell tool only, with the forbidden list plus every write verb rendered as deny rules and the provider's restricted mode (file tools confined to the working directory). Nothing is pre-approved: the delegate only runs what the provider already treats as read-only. A flag claim is therefore checked in the project's **source text**, never by running a program: `grep -rn -- <flag> <path>` over a top-level source folder of the snapshot such as `src` (never `.` or the root); it is VERIFIED only when the claim is that flag and the excerpt has a `file:line:text` hit from a source file — not `.md`/`.markdown`/`.txt`, not under `docs/ plans/ test/ tests/ fixtures/ node_modules/`, not the brief, and under the grep's path — holding the flag as a whole token (`--force-push` does not prove `--force`); a NOT-FOUND counts only over one whole top-level source folder that exists as a directory in the snapshot (not hidden, not `dist build coverage out` or the folders above). The grep's path must be an existing source directory of the snapshot (a nested one such as `src/cli` is fine for VERIFIED; a single file never is), and no other command — a plain grep included — ever proves or disproves a flag. Every check is ONE command: no pipe and no list (`| ; && || &`). There is no pre-execution gate on individual commands beyond the deny rules: the delegate runs what it runs. **Second, after the fact:** the CLI checks every `command` the delegate reports against an allow-list, fail closed, and refuses the whole sheet on the first violation. Allowed forms (never `rg`, which searches recursively by default): `ls cat head tail wc grep stat file uname sw_vers jq which` with in-cwd arguments (`grep` options only `-n -c -i -w -F` before `--`, recursive only as the whole command `grep -rn -- <--long-flag> <path>` with one relative subfolder, never `.` or a secret path; only a source folder proves a flag), `find` with only these predicates — `-name -iname -path -ipath -type -maxdepth -mindepth -print -print0 -newer -size -empty -mtime -mmin -not ! -a -o -and -or` — every other predicate refused, `git log|show|status|rev-parse|ls-files|cat-file`, `npm view|ls`, `test -e|-f|-d|-n <one operand>`, and `curl` as a HEAD request of exactly one http(s) URL with `-s -S -I -L -f --max-time <N>` only. No program ever runs with `--help`, `-h` or `--version` (BSD `rm foo --help` deletes `foo`): a version claim is checked with `npm view`, `which` or `ls`; an env claim is only `printenv NAME >/dev/null` with its excerpt blanked. Refused anywhere: backticks, `$`, redirection, unquoted globs, `~`, `..`, absolute paths outside `/usr`, `/opt/homebrew`, `/bin`, and secret-looking paths. **`npm view` and `curl -I` reach the network** — they are what makes a package version or an endpoint checkable; nothing else the delegate may run does.
|