@event4u/agent-config 14.18.0 → 14.20.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 (210) hide show
  1. package/.augment-plugin/marketplace.json +3 -3
  2. package/.augment-plugin/plugin.json +1 -1
  3. package/.claude-plugin/marketplace.json +1 -1
  4. package/CHANGELOG.md +120 -129
  5. package/README.md +71 -149
  6. package/dist/agent-src/README.md +1 -1
  7. package/dist/agent-src/commands/agent-handoff.md +44 -43
  8. package/dist/agent-src/commands/review/changes.md +19 -19
  9. package/dist/agent-src/commands/review.md +2 -2
  10. package/dist/agent-src/contexts/execution/host-capability-manifest.md +37 -4
  11. package/dist/agent-src/rules/autonomous-execution.md +1 -1
  12. package/dist/agent-src/rules/git-history-discipline.md +2 -2
  13. package/dist/agent-src/rules/guidelines.md +1 -1
  14. package/dist/agent-src/rules/session-canary.md +2 -2
  15. package/dist/agent-src/rules/token-efficiency.md +1 -1
  16. package/dist/agent-src/rules/untrusted-input-defense.md +0 -2
  17. package/dist/agent-src/skills/adversarial-review/SKILL.md +0 -8
  18. package/dist/agent-src/skills/canvas-design/SKILL.md +1 -1
  19. package/dist/agent-src/skills/design-intelligence/SKILL.md +1 -1
  20. package/dist/agent-src/skills/design-intelligence/data/app-interface.csv +1 -1
  21. package/dist/agent-src/skills/design-intelligence/data/motion.csv +17 -17
  22. package/dist/agent-src/skills/design-intelligence/data/ux-guidelines.csv +2 -2
  23. package/dist/agent-src/skills/design-intelligence/references/context-and-registers.md +17 -0
  24. package/dist/agent-src/skills/design-intelligence/references/design-rules-checklist.md +4 -4
  25. package/dist/agent-src/skills/design-review/SKILL.md +1 -1
  26. package/dist/agent-src/skills/design-variations/SKILL.md +1 -1
  27. package/dist/agent-src/skills/evaluate-llm-feature/SKILL.md +1 -1
  28. package/dist/agent-src/skills/existing-ui-audit/SKILL.md +1 -1
  29. package/dist/agent-src/skills/fe-design/SKILL.md +32 -5
  30. package/dist/agent-src/skills/fe-design/references/craft-floor.md +7 -0
  31. package/dist/agent-src/skills/fe-design/references/design-patterns.md +47 -2
  32. package/dist/agent-src/skills/laravel-scheduling/SKILL.md +3 -1
  33. package/dist/agent-src/skills/markitdown/SKILL.md +1 -1
  34. package/dist/agent-src/skills/project-analyzer/SKILL.md +0 -6
  35. package/dist/agent-src/skills/react-native-setup/SKILL.md +3 -3
  36. package/dist/agent-src/skills/react-shadcn-ui/SKILL.md +1 -1
  37. package/dist/agent-src/skills/sequential-thinking/SKILL.md +18 -8
  38. package/dist/agent-src/skills/skill-improvement-pipeline/SKILL.md +28 -6
  39. package/dist/agent-src/skills/tailwind-engineer/SKILL.md +1 -1
  40. package/dist/agent-src/skills/test-driven-development/SKILL.md +1 -3
  41. package/dist/agent-src/skills/ui-apply-generic/SKILL.md +1 -1
  42. package/dist/agent-src/skills/ui-component-architect/SKILL.md +1 -1
  43. package/dist/agent-src/skills/using-git-worktrees/SKILL.md +17 -2
  44. package/dist/agent-src/templates/agents/agent-project-settings.example.yml +1 -1
  45. package/dist/agent-src/templates/roadmaps.md +14 -1
  46. package/dist/agent-src/templates/scripts/telemetry/mcp_lite.ts +213 -0
  47. package/dist/agent-src/templates/scripts/telemetry/report_renderer.ts +16 -0
  48. package/dist/agent-src/templates/scripts/telemetry_report.ts +22 -2
  49. package/dist/cli/commands/recordTriggerEval.js +2 -2
  50. package/dist/cli/main.js +1 -1
  51. package/dist/cli/mcp/dispatch.js +10 -0
  52. package/dist/cli/mcp/stdio.js +33 -2
  53. package/dist/cli/mcp/telemetry.js +227 -0
  54. package/dist/cli-delegate/{chunk-AKJGUQNO.js → chunk-C6IPZRDA.js} +484 -353
  55. package/dist/cli-delegate/{chunk-CYH5SP6M.js → chunk-I6RHFQEG.js} +151 -16
  56. package/dist/cli-delegate/{chunk-7RXIKF62.js → chunk-NUMQHLS5.js} +304 -136
  57. package/dist/cli-delegate/{chunk-PMEKGDED.js → chunk-PI2GU3DP.js} +163 -1
  58. package/dist/cli-delegate/{chunk-7PRE6ZV2.js → chunk-YS6XTLEK.js} +199 -4
  59. package/dist/cli-delegate/chunk-ZVBOHSVF.js +1109 -0
  60. package/dist/cli-delegate/cmd_adr_effective.js +318 -81
  61. package/dist/cli-delegate/cmd_conformance.js +489 -121
  62. package/dist/cli-delegate/cmd_doctor.js +2 -2
  63. package/dist/cli-delegate/cmd_export.js +2 -2
  64. package/dist/cli-delegate/cmd_handoff.js +42 -12
  65. package/dist/cli-delegate/cmd_memory_get.js +1 -1
  66. package/dist/cli-delegate/cmd_prune.js +2 -2
  67. package/dist/cli-delegate/cmd_route_audit.js +2 -2
  68. package/dist/cli-delegate/cmd_session_recycle.js +59 -902
  69. package/dist/cli-delegate/cmd_settings_migrate.js +2 -2
  70. package/dist/cli-delegate/cmd_sync.js +2 -2
  71. package/dist/cli-delegate/cmd_uninstall.js +2 -2
  72. package/dist/cli-delegate/cmd_validate.js +2 -2
  73. package/dist/discovery/deprecation-report.md +1 -1
  74. package/dist/discovery/discovery-manifest.json +34 -33
  75. package/dist/discovery/discovery-manifest.json.sha256 +1 -1
  76. package/dist/discovery/discovery-manifest.summary.md +1 -1
  77. package/dist/discovery/dormancy-report.md +1 -1
  78. package/dist/discovery/orphan-report.md +1 -1
  79. package/dist/discovery/packs.json +2 -2
  80. package/dist/discovery/trust-report.md +1 -1
  81. package/dist/discovery/workspaces.json +2 -2
  82. package/dist/hooks/dispatch.js +4455 -3025
  83. package/dist/install/install.mjs +975 -689
  84. package/dist/mcp/registry-manifest.json +2 -2
  85. package/dist/mcp/server.json +2 -2
  86. package/dist/mcp/server.mjs +2087 -1134
  87. package/dist/scripts/_lib/model_tier.js +1 -1
  88. package/dist/scripts/mcp_server/telemetry.js +148 -0
  89. package/docs/guidelines/agent-infra/evaluator-independence-mechanics.md +1 -1
  90. package/docs/guidelines/agent-infra/naming.md +1 -1
  91. package/docs/guidelines/agent-infra/roadmap-progress-mechanics.md +1 -1
  92. package/docs/guidelines/agent-infra/security-lint-containment.md +46 -6
  93. package/docs/guidelines/agent-infra/self-improvement-pipeline.md +4 -4
  94. package/docs/guidelines/agent-infra/untrusted-input-spotlighting.md +1 -1
  95. package/docs/guidelines/design-antipatterns.md +87 -64
  96. package/docs/guidelines/redundancy-taxonomy.md +28 -0
  97. package/package.json +1 -1
  98. package/src/agent-src/README.md +1 -1
  99. package/src/agent-src/templates/scripts/telemetry/mcp_lite.ts +213 -0
  100. package/src/agent-src/templates/scripts/telemetry/report_renderer.ts +16 -0
  101. package/src/agent-src/templates/scripts/telemetry_report.ts +22 -2
  102. package/src/config/assurance-capability-registry.json +9 -0
  103. package/src/config/ci-local-parity.yml +6 -0
  104. package/src/config/gate-coverage.yml +225 -0
  105. package/src/config/gate-violation-baselines.json +18 -6
  106. package/src/config/release-gate-locality.yml +12 -0
  107. package/src/scripts/_cli/cmd_doctor.ts +2 -2
  108. package/src/scripts/_cli/cmd_handoff.ts +1 -1
  109. package/src/scripts/_cli/cmd_session_recycle.ts +51 -3
  110. package/src/scripts/_cli/cmd_update.ts +1 -1
  111. package/src/scripts/_cli/cmd_versions.ts +1 -1
  112. package/src/scripts/_lib/adr_frontmatter.ts +44 -0
  113. package/src/scripts/_lib/audit_field_provenance.ts +44 -3
  114. package/src/scripts/_lib/consequence_objects.ts +152 -0
  115. package/src/scripts/_lib/exec_evidence.ts +7 -0
  116. package/src/scripts/_lib/global_deploy_inventory.ts +2 -2
  117. package/src/scripts/_lib/host_capability.ts +38 -0
  118. package/src/scripts/_lib/host_permission_checks.ts +251 -0
  119. package/src/scripts/_lib/install_reach_checks.ts +33 -1
  120. package/src/scripts/_lib/mcp_bridge.ts +222 -10
  121. package/src/scripts/_lib/mcp_consent_residual.ts +173 -0
  122. package/src/scripts/_lib/model_tier.ts +1 -1
  123. package/src/scripts/_lib/obligation_frequency.ts +2 -2
  124. package/src/scripts/_lib/orchestration_record.ts +23 -1
  125. package/src/scripts/_lib/recycle_envelope_paths.ts +190 -0
  126. package/src/scripts/_lib/release_highlights.ts +93 -0
  127. package/src/scripts/_lib/release_material.ts +64 -1
  128. package/src/scripts/_lib/release_position.ts +96 -0
  129. package/src/scripts/_lib/review_skipped_record.ts +17 -1
  130. package/src/scripts/_lib/runtime_journal.ts +26 -3
  131. package/src/scripts/_lib/runtime_wiring_checks.ts +12 -1
  132. package/src/scripts/_lib/security_lint.ts +179 -7
  133. package/src/scripts/_lib/self_repair.ts +141 -6
  134. package/src/scripts/_lib/self_repair_store.ts +273 -7
  135. package/src/scripts/_lib/session_register.ts +11 -6
  136. package/src/scripts/_lib/sqlite_schema.ts +22 -0
  137. package/src/scripts/_lib/subagent_capsule.ts +155 -3
  138. package/src/scripts/_lib/surface.ts +86 -0
  139. package/src/scripts/_lib/ui_authority.ts +116 -0
  140. package/src/scripts/adr/regenerate_index.ts +54 -1
  141. package/src/scripts/adr_cite_check.ts +71 -15
  142. package/src/scripts/ai_council/clients.ts +1 -1
  143. package/src/scripts/autonomy_friction_corpus.ts +229 -0
  144. package/src/scripts/build_proof.ts +15 -2
  145. package/src/scripts/build_rule_trigger_matrix.ts +5 -0
  146. package/src/scripts/check_beta_review_markers.ts +141 -17
  147. package/src/scripts/check_enforcement_coverage.ts +1 -1
  148. package/src/scripts/check_enforcement_denominator.ts +121 -11
  149. package/src/scripts/check_finding_dispositions.ts +66 -9
  150. package/src/scripts/check_gate_coverage.ts +96 -0
  151. package/src/scripts/check_mcp_doc_drift.ts +217 -0
  152. package/src/scripts/check_pack_size.ts +321 -3
  153. package/src/scripts/check_release_highlights.ts +74 -1
  154. package/src/scripts/check_skill_activation_claim.ts +179 -0
  155. package/src/scripts/check_trigger_evals.ts +1 -1
  156. package/src/scripts/conformance_scan.ts +44 -3
  157. package/src/scripts/explain_run.ts +1 -1
  158. package/src/scripts/generate_capability_matrix.ts +1 -1
  159. package/src/scripts/git_authorization_hook.ts +75 -1
  160. package/src/scripts/handoff_context_hook.ts +54 -7
  161. package/src/scripts/hook_manifest.json +1 -1
  162. package/src/scripts/hook_manifest.yaml +14 -11
  163. package/src/scripts/hooks/augment-dispatcher.sh +1 -1
  164. package/src/scripts/hooks/category_a.ts +291 -0
  165. package/src/scripts/hooks/cline-dispatcher.sh +1 -1
  166. package/src/scripts/hooks/cursor-dispatcher.sh +11 -5
  167. package/src/scripts/hooks/dispatch_hook.ts +17 -22
  168. package/src/scripts/hooks/git_command_classifier.ts +156 -1
  169. package/src/scripts/hooks/host_lowering.ts +215 -0
  170. package/src/scripts/hooks/host_lowering.yaml +185 -0
  171. package/src/scripts/hooks/host_semantics.ts +105 -37
  172. package/src/scripts/hooks/journal_record_hook.ts +10 -0
  173. package/src/scripts/hooks/permission_gate.ts +117 -0
  174. package/src/scripts/hooks/session_eol_hook.ts +18 -7
  175. package/src/scripts/hooks_status.ts +12 -10
  176. package/src/scripts/install.ts +57 -86
  177. package/src/scripts/install_anthropic_key.sh +2 -2
  178. package/src/scripts/lint_agent_security.ts +316 -25
  179. package/src/scripts/lint_confusables.ts +42 -6
  180. package/src/scripts/lint_design_antipattern_parity.ts +80 -0
  181. package/src/scripts/lint_evidence_artifacts.ts +45 -1
  182. package/src/scripts/lint_handoffs.ts +0 -132
  183. package/src/scripts/lint_hidden_unicode.ts +49 -7
  184. package/src/scripts/lint_hook_manifest.ts +87 -3
  185. package/src/scripts/lint_instruction_smuggling.ts +42 -6
  186. package/src/scripts/lint_mcp_config_security.ts +42 -6
  187. package/src/scripts/lint_motion_authority_drift.ts +731 -0
  188. package/src/scripts/lint_readme_size.ts +158 -12
  189. package/src/scripts/lint_reasoning_fixtures.ts +216 -0
  190. package/src/scripts/lint_roadmap_later_disposition.ts +217 -27
  191. package/src/scripts/lint_skill_frontmatter_safety.ts +43 -7
  192. package/src/scripts/lint_skill_trigger_contradiction.ts +161 -0
  193. package/src/scripts/mcp_server/resources.ts +41 -0
  194. package/src/scripts/mcp_server/telemetry.ts +19 -4
  195. package/src/scripts/measure_sibling_naming.ts +150 -0
  196. package/src/scripts/orchestration_record.ts +12 -1
  197. package/src/scripts/redact_hook_capture.ts +1 -1
  198. package/src/scripts/release.ts +24 -24
  199. package/src/scripts/release_publication.ts +6 -2
  200. package/src/scripts/report_skill_activation.ts +319 -7
  201. package/src/scripts/schemas/review-findings.schema.json +5 -0
  202. package/src/scripts/schemas/skill.schema.json +2 -2
  203. package/src/scripts/self_repair_cli.ts +31 -1
  204. package/src/scripts/session_canary_hook.ts +1 -1
  205. package/src/scripts/session_register_hook.ts +4 -3
  206. package/src/scripts/skill_scout.ts +69 -0
  207. package/src/scripts/trigger_eval_grandfather.json +5 -1
  208. package/src/scripts/validate_agent_settings.ts +1 -1
  209. package/dist/cli-delegate/chunk-2GNWGMAL.js +0 -172
  210. package/dist/cli-delegate/chunk-LPPWO5CE.js +0 -161
