@event4u/agent-config 9.17.0 → 9.18.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.
Files changed (128) hide show
  1. package/.claude-plugin/marketplace.json +1 -1
  2. package/CHANGELOG.md +79 -182
  3. package/dist/agent-src/commands/challenge-me/vision.md +5 -0
  4. package/dist/agent-src/commands/evals/rule-compliance-audit.json +46 -0
  5. package/dist/agent-src/commands/feature/plan.md +20 -0
  6. package/dist/agent-src/commands/feature/roadmap.md +19 -0
  7. package/dist/agent-src/commands/pr/create.md +29 -0
  8. package/dist/agent-src/commands/roadmap/create.md +29 -0
  9. package/dist/agent-src/commands/rule-compliance-audit.md +28 -5
  10. package/dist/agent-src/contexts/execution/plan-confidence-gate.md +133 -0
  11. package/dist/agent-src/rules/active-remediation.md +2 -0
  12. package/dist/agent-src/rules/architecture.md +2 -0
  13. package/dist/agent-src/rules/augment-edit-discipline.md +2 -0
  14. package/dist/agent-src/rules/brand-consistency.md +10 -62
  15. package/dist/agent-src/rules/brand-source-of-truth.md +62 -11
  16. package/dist/agent-src/rules/broken-access-control.md +4 -0
  17. package/dist/agent-src/rules/cli-output-handling.md +5 -0
  18. package/dist/agent-src/rules/code-comment-discipline.md +3 -0
  19. package/dist/agent-src/rules/commit-conventions.md +2 -0
  20. package/dist/agent-src/rules/cross-source-consistency.md +3 -0
  21. package/dist/agent-src/rules/design-fidelity.md +2 -0
  22. package/dist/agent-src/rules/doc-screenshot-hygiene.md +2 -0
  23. package/dist/agent-src/rules/docker-commands.md +4 -0
  24. package/dist/agent-src/rules/domain-adoption-policy.md +2 -0
  25. package/dist/agent-src/rules/domain-safety-disclaimer.md +5 -2
  26. package/dist/agent-src/rules/engineering-safety-floor.md +2 -0
  27. package/dist/agent-src/rules/framework-neutrality-in-generic-skills.md +9 -0
  28. package/dist/agent-src/rules/image-likeness-and-rights.md +4 -0
  29. package/dist/agent-src/rules/improve-before-implement.md +4 -0
  30. package/dist/agent-src/rules/laravel-routing.md +4 -0
  31. package/dist/agent-src/rules/legal-safety-floor.md +3 -0
  32. package/dist/agent-src/rules/lethal-trifecta-guard.md +3 -0
  33. package/dist/agent-src/rules/media-governance-routing.md +5 -0
  34. package/dist/agent-src/rules/minimal-safe-diff.md +2 -0
  35. package/dist/agent-src/rules/no-roadmap-references.md +2 -0
  36. package/dist/agent-src/rules/notes-first-reasoning.md +2 -0
  37. package/dist/agent-src/rules/php-coding.md +2 -0
  38. package/dist/agent-src/rules/prefer-enums-over-literals.md +3 -0
  39. package/dist/agent-src/rules/provider-lifecycle-discipline.md +5 -0
  40. package/dist/agent-src/rules/roadmap-ci-steps-policy.md +2 -0
  41. package/dist/agent-src/rules/roadmap-progress-sync.md +2 -0
  42. package/dist/agent-src/rules/rule-type-governance.md +2 -0
  43. package/dist/agent-src/rules/scale-discipline.md +3 -0
  44. package/dist/agent-src/rules/secret-vcs-guard.md +2 -0
  45. package/dist/agent-src/rules/security-sensitive-stop.md +9 -2
  46. package/dist/agent-src/rules/senior-engineering-discipline.md +6 -0
  47. package/dist/agent-src/rules/skill-quality.md +2 -0
  48. package/dist/agent-src/rules/source-confidentiality.md +4 -0
  49. package/dist/agent-src/rules/source-discovery-gate.md +3 -0
  50. package/dist/agent-src/rules/source-of-truth.md +2 -0
  51. package/dist/agent-src/rules/strategy-safety-floor.md +2 -0
  52. package/dist/agent-src/rules/think-before-action.md +5 -0
  53. package/dist/agent-src/rules/ui-audit-gate.md +2 -0
  54. package/dist/agent-src/skills/design-intelligence/SKILL.md +6 -180
  55. package/dist/agent-src/skills/design-intelligence/references/context-and-registers.md +100 -0
  56. package/dist/agent-src/skills/design-intelligence/references/integration-mapping.md +91 -0
  57. package/dist/agent-src/skills/design-review/SKILL.md +4 -141
  58. package/dist/agent-src/skills/design-review/references/review-communication.md +92 -0
  59. package/dist/agent-src/skills/design-review/references/verification-automation.md +61 -0
  60. package/dist/agent-src/skills/existing-ui-audit/SKILL.md +8 -27
  61. package/dist/agent-src/skills/existing-ui-audit/references/anti-slop-cross-reference.md +31 -0
  62. package/dist/agent-src/skills/fe-design/SKILL.md +4 -289
  63. package/dist/agent-src/skills/fe-design/references/design-patterns.md +227 -0
  64. package/dist/agent-src/skills/fe-design/references/design-read-and-memory.md +75 -0
  65. package/dist/agent-src/skills/roadmap-management/SKILL.md +10 -0
  66. package/dist/agent-src/skills/roadmap-writing/SKILL.md +12 -1
  67. package/dist/agent-src/templates/agents/agent-project-settings.example.yml +1 -1
  68. package/dist/agent-src/templates/roadmaps.md +12 -0
  69. package/dist/agent-src/templates/scripts/work_engine/directives/ui/_fix_lane.ts +52 -0
  70. package/dist/agent-src/templates/scripts/work_engine/directives/ui/audit.ts +8 -0
  71. package/dist/agent-src/templates/scripts/work_engine/directives/ui/design.ts +6 -0
  72. package/dist/agent-src/templates/scripts/work_engine/intent/classify.ts +56 -4
  73. package/dist/cli/registry.js +2 -0
  74. package/dist/cli/registry.js.map +1 -1
  75. package/dist/cli-delegate/chunk-AIIUU73N.js +2040 -0
  76. package/dist/cli-delegate/{chunk-IMSXPRVH.js → chunk-ELMFOQPW.js} +11 -0
  77. package/dist/cli-delegate/chunk-LF2KKJ4Q.js +183 -0
  78. package/dist/cli-delegate/cmd_explain.js +36 -36
  79. package/dist/cli-delegate/cmd_export.js +1 -1
  80. package/dist/cli-delegate/cmd_handoff.js +203 -2228
  81. package/dist/cli-delegate/cmd_prune.js +1 -1
  82. package/dist/cli-delegate/cmd_route_audit.js +249 -0
  83. package/dist/cli-delegate/cmd_route_explain.js +285 -0
  84. package/dist/cli-delegate/cmd_settings_migrate.js +1 -1
  85. package/dist/cli-delegate/cmd_sync.js +1 -1
  86. package/dist/cli-delegate/cmd_uninstall.js +1 -1
  87. package/dist/cli-delegate/cmd_validate.js +1 -1
  88. package/dist/discovery/deprecation-report.md +1 -1
  89. package/dist/discovery/discovery-manifest.json +57 -57
  90. package/dist/discovery/discovery-manifest.json.sha256 +1 -1
  91. package/dist/discovery/discovery-manifest.summary.md +1 -1
  92. package/dist/discovery/orphan-report.md +1 -1
  93. package/dist/discovery/packs.json +2 -2
  94. package/dist/discovery/trust-report.md +1 -1
  95. package/dist/discovery/workspaces.json +2 -2
  96. package/dist/install/install.mjs +11 -0
  97. package/dist/mcp/registry-manifest.json +1 -1
  98. package/dist/mcp/server.json +2 -2
  99. package/dist/router.json +1 -1
  100. package/dist/server/schemas/settings.js +12 -0
  101. package/dist/server/schemas/settings.js.map +1 -1
  102. package/package.json +1 -1
  103. package/src/agent-src/templates/scripts/work_engine/directives/ui/_fix_lane.ts +52 -0
  104. package/src/agent-src/templates/scripts/work_engine/directives/ui/audit.ts +8 -0
  105. package/src/agent-src/templates/scripts/work_engine/directives/ui/design.ts +6 -0
  106. package/src/agent-src/templates/scripts/work_engine/intent/classify.ts +56 -4
  107. package/src/config/agent-settings.template.yml +46 -0
  108. package/src/config/evaluator-budgets.json +3 -3
  109. package/src/config/gate-coverage.yml +40 -0
  110. package/src/scripts/_cli/cmd_explain.ts +41 -36
  111. package/src/scripts/_cli/cmd_route_audit.ts +290 -0
  112. package/src/scripts/_cli/cmd_route_explain.ts +269 -0
  113. package/src/scripts/_dispatch.bash +23 -0
  114. package/src/scripts/_lib/git_env.ts +40 -0
  115. package/src/scripts/_lib/md_table.ts +27 -0
  116. package/src/scripts/_lib/planning_settings.ts +43 -0
  117. package/src/scripts/_lib/router_match.ts +301 -0
  118. package/src/scripts/annotate_r1_outcomes.ts +258 -0
  119. package/src/scripts/check_completion_review.ts +1140 -0
  120. package/src/scripts/compile_router.ts +20 -3
  121. package/src/scripts/dispatch_r2_reviewer.ts +968 -0
  122. package/src/scripts/eval_ui_triviality.ts +156 -0
  123. package/src/scripts/lint_abstraction_thresholds.ts +3 -1
  124. package/src/scripts/lint_plan_risk_register.ts +931 -0
  125. package/src/scripts/lint_trigger_collisions.ts +257 -0
  126. package/src/scripts/router_telemetry.ts +16 -290
  127. package/src/scripts/schemas/rule.schema.json +10 -0
  128. package/src/server/schemas/settings.ts +18 -0
