@event4u/agent-config 9.16.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 (227) hide show
  1. package/.claude-plugin/marketplace.json +1 -1
  2. package/AGENTS.md +1 -1
  3. package/CHANGELOG.md +80 -130
  4. package/README.md +1 -1
  5. package/dist/agent-src/commands/agent-handoff.md +100 -48
  6. package/dist/agent-src/commands/challenge-me/vision.md +5 -0
  7. package/dist/agent-src/commands/commit.md +1 -1
  8. package/dist/agent-src/commands/condense.md +19 -64
  9. package/dist/agent-src/commands/evals/agent-handoff.json +13 -1
  10. package/dist/agent-src/commands/evals/rule-compliance-audit.json +46 -0
  11. package/dist/agent-src/commands/feature/plan.md +20 -0
  12. package/dist/agent-src/commands/feature/roadmap.md +19 -0
  13. package/dist/agent-src/commands/memory/promote.md +14 -1
  14. package/dist/agent-src/commands/mode.md +9 -5
  15. package/dist/agent-src/commands/optimize/deep.md +72 -11
  16. package/dist/agent-src/commands/pr/create.md +29 -0
  17. package/dist/agent-src/commands/roadmap/create.md +29 -0
  18. package/dist/agent-src/commands/rule-compliance-audit.md +28 -5
  19. package/dist/agent-src/contexts/authority/commit-mechanics.md +5 -5
  20. package/dist/agent-src/contexts/communication/rules-auto/guidelines-mechanics.md +1 -0
  21. package/dist/agent-src/contexts/communication/rules-auto/skill-quality-mechanics.md +1 -1
  22. package/dist/agent-src/contexts/communication/rules-auto/think-before-action-mechanics.md +6 -1
  23. package/dist/agent-src/contexts/communication/rules-auto/token-efficiency-mechanics.md +4 -1
  24. package/dist/agent-src/contexts/execution/autonomy-mechanics.md +27 -1
  25. package/dist/agent-src/contexts/execution/plan-confidence-gate.md +133 -0
  26. package/dist/agent-src/contexts/execution/roadmap-process-loop.md +5 -2
  27. package/dist/agent-src/profiles/agency.yml +3 -3
  28. package/dist/agent-src/profiles/content_creator.yml +2 -2
  29. package/dist/agent-src/profiles/developer.yml +2 -2
  30. package/dist/agent-src/profiles/finance.yml +1 -1
  31. package/dist/agent-src/profiles/founder.yml +2 -2
  32. package/dist/agent-src/profiles/ops.yml +2 -2
  33. package/dist/agent-src/rules/active-remediation.md +2 -0
  34. package/dist/agent-src/rules/architecture.md +4 -2
  35. package/dist/agent-src/rules/augment-edit-discipline.md +2 -0
  36. package/dist/agent-src/rules/autonomous-execution.md +1 -1
  37. package/dist/agent-src/rules/brand-consistency.md +10 -62
  38. package/dist/agent-src/rules/brand-source-of-truth.md +62 -11
  39. package/dist/agent-src/rules/broken-access-control.md +4 -0
  40. package/dist/agent-src/rules/cli-output-handling.md +5 -0
  41. package/dist/agent-src/rules/code-comment-discipline.md +3 -0
  42. package/dist/agent-src/rules/commit-conventions.md +2 -0
  43. package/dist/agent-src/rules/context-hygiene.md +3 -2
  44. package/dist/agent-src/rules/cross-source-consistency.md +3 -0
  45. package/dist/agent-src/rules/design-fidelity.md +2 -0
  46. package/dist/agent-src/rules/doc-screenshot-hygiene.md +2 -0
  47. package/dist/agent-src/rules/docker-commands.md +4 -0
  48. package/dist/agent-src/rules/domain-adoption-policy.md +2 -0
  49. package/dist/agent-src/rules/domain-safety-disclaimer.md +5 -2
  50. package/dist/agent-src/rules/domain-safety-pii.md +1 -1
  51. package/dist/agent-src/rules/engineering-safety-floor.md +2 -0
  52. package/dist/agent-src/rules/framework-neutrality-in-generic-skills.md +9 -0
  53. package/dist/agent-src/rules/image-likeness-and-rights.md +4 -0
  54. package/dist/agent-src/rules/improve-before-implement.md +4 -0
  55. package/dist/agent-src/rules/laravel-routing.md +4 -0
  56. package/dist/agent-src/rules/legal-safety-floor.md +3 -0
  57. package/dist/agent-src/rules/lethal-trifecta-guard.md +3 -0
  58. package/dist/agent-src/rules/media-governance-routing.md +5 -0
  59. package/dist/agent-src/rules/minimal-safe-diff.md +2 -0
  60. package/dist/agent-src/rules/no-roadmap-references.md +2 -0
  61. package/dist/agent-src/rules/notes-first-reasoning.md +2 -0
  62. package/dist/agent-src/rules/output-discipline.md +9 -2
  63. package/dist/agent-src/rules/php-coding.md +2 -0
  64. package/dist/agent-src/rules/prefer-enums-over-literals.md +3 -0
  65. package/dist/agent-src/rules/provider-lifecycle-discipline.md +5 -0
  66. package/dist/agent-src/rules/roadmap-ci-steps-policy.md +2 -0
  67. package/dist/agent-src/rules/roadmap-progress-sync.md +2 -0
  68. package/dist/agent-src/rules/rule-type-governance.md +2 -0
  69. package/dist/agent-src/rules/scale-discipline.md +3 -0
  70. package/dist/agent-src/rules/secret-vcs-guard.md +2 -0
  71. package/dist/agent-src/rules/security-sensitive-stop.md +9 -2
  72. package/dist/agent-src/rules/senior-engineering-discipline.md +6 -0
  73. package/dist/agent-src/rules/skill-quality.md +2 -0
  74. package/dist/agent-src/rules/source-confidentiality.md +4 -0
  75. package/dist/agent-src/rules/source-discovery-gate.md +3 -0
  76. package/dist/agent-src/rules/source-of-truth.md +2 -0
  77. package/dist/agent-src/rules/strategy-safety-floor.md +2 -0
  78. package/dist/agent-src/rules/think-before-action.md +5 -0
  79. package/dist/agent-src/rules/token-budget-discipline.md +2 -2
  80. package/dist/agent-src/rules/ui-audit-gate.md +2 -0
  81. package/dist/agent-src/rules/user-interaction.md +3 -4
  82. package/dist/agent-src/skills/build-buy-partner/SKILL.md +1 -1
  83. package/dist/agent-src/skills/copilot-agents-optimization/SKILL.md +4 -4
  84. package/dist/agent-src/skills/copilot-config/SKILL.md +1 -1
  85. package/dist/agent-src/skills/deep-reading-analyst/SKILL.md +7 -4
  86. package/dist/agent-src/skills/description-assist/SKILL.md +1 -1
  87. package/dist/agent-src/skills/design-intelligence/SKILL.md +6 -180
  88. package/dist/agent-src/skills/design-intelligence/references/context-and-registers.md +100 -0
  89. package/dist/agent-src/skills/design-intelligence/references/integration-mapping.md +91 -0
  90. package/dist/agent-src/skills/design-review/SKILL.md +4 -141
  91. package/dist/agent-src/skills/design-review/references/review-communication.md +92 -0
  92. package/dist/agent-src/skills/design-review/references/verification-automation.md +61 -0
  93. package/dist/agent-src/skills/existing-ui-audit/SKILL.md +8 -27
  94. package/dist/agent-src/skills/existing-ui-audit/references/anti-slop-cross-reference.md +31 -0
  95. package/dist/agent-src/skills/fe-design/SKILL.md +4 -289
  96. package/dist/agent-src/skills/fe-design/references/design-patterns.md +227 -0
  97. package/dist/agent-src/skills/fe-design/references/design-read-and-memory.md +75 -0
  98. package/dist/agent-src/skills/market-entry-analysis/SKILL.md +1 -1
  99. package/dist/agent-src/skills/persona-writing/SKILL.md +2 -2
  100. package/dist/agent-src/skills/readme-reviewer/SKILL.md +2 -1
  101. package/dist/agent-src/skills/roadmap-management/SKILL.md +10 -0
  102. package/dist/agent-src/skills/roadmap-writing/SKILL.md +12 -1
  103. package/dist/agent-src/skills/rule-writing/SKILL.md +4 -2
  104. package/dist/agent-src/skills/skill-reviewer/SKILL.md +1 -1
  105. package/dist/agent-src/skills/skill-writing/SKILL.md +2 -2
  106. package/dist/agent-src/skills/tailwind-engineer/SKILL.md +3 -1
  107. package/dist/agent-src/skills/token-optimizer/SKILL.md +2 -2
  108. package/dist/agent-src/skills/ui-component-architect/SKILL.md +6 -3
  109. package/dist/agent-src/templates/agent-settings.md +13 -15
  110. package/dist/agent-src/templates/agents/agent-project-settings.example.yml +6 -2
  111. package/dist/agent-src/templates/persona.md +2 -2
  112. package/dist/agent-src/templates/roadmaps.md +13 -1
  113. package/dist/agent-src/templates/rule.md +1 -1
  114. package/dist/agent-src/templates/scripts/work_engine/directives/ui/_fix_lane.ts +52 -0
  115. package/dist/agent-src/templates/scripts/work_engine/directives/ui/audit.ts +8 -0
  116. package/dist/agent-src/templates/scripts/work_engine/directives/ui/design.ts +6 -0
  117. package/dist/agent-src/templates/scripts/work_engine/intent/classify.ts +56 -4
  118. package/dist/agent-src/templates/skill.md +1 -1
  119. package/dist/cli/registry.js +3 -0
  120. package/dist/cli/registry.js.map +1 -1
  121. package/dist/cli-delegate/chunk-AIIUU73N.js +2040 -0
  122. package/dist/cli-delegate/{chunk-ZXXJLL5K.js → chunk-BSLUHV2F.js} +127 -1487
  123. package/dist/cli-delegate/{chunk-H574KOUH.js → chunk-ELMFOQPW.js} +35 -1
  124. package/dist/cli-delegate/chunk-LF2KKJ4Q.js +183 -0
  125. package/dist/cli-delegate/chunk-VSYT7OHO.js +1382 -0
  126. package/dist/cli-delegate/chunk-XGA2NTU4.js +3727 -0
  127. package/dist/cli-delegate/cmd_conformance.js +4 -3
  128. package/dist/cli-delegate/cmd_converge.js +3 -3
  129. package/dist/cli-delegate/cmd_doctor.js +3 -2
  130. package/dist/cli-delegate/cmd_explain.js +36 -36
  131. package/dist/cli-delegate/cmd_export.js +4 -4
  132. package/dist/cli-delegate/cmd_handoff.js +1169 -0
  133. package/dist/cli-delegate/cmd_memory_get.js +35 -3754
  134. package/dist/cli-delegate/cmd_migrate.js +2 -2
  135. package/dist/cli-delegate/cmd_prune.js +4 -4
  136. package/dist/cli-delegate/cmd_route_audit.js +249 -0
  137. package/dist/cli-delegate/cmd_route_explain.js +285 -0
  138. package/dist/cli-delegate/cmd_settings_migrate.js +4 -4
  139. package/dist/cli-delegate/cmd_sync.js +4 -4
  140. package/dist/cli-delegate/cmd_uninstall.js +4 -4
  141. package/dist/cli-delegate/cmd_update.js +3 -3
  142. package/dist/cli-delegate/cmd_upgrade.js +3 -3
  143. package/dist/cli-delegate/cmd_validate.js +4 -4
  144. package/dist/cli-delegate/cmd_versions.js +3 -3
  145. package/dist/discovery/deprecation-report.md +1 -1
  146. package/dist/discovery/discovery-manifest.json +84 -84
  147. package/dist/discovery/discovery-manifest.json.sha256 +1 -1
  148. package/dist/discovery/discovery-manifest.summary.md +1 -1
  149. package/dist/discovery/orphan-report.md +1 -1
  150. package/dist/discovery/packs.json +2 -2
  151. package/dist/discovery/trust-report.md +1 -1
  152. package/dist/discovery/workspaces.json +2 -2
  153. package/dist/hooks/dispatch.js +602 -507
  154. package/dist/install/install.mjs +34 -0
  155. package/dist/mcp/awesome-mcp-servers.row.md +1 -1
  156. package/dist/mcp/mcp-cloudflare-catalogue.json +2 -2
  157. package/dist/mcp/registry-manifest.json +4 -4
  158. package/dist/mcp/server.json +2 -2
  159. package/dist/mcp/server.mjs +25 -3
  160. package/dist/router.json +1 -1
  161. package/dist/scripts/_lib/agent_src.js +39 -6
  162. package/dist/scripts/_lib/agent_src.js.map +1 -1
  163. package/dist/server/schemas/settings.js +25 -0
  164. package/dist/server/schemas/settings.js.map +1 -1
  165. package/docs/guidelines/abstraction-thresholds.md +53 -0
  166. package/docs/guidelines/agent-infra/agent-interaction-and-decision-quality.md +1 -1
  167. package/docs/guidelines/agent-infra/asking-and-brevity-examples.md +7 -0
  168. package/docs/guidelines/agent-infra/context-hygiene-mechanics.md +2 -2
  169. package/docs/guidelines/agent-infra/minimal-safe-diff-mechanics.md +5 -3
  170. package/docs/guidelines/agent-infra/size-and-scope.md +8 -2
  171. package/docs/guidelines/component-oriented-and-oop-development.md +11 -6
  172. package/docs/guidelines/docs/readme-size-and-splitting.md +5 -2
  173. package/llms.txt +1 -0
  174. package/package.json +1 -1
  175. package/src/agent-src/templates/scripts/work_engine/directives/ui/_fix_lane.ts +52 -0
  176. package/src/agent-src/templates/scripts/work_engine/directives/ui/audit.ts +8 -0
  177. package/src/agent-src/templates/scripts/work_engine/directives/ui/design.ts +6 -0
  178. package/src/agent-src/templates/scripts/work_engine/intent/classify.ts +56 -4
  179. package/src/config/agent-settings.template.yml +68 -0
  180. package/src/config/evaluator-budgets.json +8 -11
  181. package/src/config/gate-coverage.yml +70 -0
  182. package/src/scripts/_cli/cmd_explain.ts +41 -36
  183. package/src/scripts/_cli/cmd_handoff.ts +405 -0
  184. package/src/scripts/_cli/cmd_route_audit.ts +290 -0
  185. package/src/scripts/_cli/cmd_route_explain.ts +269 -0
  186. package/src/scripts/_cli/handoff_generate.ts +453 -0
  187. package/src/scripts/_cli/handoff_sessions.ts +435 -0
  188. package/src/scripts/_dispatch.bash +32 -0
  189. package/src/scripts/_lib/agent_src.ts +36 -6
  190. package/src/scripts/_lib/git_env.ts +40 -0
  191. package/src/scripts/_lib/md_table.ts +27 -0
  192. package/src/scripts/_lib/planning_settings.ts +43 -0
  193. package/src/scripts/_lib/release_material.ts +156 -0
  194. package/src/scripts/_lib/router_match.ts +301 -0
  195. package/src/scripts/annotate_r1_outcomes.ts +258 -0
  196. package/src/scripts/check_completion_review.ts +1140 -0
  197. package/src/scripts/check_condensation.ts +2 -1
  198. package/src/scripts/check_finding_dispositions.ts +278 -0
  199. package/src/scripts/check_pack_size.ts +4 -3
  200. package/src/scripts/check_references.ts +1 -0
  201. package/src/scripts/check_release_highlights.ts +300 -0
  202. package/src/scripts/check_release_pr_shape.ts +4 -0
  203. package/src/scripts/check_release_surface_equality.ts +132 -0
  204. package/src/scripts/check_reply_consistency.ts +187 -138
  205. package/src/scripts/compile_router.ts +20 -3
  206. package/src/scripts/dispatch_r2_reviewer.ts +968 -0
  207. package/src/scripts/eval_ui_triviality.ts +156 -0
  208. package/src/scripts/handoff_context_hook.ts +148 -0
  209. package/src/scripts/hook_manifest.yaml +17 -7
  210. package/src/scripts/hooks/concern_registry.ts +2 -0
  211. package/src/scripts/lint_abstraction_thresholds.ts +327 -0
  212. package/src/scripts/lint_framework_leakage_allowlist.json +2 -2
  213. package/src/scripts/lint_plan_risk_register.ts +931 -0
  214. package/src/scripts/lint_profile_personas.ts +153 -0
  215. package/src/scripts/lint_token_budget_discipline.ts +148 -0
  216. package/src/scripts/lint_trigger_collisions.ts +257 -0
  217. package/src/scripts/release.ts +156 -32
  218. package/src/scripts/release_drill.ts +491 -0
  219. package/src/scripts/router_telemetry.ts +16 -290
  220. package/src/scripts/schemas/command.schema.json +33 -0
  221. package/src/scripts/schemas/rule.schema.json +10 -0
  222. package/src/scripts/self_review_gate.ts +41 -3
  223. package/src/scripts/skill_linter.ts +3 -1
  224. package/src/scripts/smoke/router.sh +1 -1
  225. package/src/scripts/update_counts.ts +60 -12
  226. package/src/server/schemas/settings.ts +41 -0
  227. package/dist/cli-delegate/{chunk-XX6TOPD7.js → chunk-WR6GTRHJ.js} +3 -3