@@ -0,0 +1,173 @@
1
+ /**
2
+ * Per-host MCP consent residuals (roadmap step 3.2).
3
+ *
4
+ * A RESIDUAL is what remains non-automatic after this package has installed:
5
+ * the step the human still has to take before the MCP server actually runs.
6
+ * The step's own framing is the design constraint — "a checklist of things that
7
+ * did work is noise, a named residual is actionable" — so this module models
8
+ * only what is left, and the surfaces that print it print nothing else.
9
+ *
10
+ * Provenance is carried, not assumed.
11
+ *
12
+ * Every residual says where its text came from, and the surfaces print it:
13
+ *
14
+ * - `vendor-doc` — the host's own documentation says so. Nobody here watched it
15
+ * happen. Vendor documentation drifts, so this is a starting value, never a
16
+ * fact about the machine the reader is on.
17
+ * - `observed` — a session on that host recorded it. Only an observation may
18
+ * replace a documented value, and only in that direction.
19
+ *
20
+ * An absent host is UNRECORDED, never "no residual".
21
+ *
22
+ * A host with no entry here has not been looked at. `residualFor` returns
23
+ * `null` and the callers say so in words, because printing nothing would make
24
+ * "this host needs no step" and "nobody checked this host" the same line — the
25
+ * exact confusion the capability manifest's own `default` provenance exists to
26
+ * prevent.
27
+ */
28
+
29
+ import { describeHostCapabilities } from './host_capability.js';
30
+
31
+ /** Where a residual's text came from. Only an observation may replace a doc. */
32
+ export type ResidualSource = 'vendor-doc' | 'observed';
33
+
34
+ export interface ConsentResidual {
35
+ /** Capability-manifest host id (`claude`), not the installer tool id. */
36
+ hostId: string;
37
+ /** The step the human still has to take. One sentence, imperative. */
38
+ residual: string;
39
+ source: ResidualSource;
40
+ /** Why we believe it — a doc reference, or the session that observed it. */
41
+ cite: string;
42
+ }
43
+
44
+ /**
45
+ * Seeded residuals.
46
+ *
47
+ * DELIBERATELY SHORT. Only hosts whose residual is establishable today appear;
48
+ * a row invented for a host nobody checked would be the "vendor-doc residuals
49
+ * harden into asserted facts" risk this roadmap's own register names, arriving
50
+ * one step earlier than predicted. Absence here is the honest state, and the
51
+ * callers render it as such.
52
+ */
53
+ const SEEDED: Readonly<Record<string, ConsentResidual>> = {
54
+ claude: {
55
+ hostId: 'claude',
56
+ residual:
57
+ 'Approve the project-scoped `agent-config` server the first time the host '
58
+ + 'prompts — the installer writes `.mcp.json` but does not pre-approve it.',
59
+ source: 'vendor-doc',
60
+ cite:
61
+ 'Host documentation: a project-scope MCP server requires explicit approval, '
62
+ + 'suppressible only through the user-global `enabledMcpjsonServers` key, '
63
+ + 'which this package deliberately does not write '
64
+ + '(blocker `mcp-user-scope-approval-consent`).',
65
+ },
66
+ };
67
+
68
+ /** Installer `--tools` id → capability-manifest host id, for the report. */
69
+ export const TOOL_TO_HOST: Readonly<Record<string, string>> = {
70
+ 'claude-code': 'claude',
71
+ cursor: 'cursor',
72
+ 'gemini-cli': 'gemini',
73
+ windsurf: 'windsurf',
74
+ cline: 'cline',
75
+ copilot: 'copilot',
76
+ augment: 'augment',
77
+ };
78
+
79
+ /**
80
+ * The residual for one host, or `null` when none is recorded.
81
+ *
82
+ * An OBSERVATION outranks the documented value, and in one direction only: a
83
+ * host whose capability row explicitly records `mcp_needs_manual_activation:
84
+ * false` — an observation, since the field's provenance reads `registry` only
85
+ * when the row wrote it — has no residual left, and that clearance is itself
86
+ * reported as `observed`. The reverse never happens: a documented residual is
87
+ * not upgraded to `observed` by anything short of a session.
88
+ */
89
+ export function residualFor(hostId: string): ConsentResidual | null {
90
+ const { manifest, sources } = describeHostCapabilities(hostId);
91
+ if (sources.mcp_needs_manual_activation === 'registry' && !manifest.mcp_needs_manual_activation) {
92
+ return null;
93
+ }
94
+ return SEEDED[hostId] ?? null;
95
+ }
96
+
97
+ /** Was this host looked at at all? Distinguishes "cleared" from "unrecorded". */
98
+ export function isRecorded(hostId: string): boolean {
99
+ const { sources } = describeHostCapabilities(hostId);
100
+ return hostId in SEEDED || sources.mcp_needs_manual_activation === 'registry';
101
+ }
102
+
103
+ /** One rendered line per host, for the doctor check and the installer report. */
104
+ export interface ResidualLine {
105
+ hostId: string;
106
+ /** `residual` · `cleared` · `unrecorded` — the three honest states. */
107
+ state: 'residual' | 'cleared' | 'unrecorded';
108
+ text: string;
109
+ }
110
+
111
+ /** Render the state of one host. Never silent, in any of the three states. */
112
+ export function residualLine(hostId: string): ResidualLine {
113
+ const r = residualFor(hostId);
114
+ if (r !== null) {
115
+ return {
116
+ hostId,
117
+ state: 'residual',
118
+ text: `${hostId}: ${r.residual} (source: ${r.source} — not observed here)`,
119
+ };
120
+ }
121
+ if (isRecorded(hostId)) {
122
+ return {
123
+ hostId,
124
+ state: 'cleared',
125
+ text: `${hostId}: nothing left to do (source: observed)`,
126
+ };
127
+ }
128
+ return {
129
+ hostId,
130
+ state: 'unrecorded',
131
+ text: `${hostId}: no residual recorded — nobody checked this host, which is not the same as nothing to do`,
132
+ };
133
+ }
134
+
135
+ /**
136
+ * The lines an installer run should print, for the tools it selected.
137
+ *
138
+ * Only hosts with a real residual OR an unrecorded state are returned: a host
139
+ * whose residual was observed away has nothing actionable to say, and the step
140
+ * asks for exactly the actionable set.
141
+ */
142
+ export function residualReport(toolIds: Iterable<string>): ResidualLine[] {
143
+ const seen = new Set<string>();
144
+ const out: ResidualLine[] = [];
145
+ for (const toolId of toolIds) {
146
+ const hostId = TOOL_TO_HOST[toolId];
147
+ if (hostId === undefined || seen.has(hostId)) continue;
148
+ seen.add(hostId);
149
+ const line = residualLine(hostId);
150
+ if (line.state !== 'cleared') out.push(line);
151
+ }
152
+ return out;
153
+ }
154
+
155
+ /**
156
+ * Print the residuals for one install's selected tools, or nothing.
157
+ *
158
+ * Lives here rather than inline in the installer's closing block for the same
159
+ * reason `registerMcpHosts` does: `install.ts` sits far over the source-size
160
+ * ratchet's ceiling, and the rendering belongs beside the table it renders.
161
+ */
162
+ export function printResiduals(
163
+ toolIds: Iterable<string>,
164
+ quiet: boolean,
165
+ write: (s: string) => void = (s) => process.stdout.write(s),
166
+ ): void {
167
+ if (quiet) return;
168
+ const lines = residualReport(toolIds);
169
+ if (lines.length === 0) return;
170
+ write(' Still needs you (MCP):\n');
171
+ for (const line of lines) write(` \u2022 ${line.text}\n`);
172
+ write('\n');
173
+ }
@@ -11,7 +11,7 @@
11
11
  * - the repo generator (`condense.py generate_claude_skills`), which
