@event4u/agent-config 9.35.0 → 10.0.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/.claude-plugin/marketplace.json +1 -1
- package/CHANGELOG.md +95 -142
- package/agents/templates/.ai-council.yml.example +31 -51
- package/dist/agent-src/commands/analyze/conformance.md +15 -4
- package/dist/agent-src/commands/analyze/reference-repo.md +135 -42
- package/dist/agent-src/contexts/augment-infrastructure.md +1 -1
- package/dist/agent-src/rules/design-review-after-ui-write.md +53 -6
- package/dist/agent-src/rules/improve-before-implement.md +1 -1
- package/dist/agent-src/rules/session-canary.md +28 -0
- package/dist/agent-src/rules/ui-audit-gate.md +35 -0
- package/dist/agent-src/skills/design-system-capture/SKILL.md +2 -2
- package/dist/agent-src/skills/existing-ui-audit/SKILL.md +1 -1
- package/dist/agent-src/skills/fe-design/SKILL.md +84 -12
- package/dist/agent-src/skills/fe-design/evals/triggers.json +11 -3
- package/dist/agent-src/skills/prediction-pool-optimizer/SKILL.md +5 -5
- package/dist/agent-src/skills/react-shadcn-ui/SKILL.md +1 -1
- package/dist/agent-src/skills/skill-writing/SKILL.md +24 -0
- package/dist/agent-src/skills/subagent-orchestration/prompts/README.md +29 -0
- package/dist/agent-src/templates/AGENTS.md +1 -1
- package/dist/agent-src/templates/agents/agent-project-settings.example.yml +1 -1
- package/dist/cli-delegate/{chunk-2TGMGLBL.js → chunk-2H5SE7DU.js} +35 -5
- package/dist/cli-delegate/{chunk-DS6BLDMG.js → chunk-3JUTT26I.js} +8 -3
- package/dist/cli-delegate/{chunk-ZAXH3ZWU.js → chunk-HJSKTMIF.js} +14 -13
- package/dist/cli-delegate/{chunk-QEEB4LHO.js → chunk-LRXJK6GN.js} +1 -1
- package/dist/cli-delegate/{chunk-736JCWLM.js → chunk-M2DPGFZ4.js} +10 -0
- package/dist/cli-delegate/{chunk-H6F2CQFI.js → chunk-NX5AXSLD.js} +3 -3
- package/dist/cli-delegate/{chunk-AG47LAGQ.js → chunk-TVGXH7VA.js} +1 -1
- package/dist/cli-delegate/{chunk-55RI5UXK.js → chunk-XT2YTJCZ.js} +22 -18
- package/dist/cli-delegate/{chunk-JEJABGKB.js → chunk-XTC5ARB6.js} +16 -12
- package/dist/cli-delegate/cmd_analyze_session.js +1 -1
- package/dist/cli-delegate/cmd_brand_status.js +1 -1
- package/dist/cli-delegate/cmd_conformance.js +1038 -197
- package/dist/cli-delegate/cmd_doctor.js +4 -4
- package/dist/cli-delegate/cmd_explain.js +2 -2
- package/dist/cli-delegate/cmd_export.js +4 -4
- package/dist/cli-delegate/cmd_handoff.js +3 -3
- package/dist/cli-delegate/cmd_mcp_available.js +1 -1
- package/dist/cli-delegate/cmd_migrate.js +2 -2
- package/dist/cli-delegate/cmd_packs_active.js +2 -2
- package/dist/cli-delegate/cmd_prune.js +4 -4
- package/dist/cli-delegate/cmd_refresh.js +1 -1
- package/dist/cli-delegate/cmd_route_audit.js +2 -2
- package/dist/cli-delegate/cmd_route_explain.js +1 -1
- package/dist/cli-delegate/cmd_session_recycle.js +9 -2
- package/dist/cli-delegate/cmd_settings_get.js +1 -1
- package/dist/cli-delegate/cmd_settings_migrate.js +4 -4
- package/dist/cli-delegate/cmd_settings_set.js +1 -1
- package/dist/cli-delegate/cmd_sync.js +4 -4
- package/dist/cli-delegate/cmd_uninstall.js +4 -4
- package/dist/cli-delegate/cmd_update.js +1 -1
- package/dist/cli-delegate/cmd_upgrade.js +2 -2
- package/dist/cli-delegate/cmd_validate.js +4 -4
- package/dist/cli-delegate/cmd_versions.js +1 -1
- package/dist/discovery/deprecation-report.md +1 -1
- package/dist/discovery/discovery-manifest.json +14 -14
- 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 +654 -411
- package/dist/install/install.mjs +39 -29
- package/dist/mcp/registry-manifest.json +1 -1
- package/dist/mcp/server.json +2 -2
- package/dist/mcp/server.mjs +1580 -617
- package/dist/router.json +1 -1
- package/dist/scripts/_lib/agent_settings.js +39 -25
- package/dist/scripts/_lib/agent_settings.js.map +1 -1
- package/dist/scripts/condense.js +15 -2
- package/dist/scripts/condense.js.map +1 -1
- package/dist/server/routes/wizard.js +23 -3
- package/dist/server/routes/wizard.js.map +1 -1
- package/dist/server/schemas/settings.js +11 -6
- package/dist/server/schemas/settings.js.map +1 -1
- package/docs/contracts/settings-classes.md +16 -9
- package/docs/guidelines/agent-infra/agent-interaction-and-decision-quality.md +46 -4
- package/package.json +2 -2
- package/src/agent-src/templates/AGENTS.md +1 -1
- package/src/config/agent-settings.template.yml +38 -36
- package/src/config/ci-local-parity.yml +44 -0
- package/src/config/gate-coverage.yml +12 -0
- package/src/scripts/_cli/cmd_doctor.ts +32 -0
- package/src/scripts/_cli/cmd_migrate.ts +26 -3
- package/src/scripts/_cli/cmd_refresh.ts +29 -0
- package/src/scripts/_cli/cmd_session_recycle.ts +40 -2
- package/src/scripts/_lib/agent_settings.ts +39 -25
- package/src/scripts/_lib/environment_detector.ts +52 -1
- package/src/scripts/_lib/hook_settings.ts +79 -0
- package/src/scripts/_lib/ui_surface.ts +97 -0
- package/src/scripts/ai_council/config.ts +62 -24
- package/src/scripts/before_complete_hook.ts +39 -0
- package/src/scripts/capture_skill_catalogue.ts +483 -0
- package/src/scripts/check_bridge_derivation.ts +89 -3
- package/src/scripts/check_ci_local_parity.ts +74 -0
- package/src/scripts/check_references.ts +15 -1
- package/src/scripts/condense.ts +16 -2
- package/src/scripts/conformance_scan.ts +200 -9
- package/src/scripts/council_cli.ts +95 -3
- package/src/scripts/hook_manifest.yaml +30 -9
- package/src/scripts/hooks/block_no_verify.ts +82 -9
- package/src/scripts/hooks/concern_registry.ts +2 -0
- package/src/scripts/hooks/design_slop_hook.ts +1 -1
- package/src/scripts/hooks/turn_end_gate_hook.ts +138 -105
- package/src/scripts/hooks/ui_route_nudge_hook.ts +250 -0
- package/src/scripts/lint_roadmap_complexity.ts +60 -0
- package/src/scripts/lint_rule_skill_pack_reach.ts +263 -0
- package/src/scripts/probe_promissory_closing.ts +180 -0
- package/src/scripts/probe_session_canary.ts +180 -0
- package/src/scripts/report_consultation_rate.ts +358 -0
- package/src/scripts/report_skill_activation.ts +5 -1
- package/src/server/schemas/settings.ts +14 -13
- /package/dist/agent-src/skills/design-system-capture/{reference → references}/design-system-json.md +0 -0
- /package/dist/agent-src/skills/prediction-pool-optimizer/{reference → references}/ev-fixtures.md +0 -0
- /package/dist/agent-src/skills/prediction-pool-optimizer/{reference → references}/odds-and-bonus.md +0 -0
- /package/dist/agent-src/skills/react-shadcn-ui/{reference → references}/registry.md +0 -0
|
@@ -60,8 +60,12 @@ concerns:
|
|
|
60
60
|
severity: advisory
|
|
61
61
|
# Phase 5 — Tier-1 hook for the verify-before-complete rule. Records
|
|
62
62
|
# observable evidence of verification commands (tests, quality tools,
|
|
63
|
-
# builds) into agents/
|
|
64
|
-
#
|
|
63
|
+
# builds) into agents/state/verify-before-complete.json — the path the code
|
|
64
|
+
# actually writes (`before_complete_hook.STATE_FILE`); this comment said
|
|
65
|
+
# `agents/runtime/state/` until round 7, which is a directory it never used.
|
|
66
|
+
# The rule body cites that file as the source of truth for "verified this
|
|
67
|
+
# turn?". Since round 7 it also carries `ci_last`, the session-scoped CI-settle
|
|
68
|
+
# negative that `turn-end-gate` reads to refuse a premature completion claim.
|
|
65
69
|
verify-before-complete:
|
|
66
70
|
script: src/scripts/before_complete_hook.ts
|
|
67
71
|
args: []
|
|
@@ -152,6 +156,21 @@ concerns:
|
|
|
152
156
|
args: []
|
|
153
157
|
fail_closed: false
|
|
154
158
|
severity: advisory
|
|
159
|
+
# road-to-frontend-skill-application Phase 4 — OPTIONAL PreToolUse UI-route
|
|
160
|
+
# nudge. On a Write/Edit to a UI surface with no design consultation latched
|
|
161
|
+
# this session, WARNS (exit 2) naming the route; a read or search touching a
|
|
162
|
+
# design surface latches consultation and silences it. Anti-loop: at most 2
|
|
163
|
+
# nudges per session. It does NOT read the two UI rules — the UI-surface
|
|
164
|
+
# decision comes from _lib/ui_surface.ts — so it runs parallel to them rather
|
|
165
|
+
# than consuming their triggers, and it is a nudge, never a block, so
|
|
166
|
+
# "enforced_by: none" on those rules stays honest.
|
|
167
|
+
# Default-OFF (hooks.ui_route_nudge.enabled).
|
|
168
|
+
# fail_closed: false.
|
|
169
|
+
ui-route-nudge:
|
|
170
|
+
script: src/scripts/hooks/ui_route_nudge_hook.ts
|
|
171
|
+
args: []
|
|
172
|
+
fail_closed: false
|
|
173
|
+
severity: advisory
|
|
155
174
|
# ADR-124 Phase 4 — OPTIONAL PreToolUse code-graph nudge. When a code-graph
|
|
156
175
|
# source is present and the agent is about to search/read source, WARNS
|
|
157
176
|
# (exit 2) once per session to query the graph first (or rebuild if stale,
|
|
@@ -432,10 +451,12 @@ concerns:
|
|
|
432
451
|
# mismatch (19 measured) — because building the unsafe part twice is how a
|
|
433
452
|
# second detector becomes a second outage.
|
|
434
453
|
#
|
|
435
|
-
#
|
|
436
|
-
#
|
|
437
|
-
#
|
|
438
|
-
#
|
|
454
|
+
# ALWAYS ARMED — no settings surface (2026-08-12). It shipped default-OFF on
|
|
455
|
+
# the round-6 council's "soak before it binds", and the maintainer removed the
|
|
456
|
+
# switch on the ground that a concern which is off cannot soak: the flag made
|
|
457
|
+
# the soak it protected unreachable. Whether the gate fires is decided by each
|
|
458
|
+
# detector's own trigger conditions, which is the same shape `delegation-nudge`
|
|
459
|
+
# uses — no signal, no output. `hooks.turn_end_gate.*` is in REMOVED_KEYS.
|
|
439
460
|
#
|
|
440
461
|
# fail_closed: FALSE, deliberately and unlike every other blocking concern
|
|
441
462
|
# here: a crash in a turn-end gate must let the turn END. Failing closed on
|
|
@@ -528,7 +549,7 @@ platforms:
|
|
|
528
549
|
session_start: [chat-history, hot-context, handoff-context, session-canary, council-availability, first-run-gate, onboarding-gate, verify-before-complete, minimal-safe-diff, profile-staleness, wrapper-freshness, surface-probe, session-register]
|
|
529
550
|
session_end: [chat-history, memory-learn, session-register]
|
|
530
551
|
stop: [chat-history, hot-context, verify-before-complete, self-repair, session-register]
|
|
531
|
-
pre_tool_use: [block-no-verify, block-unauthorized-git, evidence-independence, block-kernel-rule-writes, block-config-weakening, rtk-wrap, design-slop, code-graph-nudge, reread-guard]
|
|
552
|
+
pre_tool_use: [block-no-verify, block-unauthorized-git, evidence-independence, block-kernel-rule-writes, block-config-weakening, rtk-wrap, design-slop, ui-route-nudge, code-graph-nudge, reread-guard]
|
|
532
553
|
post_tool_use: [chat-history, roadmap-progress, context-hygiene, verify-before-complete, minimal-safe-diff, injection-scan, pr-url-reminder, orchestration-record, edit-shape]
|
|
533
554
|
|
|
534
555
|
claude:
|
|
@@ -536,7 +557,7 @@ platforms:
|
|
|
536
557
|
session_end: [chat-history, memory-learn, session-register]
|
|
537
558
|
stop: [chat-history, hot-context, verify-before-complete, team-review-gate, end-review-nudge, turn-end-gate, self-repair, session-register, session-eol]
|
|
538
559
|
user_prompt_submit: [chat-history, verify-before-complete, minimal-safe-diff, language-mirror, delegation-nudge, git-authorization, session-canary, self-repair, session-register]
|
|
539
|
-
pre_tool_use: [block-no-verify, block-unauthorized-git, evidence-independence, block-kernel-rule-writes, block-config-weakening, rtk-wrap, design-slop, code-graph-nudge, reread-guard]
|
|
560
|
+
pre_tool_use: [block-no-verify, block-unauthorized-git, evidence-independence, block-kernel-rule-writes, block-config-weakening, rtk-wrap, design-slop, ui-route-nudge, code-graph-nudge, reread-guard]
|
|
540
561
|
post_tool_use: [chat-history, roadmap-progress, context-hygiene, verify-before-complete, minimal-safe-diff, injection-scan, pr-url-reminder, orchestration-record, edit-shape, language-mirror]
|
|
541
562
|
# round-5 § 6.1 — the ONLY binding on this slot in the tree. `pre_compact`
|
|
542
563
|
# sets a pin-lost marker; the `post_tool_use` entry above re-emits the pin
|
|
@@ -575,7 +596,7 @@ platforms:
|
|
|
575
596
|
# still costs the dispatch, and its entire output is thrown away.
|
|
576
597
|
stop: [chat-history, hot-context, verify-before-complete, self-repair, session-register]
|
|
577
598
|
user_prompt_submit: [chat-history, verify-before-complete, minimal-safe-diff, language-mirror, git-authorization, session-canary, self-repair, session-register]
|
|
578
|
-
pre_tool_use: [block-no-verify, block-unauthorized-git, evidence-independence, block-kernel-rule-writes, block-config-weakening, rtk-wrap, design-slop, code-graph-nudge, reread-guard]
|
|
599
|
+
pre_tool_use: [block-no-verify, block-unauthorized-git, evidence-independence, block-kernel-rule-writes, block-config-weakening, rtk-wrap, design-slop, ui-route-nudge, code-graph-nudge, reread-guard]
|
|
579
600
|
post_tool_use: [chat-history, roadmap-progress, context-hygiene, verify-before-complete, minimal-safe-diff, injection-scan, pr-url-reminder, orchestration-record, edit-shape]
|
|
580
601
|
|
|
581
602
|
# Phase 7.5 — Cursor. `.cursor/hooks.json` (project) is read by the
|
|
@@ -409,31 +409,104 @@ export function _looks_like_git_invocation(cmd: string, depth = 0): boolean {
|
|
|
409
409
|
* Heredoc bodies are removed before extraction: a backtick or `$(` inside a
|
|
410
410
|
* commit message being written to a file is data, and feeding it back through
|
|
411
411
|
* this function is exactly the false positive the round-5 repair closed.
|
|
412
|
+
*
|
|
413
|
+
* ROUND 7 — THREAT MODEL for extending that removal to the shlex path (§ 2.1).
|
|
414
|
+
* Until round 7 the stripped string fed only `substitutionPayloads`; `shlexSplit`
|
|
415
|
+
* still received the RAW command. Both halves of that were measured, on this
|
|
416
|
+
* branch, before anything changed:
|
|
417
|
+
*
|
|
418
|
+
* `git commit -F - <<'EOF' … maintainer's … EOF` → BLOCKED (false positive)
|
|
419
|
+
* `bash <<EOF … git commit --no-verify … EOF` → ALLOWED (real bypass)
|
|
420
|
+
*
|
|
421
|
+
* So this is not the round-6 trade of one false positive for two false negatives.
|
|
422
|
+
* The bypass PRE-EXISTS: with the body inline, `shlexSplit` succeeds, the tokens
|
|
423
|
+
* are `bash <<EOF git commit --no-verify EOF`, command position is `bash`, and
|
|
424
|
+
* `_git_base` never sees git. Stripping before shlex fixes the false positive;
|
|
425
|
+
* scanning the bodies fixes the bypass. Both directions verified by probe.
|
|
426
|
+
*
|
|
427
|
+
* What an attacker can hide in a heredoc body, and how each is handled:
|
|
428
|
+
*
|
|
429
|
+
* - a body consumed by a SHELL (`sh`/`bash`/`zsh`/`ksh`, with or without a path
|
|
430
|
+
* prefix or an `env` prefix) IS a command list → scanned recursively.
|
|
431
|
+
* - a body consumed by anything else (`git commit -F -`, `cat > f`, `tee`) is
|
|
432
|
+
* DATA. Scanning it would re-open the exact false positive above, since a
|
|
433
|
+
* commit message may legitimately contain the words `--no-verify`.
|
|
434
|
+
* - an UNTERMINATED heredoc never matches the delimiter, so nothing is stripped
|
|
435
|
+
* and the raw string still reaches shlex — the fail-closed branch keeps it.
|
|
436
|
+
*/
|
|
437
|
+
|
|
438
|
+
/** One heredoc: its body, and whether a shell is the thing consuming it. */
|
|
439
|
+
interface Heredoc {
|
|
440
|
+
body: string;
|
|
441
|
+
shellConsumed: boolean;
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
/**
|
|
445
|
+
* Command position on the line that opens the heredoc is a shell interpreter, so
|
|
446
|
+
* the body is a command list rather than data. Anchored to the segment start or a
|
|
447
|
+
* shell separator, exactly like `_looks_like_git_invocation` does one level up —
|
|
448
|
+
* a bare mention of `bash` in an argument must not promote a message to code.
|
|
412
449
|
*/
|
|
450
|
+
const _SHELL_CONSUMER_RE =
|
|
451
|
+
/(?:^|[|&;]|\$\(|`)\s*(?:env\s+(?:[A-Za-z_][A-Za-z0-9_]*=\S*\s+)*)?(?:\S*\/)?(?:ba|z|k)?sh\b/;
|
|
452
|
+
|
|
453
|
+
export function _heredocs(cmd: string): { stripped: string; docs: Heredoc[] } {
|
|
454
|
+
const re = /<<-?\s*(['"]?)([A-Za-z_][A-Za-z0-9_]*)\1([\s\S]*?)^\s*\2\s*$/gm;
|
|
455
|
+
const docs: Heredoc[] = [];
|
|
456
|
+
let stripped = '';
|
|
457
|
+
let last = 0;
|
|
458
|
+
let m: RegExpExecArray | null;
|
|
459
|
+
while ((m = re.exec(cmd)) !== null) {
|
|
460
|
+
const lineStart = cmd.lastIndexOf('\n', m.index) + 1;
|
|
461
|
+
const prefix = cmd.slice(lineStart, m.index);
|
|
462
|
+
docs.push({ body: m[3] ?? '', shellConsumed: _SHELL_CONSUMER_RE.test(prefix) });
|
|
463
|
+
stripped += cmd.slice(last, m.index) + '<<HEREDOC';
|
|
464
|
+
last = m.index + m[0].length;
|
|
465
|
+
}
|
|
466
|
+
stripped += cmd.slice(last);
|
|
467
|
+
return { stripped, docs };
|
|
468
|
+
}
|
|
469
|
+
|
|
413
470
|
function _check_command(cmd: string, depth = 0): [boolean, string] {
|
|
471
|
+
// Round 7 § 2.2 — the stripped string now feeds shlex as well, not only the
|
|
472
|
+
// substitution scan. `<<HEREDOC` is a plain token, so an apostrophe in a
|
|
473
|
+
// commit message can no longer abort the parse of the command carrying it.
|
|
474
|
+
const { stripped, docs } = depth <= 3 ? _heredocs(cmd) : { stripped: cmd, docs: [] };
|
|
414
475
|
if (depth <= 3) {
|
|
415
|
-
const
|
|
416
|
-
/<<-?\s*(['"]?)([A-Za-z_][A-Za-z0-9_]*)\1[\s\S]*?^\s*\2\s*$/gm,
|
|
417
|
-
'<<HEREDOC',
|
|
418
|
-
);
|
|
419
|
-
for (const payload of substitutionPayloads(withoutHeredocs)) {
|
|
476
|
+
for (const payload of substitutionPayloads(stripped)) {
|
|
420
477
|
const [blocked, reason] = _check_command(payload, depth + 1);
|
|
421
478
|
if (blocked) {
|
|
422
479
|
return [true, reason];
|
|
423
480
|
}
|
|
424
481
|
}
|
|
482
|
+
// A shell-consumed body is a command list, so it is checked like one.
|
|
483
|
+
for (const doc of docs) {
|
|
484
|
+
if (!doc.shellConsumed) {
|
|
485
|
+
continue;
|
|
486
|
+
}
|
|
487
|
+
const [blocked, reason] = _check_command(doc.body, depth + 1);
|
|
488
|
+
if (blocked) {
|
|
489
|
+
return [true, reason];
|
|
490
|
+
}
|
|
491
|
+
}
|
|
425
492
|
}
|
|
426
493
|
let tokens: string[];
|
|
427
494
|
try {
|
|
428
|
-
tokens = shlexSplit(
|
|
495
|
+
tokens = shlexSplit(stripped);
|
|
429
496
|
} catch (e) {
|
|
430
497
|
if (e instanceof ShlexError) {
|
|
431
|
-
|
|
498
|
+
// Tested against the string that actually failed to parse. Round 7
|
|
499
|
+
// § 2.4: the message no longer prescribes the message-in-a-file
|
|
500
|
+
// workaround, because a TERMINATED heredoc is now stripped before
|
|
501
|
+
// this point and can no longer be the cause. What reaches here is
|
|
502
|
+
// genuinely unbalanced quoting — an unterminated heredoc, or an odd
|
|
503
|
+
// quote outside one — which bash itself refuses to run.
|
|
504
|
+
if (_looks_like_git_invocation(stripped)) {
|
|
432
505
|
return [
|
|
433
506
|
true,
|
|
434
507
|
'command parse failed (shlex) on a git-containing command — fail-closed (git-history-discipline). \n'
|
|
435
|
-
+ '
|
|
436
|
-
+ '
|
|
508
|
+
+ 'The quoting is unbalanced OUTSIDE any terminated heredoc — an unterminated heredoc, or an odd quote. \n'
|
|
509
|
+
+ 'bash refuses this command too; fix the quoting rather than working around the guard.',
|
|
437
510
|
];
|
|
438
511
|
}
|
|
439
512
|
return [false, ''];
|
|
@@ -54,6 +54,7 @@ import { main as blockConfigWeakeningMain } from './block_config_weakening.js';
|
|
|
54
54
|
import { main as rtkWrapMain } from './rtk_wrap_hook.js';
|
|
55
55
|
import { main as designSlopMain } from './design_slop_hook.js';
|
|
56
56
|
import { main as codeGraphNudgeMain } from './code_graph_nudge_hook.js';
|
|
57
|
+
import { main as uiRouteNudgeMain } from './ui_route_nudge_hook.js';
|
|
57
58
|
import { main as orchestrationRecordMain } from './orchestration_record_hook.js';
|
|
58
59
|
import { main as delegationNudgeMain } from './delegation_nudge_hook.js';
|
|
59
60
|
import { main as endReviewNudgeMain } from './end_review_nudge_hook.js';
|
|
@@ -97,6 +98,7 @@ export const CONCERN_REGISTRY: Readonly<Record<string, ConcernMain>> = {
|
|
|
97
98
|
'src/scripts/hooks/rtk_wrap_hook.ts': rtkWrapMain as ConcernMain,
|
|
98
99
|
'src/scripts/hooks/design_slop_hook.ts': designSlopMain as ConcernMain,
|
|
99
100
|
'src/scripts/hooks/code_graph_nudge_hook.ts': codeGraphNudgeMain as ConcernMain,
|
|
101
|
+
'src/scripts/hooks/ui_route_nudge_hook.ts': uiRouteNudgeMain as ConcernMain,
|
|
100
102
|
'src/scripts/hooks/orchestration_record_hook.ts': orchestrationRecordMain as ConcernMain,
|
|
101
103
|
'src/scripts/hooks/delegation_nudge_hook.ts': delegationNudgeMain as ConcernMain,
|
|
102
104
|
'src/scripts/hooks/end_review_nudge_hook.ts': endReviewNudgeMain as ConcernMain,
|
|
@@ -23,6 +23,7 @@ import fs from "node:fs";
|
|
|
23
23
|
import path from "node:path";
|
|
24
24
|
import { fileURLToPath, pathToFileURL } from "node:url";
|
|
25
25
|
import { type DesignContext } from "../design_slop_rules.js";
|
|
26
|
+
import { UI_EXT } from "../_lib/ui_surface.js";
|
|
26
27
|
import { loadDesignContext, scanFile } from "../lint_design_slop.js";
|
|
27
28
|
import { readHookStdin } from "./hook_stdin.js";
|
|
28
29
|
|
|
@@ -30,7 +31,6 @@ const SETTINGS_FILE = ".agent-settings.yml";
|
|
|
30
31
|
const EXIT_ALLOW = 0;
|
|
31
32
|
const EXIT_WARN = 2;
|
|
32
33
|
const DEGRADE_AFTER = 3; // surfaces of the same file::rule signature before going silent
|
|
33
|
-
const UI_EXT = /\.(html|htm|css|scss|sass|less|vue|svelte|astro|jsx|tsx)$/i;
|
|
34
34
|
|
|
35
35
|
type JsonValue = string | number | boolean | null | JsonValue[] | { [k: string]: JsonValue };
|
|
36
36
|
type JsonObject = { [k: string]: JsonValue };
|
|
@@ -58,19 +58,41 @@
|
|
|
58
58
|
* matching ordinal reads as already-refused, so on such a host layer 2
|
|
59
59
|
* degrades to "may under-refuse" and layer 1 is the real guard.
|
|
60
60
|
*
|
|
61
|
-
* ##
|
|
62
|
-
*
|
|
63
|
-
*
|
|
64
|
-
*
|
|
65
|
-
*
|
|
66
|
-
*
|
|
67
|
-
*
|
|
68
|
-
*
|
|
69
|
-
*
|
|
70
|
-
*
|
|
71
|
-
*
|
|
72
|
-
*
|
|
73
|
-
*
|
|
61
|
+
* ## Always armed — the switch was removed, and why
|
|
62
|
+
*
|
|
63
|
+
* This gate shipped behind `hooks.turn_end_gate.enabled`, default OFF, on the
|
|
64
|
+
* council's round-6 reading: "the mechanism exists and soaks before it binds."
|
|
65
|
+
* The maintainer removed the switch on 2026-08-12, and the decisive argument
|
|
66
|
+
* was that the soak the switch was protecting could never happen — a concern
|
|
67
|
+
* that is off does not run, so "merged in its own PR with its own soak period"
|
|
68
|
+
* (the second condition of `blocker: stop-refusal-decision`) was unsatisfiable
|
|
69
|
+
* for as long as the condition's own mechanism stayed disabled. A default-off
|
|
70
|
+
* safety gate is not a soaking gate; it is an absent one.
|
|
71
|
+
*
|
|
72
|
+
* What replaces the switch is not "always refuse". It is the detectors' own
|
|
73
|
+
* trigger conditions, which is where the judgement belonged in the first
|
|
74
|
+
* place: A fires only on a closing paragraph that promises work, B only when a
|
|
75
|
+
* language pin exists AND the reply misses it, C only when the turn edited a
|
|
76
|
+
* file and then ran nothing. Silence is the default on every ordinary turn.
|
|
77
|
+
* `delegation-nudge` is the sibling precedent — no structural signal, no
|
|
78
|
+
* output at all.
|
|
79
|
+
*
|
|
80
|
+
* The cost of removing the switch, stated once because it is real: a
|
|
81
|
+
* false-positive detector can no longer be turned off by configuration, only
|
|
82
|
+
* by a revert. That is bounded rather than open-ended — the two re-entrancy
|
|
83
|
+
* layers below cap a misfire at ONE extra turn, never a wedge — and it is the
|
|
84
|
+
* reason a new detector ships only with a measured false-positive corpus.
|
|
85
|
+
*
|
|
86
|
+
* MEASURE IT ON YOUR OWN CORPUS, and the instruction moved here on purpose:
|
|
87
|
+
* it used to live in the settings description that this change deleted, and
|
|
88
|
+
* `src/config/pack-size-budget.json` cites that description as the reason
|
|
89
|
+
* `measure_turn_end_gate.ts` is shipped rather than excluded from the pack. So
|
|
90
|
+
* the pointer is restated rather than dropped —
|
|
91
|
+
* `./scripts-run src/scripts/measure_turn_end_gate --store <transcript dir>`
|
|
92
|
+
* re-derives each detector's fire rate against your own transcripts. No rate is
|
|
93
|
+
* quoted in this file: the round-5 number moved twice while the instrument
|
|
94
|
+
* itself was being corrected, and a fixed figure in a shipped comment would be
|
|
95
|
+
* a moving target presented as a constant.
|
|
74
96
|
*
|
|
75
97
|
* CONTRACT: dispatcher-internal exit is 1 (EXIT_BLOCK) on a fire, 0
|
|
76
98
|
* otherwise. `fail_closed: false` — deliberately. A crash in a turn-end
|
|
@@ -94,13 +116,15 @@ import * as fs from 'node:fs';
|
|
|
94
116
|
import * as path from 'node:path';
|
|
95
117
|
import { fileURLToPath, pathToFileURL } from 'node:url';
|
|
96
118
|
|
|
97
|
-
import { load_agent_settings } from '../_lib/agent_settings.js';
|
|
98
119
|
import {
|
|
99
120
|
classify,
|
|
100
121
|
isSyntheticPrompt,
|
|
101
122
|
STATE_FILE as LANGUAGE_STATE_FILE,
|
|
102
123
|
type Verdict,
|
|
103
124
|
} from '../language_mirror_hook.js';
|
|
125
|
+
// Round 7 § Phase 1 — the CI-settle producer. Imported for its STATE_FILE only,
|
|
126
|
+
// so the consumer cannot read a path the producer does not write.
|
|
127
|
+
import { STATE_FILE as CI_STATE_FILE } from '../before_complete_hook.js';
|
|
104
128
|
import { isSafeTranscriptPath } from './end_review_nudge_hook.js';
|
|
105
129
|
import { unwrap, type JsonObject, type JsonValue } from './envelope.js';
|
|
106
130
|
import { readHookStdin } from './hook_stdin.js';
|
|
@@ -121,7 +145,7 @@ const EXIT_ALLOW = 0;
|
|
|
121
145
|
export const TRANSCRIPT_READ_MAX_BYTES = 8 * 1024 * 1024; // 8 MB
|
|
122
146
|
|
|
123
147
|
/** Detector identity, used in the state marker and the refusal text. */
|
|
124
|
-
export type DetectorId = 'promissory' | 'language' | 'verification';
|
|
148
|
+
export type DetectorId = 'promissory' | 'language' | 'verification' | 'completion';
|
|
125
149
|
|
|
126
150
|
export interface Finding {
|
|
127
151
|
detector: DetectorId;
|
|
@@ -323,7 +347,84 @@ export interface LanguagePin {
|
|
|
323
347
|
language: Verdict;
|
|
324
348
|
}
|
|
325
349
|
|
|
326
|
-
/**
|
|
350
|
+
/**
|
|
351
|
+
* Round 7 § Phase 1 — where the CI-settle fact comes from, and why not from here.
|
|
352
|
+
*
|
|
353
|
+
* The obvious implementation reads the unsettled state out of the transcript this
|
|
354
|
+
* hook already parses. It cannot: `_toolCalls` keeps only name, command and target
|
|
355
|
+
* path, so a tool RESULT — where the pending count lives — is never read; and
|
|
356
|
+
* `toolCalls` is reset at every genuine user prompt by design, while the measured
|
|
357
|
+
* failure is a completion claim in a LATER turn than the poll. Building it that way
|
|
358
|
+
* meant reversing an invariant this file argues for two functions down.
|
|
359
|
+
*
|
|
360
|
+
* So the producer is `before_complete_hook`, which already sees tool output on
|
|
361
|
+
* `post_tool_use` and already owns the `pendingCount` predicate. It writes
|
|
362
|
+
* `ci_last` into `agents/state/verify-before-complete.json` and this reads it.
|
|
363
|
+
*
|
|
364
|
+
* NO network call, deliberately. Asking `gh pr checks` here would put a network
|
|
365
|
+
* round-trip on every turn-end; `road-to-hook-latency-repair` exists because that
|
|
366
|
+
* cost is real. The rejected alternative is named so the next author does not
|
|
367
|
+
* re-derive it.
|
|
368
|
+
*
|
|
369
|
+
* ASYMMETRY that makes the cross-turn read legitimate where the reader refuses it
|
|
370
|
+
* for verification: a stale POSITIVE ("I verified") wrongly vouches for work it
|
|
371
|
+
* never saw. A stale NEGATIVE ("CI was not settled") only ever refuses more often.
|
|
372
|
+
* Same freshness invariant, opposite failure direction.
|
|
373
|
+
*/
|
|
374
|
+
export function readCiSettled(workspaceRoot: string): { seen: boolean; settled: boolean } {
|
|
375
|
+
try {
|
|
376
|
+
const raw = fs.readFileSync(path.join(workspaceRoot, CI_STATE_FILE), 'utf-8');
|
|
377
|
+
const decoded: unknown = JSON.parse(raw);
|
|
378
|
+
if (typeof decoded === 'object' && decoded !== null && !Array.isArray(decoded)) {
|
|
379
|
+
const ci = (decoded as Record<string, unknown>)['ci_last'];
|
|
380
|
+
if (typeof ci === 'object' && ci !== null && !Array.isArray(ci)) {
|
|
381
|
+
return { seen: true, settled: (ci as Record<string, unknown>)['settled'] === true };
|
|
382
|
+
}
|
|
383
|
+
}
|
|
384
|
+
} catch {
|
|
385
|
+
// absent, unreadable, or malformed — all mean "no CI was observed", and a
|
|
386
|
+
// session that never polled CI must never be refused for it.
|
|
387
|
+
}
|
|
388
|
+
return { seen: false, settled: false };
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
/**
|
|
392
|
+
* A completion claim in the delivered reply. Deliberately narrow: the German and
|
|
393
|
+
* English closings the corpus actually produced, anchored to a line so a mid-reply
|
|
394
|
+
* "fertig" inside a sentence about something else does not fire.
|
|
395
|
+
*
|
|
396
|
+
* Measured shapes it must catch (round 7, § Phase 1): "Fertig, Matze." ·
|
|
397
|
+
* "Damit ist alles erledigt." · "Aufgabe erledigt." · "der komplette Auftrag ist
|
|
398
|
+
* durch".
|
|
399
|
+
*/
|
|
400
|
+
const _COMPLETION_RE =
|
|
401
|
+
/(^|\n)\s*(?:\*\*)?(?:fertig\b|damit ist alles erledigt|aufgabe erledigt|alles erledigt\b|komplett(?:er)? (?:auftrag|abgearbeitet)|der komplette auftrag ist durch|done[.!]|all done\b|task complete)/i;
|
|
402
|
+
|
|
403
|
+
export function detectCompletionClaim(
|
|
404
|
+
reply: string,
|
|
405
|
+
ci: { seen: boolean; settled: boolean },
|
|
406
|
+
): Finding | null {
|
|
407
|
+
// A session that never read CI has nothing to be premature about.
|
|
408
|
+
if (!ci.seen || ci.settled) return null;
|
|
409
|
+
const prose = visibleProse(reply);
|
|
410
|
+
const m = _COMPLETION_RE.exec(prose);
|
|
411
|
+
if (!m) return null;
|
|
412
|
+
const at = m.index;
|
|
413
|
+
return {
|
|
414
|
+
detector: 'completion',
|
|
415
|
+
evidence: prose.slice(at, at + 120).trim(),
|
|
416
|
+
reason:
|
|
417
|
+
'a completion claim while the last CI read was not settled — ' +
|
|
418
|
+
'read the verdict, then claim it (verify-before-complete)',
|
|
419
|
+
};
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
/**
|
|
423
|
+
* Read the pin the language-mirror hook wrote. Absent ⇒ no obligation.
|
|
424
|
+
*
|
|
425
|
+
* (R2 finding 8: this line was left 73 lines above, where inserting `readCiSettled`
|
|
426
|
+
* had stranded it — so it documented the wrong function and this one had none.)
|
|
427
|
+
*/
|
|
327
428
|
export function readLanguagePin(workspaceRoot: string): Verdict {
|
|
328
429
|
try {
|
|
329
430
|
const raw = fs.readFileSync(path.join(workspaceRoot, LANGUAGE_STATE_FILE), 'utf-8');
|
|
@@ -688,78 +789,6 @@ function _messageText(content: unknown): string | null {
|
|
|
688
789
|
return parts.length > 0 ? parts.join('\n') : null;
|
|
689
790
|
}
|
|
690
791
|
|
|
691
|
-
// ---------------------------------------------------------------------------
|
|
692
|
-
// Settings — master OFF, detectors ON within it
|
|
693
|
-
// ---------------------------------------------------------------------------
|
|
694
|
-
|
|
695
|
-
export interface GateSettings {
|
|
696
|
-
enabled: boolean;
|
|
697
|
-
promissory: boolean;
|
|
698
|
-
language: boolean;
|
|
699
|
-
verification: boolean;
|
|
700
|
-
}
|
|
701
|
-
|
|
702
|
-
/**
|
|
703
|
-
* YAML spellings a human writes for "on". The `yaml` package parses to the 1.2
|
|
704
|
-
* core schema, where `yes` is the STRING "yes" rather than a boolean — so a
|
|
705
|
-
* maintainer who writes `enabled: yes` means it, and a reader that silently
|
|
706
|
-
* treats it as off is the R2 finding-6b defect wearing a parser instead of a
|
|
707
|
-
* regex. Anything not in this set is off; there is no truthiness guessing.
|
|
708
|
-
*/
|
|
709
|
-
function _isOn(v: unknown): boolean | null {
|
|
710
|
-
if (typeof v === 'boolean') return v;
|
|
711
|
-
if (typeof v !== 'string') return null;
|
|
712
|
-
const s = v.trim().toLowerCase();
|
|
713
|
-
if (s === 'true' || s === 'yes' || s === 'on') return true;
|
|
714
|
-
if (s === 'false' || s === 'no' || s === 'off') return false;
|
|
715
|
-
return null;
|
|
716
|
-
}
|
|
717
|
-
|
|
718
|
-
/**
|
|
719
|
-
* Read the three switches through the real settings cascade.
|
|
720
|
-
*
|
|
721
|
-
* The previous version hand-walked `<workspaceRoot>/.agent-settings.yml` for
|
|
722
|
-
* speed, and R2 finding 6 found three defects in that trade: it ignored the
|
|
723
|
-
* user-global layer entirely (so a maintainer who opts in globally got no gate
|
|
724
|
-
* AND no diagnostic), its end-of-line-anchored regex read
|
|
725
|
-
* `enabled: true # soak opt-in` as FALSE, and it accepted a `turn_end_gate:`
|
|
726
|
-
* block under ANY parent as ON because a text walker has no notion of a parent.
|
|
727
|
-
*
|
|
728
|
-
* The stated reason for skipping the cascade does not survive contact with the
|
|
729
|
-
* sibling concerns: `delegation-nudge` calls `load_agent_settings` on
|
|
730
|
-
* `user_prompt_submit`, i.e. every turn. Correctness on a gate that can refuse
|
|
731
|
-
* a turn-end outranks one settings read per turn-end.
|
|
732
|
-
*/
|
|
733
|
-
export function readGateSettings(workspaceRoot: string): GateSettings {
|
|
734
|
-
const off: GateSettings = {
|
|
735
|
-
enabled: false,
|
|
736
|
-
promissory: true,
|
|
737
|
-
language: true,
|
|
738
|
-
verification: true,
|
|
739
|
-
};
|
|
740
|
-
let settings: Record<string, unknown>;
|
|
741
|
-
try {
|
|
742
|
-
settings = load_agent_settings({ cwd: workspaceRoot }) as Record<string, unknown>;
|
|
743
|
-
} catch {
|
|
744
|
-
return off;
|
|
745
|
-
}
|
|
746
|
-
const hooks = settings['hooks'];
|
|
747
|
-
if (typeof hooks !== 'object' || hooks === null || Array.isArray(hooks)) return off;
|
|
748
|
-
const block = (hooks as Record<string, unknown>)['turn_end_gate'];
|
|
749
|
-
if (typeof block !== 'object' || block === null || Array.isArray(block)) return off;
|
|
750
|
-
const gate = block as Record<string, unknown>;
|
|
751
|
-
const flag = (name: string, dflt: boolean): boolean => {
|
|
752
|
-
const v = _isOn(gate[name]);
|
|
753
|
-
return v === null ? dflt : v;
|
|
754
|
-
};
|
|
755
|
-
return {
|
|
756
|
-
enabled: flag('enabled', false),
|
|
757
|
-
promissory: flag('promissory', true),
|
|
758
|
-
language: flag('language', true),
|
|
759
|
-
verification: flag('verification', true),
|
|
760
|
-
};
|
|
761
|
-
}
|
|
762
|
-
|
|
763
792
|
// ---------------------------------------------------------------------------
|
|
764
793
|
// main
|
|
765
794
|
// ---------------------------------------------------------------------------
|
|
@@ -782,9 +811,6 @@ export function main(): number {
|
|
|
782
811
|
|
|
783
812
|
const workspaceRoot = str(envelope['workspace_root'] as JsonValue | undefined) || process.cwd();
|
|
784
813
|
|
|
785
|
-
const settings = readGateSettings(workspaceRoot);
|
|
786
|
-
if (!settings.enabled) return EXIT_ALLOW;
|
|
787
|
-
|
|
788
814
|
const transcriptPath = str(
|
|
789
815
|
(payload['transcript_path'] ?? payload['transcriptPath']) as JsonValue | undefined,
|
|
790
816
|
);
|
|
@@ -818,17 +844,24 @@ export function main(): number {
|
|
|
818
844
|
);
|
|
819
845
|
if (alreadyRefusedTurn(workspaceRoot, sessionKey, turnOrdinal)) return EXIT_ALLOW;
|
|
820
846
|
|
|
847
|
+
// Every detector runs on every turn-end. The gating is INSIDE each one —
|
|
848
|
+
// no promise, no pin mismatch, no unverified edit ⇒ no finding ⇒ the turn
|
|
849
|
+
// ends. That is the whole of "fires when it is warranted"; there is no
|
|
850
|
+
// second, configurable notion of warranted layered on top of it.
|
|
821
851
|
const findings: Finding[] = [];
|
|
822
|
-
|
|
823
|
-
|
|
824
|
-
|
|
825
|
-
|
|
826
|
-
|
|
827
|
-
|
|
828
|
-
|
|
829
|
-
|
|
830
|
-
|
|
831
|
-
|
|
852
|
+
for (const f of [
|
|
853
|
+
detectPromissory(lastAssistant),
|
|
854
|
+
detectLanguage(lastAssistant, readLanguagePin(workspaceRoot)),
|
|
855
|
+
detectUnverifiedEdit(toolCalls),
|
|
856
|
+
// Round 7 § Phase 1 — detector D, in the same unconditional list as the
|
|
857
|
+
// other three. It shipped with its own settings flag one commit earlier
|
|
858
|
+
// and that flag is gone: `hooks.turn_end_gate.*` was deleted on
|
|
859
|
+
// 2026-08-12 because a default-off safety gate is an absent one. Its
|
|
860
|
+
// gating is where the comment above says gating belongs — inside the
|
|
861
|
+
// detector: no CI observed, or a settled read, or no completion claim
|
|
862
|
+
// ⇒ no finding.
|
|
863
|
+
detectCompletionClaim(lastAssistant, readCiSettled(workspaceRoot)),
|
|
864
|
+
]) {
|
|
832
865
|
if (f) findings.push(f);
|
|
833
866
|
}
|
|
834
867
|
if (findings.length === 0) return EXIT_ALLOW;
|
|
@@ -840,10 +873,10 @@ export function main(): number {
|
|
|
840
873
|
);
|
|
841
874
|
process.stderr.write(
|
|
842
875
|
`turn-end-gate: REFUSED — this turn is not finished.\n${lines.join('\n')}\n` +
|
|
843
|
-
' Do the promised work now, correct the language,
|
|
844
|
-
'verification the edit needs,
|
|
845
|
-
'
|
|
846
|
-
'
|
|
876
|
+
' Do the promised work now, correct the language, run the ' +
|
|
877
|
+
'verification the edit needs, or read the CI verdict before claiming ' +
|
|
878
|
+
'it — then end the turn.\n' +
|
|
879
|
+
' This turn will not be refused a second time.\n',
|
|
847
880
|
);
|
|
848
881
|
return EXIT_BLOCK;
|
|
849
882
|
}
|