@@ -0,0 +1,269 @@
1
+ #!/usr/bin/env tsx
2
+ /**
3
+ * `agent-config route:explain "<prompt>" [--files a,b]` — deterministic,
4
+ * offline trace of which rules the router's triggers match for one prompt.
5
+ *
6
+ * The validation surface the routing-correctness mandate asks for: matched
7
+ * triggers, tier, injected-vs-pointer disposition, budget consumption, and
8
+ * rejected candidates — all from `dist/router.json` + the ONE shared matcher
9
+ * (`_lib/router_match.ts`, parity-by-construction with `router_telemetry`
10
+ * and `explain route`).
11
+ *
12
+ * Measurement level (ADR-126): this shows TRIGGER MATCHING only. Whether the
13
+ * host actually invokes a matched rule is a different measurement and is NOT
14
+ * made here — the mandatory first output line says so on every run.
15
+ *
16
+ * Exit codes: `0` at least one non-kernel rule matched, `1` nothing matched
17
+ * (kernel is always active, so "no match" still leaves the kernel), `2`
18
+ * invocation error (missing prompt, unreadable router.json).
19
+ */
20
+
21
+ import * as fs from 'node:fs';
22
+ import * as path from 'node:path';
23
+ import { fileURLToPath, pathToFileURL } from 'node:url';
24
+
25
+ import {
26
+ match_prompt,
27
+ _asRuleList,
28
+ type Router,
29
+ type Rule,
30
+ type Trigger,
31
+ } from '../_lib/router_match.js';
32
+ import { measure, method_note } from '../_lib/token_count.js';
33
+ import { load_agent_settings } from '../_lib/agent_settings.js';
34
+
35
+ const REPO_ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..', '..', '..');
36
+ const ROUTER_JSON = path.join(REPO_ROOT, 'dist', 'router.json');
37
+ const RULES_DIST_DIR = path.join(REPO_ROOT, 'dist', 'agent-src', 'rules');
38
+
39
+ export const MEASUREMENT_HEADER =
40
+ 'Measurement level: trigger matching only — what the host actually invokes is NOT measured here (ADR-126).';
41
+
42
+ interface RuleReport {
43
+ id: string;
44
+ tier: string;
45
+ matched: string[]; // "kind: value" labels
46
+ routes_to: string[];
47
+ disposition: string;
48
+ body_chars: number;
49
+ body_tokens_gpt: number;
50
+ }
51
+
52
+ interface Report {
53
+ measurement_level: string;
54
+ prompt: string;
55
+ files: string[];
56
+ profile: string;
57
+ projection_mode: string;
58
+ kernel_always: string[];
59
+ matches: RuleReport[];
60
+ rejected: { reason: string; ids: string[] }[];
61
+ budget: { matched_chars: number; matched_tokens_gpt: number; method: string };
62
+ }
63
+
64
+ function _trigger_label(trigger: Trigger): string {
65
+ for (const kind of ['keyword', 'phrase', 'command', 'path_prefix', 'file_pattern']) {
66
+ if (kind in trigger) {
67
+ return `${kind}: ${String(trigger[kind])}`;
68
+ }
69
+ }
70
+ return `trigger: ${JSON.stringify(trigger)}`;
71
+ }
72
+
73
+ /** Projection mode from settings; the shipped default is eager-all. */
74
+ export function _projection_mode(): string {
75
+ try {
76
+ const settings = load_agent_settings({ cwd: REPO_ROOT }) as Record<string, unknown>;
77
+ const lp = settings['lean_projection'];
78
+ if (lp !== null && typeof lp === 'object' && !Array.isArray(lp)) {
79
+ const mode = (lp as Record<string, unknown>)['mode'];
80
+ if (typeof mode === 'string' && mode.trim() !== '') {
81
+ return mode.trim();
82
+ }
83
+ }
84
+ } catch {
85
+ // settings unreadable → shipped default
86
+ }
87
+ return 'eager-all';
88
+ }
89
+
90
+ function _disposition(tier: string, projection_mode: string): string {
91
+ if (tier === 'kernel') {
92
+ return 'injected (kernel, always full-bodied)';
93
+ }
94
+ // Disposition is decided at projection time, not at runtime — say so
95
+ // instead of inventing a runtime injection the router does not perform.
96
+ return projection_mode === 'eager-all'
97
+ ? `injected at projection time (${tier}, lean_projection.mode: eager-all)`
98
+ : `router-resolved pointer at projection time (${tier}, lean_projection.mode: ${projection_mode})`;
99
+ }
100
+
101
+ function _rule_body_size(rule_id: string): { chars: number; tokens: number } {
102
+ const p = path.join(RULES_DIST_DIR, `${rule_id}.md`);
103
+ try {
104
+ const text = fs.readFileSync(p, 'utf-8');
105
+ const m = measure(text);
106
+ return { chars: m.chars, tokens: m.tokens_gpt };
107
+ } catch {
108
+ return { chars: 0, tokens: 0 };
109
+ }
110
+ }
111
+
112
+ export function build_report(
113
+ router: Router,
114
+ prompt: string,
115
+ files: string[],
116
+ profile: string,
117
+ ): Report {
118
+ const projection_mode = _projection_mode();
119
+ const result = match_prompt(router, prompt, profile, files.length > 0 ? files : null, null);
120
+
121
+ const matchedByRule = new Map<string, { tier: string; labels: string[] }>();
122
+ for (const mt of result.matched_triggers) {
123
+ const id = String(mt.rule);
124
+ const entry = matchedByRule.get(id) ?? { tier: mt.tier.replace('_', '-'), labels: [] };
125
+ entry.labels.push(_trigger_label(mt.trigger));
126
+ matchedByRule.set(id, entry);
127
+ }
128
+
129
+ const routesById = new Map<string, string[]>();
130
+ for (const tierKey of ['tier_1', 'tier_2'] as const) {
131
+ for (const rule of _asRuleList(router[tierKey])) {
132
+ routesById.set(String(rule['id']), ((rule['routes_to'] as unknown[]) ?? []).map(String));
133
+ }
134
+ }
135
+
136
+ const matches: RuleReport[] = [...matchedByRule.entries()]
137
+ .sort(([a], [b]) => (a < b ? -1 : a > b ? 1 : 0))
138
+ .map(([id, { tier, labels }]) => {
139
+ const size = _rule_body_size(id);
140
+ return {
141
+ id,
142
+ tier,
143
+ matched: labels,
144
+ routes_to: routesById.get(id) ?? [],
145
+ disposition: _disposition(tier, projection_mode),
146
+ body_chars: size.chars,
147
+ body_tokens_gpt: size.tokens,
148
+ };
149
+ });
150
+
151
+ const rejected_no_match: string[] = [];
152
+ for (const tierKey of ['tier_1', 'tier_2'] as const) {
153
+ if (tierKey === 'tier_2' && profile !== 'full') continue;
154
+ for (const rule of _asRuleList(router[tierKey])) {
155
+ const id = String(rule['id']);
156
+ if (!matchedByRule.has(id)) rejected_no_match.push(id);
157
+ }
158
+ }
159
+ rejected_no_match.sort();
160
+ const rejected: Report['rejected'] = [
161
+ { reason: 'no trigger matched', ids: rejected_no_match },
162
+ ];
163
+ if (profile !== 'full') {
164
+ rejected.push({
165
+ reason: `tier-2 excluded by profile '${profile}' (tier-2 is considered under profile 'full' only)`,
166
+ ids: _asRuleList(router['tier_2']).map((r: Rule) => String(r['id'])).sort(),
167
+ });
168
+ }
169
+
170
+ const kernel_always = (Array.isArray(router['kernel']) ? router['kernel'] : []).map(String);
171
+ const matched_chars = matches.reduce((s, m) => s + m.body_chars, 0);
172
+ const matched_tokens = matches.reduce((s, m) => s + m.body_tokens_gpt, 0);
173
+
174
+ return {
175
+ measurement_level: MEASUREMENT_HEADER,
176
+ prompt,
177
+ files,
178
+ profile,
179
+ projection_mode,
180
+ kernel_always,
181
+ matches,
182
+ rejected,
183
+ budget: { matched_chars, matched_tokens_gpt: matched_tokens, method: method_note() },
184
+ };
185
+ }
186
+
187
+ export function render_text(report: Report): string {
188
+ const out: string[] = [];
189
+ out.push(report.measurement_level);
190
+ out.push('');
191
+ out.push(`prompt: ${JSON.stringify(report.prompt)}`);
192
+ if (report.files.length > 0) out.push(`files: ${report.files.join(', ')}`);
193
+ out.push(`profile: ${report.profile} · projection mode: ${report.projection_mode}`);
194
+ out.push('');
195
+ out.push(`kernel (always active, ${report.kernel_always.length}): ${report.kernel_always.join(', ')}`);
196
+ out.push('');
197
+ out.push(`matched rules (${report.matches.length}):`);
198
+ for (const m of report.matches) {
199
+ out.push(` · ${m.id} [${m.tier}]`);
200
+ for (const label of m.matched) out.push(` matched ${label}`);
201
+ out.push(` disposition: ${m.disposition}`);
202
+ if (m.routes_to.length > 0) out.push(` routes_to: ${m.routes_to.join(', ')}`);
203
+ out.push(` body: ${m.body_chars} chars ≈ ${m.body_tokens_gpt} GPT tokens`);
204
+ }
205
+ if (report.matches.length === 0) {
206
+ out.push(' · (no trigger matched — only kernel rules active)');
207
+ }
208
+ out.push('');
209
+ out.push(
210
+ `budget consumption of matched non-kernel bodies: ${report.budget.matched_chars} chars`
211
+ + ` ≈ ${report.budget.matched_tokens_gpt} GPT tokens (${report.budget.method})`,
212
+ );
213
+ for (const r of report.rejected) {
214
+ out.push(`rejected — ${r.reason}: ${r.ids.length} rule(s)`);
215
+ }
216
+ return out.join('\n') + '\n';
217
+ }
218
+
219
+ export function main(argv: string[]): number {
220
+ const args = [...argv];
221
+ const as_json = args.includes('--json');
222
+ let files: string[] = [];
223
+ let profile = 'full';
224
+ const positional: string[] = [];
225
+ for (let i = 0; i < args.length; i++) {
226
+ const a = args[i] as string;
227
+ if (a === '--json') continue;
228
+ if (a === '--files') {
229
+ files = String(args[++i] ?? '').split(',').map((s) => s.trim()).filter(Boolean);
230
+ } else if (a.startsWith('--files=')) {
231
+ files = a.slice('--files='.length).split(',').map((s) => s.trim()).filter(Boolean);
232
+ } else if (a === '--profile') {
233
+ profile = String(args[++i] ?? 'full');
234
+ } else if (a.startsWith('--profile=')) {
235
+ profile = a.slice('--profile='.length);
236
+ } else if (a === '-h' || a === '--help') {
237
+ process.stdout.write(
238
+ 'usage: agent-config route:explain "<prompt>" [--files a,b] [--profile full|balanced] [--json]\n',
239
+ );
240
+ return 0;
241
+ } else {
242
+ positional.push(a);
243
+ }
244
+ }
245
+ const prompt = positional.join(' ').trim();
246
+ if (!prompt) {
247
+ process.stderr.write('route:explain: a prompt is required\n');
248
+ return 2;
249
+ }
250
+ let router: Router;
251
+ try {
252
+ router = JSON.parse(fs.readFileSync(ROUTER_JSON, 'utf-8')) as Router;
253
+ } catch (e) {
254
+ process.stderr.write(`route:explain: cannot read ${ROUTER_JSON}: ${String(e)}\n`);
255
+ return 2;
256
+ }
257
+ const report = build_report(router, prompt, files, profile);
258
+ if (as_json) {
259
+ process.stdout.write(JSON.stringify(report, null, 2) + '\n');
260
+ } else {
261
+ process.stdout.write(render_text(report));
262
+ }
263
+ return report.matches.length === 0 ? 1 : 0;
264
+ }
265
+
266
+ const _HERE = fileURLToPath(import.meta.url);
267
+ if (process.argv[1] && (import.meta.url === pathToFileURL(process.argv[1]).href || path.resolve(process.argv[1]) === _HERE)) {
268
+ process.exitCode = main(process.argv.slice(2));
269
+ }
@@ -208,6 +208,15 @@ Tier 2 — maintenance / internal (hooks, MCP, memory, telemetry):
208
208
  router + projection freshness, host bridge status.