12
12
  * builds the package's own `.claude/skills/` tree, and
13
13
  * - the consumer install finalizer
14
- * (`install.py finalize_claude_model_tiers`), which rewrites the
14
+ * (the installer's `finalize_claude_model_tiers`), which rewrites the
15
15
  * installed `.claude/skills/` tree so Claude Code performs the
16
16
  * per-turn model switch on a consumer with `model.auto_switch: auto`.
17
17
  *
@@ -275,7 +275,7 @@ export function slot_frequency(platform: string, slot: Slot): Frequency {
275
275
  }
276
276
 
277
277
  /**
278
- * Platforms with no hook surface at all.
278
+ * Platforms this package binds nothing on.
279
279
  *
280
280
  * Copilot declares `fallback_only: true` in the manifest — concerns route
281
281
  * through rule-only fallback and the dispatcher no-ops. Every hook-declared
@@ -294,7 +294,7 @@ export function is_hook_capable(platform_block: Record<string, unknown>): boolea
294
294
  export interface PlatformBinding {
295
295
  /** Platform id → slot → concern ids bound in that slot. */
296
296
  slots: Map<string, Map<Slot, string[]>>;
297
- /** Platform ids that carry no hook surface at all. */
297
+ /** Platform ids declared `fallback_only` — this package binds nothing there. */
298
298
  fallback_only: Set<string>;
299
299
  }
