@event4u/agent-config 9.27.0 → 9.29.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/AGENTS.md +13 -12
- package/CHANGELOG.md +165 -0
- package/README.md +1 -1
- package/agents/templates/.ai-council.yml.example +55 -24
- package/dist/agent-src/commands/analyze/conformance.md +24 -0
- package/dist/agent-src/commands/cost/report.md +2 -2
- package/dist/agent-src/commands/team/adversarial.md +8 -7
- package/dist/agent-src/commands/team/delegate.md +13 -12
- package/dist/agent-src/commands/team/review.md +9 -8
- package/dist/agent-src/commands/team/status.md +9 -8
- package/dist/agent-src/commands/team.md +29 -19
- package/dist/agent-src/contexts/execution/auto-dispatch-classification.md +88 -2
- package/dist/agent-src/contexts/execution/auto-orchestration-activation.md +29 -21
- package/dist/agent-src/contexts/execution/host-capability-manifest.md +26 -15
- package/dist/agent-src/contexts/execution/orchestration-telemetry.md +28 -3
- package/dist/agent-src/contexts/execution/subagent-response-contract.md +54 -0
- package/dist/agent-src/contexts/execution/subagent-routing.md +13 -11
- package/dist/agent-src/contexts/execution/subagent-spawn-contract.md +25 -0
- package/dist/agent-src/contexts/execution/subagent-steering.md +6 -3
- package/dist/agent-src/rules/artifact-engagement-recording.md +4 -1
- package/dist/agent-src/rules/autonomous-execution.md +1 -1
- package/dist/agent-src/rules/brand-source-of-truth.md +33 -6
- package/dist/agent-src/rules/context-hygiene.md +3 -1
- package/dist/agent-src/rules/council-availability.md +93 -0
- package/dist/agent-src/rules/cross-source-consistency.md +3 -0
- package/dist/agent-src/rules/delegation-policy.md +38 -10
- package/dist/agent-src/rules/design-fidelity.md +3 -0
- package/dist/agent-src/rules/evaluator-independence.md +2 -1
- package/dist/agent-src/rules/external-code-graph-interop.md +2 -1
- package/dist/agent-src/rules/finance-safety-floor.md +1 -1
- package/dist/agent-src/rules/git-history-discipline.md +1 -1
- package/dist/agent-src/rules/history-discipline.md +3 -1
- package/dist/agent-src/rules/legal-safety-floor.md +1 -1
- package/dist/agent-src/rules/no-pr-progress-comments.md +3 -1
- package/dist/agent-src/rules/onboarding-gate.md +3 -1
- package/dist/agent-src/rules/roadmap-ci-steps-policy.md +4 -1
- package/dist/agent-src/rules/role-mode-adherence.md +1 -1
- package/dist/agent-src/rules/scale-discipline.md +3 -1
- package/dist/agent-src/rules/self-repair-loop.md +103 -0
- package/dist/agent-src/rules/session-canary.md +2 -1
- package/dist/agent-src/rules/settings-ask-protocol.md +8 -7
- package/dist/agent-src/rules/strategy-safety-floor.md +1 -1
- package/dist/agent-src/rules/token-budget-discipline.md +4 -2
- package/dist/agent-src/rules/tool-safety.md +5 -1
- package/dist/agent-src/skills/adversarial-review/SKILL.md +1 -1
- package/dist/agent-src/skills/analysis-autonomous-mode/SKILL.md +1 -1
- package/dist/agent-src/skills/forensics-report/SKILL.md +114 -0
- package/dist/agent-src/skills/forensics-report/evals/triggers.json +17 -0
- package/dist/agent-src/skills/performance-analysis/SKILL.md +1 -1
- package/dist/agent-src/skills/persona-improvement/SKILL.md +1 -1
- package/dist/agent-src/skills/premortem/SKILL.md +30 -1
- package/dist/agent-src/skills/project-analyzer/SKILL.md +1 -1
- package/dist/agent-src/skills/reasoning-orchestrator/SKILL.md +2 -1
- package/dist/agent-src/skills/security-audit/SKILL.md +1 -1
- package/dist/agent-src/skills/sequential-thinking/SKILL.md +1 -1
- package/dist/agent-src/skills/skill-improvement-pipeline/SKILL.md +1 -1
- package/dist/agent-src/skills/subagent-orchestration/SKILL.md +7 -5
- package/dist/agent-src/skills/subagent-orchestration/schemas/subagent-status.json +61 -3
- package/dist/agent-src/skills/test-case-discovery/SKILL.md +1 -1
- package/dist/agent-src/skills/universal-project-analysis/SKILL.md +1 -1
- package/dist/agent-src/templates/AGENTS.md +8 -12
- package/dist/agent-src/templates/agents/agent-project-settings.example.yml +1 -1
- package/dist/agent-src/templates/roadmaps.md +23 -0
- package/dist/cli/registry.js +7 -0
- package/dist/cli/registry.js.map +1 -1
- package/dist/cli-delegate/chunk-66OVWGB5.js +270 -0
- package/dist/cli-delegate/{chunk-HJ6DAJHR.js → chunk-LCFJIQ4J.js} +89 -7
- package/dist/cli-delegate/{chunk-VSYT7OHO.js → chunk-LVRYBKTI.js} +31 -2
- package/dist/cli-delegate/{chunk-DVRS2KVR.js → chunk-MMXA5GY7.js} +10 -17
- package/dist/cli-delegate/{chunk-WR6GTRHJ.js → chunk-MP3JT3IU.js} +3 -3
- package/dist/cli-delegate/{chunk-3KJIMFTB.js → chunk-NZ5N2XQL.js} +467 -305
- package/dist/cli-delegate/{chunk-AIIUU73N.js → chunk-OJM3MBJM.js} +1 -1
- package/dist/cli-delegate/{chunk-BCPIOAGV.js → chunk-RCIA67XN.js} +117 -96
- package/dist/cli-delegate/chunk-REKCMYOU.js +2991 -0
- package/dist/cli-delegate/cmd_analyze_session.js +1 -1
- package/dist/cli-delegate/cmd_brand_status.js +134 -0
- package/dist/cli-delegate/cmd_conformance.js +10 -47
- package/dist/cli-delegate/cmd_converge.js +3 -3
- package/dist/cli-delegate/cmd_doctor.js +10 -6
- package/dist/cli-delegate/cmd_explain.js +80 -330
- package/dist/cli-delegate/cmd_export.js +6 -6
- package/dist/cli-delegate/cmd_fleet.js +3 -3
- package/dist/cli-delegate/cmd_handoff.js +3 -3
- package/dist/cli-delegate/cmd_mcp_available.js +433 -0
- package/dist/cli-delegate/cmd_migrate.js +3 -3
- package/dist/cli-delegate/cmd_packs_active.js +153 -0
- package/dist/cli-delegate/cmd_preflight.js +1 -1
- package/dist/cli-delegate/cmd_prune.js +6 -6
- package/dist/cli-delegate/cmd_route_audit.js +8 -8
- package/dist/cli-delegate/cmd_route_explain.js +4 -4
- package/dist/cli-delegate/cmd_settings_get.js +266 -0
- package/dist/cli-delegate/cmd_settings_migrate.js +6 -6
- package/dist/cli-delegate/cmd_settings_set.js +8 -2968
- package/dist/cli-delegate/cmd_sync.js +6 -6
- package/dist/cli-delegate/cmd_uninstall.js +6 -6
- package/dist/cli-delegate/cmd_update.js +4 -4
- package/dist/cli-delegate/cmd_upgrade.js +4 -4
- package/dist/cli-delegate/cmd_validate.js +6 -6
- package/dist/cli-delegate/cmd_versions.js +4 -4
- package/dist/discovery/deprecation-report.md +1 -1
- package/dist/discovery/discovery-manifest.json +164 -55
- package/dist/discovery/discovery-manifest.json.sha256 +1 -1
- package/dist/discovery/discovery-manifest.summary.md +4 -3
- package/dist/discovery/orphan-report.md +1 -1
- package/dist/discovery/packs.json +44 -5
- package/dist/discovery/trust-report.md +11 -11
- package/dist/discovery/workspaces.json +16 -6
- package/dist/hooks/dispatch.js +9341 -6131
- package/dist/install/install.mjs +739 -524
- package/dist/mcp/registry-manifest.json +3 -3
- package/dist/mcp/server.json +2 -2
- package/dist/mcp/server.mjs +777 -281
- package/dist/router.json +1 -1
- package/dist/scripts/_lib/agent_settings.js +134 -15
- package/dist/scripts/_lib/agent_settings.js.map +1 -1
- package/dist/scripts/condense.js +147 -2
- package/dist/scripts/condense.js.map +1 -1
- package/dist/server/schemas/settings.js +6 -7
- package/dist/server/schemas/settings.js.map +1 -1
- package/dist/shared/settingsCarveOut.js +0 -9
- package/dist/shared/settingsCarveOut.js.map +1 -1
- package/dist/shared/settingsClasses.js +11 -9
- package/dist/shared/settingsClasses.js.map +1 -1
- package/dist/ui/assets/{index-DmR0mbk4.js → index-JDAF00bC.js} +2 -2
- package/dist/ui/assets/{index-DmR0mbk4.js.map → index-JDAF00bC.js.map} +1 -1
- package/dist/ui/index.html +1 -1
- package/docs/contracts/settings-classes.md +68 -17
- package/docs/guidelines/agent-infra/memory-access.md +14 -3
- package/package.json +1 -1
- package/src/agent-src/templates/AGENTS.md +8 -12
- package/src/config/agent-settings.template.yml +52 -66
- package/src/config/budgets.yml +29 -0
- package/src/config/discovery/packs.yml +11 -0
- package/src/config/discovery/workspaces.yml +1 -1
- package/src/config/evaluator-budgets.json +4 -4
- package/src/config/gate-coverage.yml +26 -0
- package/src/config/gate-violation-baselines.json +3 -3
- package/src/scripts/_cli/cmd_brand_status.ts +174 -0
- package/src/scripts/_cli/cmd_conformance.ts +14 -42
- package/src/scripts/_cli/cmd_doctor.ts +65 -48
- package/src/scripts/_cli/cmd_mcp_available.ts +266 -0
- package/src/scripts/_cli/cmd_packs_active.ts +225 -0
- package/src/scripts/_cli/cmd_settings_get.ts +324 -0
- package/src/scripts/_dispatch.bash +57 -0
- package/src/scripts/_lib/agent_settings.ts +140 -16
- package/src/scripts/_lib/auto_dispatch.corpus.test.ts +1 -1
- package/src/scripts/_lib/auto_dispatch.ts +23 -18
- package/src/scripts/_lib/capsule_trigger.ts +124 -0
- package/src/scripts/_lib/carrier_divergence.ts +135 -0
- package/src/scripts/_lib/host_capability.ts +144 -4
- package/src/scripts/_lib/judgment_ladder.ts +382 -0
- package/src/scripts/_lib/orchestration_gate.ts +8 -0
- package/src/scripts/_lib/orchestration_record.ts +37 -0
- package/src/scripts/_lib/orchestration_savings.ts +1 -1
- package/src/scripts/_lib/prompt_shape.ts +88 -0
- package/src/scripts/_lib/review_skipped_record.ts +102 -0
- package/src/scripts/_lib/rule_layer_overlap.ts +254 -0
- package/src/scripts/_lib/self_repair.ts +489 -0
- package/src/scripts/_lib/self_repair_store.ts +128 -0
- package/src/scripts/_lib/subagent_capsule.ts +182 -0
- package/src/scripts/_lib/subagent_response.ts +19 -0
- package/src/scripts/_lib/subagent_spawn.ts +13 -0
- package/src/scripts/_lib/subagent_steering.ts +9 -4
- package/src/scripts/_lib/tier_budget_routing.ts +6 -1
- package/src/scripts/_lib/transcript_entry.ts +80 -0
- package/src/scripts/_lib/worker_budget.ts +37 -2
- package/src/scripts/ai_council/clients.ts +76 -54
- package/src/scripts/ai_council/config.ts +94 -6
- package/src/scripts/ai_council/handoff.ts +113 -0
- package/src/scripts/ai_council/modes.ts +19 -4
- package/src/scripts/ai_council/orchestrator.ts +105 -0
- package/src/scripts/ai_council/quorum.ts +84 -0
- package/src/scripts/ai_council/session.ts +44 -0
- package/src/scripts/ai_council/transport_resolver.ts +127 -1
- package/src/scripts/ai_team/availability.ts +110 -0
- package/src/scripts/ai_team/config.ts +34 -14
- package/src/scripts/ai_team/team_dispatch.ts +71 -29
- package/src/scripts/bench_critic_protocol.ts +396 -0
- package/src/scripts/check_branch_freshness.ts +255 -0
- package/src/scripts/check_council_config_location.ts +125 -1
- package/src/scripts/check_detector_corpus.ts +202 -0
- package/src/scripts/check_hook_bundle_freshness.ts +4 -1
- package/src/scripts/check_standing_rule_delivery.ts +314 -0
- package/src/scripts/condense.ts +162 -2
- package/src/scripts/config/profiles.ts +13 -3
- package/src/scripts/conformance_scan.ts +272 -24
- package/src/scripts/council_availability_hook.ts +188 -0
- package/src/scripts/council_cli.ts +527 -32
- package/src/scripts/forensics_report.ts +333 -0
- package/src/scripts/hook_manifest.yaml +107 -26
- package/src/scripts/hooks/block_no_verify.ts +39 -2
- package/src/scripts/hooks/block_unauthorized_git.ts +156 -7
- package/src/scripts/hooks/concern_registry.ts +10 -0
- package/src/scripts/hooks/delegation_nudge_hook.ts +484 -0
- package/src/scripts/hooks/end_review_nudge_hook.ts +726 -0
- package/src/scripts/hooks/orchestration_record_hook.ts +342 -0
- package/src/scripts/install.ts +151 -0
- package/src/scripts/language_mirror_hook.ts +30 -37
- package/src/scripts/lint_discovery_vocabulary.ts +2 -0
- package/src/scripts/lint_never_silent.ts +276 -0
- package/src/scripts/lint_no_activation_gates.ts +271 -0
- package/src/scripts/measure_scope_dedup.ts +19 -13
- package/src/scripts/onboarding_gate_hook.ts +40 -1
- package/src/scripts/orchestration_record.ts +12 -1
- package/src/scripts/preamble_byte_census.ts +9 -20
- package/src/scripts/rebuild_hook_bundle.ts +177 -0
- package/src/scripts/release.ts +74 -2
- package/src/scripts/report_carrier_divergence.ts +364 -0
- package/src/scripts/report_skill_activation.ts +257 -0
- package/src/scripts/report_skill_obligation_violations.ts +579 -0
- package/src/scripts/routing_doctor.ts +95 -39
- package/src/scripts/score_skill_selection.ts +19 -5
- package/src/scripts/self_repair_cli.ts +470 -0
- package/src/scripts/self_repair_hook.ts +241 -0
- package/src/scripts/team_events_spike.ts +271 -0
- package/src/scripts/team_review_gate_hook.ts +31 -9
- package/src/server/schemas/settings.ts +10 -17
- package/src/shared/settingsCarveOut.ts +0 -10
- package/src/shared/settingsClasses.ts +11 -9
|
@@ -0,0 +1,333 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Forensic analyzers — hotspot risk + change coupling from `git log` alone
|
|
3
|
+
* (road-to-judgment-and-forensic-evidence Phase 3; pack `forensics`,
|
|
4
|
+
* default-off, read-only, advisory).
|
|
5
|
+
*
|
|
6
|
+
* Two deterministic analyzers over a PINNED revision range:
|
|
7
|
+
*
|
|
8
|
+
* hotspot(file) = norm(change_frequency) × norm(complexity)
|
|
9
|
+
* complexity = LOC + total indentation units (tabs = 1,
|
|
10
|
+
* 4 spaces = 1) of the file in the working tree — a cheap,
|
|
11
|
+
* language-agnostic indentation proxy, documented rather
|
|
12
|
+
* than clever. A hotspot is a place to LOOK, never a code
|
|
13
|
+
* quality verdict (both inputs can be correct for a file).
|
|
14
|
+
*
|
|
15
|
+
* coupling(A,B) = co_changes(A,B) / min(changes(A), changes(B))
|
|
16
|
+
* over commits with ≤ max_commit_files files (bulk sweeps
|
|
17
|
+
* poison coupling; skipped commits are counted in the
|
|
18
|
+
* report, never silently dropped).
|
|
19
|
+
*
|
|
20
|
+
* boundary_contradictions — coupling pairs ABOVE the threshold whose files
|
|
21
|
+
* live in different top-level modules. Recorded as a finding CLASS (the
|
|
22
|
+
* intended boundaries and the actual change pattern disagree), not as a
|
|
23
|
+
* failure — per the roadmap's step 3.6.
|
|
24
|
+
*
|
|
25
|
+
* Determinism bar (step 3.3): byte-stable output on a frozen fixture — same
|
|
26
|
+
* inputs, same bytes. Inputs are the git log for the pinned range plus file
|
|
27
|
+
* metrics; `--log-file` + `--metrics-file` inject both for fixture tests, so
|
|
28
|
+
* the analyzer's whole pipeline is exercised without a live repo. All maps
|
|
29
|
+
* are sorted before emission; scores are rounded to 4 decimals.
|
|
30
|
+
*
|
|
31
|
+
* Advisory wiring (step 3.4): `--findings-out` emits
|
|
32
|
+
* review-findings.schema.json-shaped items with kind `correctness` and
|
|
33
|
+
* severity `low|medium` — non-blocking by construction in
|
|
34
|
+
* `check_finding_dispositions.isBlocking` — for ingestion into the existing
|
|
35
|
+
* release findings ledger (`check_finding_dispositions --ingest`).
|
|
36
|
+
*
|
|
37
|
+
* Gaming risk: an empty scan (dead range, wrong cwd) yielding a green "no
|
|
38
|
+
* hotspots" report — mitigated by the shared scan-scope helper: zero scanned
|
|
39
|
+
* commits throws (DeadScopeError), it never publishes an empty finding set
|
|
40
|
+
* (step 3.2). Residual: a range that is real but trivially small still
|
|
41
|
+
* produces low-signal output; the report publishes commit + file counts so a
|
|
42
|
+
* reader can judge the denominator.
|
|
43
|
+
*/
|
|
44
|
+
// ledger-exempt: on-demand advisory analyzer, not a CI gate — the report JSON
|
|
45
|
+
// itself carries the full denominator (scanned commits/files) per run.
|
|
46
|
+
import * as fs from 'node:fs';
|
|
47
|
+
import { createHash } from 'node:crypto';
|
|
48
|
+
import { execFileSync } from 'node:child_process';
|
|
49
|
+
|
|
50
|
+
import { reportScanned } from './_lib/scan_scope.js';
|
|
51
|
+
|
|
52
|
+
interface Params {
|
|
53
|
+
max_commit_files: number;
|
|
54
|
+
coupling_threshold: number;
|
|
55
|
+
min_cochanges: number;
|
|
56
|
+
top_hotspots: number;
|
|
57
|
+
}
|
|
58
|
+
const DEFAULTS: Params = {
|
|
59
|
+
max_commit_files: 50,
|
|
60
|
+
coupling_threshold: 0.5,
|
|
61
|
+
min_cochanges: 3,
|
|
62
|
+
top_hotspots: 25,
|
|
63
|
+
};
|
|
64
|
+
|
|
65
|
+
interface Commit {
|
|
66
|
+
hash: string;
|
|
67
|
+
files: string[];
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/** Parse `git log --pretty=format:'C %H' --name-only` output. */
|
|
71
|
+
export function parseLog(text: string): Commit[] {
|
|
72
|
+
const commits: Commit[] = [];
|
|
73
|
+
let current: Commit | null = null;
|
|
74
|
+
for (const raw of text.split('\n')) {
|
|
75
|
+
const line = raw.trimEnd();
|
|
76
|
+
if (line.startsWith('C ')) {
|
|
77
|
+
if (current) commits.push(current);
|
|
78
|
+
current = { hash: line.slice(2).trim(), files: [] };
|
|
79
|
+
} else if (line.length > 0 && current) {
|
|
80
|
+
current.files.push(line);
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
if (current) commits.push(current);
|
|
84
|
+
return commits;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
function gitLog(range: string): string {
|
|
88
|
+
return execFileSync('git', ['log', '--pretty=format:C %H', '--name-only', range], {
|
|
89
|
+
encoding: 'utf8',
|
|
90
|
+
maxBuffer: 256 * 1024 * 1024,
|
|
91
|
+
});
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/** LOC + indentation units — the documented complexity proxy. */
|
|
95
|
+
export function complexityOf(content: string): number {
|
|
96
|
+
const lines = content.split('\n');
|
|
97
|
+
let indent = 0;
|
|
98
|
+
for (const l of lines) {
|
|
99
|
+
const m = /^[\t ]*/.exec(l);
|
|
100
|
+
const ws = m ? m[0] : '';
|
|
101
|
+
let units = 0;
|
|
102
|
+
let spaces = 0;
|
|
103
|
+
for (const ch of ws) {
|
|
104
|
+
if (ch === '\t') units += 1;
|
|
105
|
+
else spaces += 1;
|
|
106
|
+
}
|
|
107
|
+
units += Math.floor(spaces / 4);
|
|
108
|
+
indent += units;
|
|
109
|
+
}
|
|
110
|
+
return lines.length + indent;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
function liveComplexity(file: string): number | null {
|
|
114
|
+
try {
|
|
115
|
+
const st = fs.statSync(file);
|
|
116
|
+
if (!st.isFile()) return null;
|
|
117
|
+
const buf = fs.readFileSync(file);
|
|
118
|
+
if (buf.includes(0)) return null; // binary
|
|
119
|
+
return complexityOf(buf.toString('utf8'));
|
|
120
|
+
} catch {
|
|
121
|
+
return null; // deleted / renamed away — frequency without complexity
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
function round4(n: number): number {
|
|
126
|
+
return Math.round(n * 10000) / 10000;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
/** First path segment — the module boundary the router treats as independent. */
|
|
130
|
+
export function moduleOf(file: string): string {
|
|
131
|
+
const parts = file.split('/');
|
|
132
|
+
if (parts.length >= 2 && (parts[0] === 'src' || parts[0] === 'tests' || parts[0] === 'docs')) {
|
|
133
|
+
return `${parts[0]}/${parts[1] ?? ''}`;
|
|
134
|
+
}
|
|
135
|
+
return parts[0] ?? file;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
interface Hotspot {
|
|
139
|
+
file: string;
|
|
140
|
+
changes: number;
|
|
141
|
+
complexity: number | null;
|
|
142
|
+
score: number;
|
|
143
|
+
}
|
|
144
|
+
interface CouplingPair {
|
|
145
|
+
a: string;
|
|
146
|
+
b: string;
|
|
147
|
+
co_changes: number;
|
|
148
|
+
changes_a: number;
|
|
149
|
+
changes_b: number;
|
|
150
|
+
coupling: number;
|
|
151
|
+
cross_module: boolean;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
export interface Report {
|
|
155
|
+
mode: string;
|
|
156
|
+
range: string;
|
|
157
|
+
params: Params;
|
|
158
|
+
scanned: { commits: number; commits_skipped_bulk: number; files: number };
|
|
159
|
+
hotspots: Hotspot[];
|
|
160
|
+
coupling: CouplingPair[];
|
|
161
|
+
boundary_contradictions: CouplingPair[];
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
export function analyze(
|
|
165
|
+
commits: Commit[],
|
|
166
|
+
params: Params,
|
|
167
|
+
complexity: (file: string) => number | null,
|
|
168
|
+
range: string,
|
|
169
|
+
mode: string,
|
|
170
|
+
): Report {
|
|
171
|
+
const changes = new Map<string, number>();
|
|
172
|
+
const co = new Map<string, number>();
|
|
173
|
+
let skipped = 0;
|
|
174
|
+
for (const c of commits) {
|
|
175
|
+
const files = [...new Set(c.files)].sort();
|
|
176
|
+
if (files.length > params.max_commit_files) {
|
|
177
|
+
skipped++;
|
|
178
|
+
continue;
|
|
179
|
+
}
|
|
180
|
+
for (const f of files) changes.set(f, (changes.get(f) ?? 0) + 1);
|
|
181
|
+
for (let i = 0; i < files.length; i++) {
|
|
182
|
+
for (let j = i + 1; j < files.length; j++) {
|
|
183
|
+
const key = `${files[i]}\u0000${files[j]}`;
|
|
184
|
+
co.set(key, (co.get(key) ?? 0) + 1);
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
const files = [...changes.keys()].sort();
|
|
190
|
+
const complexities = new Map<string, number | null>();
|
|
191
|
+
for (const f of files) complexities.set(f, complexity(f));
|
|
192
|
+
let maxChanges = 0;
|
|
193
|
+
for (const v of changes.values()) if (v > maxChanges) maxChanges = v;
|
|
194
|
+
let maxComplexity = 0;
|
|
195
|
+
for (const v of complexities.values()) if (v !== null && v > maxComplexity) maxComplexity = v;
|
|
196
|
+
|
|
197
|
+
const hotspots: Hotspot[] = files
|
|
198
|
+
.map((f) => {
|
|
199
|
+
const ch = changes.get(f) ?? 0;
|
|
200
|
+
const cx = complexities.get(f) ?? null;
|
|
201
|
+
const score =
|
|
202
|
+
cx === null || maxChanges === 0 || maxComplexity === 0
|
|
203
|
+
? 0
|
|
204
|
+
: round4((ch / maxChanges) * (cx / maxComplexity));
|
|
205
|
+
return { file: f, changes: ch, complexity: cx, score };
|
|
206
|
+
})
|
|
207
|
+
.filter((h) => h.score > 0)
|
|
208
|
+
.sort((a, b) => b.score - a.score || a.file.localeCompare(b.file))
|
|
209
|
+
.slice(0, params.top_hotspots);
|
|
210
|
+
|
|
211
|
+
const coupling: CouplingPair[] = [];
|
|
212
|
+
for (const [key, n] of [...co.entries()].sort()) {
|
|
213
|
+
if (n < params.min_cochanges) continue;
|
|
214
|
+
const [a, b] = key.split('\u0000') as [string, string];
|
|
215
|
+
const ca = changes.get(a) ?? 0;
|
|
216
|
+
const cb = changes.get(b) ?? 0;
|
|
217
|
+
const denom = Math.min(ca, cb);
|
|
218
|
+
if (denom === 0) continue;
|
|
219
|
+
const strength = round4(n / denom);
|
|
220
|
+
if (strength < params.coupling_threshold) continue;
|
|
221
|
+
coupling.push({
|
|
222
|
+
a,
|
|
223
|
+
b,
|
|
224
|
+
co_changes: n,
|
|
225
|
+
changes_a: ca,
|
|
226
|
+
changes_b: cb,
|
|
227
|
+
coupling: strength,
|
|
228
|
+
cross_module: moduleOf(a) !== moduleOf(b),
|
|
229
|
+
});
|
|
230
|
+
}
|
|
231
|
+
coupling.sort((x, y) => y.coupling - x.coupling || x.a.localeCompare(y.a) || x.b.localeCompare(y.b));
|
|
232
|
+
|
|
233
|
+
return {
|
|
234
|
+
mode,
|
|
235
|
+
range,
|
|
236
|
+
params,
|
|
237
|
+
scanned: {
|
|
238
|
+
commits: commits.length - skipped,
|
|
239
|
+
commits_skipped_bulk: skipped,
|
|
240
|
+
files: files.length,
|
|
241
|
+
},
|
|
242
|
+
hotspots,
|
|
243
|
+
coupling,
|
|
244
|
+
boundary_contradictions: coupling.filter((p) => p.cross_module),
|
|
245
|
+
};
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
/** Same id convention as self_review_gate.findingId — the schema pins ^[0-9a-f]{12}$. */
|
|
249
|
+
function forensicsFindingId(kind: string, title: string, file: string): string {
|
|
250
|
+
return createHash('sha256').update(`${kind}|${title}|${file}`).digest('hex').slice(0, 12);
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
/** Findings-schema-shaped advisory items — non-blocking by construction. */
|
|
254
|
+
export function toFindings(report: Report): Record<string, unknown> {
|
|
255
|
+
const findings: Record<string, unknown>[] = [];
|
|
256
|
+
for (const h of report.hotspots.slice(0, 10)) {
|
|
257
|
+
const title = `hotspot: ${h.file} (score ${String(h.score)})`;
|
|
258
|
+
findings.push({
|
|
259
|
+
finding_id: forensicsFindingId('correctness', title, h.file),
|
|
260
|
+
severity: 'low',
|
|
261
|
+
kind: 'correctness',
|
|
262
|
+
title,
|
|
263
|
+
detail: `${String(h.changes)} changes in range × complexity ${String(h.complexity ?? 'n/a')} — a place to look, not a quality verdict.`,
|
|
264
|
+
file: h.file,
|
|
265
|
+
});
|
|
266
|
+
}
|
|
267
|
+
for (const p of report.boundary_contradictions) {
|
|
268
|
+
const title = `boundary contradiction: ${p.a} <-> ${p.b} (coupling ${String(p.coupling)})`;
|
|
269
|
+
findings.push({
|
|
270
|
+
finding_id: forensicsFindingId('correctness', title, p.a),
|
|
271
|
+
severity: 'medium',
|
|
272
|
+
kind: 'correctness',
|
|
273
|
+
title,
|
|
274
|
+
detail: `Cross-module co-change ${String(p.co_changes)}/${String(Math.min(p.changes_a, p.changes_b))} above threshold ${String(report.params.coupling_threshold)} — the intended module boundary and the actual change pattern disagree. A finding class, not a failure.`,
|
|
275
|
+
file: p.a,
|
|
276
|
+
});
|
|
277
|
+
}
|
|
278
|
+
return { schema_version: 1, findings };
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
function stringifyReport(report: Report): string {
|
|
282
|
+
return JSON.stringify(report, null, 2) + '\n';
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
function argValue(argv: string[], flag: string): string | null {
|
|
286
|
+
const i = argv.indexOf(flag);
|
|
287
|
+
return i >= 0 ? (argv[i + 1] ?? null) : null;
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
function main(argv: string[]): number {
|
|
291
|
+
const range = argValue(argv, '--range');
|
|
292
|
+
const logFile = argValue(argv, '--log-file');
|
|
293
|
+
const metricsFile = argValue(argv, '--metrics-file');
|
|
294
|
+
const outFile = argValue(argv, '--out');
|
|
295
|
+
const findingsOut = argValue(argv, '--findings-out');
|
|
296
|
+
if (!range && !logFile) {
|
|
297
|
+
process.stderr.write(
|
|
298
|
+
'usage: forensics_report (--range <rev-range> | --log-file <fixture>) [--metrics-file <json>] [--out <file>] [--findings-out <file>]\n',
|
|
299
|
+
);
|
|
300
|
+
return 2;
|
|
301
|
+
}
|
|
302
|
+
const params: Params = { ...DEFAULTS };
|
|
303
|
+
const text = logFile ? fs.readFileSync(logFile, 'utf8') : gitLog(range as string);
|
|
304
|
+
const commits = parseLog(text);
|
|
305
|
+
let complexity: (file: string) => number | null = liveComplexity;
|
|
306
|
+
if (metricsFile) {
|
|
307
|
+
const metrics = JSON.parse(fs.readFileSync(metricsFile, 'utf8')) as Record<string, number>;
|
|
308
|
+
complexity = (f) => (Object.prototype.hasOwnProperty.call(metrics, f) ? metrics[f]! : null);
|
|
309
|
+
}
|
|
310
|
+
const report = analyze(commits, params, complexity, range ?? `log-file:${logFile ?? ''}`, 'run');
|
|
311
|
+
|
|
312
|
+
// Loud-fail on an empty scan BEFORE anything is written — a report over
|
|
313
|
+
// nothing is not a report, and it must not exist on disk either (throws
|
|
314
|
+
// DeadScopeError when zero commits were analyzed).
|
|
315
|
+
reportScanned({
|
|
316
|
+
gate: 'forensics_report',
|
|
317
|
+
scanned: report.scanned.commits,
|
|
318
|
+
units: 'commit(s)',
|
|
319
|
+
roots: [range ?? (logFile as string)],
|
|
320
|
+
});
|
|
321
|
+
|
|
322
|
+
const json = stringifyReport(report);
|
|
323
|
+
if (outFile) fs.writeFileSync(outFile, json);
|
|
324
|
+
process.stdout.write(json);
|
|
325
|
+
if (findingsOut) fs.writeFileSync(findingsOut, JSON.stringify(toFindings(report), null, 2) + '\n');
|
|
326
|
+
return 0;
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
if (import.meta.url === `file://${process.argv[1]}`) {
|
|
330
|
+
process.exit(main(process.argv.slice(2)));
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
export { main };
|
|
@@ -319,6 +319,29 @@ concerns:
|
|
|
319
319
|
args: []
|
|
320
320
|
fail_closed: false
|
|
321
321
|
severity: advisory
|
|
322
|
+
# Council-availability fact. Carries whether a council is configured and from
|
|
323
|
+
# where, because the always-loaded layer refers to the council conditionally
|
|
324
|
+
# and never says how to check. Measured 2026-08-08: an agent announced "kein
|
|
325
|
+
# Council konfiguriert (keine .agent-settings.yml)" and substituted a weaker
|
|
326
|
+
# path; the council was configured user-globally the whole time, and the
|
|
327
|
+
# project tree is never searched for it (ADR-104). Advisory: supplies a fact,
|
|
328
|
+
# never blocks.
|
|
329
|
+
council-availability:
|
|
330
|
+
script: src/scripts/council_availability_hook.ts
|
|
331
|
+
args: []
|
|
332
|
+
fail_closed: false
|
|
333
|
+
severity: advisory
|
|
334
|
+
# Self-repair intake (road-to-self-repair Phase 1). Two slots, one queue:
|
|
335
|
+
# user_prompt_submit opens a record when the user states the agent worked
|
|
336
|
+
# wrongly and injects the open-queue line; stop runs the deterministic
|
|
337
|
+
# detectors over the finished turn and records silently (a reminder fired
|
|
338
|
+
# after the reply cannot shape it). ADVISORY by construction — it records,
|
|
339
|
+
# it never gates; the outward step is a separate, user-run CLI command.
|
|
340
|
+
self-repair:
|
|
341
|
+
script: src/scripts/self_repair_hook.ts
|
|
342
|
+
args: []
|
|
343
|
+
fail_closed: false
|
|
344
|
+
severity: advisory
|
|
322
345
|
# Shared per-session register (road-to-parallel-session-coordination) — writes
|
|
323
346
|
# this session into <git-common-dir>/agent-sessions/<session_id>.json so every
|
|
324
347
|
# other worktree of the repo can see it, and on session_start injects the live
|
|
@@ -351,22 +374,70 @@ concerns:
|
|
|
351
374
|
args: []
|
|
352
375
|
fail_closed: false
|
|
353
376
|
severity: advisory
|
|
377
|
+
# road-to-orchestrator-discipline-carriers Phase 3 (F6) — deterministic
|
|
378
|
+
# PostToolUse orchestration-telemetry capture. Fires only on an Agent/Task
|
|
379
|
+
# tool-use completion (the two observed subagent-dispatch tool names) and
|
|
380
|
+
# appends one `orchestration_record` line — no model step, replacing the
|
|
381
|
+
# measured 0.27% model-carried capture rate for future dispatches. Silent
|
|
382
|
+
# on every path (never writes to stdout); exit 0 always, including a
|
|
383
|
+
# malformed envelope or a write failure. fail_closed: false.
|
|
384
|
+
orchestration-record:
|
|
385
|
+
script: src/scripts/hooks/orchestration_record_hook.ts
|
|
386
|
+
args: []
|
|
387
|
+
fail_closed: false
|
|
388
|
+
severity: advisory
|
|
389
|
+
tools: [Agent, Task]
|
|
390
|
+
# road-to-orchestrator-discipline-carriers Phase 4 (F3-lite) — conditional
|
|
391
|
+
# delegation nudge. Extracts cheap structural signals from the SUBMITTED
|
|
392
|
+
# prompt (enumerated file lists, "for each"/"alle …" shapes, explicit slice
|
|
393
|
+
# counts, ordered-plan markers, multi-deliverable conjunctions), classifies
|
|
394
|
+
# via `classifyTask`, and — only on a positive do-in-parallel/do-in-steps
|
|
395
|
+
# verdict — reports `{decision:"warn", reason, additional_context}` at exit
|
|
396
|
+
# 2, mirroring `language-mirror`'s shipped delivery pattern so the verdict
|
|
397
|
+
# line actually reaches `hookSpecificOutput.additionalContext` on the
|
|
398
|
+
# verified `claude` platform. Silence is the anti-canary default: no
|
|
399
|
+
# enumerated signal → no injection at all. Never blocks the turn — see the
|
|
400
|
+
# hook's file header for the `host_semantics.emitFor` proof. fail_closed:
|
|
401
|
+
# false.
|
|
402
|
+
delegation-nudge:
|
|
403
|
+
script: src/scripts/hooks/delegation_nudge_hook.ts
|
|
404
|
+
args: []
|
|
405
|
+
fail_closed: false
|
|
406
|
+
severity: advisory
|
|
407
|
+
# road-to-orchestrator-discipline-carriers Phase 5 (F4-lite) — end-review
|
|
408
|
+
# nudge. Fires ONLY when the session mutated tracked+untracked, non-doc
|
|
409
|
+
# files beyond MUTATION_LINE_THRESHOLD (50 lines; untracked files added
|
|
410
|
+
# post-review, F6) AND no reviewer ran this session (scanned from the
|
|
411
|
+
# Stop payload's `transcript_path`) AND this session has not already
|
|
412
|
+
# fired once (F2 — a once-per-session gate; state under
|
|
413
|
+
# agents/runtime/state/end-review-nudge/, gitignored). On fire, reports
|
|
414
|
+
# `{decision:"warn", reason, additional_context}` at exit 2 (same delivery
|
|
415
|
+
# pattern as `language-mirror`/`delegation-nudge` above) and appends one
|
|
416
|
+
# `review_skipped` telemetry line — hook-carried, not model-carried — so a
|
|
417
|
+
# future blocking decision has a measured distribution to cite. Never
|
|
418
|
+
# blocks the turn — see the hook's file header for the `host_semantics.
|
|
419
|
+
# emitFor` proof. fail_closed: false.
|
|
420
|
+
end-review-nudge:
|
|
421
|
+
script: src/scripts/hooks/end_review_nudge_hook.ts
|
|
422
|
+
args: []
|
|
423
|
+
fail_closed: false
|
|
424
|
+
severity: advisory
|
|
354
425
|
|
|
355
426
|
platforms:
|
|
356
427
|
augment:
|
|
357
|
-
session_start: [chat-history, hot-context, handoff-context, session-canary, first-run-gate, onboarding-gate, verify-before-complete, minimal-safe-diff, profile-staleness, wrapper-freshness, surface-probe, session-register]
|
|
428
|
+
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]
|
|
358
429
|
session_end: [chat-history, memory-learn, session-register]
|
|
359
|
-
stop: [chat-history, hot-context, verify-before-complete, session-register]
|
|
430
|
+
stop: [chat-history, hot-context, verify-before-complete, self-repair, session-register]
|
|
360
431
|
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]
|
|
361
|
-
post_tool_use: [chat-history, roadmap-progress, context-hygiene, verify-before-complete, minimal-safe-diff, injection-scan, pr-url-reminder]
|
|
432
|
+
post_tool_use: [chat-history, roadmap-progress, context-hygiene, verify-before-complete, minimal-safe-diff, injection-scan, pr-url-reminder, orchestration-record]
|
|
362
433
|
|
|
363
434
|
claude:
|
|
364
|
-
session_start: [chat-history, hot-context, handoff-context, session-canary, first-run-gate, onboarding-gate, verify-before-complete, minimal-safe-diff, profile-staleness, wrapper-freshness, surface-probe, session-register]
|
|
435
|
+
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]
|
|
365
436
|
session_end: [chat-history, memory-learn, session-register]
|
|
366
|
-
stop: [chat-history, hot-context, verify-before-complete, team-review-gate, session-register]
|
|
367
|
-
user_prompt_submit: [chat-history, verify-before-complete, minimal-safe-diff, language-mirror, git-authorization, session-canary, session-register]
|
|
437
|
+
stop: [chat-history, hot-context, verify-before-complete, team-review-gate, end-review-nudge, self-repair, session-register]
|
|
438
|
+
user_prompt_submit: [chat-history, verify-before-complete, minimal-safe-diff, language-mirror, delegation-nudge, git-authorization, session-canary, self-repair, session-register]
|
|
368
439
|
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]
|
|
369
|
-
post_tool_use: [chat-history, roadmap-progress, context-hygiene, verify-before-complete, minimal-safe-diff, injection-scan, pr-url-reminder]
|
|
440
|
+
post_tool_use: [chat-history, roadmap-progress, context-hygiene, verify-before-complete, minimal-safe-diff, injection-scan, pr-url-reminder, orchestration-record]
|
|
370
441
|
|
|
371
442
|
# Cowork — the Claude desktop app's local-agent-mode runtime, built
|
|
372
443
|
# on top of the Claude Code CLI. Same lifecycle vocabulary, same
|
|
@@ -383,12 +454,22 @@ platforms:
|
|
|
383
454
|
# Decision matrix + upstream blockers tracked in
|
|
384
455
|
# agents/settings/contexts/chat-history-platform-hooks.md § Cowork.
|
|
385
456
|
cowork:
|
|
386
|
-
session_start: [chat-history, hot-context, handoff-context, session-canary, first-run-gate, onboarding-gate, verify-before-complete, minimal-safe-diff, profile-staleness, wrapper-freshness, surface-probe, session-register]
|
|
457
|
+
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]
|
|
387
458
|
session_end: [chat-history, memory-learn, session-register]
|
|
388
|
-
|
|
389
|
-
|
|
459
|
+
# end-review-nudge and delegation-nudge are DELIBERATELY NOT bound on
|
|
460
|
+
# this platform (F10, review) — cowork-dispatcher.sh's own header states
|
|
461
|
+
# it "always exit[s] 0" and its dispatch line discards the dispatcher's
|
|
462
|
+
# exit code AND stdout unconditionally (`>/dev/null 2>&1 || true`). Both
|
|
463
|
+
# concerns' delivery depends entirely on `additional_context` reaching
|
|
464
|
+
# the model via stdout at the verified exit-2 warn path — a delivery this
|
|
465
|
+
# trampoline makes structurally impossible regardless of which platform
|
|
466
|
+
# is later verified to forward it. Binding them here would be dead
|
|
467
|
+
# weight: the concern still runs (git diff, transcript scan, classifier),
|
|
468
|
+
# still costs the dispatch, and its entire output is thrown away.
|
|
469
|
+
stop: [chat-history, hot-context, verify-before-complete, self-repair, session-register]
|
|
470
|
+
user_prompt_submit: [chat-history, verify-before-complete, minimal-safe-diff, language-mirror, git-authorization, session-canary, self-repair, session-register]
|
|
390
471
|
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]
|
|
391
|
-
post_tool_use: [chat-history, roadmap-progress, context-hygiene, verify-before-complete, minimal-safe-diff, injection-scan, pr-url-reminder]
|
|
472
|
+
post_tool_use: [chat-history, roadmap-progress, context-hygiene, verify-before-complete, minimal-safe-diff, injection-scan, pr-url-reminder, orchestration-record]
|
|
392
473
|
|
|
393
474
|
# Phase 7.5 — Cursor. `.cursor/hooks.json` (project) is read by the
|
|
394
475
|
# IDE and CLI; `~/.cursor/hooks.json` (user) is opt-in via
|
|
@@ -398,11 +479,11 @@ platforms:
|
|
|
398
479
|
# IDE-only — CLI-only users fall back to /checkpoint per
|
|
399
480
|
# agents/settings/contexts/chat-history-platform-hooks.md.
|
|
400
481
|
cursor:
|
|
401
|
-
session_start: [chat-history, hot-context, handoff-context, session-canary, first-run-gate, onboarding-gate, verify-before-complete, minimal-safe-diff, profile-staleness, wrapper-freshness, surface-probe, session-register]
|
|
482
|
+
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]
|
|
402
483
|
session_end: [chat-history, memory-learn, session-register]
|
|
403
|
-
stop: [chat-history, hot-context, verify-before-complete, session-register]
|
|
404
|
-
user_prompt_submit: [chat-history, verify-before-complete, minimal-safe-diff, language-mirror, git-authorization, session-canary, session-register]
|
|
405
|
-
post_tool_use: [chat-history, roadmap-progress, context-hygiene, verify-before-complete, minimal-safe-diff, injection-scan, pr-url-reminder]
|
|
484
|
+
stop: [chat-history, hot-context, verify-before-complete, self-repair, session-register]
|
|
485
|
+
user_prompt_submit: [chat-history, verify-before-complete, minimal-safe-diff, language-mirror, git-authorization, session-canary, self-repair, session-register]
|
|
486
|
+
post_tool_use: [chat-history, roadmap-progress, context-hygiene, verify-before-complete, minimal-safe-diff, injection-scan, pr-url-reminder, orchestration-record]
|
|
406
487
|
|
|
407
488
|
# Phase 7.6 — Cline. Hooks live under `.clinerules/hooks/<HookName>`
|
|
408
489
|
# (project, no file extension, must be executable per Cline docs) or
|
|
@@ -413,11 +494,11 @@ platforms:
|
|
|
413
494
|
# both map to session_start. TaskCancel maps to stop because the
|
|
414
495
|
# session is interrupted with partial state (mirrors Augment Stop).
|
|
415
496
|
cline:
|
|
416
|
-
session_start: [chat-history, hot-context, handoff-context, session-canary, first-run-gate, onboarding-gate, verify-before-complete, minimal-safe-diff, profile-staleness, wrapper-freshness, surface-probe, session-register]
|
|
497
|
+
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]
|
|
417
498
|
session_end: [chat-history, memory-learn, session-register]
|
|
418
|
-
stop: [chat-history, hot-context, verify-before-complete, session-register]
|
|
419
|
-
user_prompt_submit: [chat-history, verify-before-complete, minimal-safe-diff, language-mirror, git-authorization, session-canary, session-register]
|
|
420
|
-
post_tool_use: [chat-history, roadmap-progress, context-hygiene, verify-before-complete, minimal-safe-diff, injection-scan, pr-url-reminder]
|
|
499
|
+
stop: [chat-history, hot-context, verify-before-complete, self-repair, session-register]
|
|
500
|
+
user_prompt_submit: [chat-history, verify-before-complete, minimal-safe-diff, language-mirror, git-authorization, session-canary, self-repair, session-register]
|
|
501
|
+
post_tool_use: [chat-history, roadmap-progress, context-hygiene, verify-before-complete, minimal-safe-diff, injection-scan, pr-url-reminder, orchestration-record]
|
|
421
502
|
|
|
422
503
|
# Phase 7.7 — Windsurf (Cascade). Hooks live at `.windsurf/hooks.json`
|
|
423
504
|
# (project) or `~/.codeium/windsurf/hooks.json` (user). Cascade has
|
|
@@ -432,11 +513,11 @@ platforms:
|
|
|
432
513
|
# surface to record verification commands; documented limitation).
|
|
433
514
|
# minimal-safe-diff is omitted entirely on Windsurf for the same reason.
|
|
434
515
|
windsurf:
|
|
435
|
-
session_start: [chat-history, hot-context, handoff-context, session-canary, first-run-gate, onboarding-gate, verify-before-complete, profile-staleness, wrapper-freshness, surface-probe, session-register]
|
|
436
|
-
stop: [chat-history, hot-context, verify-before-complete, session-register]
|
|
516
|
+
session_start: [chat-history, hot-context, handoff-context, session-canary, council-availability, first-run-gate, onboarding-gate, verify-before-complete, profile-staleness, wrapper-freshness, surface-probe, session-register]
|
|
517
|
+
stop: [chat-history, hot-context, verify-before-complete, self-repair, session-register]
|
|
437
518
|
# language-mirror IS bound here: unlike minimal-safe-diff it needs only the
|
|
438
519
|
# user_prompt_submit slot, which Windsurf has.
|
|
439
|
-
user_prompt_submit: [chat-history, verify-before-complete, language-mirror, git-authorization, session-canary, session-register]
|
|
520
|
+
user_prompt_submit: [chat-history, verify-before-complete, language-mirror, git-authorization, session-canary, self-repair, session-register]
|
|
440
521
|
|
|
441
522
|
# Phase 7.8 — Gemini CLI. Hooks live at `.gemini/settings.json`
|
|
442
523
|
# (project) or `~/.gemini/settings.json` (user). Per Gemini docs
|
|
@@ -449,11 +530,11 @@ platforms:
|
|
|
449
530
|
# turn-check semantics. AfterAgent fires when the agent loop ends
|
|
450
531
|
# — this is our `stop` slot.
|
|
451
532
|
gemini:
|
|
452
|
-
session_start: [chat-history, hot-context, handoff-context, session-canary, first-run-gate, onboarding-gate, verify-before-complete, minimal-safe-diff, profile-staleness, wrapper-freshness, surface-probe, session-register]
|
|
533
|
+
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]
|
|
453
534
|
session_end: [chat-history, memory-learn, session-register]
|
|
454
|
-
stop: [chat-history, hot-context, verify-before-complete, session-register]
|
|
455
|
-
user_prompt_submit: [chat-history, verify-before-complete, minimal-safe-diff, language-mirror, git-authorization, session-canary, session-register]
|
|
456
|
-
post_tool_use: [chat-history, roadmap-progress, context-hygiene, verify-before-complete, minimal-safe-diff, injection-scan, pr-url-reminder]
|
|
535
|
+
stop: [chat-history, hot-context, verify-before-complete, self-repair, session-register]
|
|
536
|
+
user_prompt_submit: [chat-history, verify-before-complete, minimal-safe-diff, language-mirror, git-authorization, session-canary, self-repair, session-register]
|
|
537
|
+
post_tool_use: [chat-history, roadmap-progress, context-hygiene, verify-before-complete, minimal-safe-diff, injection-scan, pr-url-reminder, orchestration-record]
|
|
457
538
|
|
|
458
539
|
# Phase 7.9 — Copilot has no hook surface. Concerns route through
|
|
459
540
|
# rule-only fallback; the dispatcher silently no-ops on --platform copilot.
|
|
@@ -20,12 +20,21 @@
|
|
|
20
20
|
* 2 — warn (not used by this guard)
|
|
21
21
|
*
|
|
22
22
|
* No ALLOW_NO_VERIFY-style env bypass is provided. See src/rules/git-history-discipline.md
|
|
23
|
+
*
|
|
24
|
+
* WHAT THIS GUARD DOES NOT SEE. Command substitutions ARE now checked — see
|
|
25
|
+
* `_check_command` for why they were not, and why the well-formed case was the
|
|
26
|
+
* unprotected one. What remains uncovered is an invocation whose command word
|
|
27
|
+
* only exists at runtime (`P=commit; git $P --no-verify` via indirection, or a
|
|
28
|
+
* word composed by `xargs`). Those execute under bash and are not classified
|
|
29
|
+
* here. Stated rather than left implied: this file's own round-5 defect was a
|
|
30
|
+
* docstring that described coverage it did not have.
|
|
23
31
|
*/
|
|
24
32
|
|
|
25
33
|
import * as fs from 'node:fs';
|
|
26
34
|
import { fileURLToPath, pathToFileURL } from 'node:url';
|
|
27
35
|
import * as path from 'node:path';
|
|
28
36
|
import { readHookStdin } from './hook_stdin.js';
|
|
37
|
+
import { substitutionPayloads } from './block_unauthorized_git.js';
|
|
29
38
|
|
|
30
39
|
const _HERE = fileURLToPath(import.meta.url);
|
|
31
40
|
|
|
@@ -384,8 +393,36 @@ export function _looks_like_git_invocation(cmd: string, depth = 0): boolean {
|
|
|
384
393
|
return false;
|
|
385
394
|
}
|
|
386
395
|
|
|
387
|
-
/**
|
|
388
|
-
|
|
396
|
+
/**
|
|
397
|
+
* Return (blocked, reason). Fail-closed on parse error for git commands.
|
|
398
|
+
*
|
|
399
|
+
* COMMAND SUBSTITUTIONS ARE CHECKED SEPARATELY, and the reason they were not is
|
|
400
|
+
* the shape of the hole: this file does split on `$(` and backtick, but only
|
|
401
|
+
* inside `_looks_like_git_invocation`, which runs exclusively in the
|
|
402
|
+
* shlex-failure branch. A well-formed `echo "$(git commit --no-verify)"` parses
|
|
403
|
+
* cleanly, so shlex succeeds, the substitution stays inside one token, and
|
|
404
|
+
* `_git_base` never sees git. The more parseable the command, the less it was
|
|
405
|
+
* protected. Classification inside the payload is by command position, so a
|
|
406
|
+
* substitution that INVOKES git is checked and one that merely names it in an
|
|
407
|
+
* argument is not — matching what `block_unauthorized_git` does one level up.
|
|
408
|
+
*
|
|
409
|
+
* Heredoc bodies are removed before extraction: a backtick or `$(` inside a
|
|
410
|
+
* commit message being written to a file is data, and feeding it back through
|
|
411
|
+
* this function is exactly the false positive the round-5 repair closed.
|
|
412
|
+
*/
|
|
413
|
+
function _check_command(cmd: string, depth = 0): [boolean, string] {
|
|
414
|
+
if (depth <= 3) {
|
|
415
|
+
const withoutHeredocs = cmd.replace(
|
|
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)) {
|
|
420
|
+
const [blocked, reason] = _check_command(payload, depth + 1);
|
|
421
|
+
if (blocked) {
|
|
422
|
+
return [true, reason];
|
|
423
|
+
}
|
|
424
|
+
}
|
|
425
|
+
}
|
|
389
426
|
let tokens: string[];
|
|
390
427
|
try {
|
|
391
428
|
tokens = shlexSplit(cmd);
|