209
209
  Flags: --platform <p>, --workspace <path>, --json, --strict,
210
210
  --no-freshness
211
+ route:explain Deterministic rule-routing trace for one prompt: matched
212
+ triggers, tier, injected-vs-pointer disposition, budget
213
+ consumption, rejected candidates. Trigger-match level only
214
+ (what the host invokes is not measured).
215
+ Flags: --files a,b, --profile full|balanced, --json
216
+ route:audit Replay the router matcher over the last N user prompts of
217
+ the chat-history log; renders matched rules per prompt.
218
+ Trigger-match level only; read-only, no LLM call.
219
+ Flags: --last N, --record (opt-in recorder), --weekly, --json
211
220
  hooks:replay Replay a fixture through the universal dispatcher with
212
221
  AGENT_CONFIG_REPLAY=1 (no writes under agents/runtime/state/).
213
222
  Usage: hooks:replay --platform <name> --event <event>
@@ -1171,6 +1180,18 @@ cmd_versions() {
1171
1180
 
1172
1181
  # `agent-config explain <config|rule|route>` — print the decision chain
1173
1182
  # behind a configuration or routing outcome. Read-only diagnostic; never
1183
+ # Router-matcher replay over recent chat-history prompts (read-only).
1184
+ # See scripts/_cli/cmd_route_audit.ts.
1185
+ cmd_route_audit() {
1186
+ exec_ts "$PACKAGE_ROOT/src/scripts/_cli/cmd_route_audit.ts" "$@"
1187
+ }
1188
+
1189
+ # Deterministic rule-routing trace (trigger-match measurement level only).
1190
+ # See scripts/_cli/cmd_route_explain.ts.
1191
+ cmd_route_explain() {
1192
+ exec_ts "$PACKAGE_ROOT/src/scripts/_cli/cmd_route_explain.ts" "$@"
1193
+ }
1194
+
1174
1195
  # edits state. See scripts/_cli/cmd_explain.ts.
1175
1196
  cmd_explain() {
1176
1197
  exec_ts "$PACKAGE_ROOT/src/scripts/_cli/cmd_explain.ts" "$@"
@@ -1235,6 +1256,8 @@ main() {
1235
1256
  hooks:status) cmd_hooks_status "$@" ;;
1236
1257
  hooks:doctor) cmd_hooks_doctor "$@" ;;
1237
1258
  routing:doctor) cmd_routing_doctor "$@" ;;
