@event4u/agent-config 9.25.0 → 9.27.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 +58 -143
- package/README.md +1 -1
- package/dist/agent-src/commands/analyze/conformance.md +21 -2
- package/dist/agent-src/commands/review/changes.md +6 -5
- package/dist/agent-src/commands/roadmap/next.md +256 -0
- package/dist/agent-src/commands/roadmap.md +12 -4
- package/dist/agent-src/contexts/execution/subagent-routing.md +4 -1
- package/dist/agent-src/rules/active-remediation.md +2 -0
- package/dist/agent-src/rules/analysis-skill-routing.md +2 -0
- package/dist/agent-src/rules/architecture.md +2 -0
- package/dist/agent-src/rules/artifact-drafting-protocol.md +2 -0
- package/dist/agent-src/rules/artifact-engagement-recording.md +2 -0
- package/dist/agent-src/rules/augment-edit-discipline.md +2 -0
- package/dist/agent-src/rules/autonomous-execution.md +2 -0
- package/dist/agent-src/rules/brand-consistency.md +2 -0
- package/dist/agent-src/rules/brand-source-of-truth.md +2 -0
- package/dist/agent-src/rules/broken-access-control.md +2 -0
- package/dist/agent-src/rules/cli-output-handling.md +2 -0
- package/dist/agent-src/rules/code-comment-discipline.md +2 -0
- package/dist/agent-src/rules/code-provenance.md +2 -0
- package/dist/agent-src/rules/command-suggestion-policy.md +2 -0
- package/dist/agent-src/rules/commit-conventions.md +2 -0
- package/dist/agent-src/rules/communication-through-line.md +2 -0
- package/dist/agent-src/rules/content-quoting-floor.md +2 -0
- package/dist/agent-src/rules/context-hygiene.md +10 -0
- package/dist/agent-src/rules/copilot-routing.md +2 -0
- package/dist/agent-src/rules/cross-source-consistency.md +2 -0
- package/dist/agent-src/rules/decision-revisit-gate.md +2 -0
- package/dist/agent-src/rules/delegation-policy.md +5 -2
- package/dist/agent-src/rules/design-fidelity.md +19 -1
- package/dist/agent-src/rules/design-review-after-ui-write.md +120 -0
- package/dist/agent-src/rules/devcontainer-routing.md +2 -0
- package/dist/agent-src/rules/doc-screenshot-hygiene.md +2 -0
- package/dist/agent-src/rules/docker-commands.md +2 -0
- package/dist/agent-src/rules/domain-adoption-policy.md +2 -0
- package/dist/agent-src/rules/domain-safety-disclaimer.md +2 -0
- package/dist/agent-src/rules/domain-safety-pii.md +2 -0
- package/dist/agent-src/rules/domain-safety-retention.md +2 -0
- package/dist/agent-src/rules/downstream-changes.md +2 -0
- package/dist/agent-src/rules/engineering-safety-floor.md +2 -0
- package/dist/agent-src/rules/evaluator-independence.md +19 -3
- package/dist/agent-src/rules/external-code-graph-interop.md +2 -0
- package/dist/agent-src/rules/external-reference-deep-dive.md +2 -0
- package/dist/agent-src/rules/fast-path-marker-visibility.md +2 -0
- package/dist/agent-src/rules/finance-safety-floor.md +2 -0
- package/dist/agent-src/rules/framework-neutrality-in-generic-skills.md +2 -0
- package/dist/agent-src/rules/git-history-discipline.md +3 -1
- package/dist/agent-src/rules/guidelines.md +2 -0
- package/dist/agent-src/rules/history-discipline.md +2 -0
- package/dist/agent-src/rules/icon-consistency.md +2 -0
- package/dist/agent-src/rules/image-likeness-and-rights.md +2 -0
- package/dist/agent-src/rules/improve-before-implement.md +2 -0
- package/dist/agent-src/rules/invite-challenge.md +2 -0
- package/dist/agent-src/rules/laravel-routing.md +2 -0
- package/dist/agent-src/rules/laravel-translations.md +2 -0
- package/dist/agent-src/rules/legal-safety-floor.md +2 -0
- package/dist/agent-src/rules/lethal-trifecta-guard.md +2 -0
- package/dist/agent-src/rules/linked-projects-onboarding-gate.md +2 -0
- package/dist/agent-src/rules/low-impact-corpus-privacy-floor.md +2 -0
- package/dist/agent-src/rules/markdown-safe-codeblocks.md +2 -0
- package/dist/agent-src/rules/media-governance-routing.md +2 -0
- package/dist/agent-src/rules/media-sync-ground-truth.md +2 -0
- package/dist/agent-src/rules/minimal-safe-diff.md +2 -0
- package/dist/agent-src/rules/missing-tool-handling.md +2 -0
- package/dist/agent-src/rules/model-recommendation.md +2 -0
- package/dist/agent-src/rules/no-attribution-footers.md +2 -0
- package/dist/agent-src/rules/no-decorative-emojis-in-git-surfaces.md +2 -0
- package/dist/agent-src/rules/no-pr-progress-comments.md +2 -0
- package/dist/agent-src/rules/no-roadmap-references.md +2 -0
- package/dist/agent-src/rules/notes-first-reasoning.md +2 -0
- package/dist/agent-src/rules/onboarding-gate.md +2 -0
- package/dist/agent-src/rules/output-discipline.md +2 -0
- package/dist/agent-src/rules/package-ci-checks.md +2 -0
- package/dist/agent-src/rules/persona-governance.md +2 -0
- package/dist/agent-src/rules/php-coding.md +2 -0
- package/dist/agent-src/rules/prefer-enums-over-literals.md +2 -0
- package/dist/agent-src/rules/preservation-guard.md +2 -0
- package/dist/agent-src/rules/provider-lifecycle-discipline.md +2 -0
- package/dist/agent-src/rules/question-not-instruction.md +2 -0
- package/dist/agent-src/rules/reviewer-awareness.md +2 -0
- package/dist/agent-src/rules/roadmap-ci-steps-policy.md +2 -0
- package/dist/agent-src/rules/roadmap-progress-sync.md +2 -0
- package/dist/agent-src/rules/role-mode-adherence.md +2 -0
- package/dist/agent-src/rules/rule-type-governance.md +2 -0
- package/dist/agent-src/rules/runtime-safety.md +2 -0
- package/dist/agent-src/rules/scale-discipline.md +2 -0
- package/dist/agent-src/rules/secret-vcs-guard.md +2 -0
- package/dist/agent-src/rules/security-sensitive-stop.md +2 -0
- package/dist/agent-src/rules/senior-engineering-discipline.md +2 -0
- package/dist/agent-src/rules/session-canary.md +63 -19
- package/dist/agent-src/rules/settings-ask-protocol.md +180 -0
- package/dist/agent-src/rules/size-enforcement.md +2 -0
- package/dist/agent-src/rules/skill-improvement-trigger.md +2 -0
- package/dist/agent-src/rules/skill-quality.md +2 -0
- package/dist/agent-src/rules/slash-command-routing-policy.md +2 -0
- package/dist/agent-src/rules/source-confidentiality.md +2 -0
- package/dist/agent-src/rules/source-discovery-gate.md +2 -0
- package/dist/agent-src/rules/source-of-truth.md +2 -0
- package/dist/agent-src/rules/spreadsheet-source-quality.md +2 -0
- package/dist/agent-src/rules/strategy-safety-floor.md +2 -0
- package/dist/agent-src/rules/symfony-routing.md +2 -0
- package/dist/agent-src/rules/think-before-action.md +2 -0
- package/dist/agent-src/rules/token-budget-discipline.md +8 -3
- package/dist/agent-src/rules/token-efficiency.md +2 -0
- package/dist/agent-src/rules/token-optimizer-maintenance.md +2 -0
- package/dist/agent-src/rules/tool-safety.md +2 -0
- package/dist/agent-src/rules/ui-audit-gate.md +7 -0
- package/dist/agent-src/rules/untrusted-input-defense.md +2 -0
- package/dist/agent-src/rules/upstream-proposal.md +2 -0
- package/dist/agent-src/rules/user-interaction.md +5 -2
- package/dist/agent-src/rules/user-interrupt-priority.md +2 -0
- package/dist/agent-src/skills/recursive-verification/SKILL.md +13 -0
- package/dist/agent-src/templates/agents/agent-project-settings.example.yml +1 -1
- package/dist/cli/registry.js +2 -0
- package/dist/cli/registry.js.map +1 -1
- package/dist/cli-delegate/{chunk-SJQMJI7M.js → chunk-3KJIMFTB.js} +1 -1
- package/dist/cli-delegate/{chunk-BSLUHV2F.js → chunk-BCPIOAGV.js} +148 -133
- package/dist/cli-delegate/cmd_conformance.js +1 -1
- package/dist/cli-delegate/cmd_doctor.js +1 -1
- package/dist/cli-delegate/cmd_export.js +1 -1
- package/dist/cli-delegate/cmd_prune.js +1 -1
- package/dist/cli-delegate/cmd_route_audit.js +4 -1
- package/dist/cli-delegate/cmd_route_explain.js +4 -1
- package/dist/cli-delegate/cmd_settings_migrate.js +1 -1
- package/dist/cli-delegate/cmd_sync.js +1 -1
- package/dist/cli-delegate/cmd_uninstall.js +1 -1
- package/dist/cli-delegate/cmd_validate.js +1 -1
- package/dist/discovery/deprecation-report.md +1 -1
- package/dist/discovery/discovery-manifest.json +191 -120
- package/dist/discovery/discovery-manifest.json.sha256 +1 -1
- package/dist/discovery/discovery-manifest.summary.md +4 -4
- package/dist/discovery/orphan-report.md +1 -1
- package/dist/discovery/packs.json +11 -8
- package/dist/discovery/trust-report.md +3 -3
- package/dist/discovery/workspaces.json +9 -6
- package/dist/hooks/dispatch.js +1148 -349
- package/dist/install/install.mjs +1 -1
- package/dist/mcp/registry-manifest.json +3 -3
- package/dist/mcp/server.json +2 -2
- package/dist/mcp/server.mjs +571 -417
- package/dist/router.json +1 -1
- package/dist/scripts/condense.js +33 -2
- package/dist/scripts/condense.js.map +1 -1
- package/dist/server/io/yamlIO.js +85 -0
- package/dist/server/io/yamlIO.js.map +1 -1
- package/dist/server/nicknameResolver.js +116 -0
- package/dist/server/nicknameResolver.js.map +1 -0
- package/dist/server/routes/ping.js +29 -11
- package/dist/server/routes/ping.js.map +1 -1
- package/dist/server/routes/settings.js +1 -12
- package/dist/server/routes/settings.js.map +1 -1
- package/dist/server/routes/wizard.js +52 -2
- package/dist/server/routes/wizard.js.map +1 -1
- package/dist/shared/interactiveContext.js +73 -0
- package/dist/shared/interactiveContext.js.map +1 -0
- package/dist/shared/nicknamePrefill.js +59 -0
- package/dist/shared/nicknamePrefill.js.map +1 -0
- package/dist/shared/settingsAsks.js +128 -0
- package/dist/shared/settingsAsks.js.map +1 -0
- package/dist/shared/settingsCarveOut.js +118 -0
- package/dist/shared/settingsCarveOut.js.map +1 -0
- package/dist/shared/settingsConsent.js +93 -0
- package/dist/shared/settingsConsent.js.map +1 -0
- package/dist/shared/settingsSurface.js +7 -1
- package/dist/shared/settingsSurface.js.map +1 -1
- package/dist/ui/assets/index-DmR0mbk4.js.map +1 -1
- package/package.json +1 -1
- package/src/config/agents-paths.yml +15 -0
- package/src/config/evaluator-budgets.json +4 -4
- package/src/config/gate-coverage.yml +29 -1
- package/src/config/gate-violation-baselines.json +13 -7
- package/src/scripts/_cli/cmd_doctor.ts +6 -21
- package/src/scripts/_cli/cmd_route_audit.ts +4 -1
- package/src/scripts/_cli/cmd_route_explain.ts +9 -1
- package/src/scripts/_dispatch.bash +18 -0
- package/src/scripts/_lib/git_common_dir.ts +128 -0
- package/src/scripts/_lib/obligation_frequency.ts +391 -0
- package/src/scripts/_lib/session_register.ts +361 -0
- package/src/scripts/ai-video/stitch.sh +27 -9
- package/src/scripts/before_complete_hook.ts +23 -4
- package/src/scripts/bench_ab_v2_run.ts +272 -16
- package/src/scripts/bench_ab_v2_stats.ts +156 -3
- package/src/scripts/check_completion_review.ts +80 -3
- package/src/scripts/check_corpus_staleness.ts +692 -0
- package/src/scripts/check_enforcement_coverage.ts +192 -7
- package/src/scripts/check_gate_coverage.ts +67 -0
- package/src/scripts/check_hook_bundle_freshness.ts +183 -0
- package/src/scripts/check_rule_projection_integrity.ts +364 -0
- package/src/scripts/condense.ts +36 -2
- package/src/scripts/conformance_scan.ts +82 -17
- package/src/scripts/dispatch_r2_reviewer.ts +31 -2
- package/src/scripts/generate_settings_reference.ts +232 -0
- package/src/scripts/git_authorization_hook.ts +75 -10
- package/src/scripts/hook_manifest.yaml +87 -27
- package/src/scripts/hooks/block_no_verify.ts +59 -2
- package/src/scripts/hooks/block_unauthorized_git.ts +199 -16
- package/src/scripts/hooks/concern_registry.ts +2 -0
- package/src/scripts/hooks/design_slop_hook.ts +13 -1
- package/src/scripts/hooks/dispatch_hook.ts +97 -2
- package/src/scripts/hooks/evidence_independence.ts +40 -6
- package/src/scripts/language_mirror_hook.ts +256 -17
- package/src/scripts/lint_abstraction_thresholds.ts +41 -8
- package/src/scripts/lint_agents_layout.ts +2 -0
- package/src/scripts/lint_hook_manifest.ts +29 -0
- package/src/scripts/memory_learn_hook.ts +109 -6
- package/src/scripts/orchestration_backfill.ts +354 -0
- package/src/scripts/pr_url_reminder_hook.ts +17 -2
- package/src/scripts/release.ts +180 -37
- package/src/scripts/schemas/review-findings.schema.json +99 -0
- package/src/scripts/schemas/rule.schema.json +5 -0
- package/src/scripts/session_canary_hook.ts +35 -3
- package/src/scripts/session_register_hook.ts +288 -0
- package/src/scripts/sessions_cli.ts +140 -0
- package/src/scripts/validate_frontmatter.ts +68 -0
- package/src/server/io/yamlIO.ts +87 -0
- package/src/shared/interactiveContext.ts +98 -0
- package/src/shared/nicknamePrefill.ts +96 -0
- package/src/shared/settingsAsks.ts +176 -0
- package/src/shared/settingsCarveOut.ts +143 -0
- package/src/shared/settingsConsent.ts +136 -0
- package/src/shared/settingsSurface.ts +7 -1
|
@@ -0,0 +1,364 @@
|
|
|
1
|
+
#!/usr/bin/env tsx
|
|
2
|
+
/**
|
|
3
|
+
* check_rule_projection_integrity.ts — the host rule trees are COMPLETE and
|
|
4
|
+
* FRESH against `dist/agent-src/rules/`, read as they stand on disk.
|
|
5
|
+
*
|
|
6
|
+
* WHY THIS GATE EXISTS (road-to-conformance-round5 Phase 1, measured 2026-08-07)
|
|
7
|
+
* ---------------------------------------------------------------------------
|
|
8
|
+
* `docs/contracts/rule-router.md:234` states that nothing loads
|
|
9
|
+
* `dist/router.json` at runtime, so **projection is the only reach mechanism
|
|
10
|
+
* there is**: a non-kernel rule activates by the model's judgment over text
|
|
11
|
+
* already in context, and the per-tool rule tree is what puts it there. That
|
|
12
|
+
* makes the state of the projection load-bearing, and it was broken:
|
|
13
|
+
*
|
|
14
|
+
* - `.claude/rules/` in the working checkout held **92** entries. A
|
|
15
|
+
* regeneration in a clean worktree produces **108**. The tree was last
|
|
16
|
+
* generated **2026-07-05**, and **21 rules added since then had no entry** —
|
|
17
|
+
* among them `secret-vcs-guard`, `broken-access-control`,
|
|
18
|
+
* `senior-engineering-discipline`, `evaluator-independence`,
|
|
19
|
+
* `session-canary`, `settings-ask-protocol`, `active-remediation`. At
|
|
20
|
+
* project scope those 21 obligations were carried by nothing. The same 21
|
|
21
|
+
* were absent from `.clinerules/` and `.cursor/rules/*.md`.
|
|
22
|
+
* - The trees are gitignored and untracked, so no diff review sees the gap.
|
|
23
|
+
*
|
|
24
|
+
* WHY THE EXISTING GATES DO NOT CATCH IT
|
|
25
|
+
* --------------------------------------
|
|
26
|
+
* - `check_bridge_derivation` **regenerates** the trees and then diffs, so it
|
|
27
|
+
* can only ever observe a fixpoint; both CI and the pre-push chain run it
|
|
28
|
+
* ahead of anything else, which is why five weeks of staleness were invisible.
|
|
29
|
+
* Its symlink leg asserts that entries which EXIST resolve — a missing entry
|
|
30
|
+
* is not a wrong entry.
|
|
31
|
+
* - `check_generator_output_coverage.ts:37` asserts only that the output
|
|
32
|
+
* *root* is classified in the ignore manifest, never its contents.
|
|
33
|
+
* - `check_condensation.ts` stops at `dist/`.
|
|
34
|
+
*
|
|
35
|
+
* So the last hop — the one that decides what the model actually sees — was
|
|
36
|
+
* ungated. This gate reads the tree WITHOUT touching it: no regeneration, no
|
|
37
|
+
* writes. That is the whole design constraint, and it is why the check is a
|
|
38
|
+
* separate script rather than a third leg on `check_bridge_derivation`.
|
|
39
|
+
*
|
|
40
|
+
* HONEST SCOPE — where each leg has teeth
|
|
41
|
+
* ---------------------------------------
|
|
42
|
+
* On a fresh CI checkout the trees do not exist at all (gitignored), so CI must
|
|
43
|
+
* run this AFTER `task generate-tools`, where it is a **generator-regression**
|
|
44
|
+
* check: it proves the generator emitted every rule it planned to. The stale
|
|
45
|
+
* case is caught on a developer's working checkout, which is why the pre-push
|
|
46
|
+
* `preflight` chain runs it FIRST — before `check_bridge_derivation` regenerates
|
|
47
|
+
* and erases the evidence. Both registrations are deliberate; neither subsumes
|
|
48
|
+
* the other.
|
|
49
|
+
*
|
|
50
|
+
* THE TWO ASSERTIONS
|
|
51
|
+
* ------------------
|
|
52
|
+
* 1. COMPLETENESS. Every rule the generator would emit has an entry in every
|
|
53
|
+
* active host rule tree. The expected set comes from
|
|
54
|
+
* `condense.projected_rule_trees()` — the generator's OWN emit plan, which
|
|
55
|
+
* `generate_rule_symlinks` also consumes. It is deliberately NOT a list in
|
|
56
|
+
* this file and deliberately NOT `dist/agent-src/rules/` verbatim: a
|
|
57
|
+
* hand-maintained array goes obsolete silently, and the dist directory
|
|
58
|
+
* over-counts by the ADR-004 `type: manual` rules the trees omit on purpose
|
|
59
|
+
* (113 files on disk, 5 manual, 108 projected). A completeness check keyed on
|
|
60
|
+
* a non-canonical source propagates that source's mistakes as requirements.
|
|
61
|
+
* 2. FRESHNESS. No rule in `dist/agent-src/rules/` may be newer than the
|
|
62
|
+
* projection entry pointing at it. Entries are symlinks, so the comparison
|
|
63
|
+
* uses `lstat` on the LINK, never `stat`: `stat` follows the link and returns
|
|
64
|
+
* the target's own mtime, which would compare the dist file with itself and
|
|
65
|
+
* make the assertion vacuous. Verified empirically before wiring — with a
|
|
66
|
+
* target touched after the link was created, `lstat(link).mtimeMs !==
|
|
67
|
+
* stat(target).mtimeMs` while `stat(link).mtimeMs === stat(target).mtimeMs`.
|
|
68
|
+
* Under `lean_projection: thin` the entries are real files rather than
|
|
69
|
+
* symlinks; `lstat` is correct there too (the file's own mtime).
|
|
70
|
+
*
|
|
71
|
+
* NOT IN SCOPE. `.cursor/rules/*.mdc` and `.windsurf/rules/` are written by
|
|
72
|
+
* different generators with their own filters; this gate covers the three trees
|
|
73
|
+
* `condense.TOOL_DIRS` projects (`.claude/rules`, `.cursor/rules`, `.clinerules`)
|
|
74
|
+
* and their `.md` entries, which is where the measured defect was.
|
|
75
|
+
*
|
|
76
|
+
* CLI: `--root <path>` (default: this repo) · `--quiet` suppresses the
|
|
77
|
+
* per-target ledger line. Exit 0 = complete and fresh, or no tool active at all
|
|
78
|
+
* (announced on stderr — see `auditRuleProjection`); 1 = any gap, any stale
|
|
79
|
+
* entry, a ledger accounting error, or a dead scan scope.
|
|
80
|
+
*/
|
|
81
|
+
import * as fs from 'node:fs';
|
|
82
|
+
import * as path from 'node:path';
|
|
83
|
+
import process from 'node:process';
|
|
84
|
+
import { fileURLToPath, pathToFileURL } from 'node:url';
|
|
85
|
+
|
|
86
|
+
import { GateLedger, UnaccountedTargetsError } from './_lib/gate_ledger.js';
|
|
87
|
+
import { assertScanned, DeadScopeError } from './_lib/scan_scope.js';
|
|
88
|
+
import { _resetStateForTest as _pointCondenseAt, projected_rule_trees } from './condense.js';
|
|
89
|
+
|
|
90
|
+
const _HERE = path.resolve(fileURLToPath(import.meta.url));
|
|
91
|
+
const REPO_ROOT = path.resolve(path.dirname(_HERE), '..', '..');
|
|
92
|
+
|
|
93
|
+
/** Where the projected rules are condensed to — the freshness comparison's left side. */
|
|
94
|
+
const DIST_RULES = path.join('dist', 'agent-src', 'rules');
|
|
95
|
+
|
|
96
|
+
export type ProjectionFindingKind = 'missing' | 'stale' | 'dangling';
|
|
97
|
+
|
|
98
|
+
export interface ProjectionFinding {
|
|
99
|
+
/** Repo-relative tool dir, e.g. `.claude/rules`. */
|
|
100
|
+
tree: string;
|
|
101
|
+
/** Rule basename, e.g. `secret-vcs-guard.md`. */
|
|
102
|
+
rule: string;
|
|
103
|
+
kind: ProjectionFindingKind;
|
|
104
|
+
message: string;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
export interface ProjectionAudit {
|
|
108
|
+
findings: ProjectionFinding[];
|
|
109
|
+
/** `tree` → whether the directory exists at all; an absent tree fails every rule under it. */
|
|
110
|
+
treePresent: Record<string, boolean>;
|
|
111
|
+
ledger: GateLedger;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
function _lstatOrNull(p: string): fs.Stats | null {
|
|
115
|
+
try {
|
|
116
|
+
return fs.lstatSync(p);
|
|
117
|
+
} catch {
|
|
118
|
+
return null;
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
/**
|
|
123
|
+
* Audit `expected` (tool dir → rule basenames) against the trees under `repoRoot`.
|
|
124
|
+
*
|
|
125
|
+
* Pure read: every branch stats, none writes. `expected` is injected rather than
|
|
126
|
+
* computed here so the caller controls which root the generator's emit plan was
|
|
127
|
+
* derived from, and so a test can pin an expectation without a settings file.
|
|
128
|
+
*/
|
|
129
|
+
export function auditRuleProjection(
|
|
130
|
+
repoRoot: string,
|
|
131
|
+
expected: Readonly<Record<string, readonly string[]>>,
|
|
132
|
+
): ProjectionAudit {
|
|
133
|
+
const ledger = new GateLedger('check_rule_projection_integrity');
|
|
134
|
+
const findings: ProjectionFinding[] = [];
|
|
135
|
+
const treePresent: Record<string, boolean> = {};
|
|
136
|
+
|
|
137
|
+
for (const [tree, rules] of Object.entries(expected)) {
|
|
138
|
+
ledger.plan(rules.map((r) => `${tree}/${r}`));
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
// The planned set is every (tree × rule) pair, and zero has TWO causes that
|
|
142
|
+
// are not the same defect. `projected_rule_trees()` returns one key per
|
|
143
|
+
// ACTIVE tool dir, so the key count is the discriminator:
|
|
144
|
+
//
|
|
145
|
+
// - keys > 0, rules == 0 → the rule corpus is dead. No rule reached
|
|
146
|
+
// `dist/agent-src/rules/`, or every scope filter rejected every rule,
|
|
147
|
+
// and a green checkmark here would cover an assertion never evaluated.
|
|
148
|
+
// This still throws.
|
|
149
|
+
// - keys == 0 → `agents/.agent-tools.yml` selects zero tools, which
|
|
150
|
+
// `_active_tools()` honours by design. There is no projection surface
|
|
151
|
+
// in this checkout, so there is nothing to be stale or missing. That is
|
|
152
|
+
// an absent optional surface, not a moved root.
|
|
153
|
+
//
|
|
154
|
+
// Conflating the two made the gate unsatisfiable on a supported config: it
|
|
155
|
+
// runs FIRST in the pre-push `preflight` chain, so a maintainer who
|
|
156
|
+
// deactivates the per-tool trees (they duplicate a globally installed
|
|
157
|
+
// `~/.claude`) could not push at all, with no fix available short of the
|
|
158
|
+
// env escape hatch. A gate whose only compliant path is to change an
|
|
159
|
+
// unrelated setting is not a gate. `main()` prints the skip explicitly —
|
|
160
|
+
// the empty case must be LOUD, never a silent green.
|
|
161
|
+
const noActiveTree = Object.keys(expected).length === 0;
|
|
162
|
+
assertScanned({
|
|
163
|
+
gate: 'check_rule_projection_integrity',
|
|
164
|
+
scanned: Object.values(expected).reduce((n, rules) => n + rules.length, 0),
|
|
165
|
+
units: 'projected rule entries',
|
|
166
|
+
roots: [DIST_RULES, ...Object.keys(expected)],
|
|
167
|
+
...(noActiveTree
|
|
168
|
+
? {
|
|
169
|
+
allowEmpty:
|
|
170
|
+
'OPTIONAL_INPUT: no host rule tree is active — agents/.agent-tools.yml ' +
|
|
171
|
+
'selects zero tools, so the surface this gate audits does not exist in ' +
|
|
172
|
+
'this checkout. With one or more active tools the emit plan carries the ' +
|
|
173
|
+
'full rule set by construction, so a dead dist/agent-src/rules/ still fails here.',
|
|
174
|
+
}
|
|
175
|
+
: {}),
|
|
176
|
+
});
|
|
177
|
+
|
|
178
|
+
for (const [tree, rules] of Object.entries(expected)) {
|
|
179
|
+
const treeDir = path.join(repoRoot, tree);
|
|
180
|
+
const treeStat = _lstatOrNull(treeDir);
|
|
181
|
+
treePresent[tree] = treeStat !== null && treeStat.isDirectory();
|
|
182
|
+
|
|
183
|
+
for (const rule of rules) {
|
|
184
|
+
const target = `${tree}/${rule}`;
|
|
185
|
+
const entry = _lstatOrNull(path.join(treeDir, rule));
|
|
186
|
+
if (entry === null) {
|
|
187
|
+
findings.push({
|
|
188
|
+
tree,
|
|
189
|
+
rule,
|
|
190
|
+
kind: 'missing',
|
|
191
|
+
message: treePresent[tree]
|
|
192
|
+
? 'no projection entry — the rule reaches the model at no project scope'
|
|
193
|
+
: 'projection tree does not exist',
|
|
194
|
+
});
|
|
195
|
+
ledger.fail(target, 'missing projection entry');
|
|
196
|
+
continue;
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
const dist = path.join(repoRoot, DIST_RULES, rule);
|
|
200
|
+
let distStat: fs.Stats;
|
|
201
|
+
try {
|
|
202
|
+
distStat = fs.statSync(dist);
|
|
203
|
+
} catch {
|
|
204
|
+
// Unreachable while `expected` derives from this same dist tree,
|
|
205
|
+
// but reachable if the tree changes under a running gate. Report
|
|
206
|
+
// rather than crash: a projection entry whose source is gone is
|
|
207
|
+
// a dangling link, which is a real defect either way.
|
|
208
|
+
findings.push({
|
|
209
|
+
tree,
|
|
210
|
+
rule,
|
|
211
|
+
kind: 'dangling',
|
|
212
|
+
message: `projection entry has no source at ${DIST_RULES}/${rule}`,
|
|
213
|
+
});
|
|
214
|
+
ledger.fail(target, 'dangling projection entry');
|
|
215
|
+
continue;
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
// `entry` is an lstat: for a symlink that is the LINK's mtime, which
|
|
219
|
+
// is what "when was this projected" means. `statSync` here follows
|
|
220
|
+
// nothing — dist rules are regular files.
|
|
221
|
+
if (distStat.mtimeMs > entry.mtimeMs) {
|
|
222
|
+
findings.push({
|
|
223
|
+
tree,
|
|
224
|
+
rule,
|
|
225
|
+
kind: 'stale',
|
|
226
|
+
message:
|
|
227
|
+
`source is newer than the projection entry ` +
|
|
228
|
+
`(${DIST_RULES}/${rule} ${new Date(distStat.mtimeMs).toISOString()} > ` +
|
|
229
|
+
`entry ${new Date(entry.mtimeMs).toISOString()})`,
|
|
230
|
+
});
|
|
231
|
+
ledger.fail(target, 'stale projection entry');
|
|
232
|
+
continue;
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
ledger.complete(target);
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
return { findings, treePresent, ledger };
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
/** Group findings by tree, preserving rule order, for a report a reader can act on. */
|
|
243
|
+
export function renderFindings(audit: ProjectionAudit): string[] {
|
|
244
|
+
const lines: string[] = [];
|
|
245
|
+
const byTree = new Map<string, ProjectionFinding[]>();
|
|
246
|
+
for (const f of audit.findings) {
|
|
247
|
+
const bucket = byTree.get(f.tree);
|
|
248
|
+
if (bucket === undefined) byTree.set(f.tree, [f]);
|
|
249
|
+
else bucket.push(f);
|
|
250
|
+
}
|
|
251
|
+
for (const [tree, group] of byTree) {
|
|
252
|
+
const missing = group.filter((f) => f.kind === 'missing');
|
|
253
|
+
const stale = group.filter((f) => f.kind === 'stale');
|
|
254
|
+
const dangling = group.filter((f) => f.kind === 'dangling');
|
|
255
|
+
if (audit.treePresent[tree] !== true) {
|
|
256
|
+
lines.push(`${tree}: tree does not exist — ${String(group.length)} rule(s) reach the model at no project scope`);
|
|
257
|
+
}
|
|
258
|
+
if (missing.length > 0) {
|
|
259
|
+
lines.push(`${tree}: ${String(missing.length)} rule(s) missing from the projection:`);
|
|
260
|
+
for (const f of missing) lines.push(` - ${f.rule}`);
|
|
261
|
+
}
|
|
262
|
+
if (stale.length > 0) {
|
|
263
|
+
lines.push(`${tree}: ${String(stale.length)} projection entry/entries older than their source:`);
|
|
264
|
+
for (const f of stale) lines.push(` - ${f.rule} — ${f.message}`);
|
|
265
|
+
}
|
|
266
|
+
if (dangling.length > 0) {
|
|
267
|
+
lines.push(`${tree}: ${String(dangling.length)} dangling projection entry/entries:`);
|
|
268
|
+
for (const f of dangling) lines.push(` - ${f.rule} — ${f.message}`);
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
return lines;
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
export function main(argv?: readonly string[]): number {
|
|
275
|
+
const args = argv ?? process.argv.slice(2);
|
|
276
|
+
const quiet = args.includes('--quiet');
|
|
277
|
+
const idx = args.indexOf('--root');
|
|
278
|
+
const root = path.resolve(idx !== -1 ? (args[idx + 1] ?? REPO_ROOT) : REPO_ROOT);
|
|
279
|
+
|
|
280
|
+
// `condense`'s emit plan reads `MODULE_STATE`, whose only root seam is this
|
|
281
|
+
// reassignment. Repointing it is what makes `--root` mean the same thing for
|
|
282
|
+
// the expected set and for the trees being audited — without it the gate
|
|
283
|
+
// would compare this repo's rules against another checkout's projection.
|
|
284
|
+
_pointCondenseAt(root);
|
|
285
|
+
|
|
286
|
+
let audit: ProjectionAudit;
|
|
287
|
+
try {
|
|
288
|
+
audit = auditRuleProjection(root, projected_rule_trees());
|
|
289
|
+
} catch (exc) {
|
|
290
|
+
if (exc instanceof DeadScopeError) {
|
|
291
|
+
process.stderr.write(`❌ ${exc.message}\n`);
|
|
292
|
+
return 1;
|
|
293
|
+
}
|
|
294
|
+
throw exc;
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
// Zero active trees is a legitimate config (see `auditRuleProjection`), but
|
|
298
|
+
// it means this run audited nothing. Say so on its own line instead of
|
|
299
|
+
// letting the success line below report "0 entries across 0 trees", which
|
|
300
|
+
// reads as a pass to every human and every log scraper.
|
|
301
|
+
if (Object.keys(audit.treePresent).length === 0) {
|
|
302
|
+
process.stderr.write(
|
|
303
|
+
'⚠️ check_rule_projection_integrity: no host rule tree is active ' +
|
|
304
|
+
'(agents/.agent-tools.yml selects zero tools) — nothing to audit. This gate ' +
|
|
305
|
+
'has teeth only where a tool is active; CI activates all eight.\n',
|
|
306
|
+
);
|
|
307
|
+
return 0;
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
const lines = renderFindings(audit);
|
|
311
|
+
|
|
312
|
+
let tally;
|
|
313
|
+
try {
|
|
314
|
+
tally = quiet ? audit.ledger.finalize() : audit.ledger.report();
|
|
315
|
+
} catch (exc) {
|
|
316
|
+
if (exc instanceof UnaccountedTargetsError) {
|
|
317
|
+
process.stderr.write(`❌ ${exc.message}\n`);
|
|
318
|
+
return 1;
|
|
319
|
+
}
|
|
320
|
+
throw exc;
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
if (lines.length > 0) {
|
|
324
|
+
for (const l of lines) process.stderr.write(`❌ ${l}\n`);
|
|
325
|
+
process.stderr.write(
|
|
326
|
+
`\nThe host rule trees are the ONLY carrier for a non-kernel rule's prose ` +
|
|
327
|
+
`(docs/contracts/rule-router.md § no runtime router). Regenerate with ` +
|
|
328
|
+
`\`task generate-tools\`, then re-run this gate.\n`,
|
|
329
|
+
);
|
|
330
|
+
return 1;
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
if (!quiet) {
|
|
334
|
+
process.stdout.write(
|
|
335
|
+
`✅ ${String(tally.completed)} rule projection entries complete and fresh across ` +
|
|
336
|
+
`${String(Object.keys(audit.treePresent).length)} host rule tree(s)\n`,
|
|
337
|
+
);
|
|
338
|
+
}
|
|
339
|
+
return 0;
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
function _isCliEntry(): boolean {
|
|
343
|
+
if (process.argv[1] === undefined) {
|
|
344
|
+
return false;
|
|
345
|
+
}
|
|
346
|
+
if (import.meta.url === pathToFileURL(path.resolve(process.argv[1])).href) {
|
|
347
|
+
return true;
|
|
348
|
+
}
|
|
349
|
+
// A symlinked invocation makes the raw URLs differ (import.meta.url is the
|
|
350
|
+
// resolved real path, argv[1] keeps the symlink). Compare realpaths, or the
|
|
351
|
+
// CLI silently no-ops when run through a projection — the bundled-CLI-entry
|
|
352
|
+
// landmine this repo has already paid for once.
|
|
353
|
+
try {
|
|
354
|
+
return (
|
|
355
|
+
fs.realpathSync(fileURLToPath(import.meta.url)) === fs.realpathSync(path.resolve(process.argv[1]))
|
|
356
|
+
);
|
|
357
|
+
} catch {
|
|
358
|
+
return false;
|
|
359
|
+
}
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
if (_isCliEntry()) {
|
|
363
|
+
process.exit(main());
|
|
364
|
+
}
|
package/src/scripts/condense.ts
CHANGED
|
@@ -1108,6 +1108,36 @@ function _scoped_rule_basenames(): string[] {
|
|
|
1108
1108
|
.sort((a, b) => (a < b ? -1 : a > b ? 1 : 0));
|
|
1109
1109
|
}
|
|
1110
1110
|
|
|
1111
|
+
/**
|
|
1112
|
+
* The emit plan of {@link generate_rule_symlinks}: active tool dir → the exact
|
|
1113
|
+
* rule basenames it projects there.
|
|
1114
|
+
*
|
|
1115
|
+
* Exported so a completeness gate can assert a host rule tree matches the
|
|
1116
|
+
* generator WITHOUT regenerating it. That distinction is the whole point:
|
|
1117
|
+
* `check_bridge_derivation` regenerates and then diffs, so it can only ever see
|
|
1118
|
+
* a fixpoint, and a five-week-stale tree on a working checkout is invisible to
|
|
1119
|
+
* it. A gate that reads the tree as it stands needs the expected set from
|
|
1120
|
+
* somewhere, and every other candidate source is wrong in a way that
|
|
1121
|
+
* propagates: a hand-maintained array goes obsolete silently, and
|
|
1122
|
+
* `dist/agent-src/rules/` over-counts by the ADR-004 `type: manual` rules these
|
|
1123
|
+
* trees deliberately omit (113 files, 5 manual, 108 projected).
|
|
1124
|
+
*
|
|
1125
|
+
* `generate_rule_symlinks` consumes this too, so there is one emit plan rather
|
|
1126
|
+
* than a gate's reconstruction of one.
|
|
1127
|
+
*/
|
|
1128
|
+
export function projected_rule_trees(
|
|
1129
|
+
user_home: string = process.env['HOME'] ?? os.homedir(),
|
|
1130
|
+
): Record<string, string[]> {
|
|
1131
|
+
const rules = _scoped_rule_basenames();
|
|
1132
|
+
const dedup_on = _read_projection_scope_dedup();
|
|
1133
|
+
const out: Record<string, string[]> = {};
|
|
1134
|
+
for (const tool_dir of Object.keys(_filter_tool_dirs(TOOL_DIRS))) {
|
|
1135
|
+
const skip = dedup_on ? _dedupable_rules(tool_dir, rules, user_home) : new Set<string>();
|
|
1136
|
+
out[tool_dir] = rules.filter((r) => !skip.has(r));
|
|
1137
|
+
}
|
|
1138
|
+
return out;
|
|
1139
|
+
}
|
|
1140
|
+
|
|
1111
1141
|
export function generate_rule_symlinks(): number {
|
|
1112
1142
|
const rules = _scoped_rule_basenames();
|
|
1113
1143
|
const tool_dirs = _filter_tool_dirs(TOOL_DIRS);
|
|
@@ -1130,14 +1160,18 @@ export function generate_rule_symlinks(): number {
|
|
|
1130
1160
|
);
|
|
1131
1161
|
}
|
|
1132
1162
|
|
|
1133
|
-
const dedup_on = _read_projection_scope_dedup();
|
|
1134
1163
|
const user_home = process.env['HOME'] ?? os.homedir();
|
|
1164
|
+
// Read the emit plan from the exported function rather than recomputing the
|
|
1165
|
+
// dedup skip here, so the gate that asserts this tree is complete and the
|
|
1166
|
+
// code that writes it cannot disagree.
|
|
1167
|
+
const plan = projected_rule_trees(user_home);
|
|
1135
1168
|
const skipped_per_tool: Record<string, Set<string>> = {};
|
|
1136
1169
|
|
|
1137
1170
|
let total = 0;
|
|
1138
1171
|
for (const [tool_dir, rel_prefix] of Object.entries(tool_dirs)) {
|
|
1139
1172
|
const target_dir = path.join(MODULE_STATE.PROJECT_ROOT, tool_dir);
|
|
1140
|
-
const
|
|
1173
|
+
const emit_here = new Set(plan[tool_dir] ?? rules);
|
|
1174
|
+
const skip = new Set(rules.filter((r) => !emit_here.has(r)));
|
|
1141
1175
|
skipped_per_tool[tool_dir] = skip;
|
|
1142
1176
|
_mkdirp(target_dir);
|
|
1143
1177
|
// Clean stale symlinks — a rule that became de-duplicable this run is
|
|
@@ -42,6 +42,16 @@ export interface Violation {
|
|
|
42
42
|
session: string;
|
|
43
43
|
at: string;
|
|
44
44
|
detail: string;
|
|
45
|
+
/**
|
|
46
|
+
* Language-pin only. Assistant turns since the last genuine user prompt, and
|
|
47
|
+
* whether a compaction boundary fell in between. Together they separate the
|
|
48
|
+
* two failures the round-5 audit found hiding under one count: a pin that was
|
|
49
|
+
* ABSENT because compaction removed it, and a pin that was PRESENT and
|
|
50
|
+
* ignored. The first is a state defect; the second is non-compliance, and
|
|
51
|
+
* only the second is unfixable by re-injection.
|
|
52
|
+
*/
|
|
53
|
+
turns_since_prompt?: number;
|
|
54
|
+
compaction_since_prompt?: boolean;
|
|
45
55
|
}
|
|
46
56
|
|
|
47
57
|
export interface SessionReport {
|
|
@@ -139,6 +149,15 @@ export function isInjectedBody(text: string): boolean {
|
|
|
139
149
|
return text.length > 2500 && classify(text).language === "en";
|
|
140
150
|
}
|
|
141
151
|
|
|
152
|
+
/**
|
|
153
|
+
* Harness-generated text that occupies an assistant turn but is not the
|
|
154
|
+
* assistant writing. Round-2 self-scan: 8 of 9 "violations" in one session were
|
|
155
|
+
* `API Error: 529 Overloaded` retry banners. Counting the harness as the model
|
|
156
|
+
* inflates the very number this scan exists to report honestly.
|
|
157
|
+
*/
|
|
158
|
+
const HARNESS_TEXT =
|
|
159
|
+
/^(API Error:|Request (timed out|was aborted)|\[Request interrupted|Error: |Credit balance is too low|You've hit your|Prompt is too long)/i;
|
|
160
|
+
|
|
142
161
|
/** Rough English-opener test for the language check (first prose line only). */
|
|
143
162
|
const EN_OPENER =
|
|
144
163
|
/^(let me|i'?ll |i'?m |found it|ok[,. ]|okay|alright|here'?s|now[,. ]|looking|checking|reading|running|the |this |that |there |we |you |good |right[,. ]|perfect|done[.,]|all |confirmed|correct)/i;
|
|
@@ -152,9 +171,16 @@ export function scanSession(sessionId: string, lines: string[]): SessionReport {
|
|
|
152
171
|
};
|
|
153
172
|
|
|
154
173
|
let pinned: "de" | "en" | null = null;
|
|
174
|
+
let pendingPoll: { at: string; cmd: string } | null = null;
|
|
155
175
|
let authorized = new Set<GitOp>();
|
|
156
176
|
let sawPending = false;
|
|
157
177
|
let evaluationsThisTurn = 0;
|
|
178
|
+
// Round-5 provenance for language violations. The round-5 audit could only
|
|
179
|
+
// separate "the pin was absent" from "the pin was ignored" by reconstructing
|
|
180
|
+
// both facts from the raw store afterwards; recording them here is what makes
|
|
181
|
+
// that split reproducible against a future corpus instead of re-argued.
|
|
182
|
+
let turnsSincePrompt = 0;
|
|
183
|
+
let compactionSincePrompt = false;
|
|
158
184
|
|
|
159
185
|
for (const line of lines) {
|
|
160
186
|
if (!line.trim()) {
|
|
@@ -167,13 +193,48 @@ export function scanSession(sessionId: string, lines: string[]): SessionReport {
|
|
|
167
193
|
continue;
|
|
168
194
|
}
|
|
169
195
|
|
|
196
|
+
// Tool results arrive as `user` entries carrying `toolUseResult`.
|
|
197
|
+
if (entry["type"] === "user" && entry["toolUseResult"] !== undefined && pendingPoll !== null) {
|
|
198
|
+
const raw = entry["toolUseResult"];
|
|
199
|
+
const out = typeof raw === "string" ? raw : JSON.stringify(raw);
|
|
200
|
+
const pending = pendingCount(out);
|
|
201
|
+
if (pending !== null && pending > 0) {
|
|
202
|
+
sawPending = true;
|
|
203
|
+
} else if (isVacuousOutput(out) || (pending === 0 && !sawPending)) {
|
|
204
|
+
report.violations.push({
|
|
205
|
+
check: "vacuous-evidence",
|
|
206
|
+
session: sessionId,
|
|
207
|
+
at: pendingPoll.at,
|
|
208
|
+
detail: `CI poll read as settled without an in-flight observation: ${out.slice(0, 90)}`,
|
|
209
|
+
});
|
|
210
|
+
}
|
|
211
|
+
pendingPoll = null;
|
|
212
|
+
continue;
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
// A compaction boundary removes context the pin lived in. It is not a turn
|
|
216
|
+
// and it is not a prompt, but it changes what the model can still see — so
|
|
217
|
+
// it is recorded, not skipped silently.
|
|
218
|
+
if (entry["type"] === "system" && entry["subtype"] === "compact_boundary") {
|
|
219
|
+
compactionSincePrompt = true;
|
|
220
|
+
continue;
|
|
221
|
+
}
|
|
222
|
+
|
|
170
223
|
const ut = userText(entry);
|
|
171
224
|
if (ut !== null) {
|
|
225
|
+
// A compaction SUMMARY arrives as a `user` entry. Treating it as a chat
|
|
226
|
+
// message would let its language reset the pin — the exact defect class
|
|
227
|
+
// that made the first detector report 303 instead of 626.
|
|
228
|
+
if (entry["isCompactSummary"] === true) {
|
|
229
|
+
continue;
|
|
230
|
+
}
|
|
172
231
|
if (isInjectedBody(ut)) {
|
|
173
232
|
// Not a chat message — it changes neither the pin nor the ledger.
|
|
174
233
|
continue;
|
|
175
234
|
}
|
|
176
235
|
report.user_turns += 1;
|
|
236
|
+
turnsSincePrompt = 0;
|
|
237
|
+
compactionSincePrompt = false;
|
|
177
238
|
const c = classify(ut);
|
|
178
239
|
if (c.language !== "und") {
|
|
179
240
|
pinned = c.language;
|
|
@@ -181,6 +242,7 @@ export function scanSession(sessionId: string, lines: string[]): SessionReport {
|
|
|
181
242
|
authorized = new Set(classifyAuthorization(ut).authorized);
|
|
182
243
|
sawPending = false;
|
|
183
244
|
evaluationsThisTurn = 0;
|
|
245
|
+
pendingPoll = null;
|
|
184
246
|
continue;
|
|
185
247
|
}
|
|
186
248
|
|
|
@@ -192,8 +254,12 @@ export function scanSession(sessionId: string, lines: string[]): SessionReport {
|
|
|
192
254
|
const prose = assistantText(entry);
|
|
193
255
|
if (prose !== null) {
|
|
194
256
|
report.assistant_turns += 1;
|
|
257
|
+
turnsSincePrompt += 1;
|
|
195
258
|
if (pinned === "de") {
|
|
196
259
|
const first = prose.split("\n").find((l) => l.trim()) ?? "";
|
|
260
|
+
if (HARNESS_TEXT.test(first.trim())) {
|
|
261
|
+
continue; // harness banner, not the assistant's prose
|
|
262
|
+
}
|
|
197
263
|
const c = classify(first);
|
|
198
264
|
if (first.length > 12 && (c.language === "en" || EN_OPENER.test(first.trim()))) {
|
|
199
265
|
report.violations.push({
|
|
@@ -201,6 +267,8 @@ export function scanSession(sessionId: string, lines: string[]): SessionReport {
|
|
|
201
267
|
session: sessionId,
|
|
202
268
|
at,
|
|
203
269
|
detail: `German pin, English reply opener: "${first.slice(0, 110)}"`,
|
|
270
|
+
turns_since_prompt: turnsSincePrompt,
|
|
271
|
+
compaction_since_prompt: compactionSincePrompt,
|
|
204
272
|
});
|
|
205
273
|
}
|
|
206
274
|
}
|
|
@@ -220,21 +288,14 @@ export function scanSession(sessionId: string, lines: string[]): SessionReport {
|
|
|
220
288
|
});
|
|
221
289
|
}
|
|
222
290
|
if (isCiPoll(cmd)) {
|
|
223
|
-
//
|
|
224
|
-
//
|
|
225
|
-
//
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
report.violations.push({
|
|
232
|
-
check: "vacuous-evidence",
|
|
233
|
-
session: sessionId,
|
|
234
|
-
at,
|
|
235
|
-
detail: `CI poll read as settled without an in-flight observation: ${out.slice(0, 90)}`,
|
|
236
|
-
});
|
|
237
|
-
}
|
|
291
|
+
// The RESULT of this call arrives on the NEXT entry, and that entry is
|
|
292
|
+
// role `user` — not assistant. The first version read
|
|
293
|
+
// `entry["toolUseResult"]` off the assistant entry, where it never
|
|
294
|
+
// exists: measured on a 20 MB transcript, 1913 entries carry the key
|
|
295
|
+
// and all 1913 are `user`. So this check could not fire at all and the
|
|
296
|
+
// scan printed a permanent `✅ vacuous-evidence 0` — a false green in
|
|
297
|
+
// the one report whose job is to say whether the gates bite.
|
|
298
|
+
pendingPoll = { at, cmd };
|
|
238
299
|
}
|
|
239
300
|
}
|
|
240
301
|
|
|
@@ -269,7 +330,8 @@ export function scanSession(sessionId: string, lines: string[]): SessionReport {
|
|
|
269
330
|
|
|
270
331
|
/** Default Claude Code transcript store for a project directory. */
|
|
271
332
|
export function defaultStore(projectDir: string): string {
|
|
272
|
-
|
|
333
|
+
// Claude Code slugs BOTH separators and dots: /Users/x/.claude → -Users-x--claude.
|
|
334
|
+
const slug = projectDir.replace(/[/.]/g, "-");
|
|
273
335
|
return path.join(process.env["HOME"] ?? "", ".claude", "projects", slug);
|
|
274
336
|
}
|
|
275
337
|
|
|
@@ -362,7 +424,10 @@ export function main(argv?: string[]): number {
|
|
|
362
424
|
if (out) {
|
|
363
425
|
fs.writeFileSync(out, `${JSON.stringify(report, null, 2)}\n`, "utf8");
|
|
364
426
|
}
|
|
365
|
-
process.stdout.write
|
|
427
|
+
// fs.writeSync, not process.stdout.write: `process.exit()` below does not
|
|
428
|
+
// flush an async pipe write, so `--json | jq` reproducibly received a
|
|
429
|
+
// truncated 64 KB document with no error (measured: 85,169 vs 65,536 bytes).
|
|
430
|
+
fs.writeSync(1, json ? `${JSON.stringify(report, null, 2)}\n` : `${render(report)}\n`);
|
|
366
431
|
return 0;
|
|
367
432
|
}
|
|
368
433
|
|
|
@@ -548,16 +548,38 @@ function reviewerPrompt(args: {
|
|
|
548
548
|
].join('\n');
|
|
549
549
|
}
|
|
550
550
|
|
|
551
|
+
/**
|
|
552
|
+
* The findings skeleton, carrying `prompt_hash`.
|
|
553
|
+
*
|
|
554
|
+
* The contract's most dangerous residual is the PROMPT channel (§ 5): the
|
|
555
|
+
* dispatcher fixes the reviewer's inputs but not the instructions wrapped around
|
|
556
|
+
* them, so "a hash-verified artefact is not evidence of an unbiased review" — its
|
|
557
|
+
* recorded case zero is an honest-null whose prompt stated prior rounds were
|
|
558
|
+
* clean, where a later unsteered round on the identical scope found seven
|
|
559
|
+
* defects.
|
|
560
|
+
*
|
|
561
|
+
* Writing the hash of the prompt THIS dispatcher built makes the channel
|
|
562
|
+
* attributable: the artefact now records which prompt produced it, so a review
|
|
563
|
+
* run against a different prompt is distinguishable from one run against the
|
|
564
|
+
* dispatcher's. `--verify` re-derives it from the same inputs and compares.
|
|
565
|
+
*
|
|
566
|
+
* It does not CLOSE the residual and is not sold as doing so: the same host that
|
|
567
|
+
* authors a steered prompt could hash the steered text. The gain is an
|
|
568
|
+
* attributable record where there was none, which is what turns the audit trail
|
|
569
|
+
* (§ 2.7, round records are never edited in place) into something a contradicting
|
|
570
|
+
* later round can be checked against.
|
|
571
|
+
*/
|
|
551
572
|
function findingsSkeleton(args: {
|
|
552
573
|
slug: string;
|
|
553
574
|
headSha: string;
|
|
554
575
|
scopeHash: string;
|
|
555
576
|
reviewedDate: string;
|
|
556
577
|
manifest: string;
|
|
578
|
+
promptHash: string;
|
|
557
579
|
}): string {
|
|
558
580
|
return [
|
|
559
581
|
`# Findings: ${args.slug}`,
|
|
560
|
-
`<!-- completion-review: v1 | reviewed: ${args.reviewedDate} | scope: ${args.scopeHash} | diff: ${args.headSha} | reviewer: r2-fresh-subagent-${args.slug} -->`,
|
|
582
|
+
`<!-- completion-review: v1 | reviewed: ${args.reviewedDate} | scope: ${args.scopeHash} | diff: ${args.headSha} | reviewer: r2-fresh-subagent-${args.slug} | prompt_hash: ${args.promptHash} -->`,
|
|
561
583
|
'',
|
|
562
584
|
args.manifest,
|
|
563
585
|
'',
|
|
@@ -890,7 +912,14 @@ function runDispatch(args: Args): number {
|
|
|
890
912
|
acHash: hashes.ac_hash,
|
|
891
913
|
dispatched,
|
|
892
914
|
});
|
|
893
|
-
const skeleton = findingsSkeleton({
|
|
915
|
+
const skeleton = findingsSkeleton({
|
|
916
|
+
slug,
|
|
917
|
+
headSha,
|
|
918
|
+
scopeHash: hashes.scope_hash,
|
|
919
|
+
reviewedDate,
|
|
920
|
+
manifest,
|
|
921
|
+
promptHash: sha256(promptText),
|
|
922
|
+
});
|
|
894
923
|
|
|
895
924
|
fs.mkdirSync(inputDirAbs, { recursive: true });
|
|
896
925
|
fs.writeFileSync(path.join(inputDirAbs, 'diff.patch'), scopeDiffText, 'utf-8');
|