300
300
 
@@ -16,6 +16,7 @@
16
16
  * add one. (Mirrors `domain-safety-pii` § Surface 2 / artifact-engagement.)
17
17
  */
18
18
 
19
+ import { appliedIds } from './audit_field_provenance.js';
19
20
  import type { LookupClass } from './auto_dispatch.js';
20
21
  import type { EvidenceBasis } from './evidence_basis.js';
21
22
  // TYPE-ONLY on purpose. `runtime_journal.ts` imports `node:sqlite` at runtime,
@@ -104,6 +105,24 @@ export interface RecordInput {
104
105
  * the rules bound in the contract.
105
106
  */
106
107
  skills_applied?: string[] | null | undefined;
108
+ /**
109
+ * Stable rule ids the run ACTUALLY carried, as an observation.
110
+ *
111
+ * Until 2026-09-07 this producer wrote the literal `['delegation-policy']`
112
+ * on every line, and so did `review_skipped_record.ts`. Mining the real
113
+ * stream with `extract_audit_patterns --min-count 2` therefore minted
114
+ * exactly ONE pattern at count 1074 over 1104 lines: not a regularity, just
115
+ * arithmetic over a constant. Every per-asset reader downstream was
116
+ * aggregating over the writer rather than over the work.
117
+ *
118
+ * ABSENT now means an EMPTY list, not a fabricated one. An empty list is an
119
+ * honest absence — the same answer `activation_receipt_producer.ts` has
120
+ * always given — and it is what makes the field's variation, when a caller
121
+ * does supply ids, evidence about the run.
122
+ *
123
+ * Ids only, never bodies. Bounded to <= 32, mirroring the contract.
124
+ */
125
+ rules_applied?: string[] | null | undefined;
107
126
  /**
108
127
  * What this dispatch was supposed to produce. Drives the anti-forgery gate
109
128
  * in `envelopeOutcome`. Absent or `unknown` disables the gate for this
@@ -253,6 +272,7 @@ const ORIGIN_RE = /^[a-z0-9][a-z0-9-]{0,63}$/;
253
272
  */
254
273
  const MAX_APPLIED_IDS = 32;
255
274
 
275
+
256
276
  /**
257
277
  * What THIS producer's lines carry. `ids-only` and not `counts-only` because
258
278
  * every line carries `rules_applied`, and some carry `skills_applied`.
@@ -626,7 +646,9 @@ export function buildOrchestrationLine(input: RecordInput): BuiltLine {
626
646
  risk_class: risk,
627
647
  memory: { asks: 0, hits: 0 },
628
648
  verify: { claims: 0, first_try_passes: 0 },
629
- rules_applied: ['delegation-policy'],
649
+ // COMPUTED, never a producer constant. See `rules_applied` on the input
650
+ // interface for the measurement that retired the constant.
651
+ rules_applied: appliedIds(input.rules_applied),
630
652
  // MANDATORY, never optional and never derived by the reader. This line
631
653
  // carries `rules_applied` and may carry `skills_applied`, both of which
632
654
  // are stable artefact ids, so the class it declares is `ids-only`
@@ -5,8 +5,11 @@
5
5
  * when it is stale. A second copy of any of these values is the drift seam
6
6
  * Risk 6 warns about.
7
7
  */
8
+ import * as fs from 'node:fs';
8
9
  import * as path from 'node:path';
9
10
 
11
+ import { safe_stem } from './session_register.js';
12
+
10
13
  /** Where `session:recycle` writes the pending envelope (gitignored runtime state). */
11
14
  export const RECYCLE_ENVELOPE_REL = path.join('agents', 'runtime', 'state', 'recycle-envelope.json');
12
15
 
@@ -33,3 +36,190 @@ export const RECYCLE_MAX_AGE_HOURS = 48;
33
36
  * for the wrapper block and a co-pending handoff.
34
37
  */
35
38
  export const RECYCLE_ENVELOPE_MAX_BYTES = 6144;
39
+
40
+ // ---------------------------------------------------------------------
41
+ // Per-SESSION keying (road-to-one-continuity-record Phase 2.1)
42
+ // ---------------------------------------------------------------------
43
+
44
+ /** State directory the two constants above live in. */
45
+ const _STATE_REL = path.join('agents', 'runtime', 'state');
46
+
47
+ /**
48
+ * Per-SESSION record path. The two constants above name **one file per
49
+ * workspace**, and the session register in the same tree models concurrent
50
+ * sessions per checkout — so the continuity artifact and the identity model
51
+ * disagreed, and the artifact lost. Two sessions in one checkout overwrite
52
+ * each other's record and each resumes from whichever wrote last.
53
+ *
54
+ * This is the same defect `roadmap_claim_rel` (`session_register_hook.ts:105`)
55
+ * was repaired for, and it is deliberately repaired the same way rather than a
56
+ * better way: one shape for "state keyed by session", so a reader who has seen
57
+ * one recognises the other. `safe_stem` is imported from the register rather
58
+ * than re-implemented — a second sanitiser is a second traversal guard to keep
59
+ * correct.
60
+ *
61
+ * **No `latest` index, by decision.** A pointer file naming the newest record
62
+ * would restore the shared write target one layer up and re-create the same
63
+ * race with an extra indirection. Resolution is by register identity,
64
+ * workspace and branch — never by recency.
65
+ *
66
+ * When no session id is resolvable the legacy shared path is returned, so an
67
+ * older record keeps working and the fallback degrades to the previous
68
+ * behaviour instead of losing the record. The reader's ambiguity rule
69
+ * (`resolveContinuityRecord`) is what keeps that fallback from becoming a
70
+ * cross-session read.
71
+ */
72
+ export function recycle_envelope_rel(session_id: string | null | undefined): string {
73
+ return _sessionKeyed(session_id, RECYCLE_ENVELOPE_REL, 'recycle-envelope', '.json');
74
+ }
75
+
76
+ /** Consumed sibling of {@link recycle_envelope_rel}, same keying. */
77
+ export function recycle_consumed_rel(session_id: string | null | undefined): string {
78
+ return _sessionKeyed(session_id, RECYCLE_CONSUMED_REL, 'recycle-envelope', '.consumed.json');
79
+ }
80
+
81
+ /**
82
+ * Shared builder. Containment is asserted locally rather than trusted from
83
+ * `safe_stem`: a guarantee living in another module's implementation is one
84
+ * refactor away from being untrue here, and the failure mode of being wrong is
85
+ * a write outside the state directory.
86
+ */
87
+ function _sessionKeyed(
88
+ session_id: string | null | undefined,
89
+ legacy: string,
90
+ stem: string,
91
+ suffix: string,
92
+ ): string {
93
+ const id = String(session_id ?? '').trim();
94
+ if (id === '') {
95
+ return legacy;
96
+ }
97
+ const rel = path.join(_STATE_REL, `${stem}-${safe_stem(id)}${suffix}`);
98
+ const norm = path.normalize(rel);
99
+ if (!norm.startsWith(`${_STATE_REL}${path.sep}`) || norm.includes('..')) {
100
+ return legacy;
101
+ }
102
+ return norm;
103
+ }
104
+
105
+ /** Every session-keyed record present in a workspace, plus the legacy file. */
106
+ export function listContinuityRecords(workspace_root: string): string[] {
107
+ const dir = path.join(workspace_root, _STATE_REL);
108
+ let names: string[];
109
+ try {
110
+ names = fs.readdirSync(dir);
111
+ } catch {
112
+ return [];
113
+ }
114
+ return names
115
+ .filter(
116
+ (n) =>
117
+ n.startsWith('recycle-envelope') &&
118
+ n.endsWith('.json') &&
119
+ !n.endsWith('.consumed.json'),
120
+ )
121
+ .sort()
122
+ .map((n) => path.join(dir, n));
123
+ }
124
+
125
+ /** What {@link resolveContinuityRecord} decided, and why — the reason is emitted, never dropped. */
126
+ export interface ContinuityResolution {
127
+ /** Absolute path to read, or `null` when the reader must start clean. */
128
+ file: string | null;
129
+ /** One line, always present. On `file === null` this is what the reader says out loud. */
130
+ reason: string;
131
+ }
132
+
133
+ /**
134
+ * Which record THIS session may read.
135
+ *
136
+ * Three cases, and the third is the one the per-session key exists for:
137
+ *
138
+ * 1. **Own id resolves.** Read the record keyed to it, and nothing else. A peer
139
+ * session's record is not a candidate at any point, so "neither observes the
140
+ * other's" holds by construction rather than by a comparison that could be
141
+ * skipped.
142
+ * 2. **No id, one candidate.** Read it. A single record in a checkout with no
143
+ * resolvable identity is the pre-key situation and is unchanged.
144
+ * 3. **No id, several candidates.** START CLEAN and say so. Picking the newest
145
+ * is precisely the recency resolution the no-index rule forbids: it would
146
+ * hand this session a stranger's state and look like a successful resume.
147
+ */
148
+ export function resolveContinuityRecord(
149
+ workspace_root: string,
150
+ session_id: string | null | undefined,
151
+ ): ContinuityResolution {
152
+ const id = String(session_id ?? '').trim();
153
+ if (id !== '') {
154
+ const own = path.join(workspace_root, recycle_envelope_rel(id));
155
+ return fs.existsSync(own)
156
+ ? { file: own, reason: `continuity record for this session (${safe_stem(id)})` }
157
+ : { file: null, reason: `no continuity record for this session (${safe_stem(id)})` };
158
+ }
159
+ const candidates = listContinuityRecords(workspace_root);
160
+ if (candidates.length === 0) {
161
+ return { file: null, reason: 'no continuity record present' };
162
+ }
163
+ if (candidates.length === 1) {
164
+ return { file: candidates[0] as string, reason: 'one continuity record, no session id to key on' };
165
+ }
166
+ return {
167
+ file: null,
168
+ reason:
169
+ `starting clean: ${candidates.length} continuity records in this workspace and no session id ` +
170
+ 'to tell them apart — resuming from the newest would resume a peer session, not this one',
171
+ };
172
+ }
173
+
174
+ /**
175
+ * Is there any trace in this workspace of the session a record names as its
176
+ * predecessor?
177
+ *
178
+ * The normal case resolves through the consumed sibling: when this session
179
+ * started, its reader consumed the predecessor's record and moved it to this
180
+ * session's consumed path, so that file carries `session_id === predecessor`.
181
+ * A live record under the predecessor's own key also counts — that is the
182
+ * shape when the chain has not been walked yet.
183
+ *
184
+ * **A `false` here is a refusal, not a repair.** The reader discards the record
185
+ * and says why. The alternative — injecting a record whose stated lineage
186
+ * cannot be corroborated — is the guess this check exists to prevent, and it is
187
+ * the expensive direction: a refused resume costs one session's context, a
188
+ * wrong resume costs a session working from a stranger's state without knowing
189
+ * it.
190
+ *
191
+ * **Known false-refusal, stated rather than hidden:** the janitor prunes old
192
+ * state, so a long-idle chain can lose its consumed sibling and be refused
193
+ * although it was genuine. That is the same cost as the record having expired,
194
+ * which at {@link RECYCLE_MAX_AGE_HOURS} it very likely has.
195
+ */
196
+ export function predecessorTracePresent(workspace_root: string, predecessor: string): boolean {
197
+ const id = String(predecessor ?? '').trim();
198
+ if (id === '' || id === 'none') {
199
+ return true;
200
+ }
201
+ if (fs.existsSync(path.join(workspace_root, recycle_envelope_rel(id)))) {
202
+ return true;
203
+ }
204
+ const dir = path.join(workspace_root, _STATE_REL);
205
+ let names: string[];
206
+ try {
207
+ names = fs.readdirSync(dir);
208
+ } catch {
209
+ return false;
210
+ }
211
+ for (const n of names) {
212
+ if (!n.startsWith('recycle-envelope') || !n.endsWith('.json')) continue;
213
+ try {
214
+ const raw = JSON.parse(fs.readFileSync(path.join(dir, n), 'utf-8')) as {
215
+ session_id?: unknown;
216
+ };
217
+ if (String(raw.session_id ?? '').trim() === id) {
218
+ return true;
219
+ }
220
+ } catch {
221
+ // unreadable file proves nothing in either direction
222
+ }
223
+ }
224
+ return false;
225
+ }
@@ -29,6 +29,10 @@ import {
29
29
  CURATED_HEAD_INSTRUCTION,
30
30
  MIX_RESPONSE_MARKER,
31
31
  MIX_RESPONSE_PLACEHOLDERS,
32
+ PROMISE_OUTCOMES,
33
+ PROMISE_PHRASE,
34
+ PROMISE_READBACK_MARKER,
35
+ extract_changelog_section,
32
36
  } from './release_material.js';
33
37
 
34
38
  /** The five curated labels, in the order an operator reads them. */
@@ -546,6 +550,95 @@ export function mix_response_blockers(
546
550
  return [];
547
551
  }
548
552
 
553
+ /**
554
+ * The promise the PREVIOUS release head made, or null when it made none.
555
+ *
556
+ * Scoped to that section's governance-response block, not to its whole body: a
557
+ * product line that happens to contain the phrase is not a promise the project
558
+ * made about the next cycle.
559
+ */
560
+ export function previous_promise(changelogText: string, previousVersion: string): string | null {
561
+ const section = extract_changelog_section(changelogText, previousVersion);
562
+ if (section === null) return null;
563
+ const idx = section.body.indexOf(MIX_RESPONSE_MARKER);
564
+ if (idx === -1) return null;
565
+ const block = mix_response_block(section.body, idx);
566
+ return block.includes(PROMISE_PHRASE) ? block : null;
567
+ }
568
+
569
+ /**
570
+ * The ONE predicate for "did this head answer the previous head's promise?".
571
+ *
572
+ * The defect: `_lib/release_material.ts` generates a `Next cycle ships …` line
573
+ * and nothing has ever read the previous one back, so an unmet promise can be
574
+ * restated indefinitely at zero cost — the debt is booked and never called. A
575
+ * promise nobody reads is not a commitment, it is a sentence.
576
+ *
577
+ * Three outcomes are accepted and one of them must be chosen, which is the
578
+ * whole mechanism: it does not require the promise to have been KEPT, and no
579
+ * ratio, deadline or count is enforced here. Refusing an unkept promise would
580
+ * make the honest answer the expensive one and teach an author to promise less
581
+ * rather than to answer truthfully.
582
+ *
583
+ * Length floor and placeholder scan reuse the governance-response discipline
584
+ * for the same reason it exists there: a bare marker, or the writer's own
585
+ * scaffolding, is not an answer a human wrote.
586
+ */
587
+ export function promise_readback_blockers(
588
+ sectionBody: string,
589
+ version: string,
590
+ previousVersion: string,
591
+ promise: string | null,
592
+ where = '`main`',
593
+ ): string[] {
594
+ if (promise === null) return [];
595
+ const idx = sectionBody.indexOf(PROMISE_READBACK_MARKER);
596
+ if (idx === -1) {
597
+ return [
598
+ `the ${version} section does not answer the ${previousVersion} head's ` +
599
+ `\`${PROMISE_PHRASE}\` promise. Add one \`> ${PROMISE_READBACK_MARKER}\` line ` +
600
+ `immediately under the curated head on ${where}, naming the promise and whether ` +
601
+ `it ${PROMISE_OUTCOMES.join(', ')} (the last one with its reason).`,
602
+ ];
603
+ }
604
+ const block = mix_response_block(sectionBody, idx);
605
+ const left = MIX_RESPONSE_PLACEHOLDERS.filter((t) => block.includes(t));
606
+ if (left.length > 0) {
607
+ return [
608
+ `the ${version} previous-cycle answer still carries the writer's placeholder(s) ` +
609
+ `(${left.map((t) => `\`${t}\``).join(', ')}). \`CHANGELOG.md\` is published to npm. ` +
610
+ `Write the outcome on ${where} and re-run.`,
611
+ ];
612
+ }
613
+ const lower = block.toLowerCase();
614
+ const named = PROMISE_OUTCOMES.filter((o) => lower.includes(o));
615
+ if (named.length === 0) {
616
+ return [
617
+ `the ${version} previous-cycle answer names no outcome. It has to say which of ` +
618
+ `${PROMISE_OUTCOMES.join(', ')} applies to the ${previousVersion} promise — an ` +
619
+ 'answer that restates the promise without resolving it is the failure this reads ' +
620
+ 'back for.',
621
+ ];
622
+ }
623
+ if (readback_answer(block).length < MIX_RESPONSE_MIN_CHARS) {
624
+ return [
625
+ `the ${version} previous-cycle answer is shorter than a written answer ` +
626
+ `(${String(MIX_RESPONSE_MIN_CHARS)} characters after the marker). Name what was ` +
627
+ `promised and what became of it on ${where}, not the outcome word alone.`,
628
+ ];
629
+ }
630
+ return [];
631
+ }
632
+
633
+ /** What the author wrote under the read-back marker, marker and quoting removed. */
634
+ export function readback_answer(block: string): string {
635
+ return block
636
+ .replace(PROMISE_READBACK_MARKER, '')
637
+ .replace(/^[>\s.]+|[>\s.]+$/gu, '')
638
+ .replace(/[>\s]+/gu, ' ')
639
+ .trim();
640
+ }
641
+
549
642
  /**
550
643
  * The whole response block: the marker's line plus every blockquote line under it.
551
644
  *
@@ -351,6 +351,69 @@ export const MIX_RESPONSE_PLACEHOLDERS: readonly string[] = [
351
351
  export function render_mix_response(level: string): string[] {
352
352
  return [
353
353
  `> ${MIX_RESPONSE_MARKER} ${level}.`,
354
- `> Next cycle ships ${MIX_RESPONSE_PLACEHOLDER}, tracked in <roadmap or issue>.`,
354
+ `> ${PROMISE_PHRASE} ${MIX_RESPONSE_PLACEHOLDER}, tracked in <roadmap or issue>.`,
355
355
  ];
356
356
  }
357
+
358
+ /**
359
+ * The words that turn a governance response into a PROMISE about a later cycle.
360
+ *
361
+ * Named rather than inlined because two sides now read it: the writer emits it,
362
+ * and the next release reads the previous section for it to decide whether a
363
+ * read-back is owed. A promise nothing reads back costs nothing to restate, so
364
+ * it can be restated for as many cycles as there are cycles — the defect this
365
+ * constant exists to make addressable.
366
+ */
367
+ export const PROMISE_PHRASE = 'Next cycle ships';
368
+
369
+ /**
370
+ * The marker under which a release head answers the PREVIOUS head's promise.
371
+ *
372
+ * A named sentinel, on the same discipline as `MIX_RESPONSE_MARKER` and
373
+ * `DERIVED_MARKER`: a shape match over prose would accept a sentence that
374
+ * happens to mention last cycle and reject a reworded answer.
375
+ */
376
+ export const PROMISE_READBACK_MARKER = '**Previous cycle:**';
377
+
378
+ /**
379
+ * The three outcomes a read-back may report, and the reason there are exactly
380
+ * three.
381
+ *
382
+ * `shipped` and `did not ship` are the honest halves of the fact. `withdrawn`
383
+ * exists because a promise the project decided not to keep is a legitimate
384
+ * third state — but only WITH a reason, which is why the answer also has to
385
+ * clear a written-length floor rather than being one of these three words on
386
+ * its own. Without the third state the mechanism would push an author toward
387
+ * restating a dead promise as still-outstanding, which is the failure it is
388
+ * built to stop.
389
+ */
390
+ export const PROMISE_OUTCOMES: readonly string[] = ['shipped', 'did not ship', 'withdrawn'];
391
+
392
+ /** Every `## X.Y.Z` release version in the file, in document order (newest first). */
393
+ export function changelog_versions(text: string): string[] {
394
+ const out: string[] = [];
395
+ for (const m of text.matchAll(/^##\s+\[?(\d+\.\d+\.\d+)\b/gmu)) {
396
+ out.push(m[1] as string);
397
+ }
398
+ return out;
399
+ }
400
+
401
+ /**
402
+ * The release published immediately before `version`, or null when there is none.
403
+ *
404
+ * Read from the CHANGELOG rather than from git tags on purpose: the promise
405
+ * being read back is a property of the SECTION, and a tag can exist for a
406
+ * version whose section was archived into `docs/archive/` under an era split —
407
+ * in which case there is no previous promise reachable in this file and the
408
+ * honest answer is that none is owed.
409
+ *
410
+ * `version` itself need not be present: a section still under authoring is
411
+ * named `[Unreleased]`, so the caller passes the version it is releasing and
412
+ * gets the newest RELEASED version that is not it.
413
+ */
414
+ export function previous_changelog_version(text: string, version: string): string | null {
415
+ const versions = changelog_versions(text);
416
+ const idx = versions.indexOf(version);
417
+ if (idx !== -1) return versions[idx + 1] ?? null;
418
+ return versions[0] ?? null;
419
+ }