@@ -0,0 +1,435 @@
1
+ /**
2
+ * Session enumeration for `agent-config handoff` (road-to-agent-handoff-
3
+ * resume Phase 1). Pure library — no CLI entry, no prompts, no writes.
4
+ *
5
+ * Sources, merged newest-first into one list:
6
+ * - `chat-history` — the cross-host JSONL log (primary; works for
7
+ * every hook-capable host). Reuses `chat_history.list_sessions()`.
8
+ * - `claude-transcript` — native Claude Code transcripts under
9
+ * `~/.claude/projects/<slug>/*.jsonl` (fallback for sessions the log
10
+ * missed; slug = cwd with `/` and `.` mapped to `-`).
11
+ * - `codex-session` — `~/.codex/sessions/YYYY/MM/DD/rollout-*.jsonl`
12
+ * (Codex's ONLY source — a bundle host without hooks never writes
13
+ * chat-history). Format verified 2026-08-04 against the live store:
14
+ * line 0 is `{type:'session_meta', payload:{id, timestamp, cwd,
15
+ * git:{branch}}}`; user prompts arrive as `event_msg` records with
16
+ * `payload:{type:'user_message', message}` (fixture:
17
+ * tests/scripts/fixtures/handoff/codex-session.jsonl).
18
+ *
19
+ * De-dup: native-store sessions whose platform id maps (via
20
+ * `derive_session_tag`) to a chat-history bucket are dropped — the
21
+ * chat-history entry wins.
22
+ */
23
+
24
+ import * as fs from 'node:fs';
25
+ import * as os from 'node:os';
26
+ import * as path from 'node:path';
27
+
28
+ import { derive_session_tag, list_sessions, read_entries } from '../chat_history.js';
29
+
30
+ export const DEFAULT_CAP = 15;
31
+ const SUMMARY_SNIPPET_CHARS = 120;
32
+
33
+ export type HandoffSource = 'chat-history' | 'claude-transcript' | 'codex-session';
34
+
35
+ export interface HandoffSession {
36
+ id: string;
37
+ source: HandoffSource;
38
+ startedAt: string | null;
39
+ endedAt: string | null;
40
+ branch?: string;
41
+ entryCount: number;
42
+ summary: string;
43
+ /** Absolute transcript path — set for the native-store sources. */
44
+ transcriptPath?: string;
45
+ }
46
+
47
+ export interface ListOptions {
48
+ /** Repo root the sessions must belong to. Default: process.cwd(). */
49
+ cwd?: string;
50
+ /** Chat-history JSONL override (else AGENT_CHAT_HISTORY_FILE / default). */
51
+ chatHistoryPath?: string | null;
52
+ /** Claude Code projects root. Default: ~/.claude/projects. */
53
+ claudeProjectsRoot?: string;
54
+ /** Codex sessions root. Default: ~/.codex/sessions. */
55
+ codexSessionsRoot?: string;
56
+ /** Max sessions per source AND in the merged result. Default 15. */
57
+ cap?: number;
58
+ }
59
+
60
+ /** Mirrors the Claude Code project-dir naming: `/` and `.` → `-`. */
61
+ export function encode_project_path(cwd: string): string {
62
+ return cwd.replace(/[/.]/g, '-');
63
+ }
64
+
65
+ function _snippet(text: string, max: number = SUMMARY_SNIPPET_CHARS): string {
66
+ const oneLine = text.replace(/\s+/g, ' ').trim();
67
+ return oneLine.length <= max ? oneLine : oneLine.slice(0, max - 1) + '…';
68
+ }
69
+
70
+ /** first-prompt → last-prompt summary (Step 3 contract). */
71
+ function _summarize(first: string | null, last: string | null): string {
72
+ const a = first ? _snippet(first) : '';
73
+ const b = last ? _snippet(last) : '';
74
+ if (a && b && a !== b) return `${a} → ${b}`;
75
+ return a || b || '(no user prompts)';
76
+ }
77
+
78
+ function _read_lines(file: string): string[] {
79
+ try {
80
+ return fs.readFileSync(file, 'utf-8').split('\n');
81
+ } catch {
82
+ return [];
83
+ }
84
+ }
85
+
86
+ function _parse_json(line: string): Record<string, unknown> | null {
87
+ const trimmed = line.trim();
88
+ if (!trimmed) return null;
89
+ try {
90
+ const parsed = JSON.parse(trimmed) as unknown;
91
+ if (parsed && typeof parsed === 'object' && !Array.isArray(parsed)) {
92
+ return parsed as Record<string, unknown>;
93
+ }
94
+ } catch {
95
+ // fail-open — skip malformed lines
96
+ }
97
+ return null;
98
+ }
99
+
100
+ // ---------------------------------------------------------------------
101
+ // chat-history (primary)
102
+ // ---------------------------------------------------------------------
103
+
104
+ const CHAT_HISTORY_REL = path.join('agents', 'runtime', '.agent-chat-history');
105
+
106
+ /** Explicit option > AGENT_CHAT_HISTORY_FILE env > repo-root-relative default. */
107
+ function _chat_history_path(opts: ListOptions): string {
108
+ if (opts.chatHistoryPath) return opts.chatHistoryPath;
109
+ return (
110
+ process.env.AGENT_CHAT_HISTORY_FILE ||
111
+ path.join(opts.cwd ?? process.cwd(), CHAT_HISTORY_REL)
112
+ );
113
+ }
114
+
115
+ /**
116
+ * Real-prompt summaries per session: hosts log system/task notifications as
117
+ * `t:'user'` entries, so the raw bucket summary can read like
118
+ * `<task-notification>…` — filtered here (council review 2026-08-04,
119
+ * claude-sonnet-4-5 + gpt-4o: picker noise is a UX defect; the log itself
120
+ * stays unfiltered).
121
+ */
122
+ function _chat_history_summaries(histPath: string): Map<string, { first: string | null; last: string | null }> {
123
+ const bySession = new Map<string, { first: string | null; last: string | null }>();
124
+ let entries: Array<Record<string, unknown>>;
125
+ try {
126
+ entries = read_entries({ path: histPath, last: null });
127
+ } catch {
128
+ return bySession;
129
+ }
130
+ for (const e of entries) {
131
+ if (e.t !== 'user' || typeof e.text !== 'string') continue;
132
+ const text = e.text.trim();
133
+ if (!text || text.startsWith('<') || text.startsWith('Caveat:')) continue;
134
+ const sid = String(e.s ?? '<legacy>');
135
+ const slot = bySession.get(sid) ?? { first: null, last: null };
136
+ if (!slot.first) slot.first = text;
137
+ slot.last = text;
138
+ bySession.set(sid, slot);
139
+ }
140
+ return bySession;
141
+ }
142
+
143
+ function _chat_history_sessions(opts: ListOptions): HandoffSession[] {
144
+ const histPath = _chat_history_path(opts);
145
+ let buckets: Array<Record<string, unknown>>;
146
+ try {
147
+ buckets = list_sessions({ path: histPath, summary: true });
148
+ } catch {
149
+ return [];
150
+ }
151
+ const summaries = _chat_history_summaries(histPath);
152
+ return buckets
153
+ .filter((b) => Number(b.count ?? 0) > 0)
154
+ .map((b) => {
155
+ const id = String(b.id ?? '');
156
+ const prompts = summaries.get(id);
157
+ const summary = prompts
158
+ ? _summarize(prompts.first, prompts.last)
159
+ : '(no user prompts)';
160
+ return {
161
+ id,
162
+ source: 'chat-history' as const,
163
+ startedAt: b.first_ts ? String(b.first_ts) : null,
164
+ endedAt: b.last_ts ? String(b.last_ts) : null,
165
+ entryCount: Number(b.count ?? 0),
166
+ summary,
167
+ };
168
+ });
169
+ }
170
+
171
+ // ---------------------------------------------------------------------
172
+ // claude-transcript (native fallback)
173
+ // ---------------------------------------------------------------------
174
+
175
+ interface TranscriptScan {
176
+ entryCount: number;
177
+ firstTs: string | null;
178
+ lastTs: string | null;
179
+ branch: string | null;
180
+ firstPrompt: string | null;
181
+ lastPrompt: string | null;
182
+ }
183
+
184
+ /** A real prompt has string content; tool_results arrive as block lists. */
185
+ function _cc_prompt_text(rec: Record<string, unknown>): string | null {
186
+ if (rec.type !== 'user' || rec.isSidechain === true) return null;
187
+ const msg = rec.message;
188
+ if (!msg || typeof msg !== 'object' || Array.isArray(msg)) return null;
189
+ const content = (msg as Record<string, unknown>).content;
190
+ if (typeof content !== 'string') return null;
191
+ const trimmed = content.trim();
192
+ // skip host-injected meta blocks (caveats, command wrappers)
193
+ if (!trimmed || trimmed.startsWith('<') || trimmed.startsWith('Caveat:')) return null;
194
+ return trimmed;
195
+ }
196
+
197
+ function _scan_claude_transcript(file: string): TranscriptScan {
198
+ const scan: TranscriptScan = {
199
+ entryCount: 0,
200
+ firstTs: null,
201
+ lastTs: null,
202
+ branch: null,
203
+ firstPrompt: null,
204
+ lastPrompt: null,
205
+ };
206
+ for (const line of _read_lines(file)) {
207
+ const rec = _parse_json(line);
208
+ if (!rec) continue;
209
+ scan.entryCount += 1;
210
+ const ts = typeof rec.timestamp === 'string' ? rec.timestamp : null;
211
+ if (ts) {
212
+ if (!scan.firstTs) scan.firstTs = ts;
213
+ scan.lastTs = ts;
214
+ }
215
+ if (!scan.branch && typeof rec.gitBranch === 'string' && rec.gitBranch) {
216
+ scan.branch = rec.gitBranch;
217
+ }
218
+ const prompt = _cc_prompt_text(rec);
219
+ if (prompt) {
220
+ if (!scan.firstPrompt) scan.firstPrompt = prompt;
221
+ scan.lastPrompt = prompt;
222
+ }
223
+ }
224
+ return scan;
225
+ }
226
+
227
+ function _claude_transcript_sessions(opts: ListOptions, cap: number): HandoffSession[] {
228
+ const root = opts.claudeProjectsRoot ?? path.join(os.homedir(), '.claude', 'projects');
229
+ const cwd = opts.cwd ?? process.cwd();
230
+ const projectDir = path.join(root, encode_project_path(cwd));
231
+ let names: string[];
232
+ try {
233
+ names = fs.readdirSync(projectDir).filter((n) => n.endsWith('.jsonl'));
234
+ } catch {
235
+ return [];
236
+ }
237
+ const files = names
238
+ .map((n) => {
239
+ const full = path.join(projectDir, n);
240
+ try {
241
+ return { full, id: path.basename(n, '.jsonl'), mtime: fs.statSync(full).mtimeMs };
242
+ } catch {
243
+ return null;
244
+ }
245
+ })
246
+ .filter((f): f is { full: string; id: string; mtime: number } => f !== null)
247
+ .sort((a, b) => b.mtime - a.mtime)
248
+ .slice(0, cap);
249
+
250
+ const sessions: HandoffSession[] = [];
251
+ for (const f of files) {
252
+ const scan = _scan_claude_transcript(f.full);
253
+ if (scan.entryCount === 0) continue;
254
+ sessions.push({
255
+ id: f.id,
256
+ source: 'claude-transcript',
257
+ startedAt: scan.firstTs,
258
+ endedAt: scan.lastTs,
259
+ ...(scan.branch ? { branch: scan.branch } : {}),
260
+ entryCount: scan.entryCount,
261
+ summary: _summarize(scan.firstPrompt, scan.lastPrompt),
262
+ transcriptPath: f.full,
263
+ });
264
+ }
265
+ return sessions;
266
+ }
267
+
268
+ // ---------------------------------------------------------------------
269
+ // codex-session (native adapter — Codex's only source)
270
+ // ---------------------------------------------------------------------
271
+
272
+ interface CodexMeta {
273
+ id: string;
274
+ timestamp: string | null;
275
+ cwd: string;
276
+ branch: string | null;
277
+ }
278
+
279
+ function _codex_meta(file: string): CodexMeta | null {
280
+ let firstLine = '';
281
+ try {
282
+ // metadata is always line 0 — never read the whole file here
283
+ const fd = fs.openSync(file, 'r');
284
+ try {
285
+ const buf = Buffer.alloc(8192);
286
+ const n = fs.readSync(fd, buf, 0, buf.length, 0);
287
+ firstLine = buf.toString('utf-8', 0, n).split('\n')[0] ?? '';
288
+ } finally {
289
+ fs.closeSync(fd);
290
+ }
291
+ } catch {
292
+ return null;
293
+ }
294
+ const rec = _parse_json(firstLine);
295
+ if (!rec || rec.type !== 'session_meta') return null;
296
+ const payload = rec.payload;
297
+ if (!payload || typeof payload !== 'object' || Array.isArray(payload)) return null;
298
+ const p = payload as Record<string, unknown>;
299
+ const git =
300
+ p.git && typeof p.git === 'object' && !Array.isArray(p.git)
301
+ ? (p.git as Record<string, unknown>)
302
+ : {};
303
+ return {
304
+ id: String(p.id ?? ''),
305
+ timestamp: typeof p.timestamp === 'string' ? p.timestamp : null,
306
+ cwd: String(p.cwd ?? ''),
307
+ branch: typeof git.branch === 'string' && git.branch ? git.branch : null,
308
+ };
309
+ }
310
+
311
+ function _codex_files(root: string): string[] {
312
+ const out: string[] = [];
313
+ const stack = [root];
314
+ while (stack.length > 0) {
315
+ const dir = stack.pop() as string;
316
+ let entries: fs.Dirent[];
317
+ try {
318
+ entries = fs.readdirSync(dir, { withFileTypes: true });
319
+ } catch {
320
+ continue;
321
+ }
322
+ for (const e of entries) {
323
+ const full = path.join(dir, e.name);
324
+ if (e.isDirectory()) stack.push(full);
325
+ else if (e.isFile() && e.name.endsWith('.jsonl')) out.push(full);
326
+ }
327
+ }
328
+ // rollout filenames embed the timestamp — lexicographic basename sort
329
+ // is chronological; newest first
330
+ return out.sort((a, b) => (path.basename(a) < path.basename(b) ? 1 : -1));
331
+ }
332
+
333
+ interface CodexScan {
334
+ entryCount: number;
335
+ lastTs: string | null;
336
+ firstPrompt: string | null;
337
+ lastPrompt: string | null;
338
+ }
339
+
340
+ function _codex_prompt_text(rec: Record<string, unknown>): string | null {
341
+ if (rec.type !== 'event_msg') return null;
342
+ const p = rec.payload;
343
+ if (!p || typeof p !== 'object' || Array.isArray(p)) return null;
344
+ const payload = p as Record<string, unknown>;
345
+ if (payload.type !== 'user_message') return null;
346
+ const msg = typeof payload.message === 'string' ? payload.message.trim() : '';
347
+ if (!msg || msg.startsWith('<')) return null; // skip injected meta blocks
348
+ return msg;
349
+ }
350
+
351
+ function _scan_codex_session(file: string): CodexScan {
352
+ const scan: CodexScan = { entryCount: 0, lastTs: null, firstPrompt: null, lastPrompt: null };
353
+ for (const line of _read_lines(file)) {
354
+ const rec = _parse_json(line);
355
+ if (!rec) continue;
356
+ scan.entryCount += 1;
357
+ if (typeof rec.timestamp === 'string') scan.lastTs = rec.timestamp;
358
+ const prompt = _codex_prompt_text(rec);
359
+ if (prompt) {
360
+ if (!scan.firstPrompt) scan.firstPrompt = prompt;
361
+ scan.lastPrompt = prompt;
362
+ }
363
+ }
364
+ return scan;
365
+ }
366
+
367
+ function _matches_repo(sessionCwd: string, repoCwd: string): boolean {
368
+ const norm = (p: string): string => path.resolve(p).replace(/\/+$/, '');
369
+ const a = norm(sessionCwd);
370
+ const b = norm(repoCwd);
371
+ return a === b || a.startsWith(b + path.sep);
372
+ }
373
+
374
+ function _codex_sessions(opts: ListOptions, cap: number): HandoffSession[] {
375
+ const root = opts.codexSessionsRoot ?? path.join(os.homedir(), '.codex', 'sessions');
376
+ const cwd = opts.cwd ?? process.cwd();
377
+ const matching: Array<{ file: string; meta: CodexMeta }> = [];
378
+ for (const file of _codex_files(root)) {
379
+ if (matching.length >= cap) break;
380
+ const meta = _codex_meta(file); // line-0 read only — cheap per file
381
+ if (!meta || !meta.id) continue;
382
+ if (!_matches_repo(meta.cwd, cwd)) continue;
383
+ matching.push({ file, meta });
384
+ }
385
+ const sessions: HandoffSession[] = [];
386
+ for (const { file, meta } of matching) {
387
+ const scan = _scan_codex_session(file);
388
+ if (scan.entryCount === 0) continue;
389
+ sessions.push({
390
+ id: meta.id,
391
+ source: 'codex-session',
392
+ startedAt: meta.timestamp,
393
+ endedAt: scan.lastTs ?? meta.timestamp,
394
+ ...(meta.branch ? { branch: meta.branch } : {}),
395
+ entryCount: scan.entryCount,
396
+ summary: _summarize(scan.firstPrompt, scan.lastPrompt),
397
+ transcriptPath: file,
398
+ });
399
+ }
400
+ return sessions;
401
+ }
402
+
403
+ // ---------------------------------------------------------------------
404
+ // merge
405
+ // ---------------------------------------------------------------------
406
+
407
+ function _sort_key(s: HandoffSession): string {
408
+ return s.endedAt ?? s.startedAt ?? '';
409
+ }
410
+
411
+ /**
412
+ * Unified newest-first session list. Native-store sessions whose platform
413
+ * id maps to an existing chat-history bucket are dropped (chat-history is
414
+ * the primary source).
415
+ */
416
+ export function list_handoff_sessions(opts: ListOptions = {}): HandoffSession[] {
417
+ const cap = opts.cap ?? DEFAULT_CAP;
418
+ const primary = _chat_history_sessions(opts);
419
+ const knownTags = new Set(primary.map((s) => s.id));
420
+
421
+ const native = [
422
+ ..._claude_transcript_sessions(opts, cap),
423
+ ..._codex_sessions(opts, cap),
424
+ ].filter((s) => {
425
+ try {
426
+ return !knownTags.has(derive_session_tag(s.id));
427
+ } catch {
428
+ return true;
429
+ }
430
+ });
431
+
432
+ return [...primary, ...native]
433
+ .sort((a, b) => (_sort_key(a) < _sort_key(b) ? 1 : _sort_key(a) > _sort_key(b) ? -1 : 0))
434
+ .slice(0, cap);
435
+ }
@@ -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>
@@ -257,6 +266,7 @@ Tier 2 — maintenance / internal (hooks, MCP, memory, telemetry):
257
266
  telemetry:status Print artefact-engagement telemetry status (read-only)