1259
+ route:explain) cmd_route_explain "$@" ;;
1260
+ route:audit) cmd_route_audit "$@" ;;
1238
1261
  hooks:replay) cmd_hooks_replay "$@" ;;
1239
1262
  reach:doctor) cmd_reach_doctor "$@" ;;
1240
1263
  telemetry:record) cmd_telemetry_record "$@" ;;
@@ -0,0 +1,40 @@
1
+ /**
2
+ * Environment for child `git` invocations that must resolve from their `cwd`.
3
+ *
4
+ * Git hooks export `GIT_DIR` (and often `GIT_INDEX_FILE`), and every child
5
+ * process inherits them. An inherited `GIT_DIR` **overrides** repository
6
+ * discovery, so `execFileSync('git', […], { cwd })` silently operates against
7
+ * the hook's repository instead of `cwd`. In a linked worktree the two differ,
8
+ * and the symptom is not an error but wrong data: `git log -- <path>` returns
9
+ * nothing, which the Gate R1 grandfather clause reads as "no pre-activation
10
+ * baseline" and reports as `missing_register` on every grandfathered roadmap.
11
+ *
12
+ * That made the pre-push enforcement layer fire spuriously while CI — which
13
+ * runs the same gate WITHOUT `GIT_DIR` — stayed green: the worst shape for a
14
+ * gate, since the failure looks like a content problem and the authoritative
15
+ * layer disagrees with the local one.
16
+ *
17
+ * Every gate in this family therefore runs git through this env: the variables
18
+ * that redirect discovery are removed, so `cwd` is the only thing that decides.
19
+ */
20
+
21
+ /** Git env vars that override repository discovery and must not be inherited. */
22
+ export const GIT_DISCOVERY_VARS: readonly string[] = [
23
+ 'GIT_DIR',
24
+ 'GIT_WORK_TREE',
25
+ 'GIT_INDEX_FILE',
26
+ 'GIT_OBJECT_DIRECTORY',
27
+ 'GIT_ALTERNATE_OBJECT_DIRECTORIES',
28
+ 'GIT_COMMON_DIR',
29
+ 'GIT_NAMESPACE',
30
+ 'GIT_PREFIX',
31
+ ];
32
+
33
+ /** `process.env` minus the discovery-overriding variables. */
34
+ export function gitEnv(base: NodeJS.ProcessEnv = process.env): NodeJS.ProcessEnv {
35
+ const env: NodeJS.ProcessEnv = { ...base };
36
+ for (const key of GIT_DISCOVERY_VARS) {
37
+ delete env[key];
38
+ }
39
+ return env;
40
+ }
@@ -0,0 +1,27 @@
1
+ /**
2
+ * Markdown table-row splitting, escape-aware.
3
+ *
4
+ * Both plan-governance validators parse pipe tables whose cells legitimately
5
+ * contain an escaped pipe: the Risk-Register contract example writes
6
+ * `product \| implementation`, and findings text routinely describes unions or
7
+ * flags (`text\|json`). Splitting on every `|` shifts every later cell by one,
8
+ * so `Risk type` becomes `product \` and a findings row's `Status` becomes a
9
+ * fragment of its own Finding text — a spurious block with no fix except
10
+ * rewording the prose.
11
+ *
12
+ * One definition, imported by both validators: the same defect appearing twice
13
+ * is what made this a shared helper rather than a local function.
14
+ */
15
+
16
+ /** Split on unescaped `|` only, drop the outer delimiters, unescape, trim. */
17
+ export function splitMarkdownRow(line: string): string[] {
18
+ let inner = line.trim();
19
+ if (inner.startsWith('|')) {
20
+ inner = inner.slice(1);
21
+ }
22
+ // A trailing pipe is a delimiter only when it is not itself escaped.
23
+ if (inner.endsWith('|') && !inner.endsWith('\\|')) {
24
+ inner = inner.slice(0, -1);
25
+ }
26
+ return inner.split(/(?<!\\)\|/).map((c) => c.replace(/\\\|/g, '|').trim());
27
+ }
@@ -0,0 +1,43 @@
1
+ /**
2
+ * Gate-R2 settings escape hatch, shared by the validator and the dispatcher.
3
+ *
4
+ * It lives here rather than in either script because BOTH R2 enforcement layers
5
+ * must honour it: `check_completion_review` (the gate) and
6
+ * `dispatch_r2_reviewer --verify` / `--verify-current` (the manifest
7
+ * re-derivation CI step). A reader that only the gate consulted left the second,
8
+ * blocking layer firing with the gate nominally switched off. `check_*` already
9
+ * imports the scope hash from `dispatch_*`, so putting the predicate in either
10
+ * one would make the import cycle back on itself.
11
+ *
12
+ * Fail-open by design: an unreadable or unparseable settings file, or a missing
13
+ * key, leaves the gate ACTIVE (`=== false` is the only disabling value).
14
+ */
15
+
16
+ import * as fs from 'node:fs';
17
+ import * as path from 'node:path';
18
+
19
+ import { parse as parseYaml } from 'yaml';
20
+
21
+ function planningFlagIsFalse(settingsDir: string, key: string): boolean {
22
+ const settingsPath = path.join(settingsDir, '.agent-settings.yml');
23
+ let raw: string;
24
+ try {
25
+ raw = fs.readFileSync(settingsPath, 'utf-8');
26
+ } catch {
27
+ return false;
28
+ }
29
+ try {
30
+ const parsed = parseYaml(raw) as unknown;
31
+ if (parsed === null || typeof parsed !== 'object') return false;
32
+ const planning = (parsed as Record<string, unknown>)['planning'];
33
+ if (planning === null || typeof planning !== 'object') return false;
34
+ return (planning as Record<string, unknown>)[key] === false;
35
+ } catch {
36
+ return false;
37
+ }
38
+ }
39
+
40
+ /** `planning.completion_review === false` in `<dir>/.agent-settings.yml`. */
41
+ export function completionReviewDisabled(settingsDir: string): boolean {
42
+ return planningFlagIsFalse(settingsDir, 'completion_review');
43
+ }