@event4u/agent-config 16.1.0 → 16.2.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/.augment-plugin/marketplace.json +3 -3
- package/.augment-plugin/plugin.json +1 -1
- package/.claude-plugin/marketplace.json +1 -1
- package/CHANGELOG.md +237 -248
- package/dist/agent-src/commands/roadmap/process-full.md +1 -1
- package/dist/agent-src/commands/work.md +4 -0
- package/dist/agent-src/contexts/execution/autonomy-mechanics.md +4 -8
- package/dist/agent-src/rules/ui-audit-gate.md +1 -2
- package/dist/agent-src/skills/accessibility-auditor/data/aria-patterns.csv +1 -1
- package/dist/agent-src/skills/accessibility-auditor/data/manifest.json +2 -2
- package/dist/agent-src/skills/design-review/SKILL.md +30 -4
- package/dist/agent-src/skills/design-review/references/verification-automation.md +32 -0
- package/dist/agent-src/skills/existing-ui-audit/SKILL.md +16 -1
- package/dist/agent-src/skills/existing-ui-audit/references/component-taxonomy-detection.md +56 -0
- package/dist/agent-src/skills/markitdown/SKILL.md +4 -0
- package/dist/agent-src/skills/playwright-testing/SKILL.md +30 -6
- package/dist/agent-src/skills/ui-component-architect/SKILL.md +23 -0
- package/dist/agent-src/templates/agents/agent-project-settings.example.yml +1 -1
- package/dist/agent-src/templates/roadmaps.md +30 -0
- package/dist/agent-src/templates/scripts/work_engine/directives/ui/app_spec.ts +10 -4
- package/dist/agent-src/templates/scripts/work_engine/directives/ui/apply.ts +260 -21
- package/dist/agent-src/templates/scripts/work_engine/directives/ui/audit.ts +22 -3
- package/dist/agent-src/templates/scripts/work_engine/directives/ui/scaffold.ts +6 -1
- package/dist/agent-src/templates/scripts/work_engine/state.ts +57 -7
- package/dist/agent-src/templates/scripts/work_engine/taxonomy/detect.ts +500 -0
- package/dist/agent-src/templates/scripts/work_engine/taxonomy/place.ts +183 -0
- package/dist/cli/python/knowledge_ingest.js +1 -1
- package/dist/cli/python/workspace_drive.js +1 -1
- package/dist/cli-delegate/{chunk-S3VFXMTM.js → chunk-4XFP2Y3E.js} +1 -1
- package/dist/cli-delegate/{chunk-HDVVGPQW.js → chunk-7XGBX6ZH.js} +471 -387
- package/dist/cli-delegate/{chunk-GJCBB2UG.js → chunk-FP36SWJD.js} +432 -144
- package/dist/cli-delegate/{chunk-UNCRJBT3.js → chunk-IGD6ERX6.js} +31 -0
- package/dist/cli-delegate/{chunk-FXYUYU4N.js → chunk-JNMVLWLR.js} +42 -1
- package/dist/cli-delegate/{chunk-QJWAUMJW.js → chunk-YJ72UIAK.js} +730 -15
- package/dist/cli-delegate/cmd_conformance.js +902 -782
- package/dist/cli-delegate/cmd_doctor.js +2 -2
- package/dist/cli-delegate/cmd_export.js +2 -2
- package/dist/cli-delegate/cmd_handoff.js +4 -4
- package/dist/cli-delegate/cmd_memory_get.js +1 -1
- package/dist/cli-delegate/cmd_prune.js +2 -2
- package/dist/cli-delegate/cmd_route_audit.js +1 -1
- package/dist/cli-delegate/cmd_session_recycle.js +2 -2
- package/dist/cli-delegate/cmd_settings_migrate.js +2 -2
- package/dist/cli-delegate/cmd_settings_set.js +1 -1
- package/dist/cli-delegate/cmd_sync.js +2 -2
- package/dist/cli-delegate/cmd_uninstall.js +2 -2
- package/dist/cli-delegate/cmd_validate.js +2 -2
- package/dist/discovery/deprecation-report.md +1 -1
- package/dist/discovery/discovery-manifest.json +10 -10
- package/dist/discovery/discovery-manifest.json.sha256 +1 -1
- package/dist/discovery/discovery-manifest.summary.md +1 -1
- package/dist/discovery/dormancy-report.md +1 -1
- package/dist/discovery/orphan-report.md +1 -1
- package/dist/discovery/packs.json +2 -2
- package/dist/discovery/trust-report.md +1 -1
- package/dist/discovery/workspaces.json +2 -2
- package/dist/hooks/dispatch.js +6666 -2581
- package/dist/hooks/dispatch.sha256 +1 -0
- package/dist/install/install.mjs +848 -733
- package/dist/mcp/registry-manifest.json +2 -2
- package/dist/mcp/server.json +2 -2
- package/dist/mcp/server.mjs +2747 -1510
- package/dist/server/schemas/settings.js +23 -0
- package/dist/ui/assets/index-Cwq4d0hX.js +23 -0
- package/dist/ui/assets/index-Cyjy8bWO.css +1 -0
- package/dist/ui/index.html +2 -2
- package/docs/contracts/settings-classes.md +6 -4
- package/package.json +4 -4
- package/src/agent-src/templates/scripts/work_engine/directives/ui/app_spec.ts +10 -4
- package/src/agent-src/templates/scripts/work_engine/directives/ui/apply.ts +260 -21
- package/src/agent-src/templates/scripts/work_engine/directives/ui/audit.ts +22 -3
- package/src/agent-src/templates/scripts/work_engine/directives/ui/scaffold.ts +6 -1
- package/src/agent-src/templates/scripts/work_engine/state.ts +57 -7
- package/src/agent-src/templates/scripts/work_engine/taxonomy/detect.ts +500 -0
- package/src/agent-src/templates/scripts/work_engine/taxonomy/place.ts +183 -0
- package/src/config/agent-settings.template.yml +29 -0
- package/src/config/ci-local-parity.yml +18 -0
- package/src/config/continuity-surface.json +2 -2
- package/src/config/gate-coverage.yml +234 -3
- package/src/config/gate-violation-baselines.json +7 -3
- package/src/config/hook-latency-budget.json +21 -0
- package/src/config/hook-token-budget.json +2 -2
- package/src/config/metric-registry.yml +114 -0
- package/src/config/placeholder-drift-budget.json +10 -0
- package/src/config/routing-coverage-seed.json +18 -4
- package/src/scripts/_cli/cmd_doctor.ts +5 -19
- package/src/scripts/_cli/doctor_execution.ts +100 -17
- package/src/scripts/_cli/doctor_network_posture.ts +226 -0
- package/src/scripts/_lib/body_portable.ts +102 -0
- package/src/scripts/_lib/cc_transcript.ts +132 -1
- package/src/scripts/_lib/concern_sla_window.ts +200 -0
- package/src/scripts/_lib/forge_protection.ts +114 -0
- package/src/scripts/_lib/forge_reader.ts +558 -0
- package/src/scripts/_lib/host_env_write.ts +111 -0
- package/src/scripts/_lib/llm_proposer_transport.ts +14 -4
- package/src/scripts/_lib/obligations.ts +119 -8
- package/src/scripts/_lib/probe_inputs.ts +271 -0
- package/src/scripts/_lib/read_surface_scan.ts +322 -0
- package/src/scripts/_lib/release_highlights.ts +2 -1
- package/src/scripts/_lib/retrieval_sanitize.ts +77 -16
- package/src/scripts/_lib/shell_write_shapes.ts +108 -0
- package/src/scripts/_lib/structural_hiding.ts +622 -0
- package/src/scripts/_lib/turn_end_refusals.ts +525 -4
- package/src/scripts/_lib/turn_end_transcript.ts +216 -0
- package/src/scripts/_lib/verification_command.ts +324 -0
- package/src/scripts/_lib/verification_evidence.ts +647 -0
- package/src/scripts/_lib/verify_clause.ts +280 -0
- package/src/scripts/ai_team/team_dispatch.ts +4 -0
- package/src/scripts/before_complete_hook.ts +340 -23
- package/src/scripts/bench_hook_latency.ts +412 -5
- package/src/scripts/cache_realization_report.ts +61 -1
- package/src/scripts/check_argument_hint.ts +287 -0
- package/src/scripts/check_claims.ts +125 -1
- package/src/scripts/check_enforcement_coverage.ts +38 -9
- package/src/scripts/check_enforcement_matrix.ts +245 -3
- package/src/scripts/check_finding_dispositions.ts +22 -3
- package/src/scripts/check_generator_sync.ts +71 -0
- package/src/scripts/check_host_docs_digest.ts +786 -0
- package/src/scripts/check_host_format_column.ts +559 -0
- package/src/scripts/check_input_references.ts +228 -0
- package/src/scripts/check_inputs_parity.ts +235 -0
- package/src/scripts/check_kernel_edit_ratified.ts +47 -5
- package/src/scripts/check_kill_switch_table.ts +294 -0
- package/src/scripts/check_placeholder_drift.ts +221 -0
- package/src/scripts/check_read_surface_coverage.ts +515 -0
- package/src/scripts/check_release_holds.ts +2 -1
- package/src/scripts/check_routing_coverage.ts +378 -11
- package/src/scripts/check_static_parity.ts +17 -0
- package/src/scripts/ci_settle.ts +145 -19
- package/src/scripts/closure_scan.ts +115 -1
- package/src/scripts/compile_hook_manifest.ts +60 -16
- package/src/scripts/external_sources_denylist.json +29 -4
- package/src/scripts/hook_manifest.json +1 -1
- package/src/scripts/hook_manifest.yaml +42 -5
- package/src/scripts/hooks/block_config_weakening.ts +229 -4
- package/src/scripts/hooks/block_kernel_rule_writes.ts +9 -78
- package/src/scripts/hooks/block_plumbing_writes.ts +314 -0
- package/src/scripts/hooks/bundle_integrity.ts +291 -0
- package/src/scripts/hooks/chain_nudge_hook.ts +1 -2
- package/src/scripts/hooks/code_graph_context_hook.ts +1 -2
- package/src/scripts/hooks/comment_discipline_hook.ts +1 -3
- package/src/scripts/hooks/concern_registry.ts +2 -0
- package/src/scripts/hooks/concern_timings.ts +94 -0
- package/src/scripts/hooks/delegation_nudge_hook.ts +1 -2
- package/src/scripts/hooks/design_pass_hook.ts +100 -9
- package/src/scripts/hooks/design_slop_hook.ts +1 -2
- package/src/scripts/hooks/dispatch_hook.ts +25 -19
- package/src/scripts/hooks/edit_shape_hook.ts +1 -2
- package/src/scripts/hooks/end_review_nudge_hook.ts +1 -1
- package/src/scripts/hooks/evidence_independence.ts +1 -2
- package/src/scripts/hooks/exit_codes.ts +128 -0
- package/src/scripts/hooks/host_lowering.json +1 -0
- package/src/scripts/hooks/host_lowering.ts +128 -3
- package/src/scripts/hooks/host_lowering.yaml +262 -46
- package/src/scripts/hooks/host_semantics.ts +30 -2
- package/src/scripts/hooks/injection_budget.ts +9 -2
- package/src/scripts/hooks/interruption_ledger_hook.ts +1 -2
- package/src/scripts/hooks/journal_record_hook.ts +1 -2
- package/src/scripts/hooks/memory_recall_hook.ts +1 -2
- package/src/scripts/hooks/obligation_settle_hook.ts +151 -5
- package/src/scripts/hooks/one_question_per_ask_hook.ts +25 -4
- package/src/scripts/hooks/orchestration_record_hook.ts +1 -2
- package/src/scripts/hooks/permission_gate.ts +1 -2
- package/src/scripts/hooks/push_settle_hook.ts +1 -3
- package/src/scripts/hooks/replay_hook.ts +2 -1
- package/src/scripts/hooks/reread_guard_hook.ts +1 -2
- package/src/scripts/hooks/review_baseline_hook.ts +1 -2
- package/src/scripts/hooks/rtk_wrap_hook.ts +1 -2
- package/src/scripts/hooks/rule_inject_hook.ts +8 -5
- package/src/scripts/hooks/run_continuation_hook.ts +13 -15
- package/src/scripts/hooks/skill_route_hook.ts +1 -2
- package/src/scripts/hooks/source_first_gate_hook.ts +1 -2
- package/src/scripts/hooks/spawn_guard_shadow_hook.ts +1 -2
- package/src/scripts/hooks/subagent_ledger_hook.ts +1 -2
- package/src/scripts/hooks/suggestion_capture_hook.ts +1 -1
- package/src/scripts/hooks/table_fingerprint.ts +49 -0
- package/src/scripts/hooks/telemetry_flush_hook.ts +1 -2
- package/src/scripts/hooks/telemetry_usage_hook.ts +1 -2
- package/src/scripts/hooks/tool_result_bytes_hook.ts +93 -9
- package/src/scripts/hooks/turn_end_gate_hook.ts +337 -324
- package/src/scripts/hooks/ui_route_nudge_hook.ts +1 -2
- package/src/scripts/install.ts +3 -4
- package/src/scripts/lint_deny_text.ts +419 -0
- package/src/scripts/lint_exit_codes.ts +262 -0
- package/src/scripts/lint_hook_manifest.ts +55 -0
- package/src/scripts/lint_roadmap_blockers.ts +78 -12
- package/src/scripts/lint_skill_descriptions.ts +1 -1
- package/src/scripts/lint_skill_scripts_readonly.ts +1 -1
- package/src/scripts/mcp_server/prompts.ts +142 -1
- package/src/scripts/measure_skill_ranker_baseline.ts +305 -51
- package/src/scripts/measure_turn_end_gate.ts +193 -0
- package/src/scripts/memory_lookup.ts +1 -1
- package/src/scripts/prepack-check.mjs +53 -0
- package/src/scripts/release_publication.ts +13 -1
- package/src/scripts/report_invocation_surface.ts +383 -0
- package/src/scripts/report_obligation_carriers.ts +699 -0
- package/src/scripts/report_obligation_writer_split.ts +236 -0
- package/src/scripts/report_skill_menu_census.ts +208 -0
- package/src/scripts/report_standing_payload_by_host.ts +1 -1
- package/src/scripts/roadmap_verify_share.ts +213 -0
- package/src/scripts/schemas/command.schema.json +44 -0
- package/src/scripts/schemas/review-findings.schema.json +4 -0
- package/src/scripts/schemas/skill.schema.json +44 -0
- package/src/scripts/self_review_gate.ts +359 -10
- package/src/scripts/semantic_parity_census.ts +314 -0
- package/src/scripts/session_eol_report.ts +54 -1
- package/src/scripts/sweep_dead_scan_roots.ts +1 -1
- package/src/scripts/trigger_eval_grandfather.json +0 -2
- package/src/scripts/trigger_eval_rotation.ts +122 -27
- package/src/scripts/ui_conformance_probe.ts +156 -4
- package/src/scripts/update_prices.ts +85 -3
- package/src/scripts/write_bundle_digest.mjs +74 -0
- package/src/server/schemas/settings.ts +31 -0
- package/src/templates/consumer-settings/ONBOARDING.md +1 -0
- package/dist/ui/assets/index-DH1xc62v.js +0 -29
- package/dist/ui/assets/index-jSfMW8_t.css +0 -1
- /package/dist/cli-delegate/{chunk-YUM5KURG.js → chunk-7RQP4UDH.js} +0 -0
|
@@ -62,8 +62,11 @@ import {
|
|
|
62
62
|
prune_stale_session_states,
|
|
63
63
|
session_state_file,
|
|
64
64
|
update_json_under_lock,
|
|
65
|
+
owns_session_state,
|
|
65
66
|
} from "./hooks/state_io.js";
|
|
66
67
|
import { readHookStdin } from "./hooks/hook_stdin.js";
|
|
68
|
+
import { isVerificationCommand, mightBeVerification } from "./_lib/verification_command.js";
|
|
69
|
+
import { runnerOf } from "./_lib/verification_evidence.js";
|
|
67
70
|
|
|
68
71
|
// NOTE: the Python docstring says `agents/runtime/state/`, but the code
|
|
69
72
|
// constant is `agents/state/`. Replicated verbatim — latent docstring/code
|
|
@@ -144,28 +147,6 @@ export const COMMAND_TOOLS: ReadonlySet<string> = new Set([
|
|
|
144
147
|
"RunShellCommand", // Cursor
|
|
145
148
|
]);
|
|
146
149
|
|
|
147
|
-
// Permissive verification-command pattern. Observability — false positives
|
|
148
|
-
// are cheaper than false negatives. Word-boundary anchored on common
|
|
149
|
-
// shell separators so chained commands (`task sync && task ci`) match.
|
|
150
|
-
//
|
|
151
|
-
// Python re flags: IGNORECASE. The pattern uses no Python-specific syntax;
|
|
152
|
-
// it ports verbatim. The leading `(?:^|[\s;&|`(])` class includes backtick.
|
|
153
|
-
const _VERIFICATION_RE = new RegExp(
|
|
154
|
-
"(?:^|[\\s;&|`(])(" +
|
|
155
|
-
"task\\s+(?:ci|test|tests|lint|check|qa|phpstan|rector|ecs|pest|pytest)" +
|
|
156
|
-
"|(?:\\./|\\.venv/bin/|vendor/bin/)?(?:pest|phpunit|phpstan|psalm|rector|ecs)\\b" +
|
|
157
|
-
"|(?:python3?|\\.venv/bin/python3?)\\s+-m\\s+pytest" +
|
|
158
|
-
"|pytest\\b" +
|
|
159
|
-
"|(?:npm|pnpm|yarn|bun)\\s+(?:run\\s+)?(?:test|check|lint|typecheck|tsc)" +
|
|
160
|
-
"|cargo\\s+(?:test|check|clippy)" +
|
|
161
|
-
"|go\\s+test" +
|
|
162
|
-
"|make\\s+(?:test|check|lint)" +
|
|
163
|
-
"|composer\\s+(?:test|check|lint|phpstan)" +
|
|
164
|
-
"|(?:php\\s+)?artisan\\s+test" +
|
|
165
|
-
")",
|
|
166
|
-
"i",
|
|
167
|
-
);
|
|
168
|
-
|
|
169
150
|
type StateDict = Record<string, unknown>;
|
|
170
151
|
|
|
171
152
|
/** Python datetime.now(timezone.utc).isoformat(timespec="seconds"). */
|
|
@@ -193,6 +174,11 @@ function _empty_state(): StateDict {
|
|
|
193
174
|
// has to survive it: the measured failure is a completion claim in a LATER
|
|
194
175
|
// turn than the poll it rests on.
|
|
195
176
|
ci_last: null,
|
|
177
|
+
// Round 8 — see `_reset_turn`. Present in the empty state so a freshly
|
|
178
|
+
// created file has the same shape as one that has seen a turn boundary;
|
|
179
|
+
// a reader must never have to distinguish "no runs yet" from "key absent".
|
|
180
|
+
verification_runs: [],
|
|
181
|
+
edits_this_turn: 0,
|
|
196
182
|
checked_at: _now(),
|
|
197
183
|
};
|
|
198
184
|
}
|
|
@@ -230,7 +216,7 @@ function _is_verification(command: string): boolean {
|
|
|
230
216
|
// settle is genuine, which is what the FC-3b guard below decides. Before this
|
|
231
217
|
// change a CI poll counted for nothing at all, so nothing observed the
|
|
232
218
|
// difference between "settled" and "never started".
|
|
233
|
-
return
|
|
219
|
+
return isVerificationCommand(command) || isCiPoll(command);
|
|
234
220
|
}
|
|
235
221
|
|
|
236
222
|
/**
|
|
@@ -334,11 +320,207 @@ function _extract_output(payload: StateDict): string | null {
|
|
|
334
320
|
return null;
|
|
335
321
|
}
|
|
336
322
|
|
|
323
|
+
/**
|
|
324
|
+
* Tool names that CHANGE the tree, across the platforms this concern sees.
|
|
325
|
+
*
|
|
326
|
+
* The first four mirror `_EDIT_TOOLS` in the turn-end gate exactly; the rest are
|
|
327
|
+
* the same three tools under the names Augment, Cline and Cursor give them. The
|
|
328
|
+
* gate reads a Claude transcript and can afford four names; this recorder runs
|
|
329
|
+
* on every platform the manifest binds `post_tool_use` on, so it needs all of
|
|
330
|
+
* them — a missed edit name means `edits_this_turn` undercounts and a
|
|
331
|
+
* verification run looks LATER in the sequence than it was, which is the one
|
|
332
|
+
* direction that could clear an unverified edit.
|
|
333
|
+
*/
|
|
334
|
+
const EDIT_TOOLS: ReadonlySet<string> = new Set([
|
|
335
|
+
"Edit",
|
|
336
|
+
"Write",
|
|
337
|
+
"MultiEdit",
|
|
338
|
+
"NotebookEdit",
|
|
339
|
+
"str-replace-editor",
|
|
340
|
+
"save-file",
|
|
341
|
+
"str_replace_editor",
|
|
342
|
+
"create_file",
|
|
343
|
+
"write_to_file",
|
|
344
|
+
"replace_in_file",
|
|
345
|
+
"apply_diff",
|
|
346
|
+
"edit_file",
|
|
347
|
+
]);
|
|
348
|
+
|
|
349
|
+
/**
|
|
350
|
+
* How many run records one turn may keep.
|
|
351
|
+
*
|
|
352
|
+
* The state file is read by a stop-slot hook on every turn, so it is on the
|
|
353
|
+
* latency path and cannot grow without a ceiling. The NEWEST are kept because
|
|
354
|
+
* the only question asked of them is whether a passing run follows the LAST
|
|
355
|
+
* edit, which is always answered by the tail.
|
|
356
|
+
*/
|
|
357
|
+
export const MAX_VERIFICATION_RUNS_PER_TURN = 24;
|
|
358
|
+
|
|
359
|
+
/**
|
|
360
|
+
* Characters of tool output retained per stream per record.
|
|
361
|
+
*
|
|
362
|
+
* CHARACTERS, not bytes, and the name is kept for its existing importers: the
|
|
363
|
+
* slice is applied to a JS string. Multibyte output can therefore exceed this
|
|
364
|
+
* many bytes, which is a bound on the state file rather than a promise about
|
|
365
|
+
* it, and 4,096 characters is far more than any runner's summary needs.
|
|
366
|
+
*/
|
|
367
|
+
export const RUN_OUTPUT_TAIL_BYTES = 4096;
|
|
368
|
+
|
|
369
|
+
/**
|
|
370
|
+
* Apply the per-turn cap, keeping the newest AND the earliest failing record.
|
|
371
|
+
*
|
|
372
|
+
* Two detectors read this array and they ask opposite-ended questions. Detector
|
|
373
|
+
* C asks whether a pass follows the last edit — always answered by the tail.
|
|
374
|
+
* Detector F asks whether a red preceded the green — answered by the head. A
|
|
375
|
+
* plain `slice(-N)` serves the first and can silently drop the evidence the
|
|
376
|
+
* second needs, turning a turn that did honest red-green work into a
|
|
377
|
+
* `no_red_evidence` refusal once the run count passes the cap.
|
|
378
|
+
*/
|
|
379
|
+
function _cap_runs(runs: unknown[]): unknown[] {
|
|
380
|
+
if (runs.length <= MAX_VERIFICATION_RUNS_PER_TURN) return runs;
|
|
381
|
+
const tail = runs.slice(-(MAX_VERIFICATION_RUNS_PER_TURN - 1));
|
|
382
|
+
const head = runs.find((r) => {
|
|
383
|
+
if (r === null || typeof r !== "object" || Array.isArray(r)) return false;
|
|
384
|
+
const rec = r as StateDict;
|
|
385
|
+
const code = rec["exit_code"];
|
|
386
|
+
return typeof code === "number" && code !== 0;
|
|
387
|
+
});
|
|
388
|
+
if (head === undefined || tail.includes(head)) {
|
|
389
|
+
return runs.slice(-MAX_VERIFICATION_RUNS_PER_TURN);
|
|
390
|
+
}
|
|
391
|
+
return [head, ...tail];
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
/**
|
|
395
|
+
* The prefix a host puts on a FAILED shell result when it carries no exit field.
|
|
396
|
+
*
|
|
397
|
+
* Claude Code's Bash tool does exactly this: a failing call's whole tool
|
|
398
|
+
* response is the string `Error: Exit code 1\n<output>`. Measured over 1,077
|
|
399
|
+
* object-shaped and 11 string-shaped tool results in this machine's own
|
|
400
|
+
* transcripts (2026-09-29): every string-shaped result carried this prefix, and
|
|
401
|
+
* no shape of either kind carried any of the numeric field names below.
|
|
402
|
+
*/
|
|
403
|
+
const _ERROR_EXIT_PREFIX = /^Error:\s*Exit code\s*(\d+)/i;
|
|
404
|
+
|
|
405
|
+
/** The status a post-tool payload actually reports, and HOW it reported it. */
|
|
406
|
+
interface ExitReading {
|
|
407
|
+
readonly code: number | null;
|
|
408
|
+
/** Provenance, recorded so a reader can tell an observed code from a derived one. */
|
|
409
|
+
readonly source: "field" | "error_prefix" | "response_shape" | null;
|
|
410
|
+
/** The host said the call was interrupted — a kill, not a verdict on the work. */
|
|
411
|
+
readonly interrupted: boolean;
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
const _NO_EXIT: ExitReading = { code: null, source: null, interrupted: false };
|
|
415
|
+
|
|
416
|
+
function _numeric_exit_field(obj: StateDict): number | null {
|
|
417
|
+
for (const key of ["exit_code", "exitCode", "returncode", "returnCode", "status_code"]) {
|
|
418
|
+
const v = obj[key];
|
|
419
|
+
if (typeof v === "number" && Number.isFinite(v)) return Math.trunc(v);
|
|
420
|
+
if (typeof v === "string" && v.trim() !== "" && Number.isFinite(Number(v))) {
|
|
421
|
+
return Math.trunc(Number(v));
|
|
422
|
+
}
|
|
423
|
+
}
|
|
424
|
+
return null;
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
/**
|
|
428
|
+
* What the host said about how the command ended.
|
|
429
|
+
*
|
|
430
|
+
* `null` is a REAL value and is never normalised to 0: several hosts surface no
|
|
431
|
+
* exit status at all, and writing 0 for "the host said nothing" would
|
|
432
|
+
* manufacture the strongest possible evidence out of silence. The classifier's
|
|
433
|
+
* `exit_code_unavailable` verdict exists so that gap stays visible.
|
|
434
|
+
*
|
|
435
|
+
* THREE SOURCES, in descending directness, and the third is the one that took a
|
|
436
|
+
* measurement rather than a guess.
|
|
437
|
+
*
|
|
438
|
+
* · `field` — a numeric exit field, wherever a host provides one.
|
|
439
|
+
* · `error_prefix` — the `Error: Exit code N` string form above. This is a
|
|
440
|
+
* real non-zero code, stated by the host in the only place it states it.
|
|
441
|
+
* · `response_shape` — an OBJECT response carrying `interrupted: false`
|
|
442
|
+
* alongside a `stdout` or `stderr` key. On Claude Code that is the success
|
|
443
|
+
* form, and the discriminator is deliberately the PRESENCE of
|
|
444
|
+
* `interrupted: false` rather than the ABSENCE of an error prefix: a
|
|
445
|
+
* positive signal from the host, not an inference from silence. A shape
|
|
446
|
+
* that carries neither reads as `null` and stays an instrument gap.
|
|
447
|
+
*
|
|
448
|
+
* Without the second and third readings this recorder wrote `exit_code: null`
|
|
449
|
+
* for every command on the one host that binds the turn-end gate, so the whole
|
|
450
|
+
* record path was inert — found by an independent review of the branch that
|
|
451
|
+
* introduced it, against 1,077 real tool results.
|
|
452
|
+
*/
|
|
453
|
+
function _extract_exit_reading(payload: StateDict): ExitReading {
|
|
454
|
+
const direct = _numeric_exit_field(payload);
|
|
455
|
+
if (direct !== null) return { code: direct, source: "field", interrupted: false };
|
|
456
|
+
|
|
457
|
+
for (const key of ["tool_response", "toolResponse", "result", "output"]) {
|
|
458
|
+
const v = payload[key];
|
|
459
|
+
if (typeof v === "string") {
|
|
460
|
+
const m = _ERROR_EXIT_PREFIX.exec(v);
|
|
461
|
+
if (m?.[1] !== undefined) {
|
|
462
|
+
return { code: Number(m[1]), source: "error_prefix", interrupted: false };
|
|
463
|
+
}
|
|
464
|
+
continue;
|
|
465
|
+
}
|
|
466
|
+
if (v === null || typeof v !== "object" || Array.isArray(v)) continue;
|
|
467
|
+
const obj = v as StateDict;
|
|
468
|
+
const nested = _numeric_exit_field(obj);
|
|
469
|
+
if (nested !== null) return { code: nested, source: "field", interrupted: false };
|
|
470
|
+
if (obj["interrupted"] === true) return { code: null, source: null, interrupted: true };
|
|
471
|
+
const hasStream = typeof obj["stdout"] === "string" || typeof obj["stderr"] === "string";
|
|
472
|
+
if (obj["interrupted"] === false && hasStream) {
|
|
473
|
+
return { code: 0, source: "response_shape", interrupted: false };
|
|
474
|
+
}
|
|
475
|
+
}
|
|
476
|
+
return _NO_EXIT;
|
|
477
|
+
}
|
|
478
|
+
|
|
479
|
+
/**
|
|
480
|
+
* The command's stdout and stderr as REAL strings, never as a stringified blob.
|
|
481
|
+
*
|
|
482
|
+
* Separate from `_extract_output` above, which feeds the pre-existing `vacuous`
|
|
483
|
+
* counters and whose behavior must not move: that one returns
|
|
484
|
+
* `JSON.stringify(v)` for an object response, and every parser in
|
|
485
|
+
* `verification_evidence.ts` is line-anchored (`/^[ \t]*Tests:?.../m` and
|
|
486
|
+
* friends). JSON escapes a newline as the two characters `\` and `n`, so a
|
|
487
|
+
* stringified response can never match any of them — the classifier was blind
|
|
488
|
+
* to every summary on the host's success shape. Found by the same review.
|
|
489
|
+
*/
|
|
490
|
+
function _extract_run_streams(payload: StateDict): { stdout: string; stderr: string } {
|
|
491
|
+
for (const key of ["tool_response", "toolResponse", "result", "output"]) {
|
|
492
|
+
const v = payload[key];
|
|
493
|
+
if (typeof v === "string") return { stdout: v, stderr: "" };
|
|
494
|
+
if (v === null || typeof v !== "object" || Array.isArray(v)) continue;
|
|
495
|
+
const obj = v as StateDict;
|
|
496
|
+
const out = obj["stdout"] ?? obj["output"];
|
|
497
|
+
const err = obj["stderr"];
|
|
498
|
+
if (typeof out === "string" || typeof err === "string") {
|
|
499
|
+
return {
|
|
500
|
+
stdout: typeof out === "string" ? out : "",
|
|
501
|
+
stderr: typeof err === "string" ? err : "",
|
|
502
|
+
};
|
|
503
|
+
}
|
|
504
|
+
}
|
|
505
|
+
const topOut = payload["stdout"] ?? payload["output"];
|
|
506
|
+
const topErr = payload["stderr"];
|
|
507
|
+
return {
|
|
508
|
+
stdout: typeof topOut === "string" ? topOut : "",
|
|
509
|
+
stderr: typeof topErr === "string" ? topErr : "",
|
|
510
|
+
};
|
|
511
|
+
}
|
|
512
|
+
|
|
337
513
|
function _reset_turn(state: StateDict, session_id: string): StateDict {
|
|
338
514
|
state["session_id"] = session_id || state["session_id"] || "";
|
|
339
515
|
state["turn_started_at"] = _now();
|
|
340
516
|
state["verifications_this_turn"] = 0;
|
|
341
517
|
state["verified_this_turn"] = false;
|
|
518
|
+
// Round 8 — the run records the stop gate reads instead of the command text,
|
|
519
|
+
// and the edit counter that places them in the turn's sequence. Both are
|
|
520
|
+
// TURN-scoped: a run from the previous turn demonstrably did not exercise this
|
|
521
|
+
// turn's edits, which is the freshness argument `verify-before-complete` makes.
|
|
522
|
+
state["verification_runs"] = [];
|
|
523
|
+
state["edits_this_turn"] = 0;
|
|
342
524
|
// FC-3b turn-scoped counters: a CI settle must be witnessed within the same
|
|
343
525
|
// turn that claims it, so the in-flight observation does not survive a turn.
|
|
344
526
|
state["ci_saw_pending"] = false;
|
|
@@ -386,6 +568,11 @@ function _update(state: StateDict, event: string, envelope: StateDict): StateDic
|
|
|
386
568
|
state = _reset_turn(state, session_id);
|
|
387
569
|
} else if (event === "post_tool_use") {
|
|
388
570
|
const [tool, cmd] = _extract_command(pl);
|
|
571
|
+
// Counted BEFORE the verification branch, so a run recorded in this same
|
|
572
|
+
// event is placed after the edits that preceded it and never after itself.
|
|
573
|
+
if (tool !== null && EDIT_TOOLS.has(tool)) {
|
|
574
|
+
state["edits_this_turn"] = _asInt(state["edits_this_turn"]) + 1;
|
|
575
|
+
}
|
|
389
576
|
if (cmd && _is_verification(cmd)) {
|
|
390
577
|
const output = _extract_output(pl);
|
|
391
578
|
// No readable output → the guard has nothing to judge, so behaviour is
|
|
@@ -439,6 +626,7 @@ function _update(state: StateDict, event: string, envelope: StateDict): StateDic
|
|
|
439
626
|
vacuous,
|
|
440
627
|
counted: counts,
|
|
441
628
|
};
|
|
629
|
+
|
|
442
630
|
if (counts) {
|
|
443
631
|
state["verifications_this_turn"] = _asInt(state["verifications_this_turn"]) + 1;
|
|
444
632
|
state["verifications_this_session"] =
|
|
@@ -448,6 +636,67 @@ function _update(state: StateDict, event: string, envelope: StateDict): StateDic
|
|
|
448
636
|
state["nonevidence_this_turn"] = _asInt(state["nonevidence_this_turn"]) + 1;
|
|
449
637
|
}
|
|
450
638
|
}
|
|
639
|
+
|
|
640
|
+
// The RUN record, written beside `last_verification` and gated on a
|
|
641
|
+
// DIFFERENT, wider selector — the one the turn-end gate's detector C uses.
|
|
642
|
+
//
|
|
643
|
+
// Two selectors is a smell, so here is why it is not one — and the reason
|
|
644
|
+
// was rewritten 2026-09-30, because the version standing here was half
|
|
645
|
+
// wrong. It claimed the narrow selector protected `verifications_this_turn`
|
|
646
|
+
// AND the FC-3b `ci_last` discrimination. `ci_last` is written inside the
|
|
647
|
+
// nested `isCiPoll` branch above, reached through the second disjunct of
|
|
648
|
+
// `_is_verification` whatever the first one says, so no widening of the
|
|
649
|
+
// first could ever have moved it; and nothing branches on the counter.
|
|
650
|
+
// Both are now pinned by tests rather than asserted in a comment.
|
|
651
|
+
//
|
|
652
|
+
// What survives is a real distinction, and it is about ERROR COST, not
|
|
653
|
+
// about two spellings of one idea. `isVerificationCommand` answers *did
|
|
654
|
+
// this verify* — a false positive there clears an unverified edit, so it is
|
|
655
|
+
// head-anchored. `mightBeVerification` answers *is this worth writing down*
|
|
656
|
+
// — and the run record decides nothing by itself, since `classifyRun` is
|
|
657
|
+
// what judges it. So the record path wants every command the gate might
|
|
658
|
+
// read, including the ones the classifier will then reject —
|
|
659
|
+
// `not_a_verification_command` is a verdict the gate needs to SEE, and a
|
|
660
|
+
// recorder that filtered those out would leave the gate unable to tell "the
|
|
661
|
+
// turn ran `echo test`" from "the turn ran nothing at all".
|
|
662
|
+
//
|
|
663
|
+
// `after_edits` is the edit counter as it stood when the command ran. The
|
|
664
|
+
// reader compares it against the number of edits the turn made in total:
|
|
665
|
+
// equal or greater means nothing was edited afterwards. That is an ORDINAL
|
|
666
|
+
// comparison needing no clock, so it survives a host whose timestamps are
|
|
667
|
+
// coarse or absent, and it degrades toward under-refusing when the reader's
|
|
668
|
+
// own count is short.
|
|
669
|
+
if (cmd && mightBeVerification(cmd)) {
|
|
670
|
+
const streams = _extract_run_streams(pl);
|
|
671
|
+
const exit = _extract_exit_reading(pl);
|
|
672
|
+
const runs = Array.isArray(state["verification_runs"])
|
|
673
|
+
? [...(state["verification_runs"] as unknown[])]
|
|
674
|
+
: [];
|
|
675
|
+
runs.push({
|
|
676
|
+
command: cmd.slice(0, 512),
|
|
677
|
+
tool,
|
|
678
|
+
exit_code: exit.code,
|
|
679
|
+
// Provenance, not decoration: `response_shape` is a code this recorder
|
|
680
|
+
// DERIVED from the host's success form, and a reader disputing a verdict
|
|
681
|
+
// needs to know which of the three readings produced it.
|
|
682
|
+
exit_source: exit.source,
|
|
683
|
+
interrupted: exit.interrupted,
|
|
684
|
+
stdout_tail: streams.stdout.slice(-RUN_OUTPUT_TAIL_BYTES),
|
|
685
|
+
// Written, and previously not: `RunRecord.stderr_tail` was declared and
|
|
686
|
+
// read by the classifier's `output()` while nothing populated it, so
|
|
687
|
+
// stderr reached a parser only by accident inside a stringified blob.
|
|
688
|
+
stderr_tail: streams.stderr.slice(-RUN_OUTPUT_TAIL_BYTES),
|
|
689
|
+
runner: runnerOf(cmd),
|
|
690
|
+
after_edits: _asInt(state["edits_this_turn"]),
|
|
691
|
+
at: _now(),
|
|
692
|
+
});
|
|
693
|
+
// The NEWEST are kept for detector C, which asks whether a pass follows
|
|
694
|
+
// the last edit. Detector F asks a question the head answers — was there a
|
|
695
|
+
// red before the green — so the cap keeps the FIRST failing record too,
|
|
696
|
+
// ahead of the tail, rather than letting a chatty turn drop the red half
|
|
697
|
+
// of its own red-then-green pair.
|
|
698
|
+
state["verification_runs"] = _cap_runs(runs);
|
|
699
|
+
}
|
|
451
700
|
} else if (event === "stop") {
|
|
452
701
|
state["last_stop_at"] = _now();
|
|
453
702
|
}
|
|
@@ -613,6 +862,74 @@ export function main(argv?: string[]): number {
|
|
|
613
862
|
return run(_readStdin(), { consumer_root: process.cwd(), verbose: args.verbose });
|
|
614
863
|
}
|
|
615
864
|
|
|
865
|
+
// ---------------------------------------------------------------------------
|
|
866
|
+
// The consumer side of `verification_runs`, living with its producer
|
|
867
|
+
// ---------------------------------------------------------------------------
|
|
868
|
+
|
|
869
|
+
/**
|
|
870
|
+
* Why the turn-end gate's reader sits HERE and not in the gate.
|
|
871
|
+
*
|
|
872
|
+
* The shape of `verification_runs` and `edits_this_turn` is this file's; the
|
|
873
|
+
* reader is the only consumer of that shape, and the gate already imported this
|
|
874
|
+
* module's path BUILDER rather than a path constant for exactly the reason that
|
|
875
|
+
* applies here too — "the consumer cannot read a path the producer does not
|
|
876
|
+
* write" is only true while the two agree, and separating them made the
|
|
877
|
+
* agreement invisible when the layout moved. Keeping the reader beside the
|
|
878
|
+
* writer makes a shape change a type error in one file instead of a silent
|
|
879
|
+
* disagreement across two.
|
|
880
|
+
*
|
|
881
|
+
* It also keeps the gate under its source-size ceiling without deleting
|
|
882
|
+
* anything, which is the move the ratchet asks for rather than a trim.
|
|
883
|
+
*/
|
|
884
|
+
export interface TurnRunState {
|
|
885
|
+
readonly runs: readonly unknown[];
|
|
886
|
+
readonly edits_this_turn: number;
|
|
887
|
+
}
|
|
888
|
+
|
|
889
|
+
/**
|
|
890
|
+
* The turn's recorded verification runs, or `null` when the recorder is not live.
|
|
891
|
+
*
|
|
892
|
+
* WHY THE LIVENESS TEST IS `edits_this_turn >= 1` AND NOT "the file exists".
|
|
893
|
+
* The obvious predicate — a state file with a `verification_runs` key — is
|
|
894
|
+
* wrong in a way that would refuse honest work on entire platforms. That key is
|
|
895
|
+
* present in the recorder's EMPTY state, so a host whose `post_tool_use` slot
|
|
896
|
+
* the manifest does not bind still has a file carrying `[]`, written by the
|
|
897
|
+
* prompt and stop events alone. Reading that as "this turn ran nothing" would
|
|
898
|
+
* refuse every editing turn on such a host, whatever the operator actually ran.
|
|
899
|
+
*
|
|
900
|
+
* `edits_this_turn` is the one field only a `post_tool_use` event can raise. The
|
|
901
|
+
* detector reaches here having already found an edit in the transcript, so a
|
|
902
|
+
* recorder that saw none of this turn's tool events is exactly the case this
|
|
903
|
+
* returns `null` for — and `null` means the transcript path answers, which is
|
|
904
|
+
* the behavior that predates the record path.
|
|
905
|
+
*
|
|
906
|
+
* The ownership check is the same one detector D applies to `ci_last`, for the
|
|
907
|
+
* same reason: a FOREIGN file's passing record would vouch for a run this
|
|
908
|
+
* session never made.
|
|
909
|
+
*/
|
|
910
|
+
export function readTurnRunState(
|
|
911
|
+
workspaceRoot: string,
|
|
912
|
+
session_id: string,
|
|
913
|
+
): TurnRunState | null {
|
|
914
|
+
if (!has_stable_session_id(session_id)) return null;
|
|
915
|
+
try {
|
|
916
|
+
const raw = fs.readFileSync(path.join(workspaceRoot, statePathFor(session_id)), 'utf-8');
|
|
917
|
+
const decoded: unknown = JSON.parse(raw);
|
|
918
|
+
if (!owns_session_state(decoded, session_id)) return null;
|
|
919
|
+
if (typeof decoded !== 'object' || decoded === null || Array.isArray(decoded)) return null;
|
|
920
|
+
const state = decoded as Record<string, unknown>;
|
|
921
|
+
const runs = state['verification_runs'];
|
|
922
|
+
const edits = state['edits_this_turn'];
|
|
923
|
+
if (!Array.isArray(runs)) return null;
|
|
924
|
+
if (typeof edits !== 'number' || !Number.isFinite(edits) || edits < 1) return null;
|
|
925
|
+
return { runs: runs as readonly unknown[], edits_this_turn: edits };
|
|
926
|
+
} catch {
|
|
927
|
+
// Absent, unreadable or malformed — the recorder said nothing, so the
|
|
928
|
+
// transcript answers. Never a refusal of its own.
|
|
929
|
+
return null;
|
|
930
|
+
}
|
|
931
|
+
}
|
|
932
|
+
|
|
616
933
|
// Bundle-safety: never auto-run when inlined into an esbuild bundle, where
|
|
617
934
|
// every module shares the bundle's `import.meta.url` (see cmd_migrate.ts).
|
|
618
935
|
declare const __AGENT_CONFIG_BUNDLE__: boolean | undefined;
|