258
267
  telemetry:report Aggregate the engagement log into a quartile report
259
268
  analyze-session Read-only post-session report from on-disk runtime state
269
+ handoff Pick a recent session, generate a handoff, seed a fresh session
260
270
  EOF
261
271
  fi
262
272
 
@@ -1170,6 +1180,18 @@ cmd_versions() {
1170
1180
 
1171
1181
  # `agent-config explain <config|rule|route>` — print the decision chain
1172
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
+
1173
1195
  # edits state. See scripts/_cli/cmd_explain.ts.
1174
1196
  cmd_explain() {
1175
1197
  exec_ts "$PACKAGE_ROOT/src/scripts/_cli/cmd_explain.ts" "$@"
@@ -1182,6 +1204,13 @@ cmd_analyze_session() {
1182
1204
  exec_ts "$PACKAGE_ROOT/src/scripts/_cli/cmd_analyze_session.ts" "$@"
1183
1205
  }
1184
1206
 
1207
+ # `agent-config handoff` — pick a recent session, generate a deterministic
1208
+ # handoff into agents/runtime/state/handoff-context.md, optionally --launch a
1209
+ # fresh host session. See scripts/_cli/cmd_handoff.ts.
1210
+ cmd_handoff() {
1211
+ exec_ts "$PACKAGE_ROOT/src/scripts/_cli/cmd_handoff.ts" "$@"
1212
+ }
1213
+
1185
1214
  main() {
1186
1215
  local cmd="${1-}"
1187
1216
  [[ $# -gt 0 ]] && shift || true
@@ -1227,6 +1256,8 @@ main() {
1227
1256
  hooks:status) cmd_hooks_status "$@" ;;
1228
1257
  hooks:doctor) cmd_hooks_doctor "$@" ;;
1229
1258
  routing:doctor) cmd_routing_doctor "$@" ;;
1259
+ route:explain) cmd_route_explain "$@" ;;
1260
+ route:audit) cmd_route_audit "$@" ;;
1230
1261
  hooks:replay) cmd_hooks_replay "$@" ;;
1231
1262
  reach:doctor) cmd_reach_doctor "$@" ;;
1232
1263
  telemetry:record) cmd_telemetry_record "$@" ;;
@@ -1255,6 +1286,7 @@ main() {
1255
1286
  versions) cmd_versions "$@" ;;
1256
1287
  explain) cmd_explain "$@" ;;
1257
1288
  analyze-session) cmd_analyze_session "$@" ;;
1289
+ handoff) cmd_handoff "$@" ;;
1258
1290
  help|--help|-h|"")
1259
1291
  # Optional `--tier=0|1|all` filter (default 0).
1260
1292
  local tier_arg="0"
@@ -179,6 +179,27 @@ function _iterdirSorted(p: string): string[] {
179
179
  function _rglobSorted(root: string, pattern: string): string[] {
180
180
  const out: string[] = [];
181
181
  const suffix = pattern === '*' ? null : pattern.slice(1); // drop leading '*'
182
+ // Symlink confinement: every symlink (leaf or directory) must resolve
183
+ // inside the walk root, or it is ignored. A visited-realpath set bounds
184
+ // symlink cycles, and a broken symlink (realpath throws) is skipped
185
+ // explicitly. Without this, a directory symlink planted under a source
186
+ // root walks arbitrary paths outside the package and a cycle recurses
187
+ // until stack exhaustion (release-truth Phase 3).
188
+ let rootReal: string;
189
+ try {
190
+ rootReal = fs.realpathSync(root);
191
+ } catch {
192
+ return out;
193
+ }
194
+ const _confined = (p: string): string | null => {
195
+ try {
196
+ const real = fs.realpathSync(p);
197
+ return real === rootReal || _isUnder(real, rootReal) ? real : null;
198
+ } catch {
199
+ return null; // broken symlink → handled explicitly as "not yielded"
200
+ }
201
+ };
202
+ const visited = new Set<string>([rootReal]);
182
203
  const walk = (dir: string): void => {
183
204
  let entries: fs.Dirent[];
184
205
  try {
@@ -189,14 +210,21 @@ function _rglobSorted(root: string, pattern: string): string[] {
189
210
  for (const ent of entries) {
190
211
  const full = path.join(dir, ent.name);
191
212
  const matches = suffix === null ? true : ent.name.endsWith(suffix);
192
- if (matches) {
213
+ if (matches && (!ent.isSymbolicLink() || _confined(full) !== null)) {
193
214
  out.push(full);
194
215
  }
195
216
  // Recurse into directories. `isDirectory()` on a Dirent does not
196
- // follow symlinks; pathlib.rglob follows directory symlinks. Use a
197
- // stat-based dir check to match Python's traversal, but guard
198
- // against unreadable entries.
199
- if (ent.isDirectory() || (ent.isSymbolicLink() && _isDir(full))) {
217
+ // follow symlinks; pathlib.rglob follows directory symlinks. Follow
218
+ // a directory symlink only when its target stays inside the walk
219
+ // root and has not been visited yet (cycle guard).
220
+ if (ent.isDirectory()) {
221
+ walk(full);
222
+ } else if (ent.isSymbolicLink() && _isDir(full)) {
223
+ const real = _confined(full);
224
+ if (real === null || visited.has(real)) {
225
+ continue;
226
+ }
227
+ visited.add(real);
200
228
  walk(full);
201
229
  }
202
230
  }
@@ -432,7 +460,9 @@ export function artefact_roots(): string[] {
432
460
  *
433
461
  * Walks `_root_specs` (the leaf view). Deterministic order; deduplicated on
434
462
  * logical path so a file present in two roots during the move window is
435
- * yielded once. Symlinks and non-files are skipped. Mirrors `iter_artefacts`.
463
+ * yielded once. Non-files are skipped; symlinks are followed only when they
464
+ * resolve inside their walk root (external targets, broken links, and cycles
465
+ * are ignored). Mirrors `iter_artefacts`.
436
466
  */
437
467
  export function* iter_artefacts(suffix = '.md'): Generator<string> {
438
468
  const seen: Set<string> = new Set();
@@ -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
+ }