@event4u/agent-config 14.22.0 → 15.0.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 (557) 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 +333 -356
  5. package/README.md +1 -1
  6. package/dist/agent-src/commands/agent-handoff.md +8 -7
  7. package/dist/agent-src/commands/agents/init.md +1 -1
  8. package/dist/agent-src/commands/agents/user/accept.md +4 -4
  9. package/dist/agent-src/commands/agents/user/delete.md +2 -2
  10. package/dist/agent-src/commands/agents/user/init.md +3 -3
  11. package/dist/agent-src/commands/agents/user/review.md +3 -3
  12. package/dist/agent-src/commands/agents/user/show.md +3 -3
  13. package/dist/agent-src/commands/agents/user/update.md +2 -2
  14. package/dist/agent-src/commands/agents/user.md +6 -6
  15. package/dist/agent-src/commands/analytics/prune.md +1 -1
  16. package/dist/agent-src/commands/analytics/show.md +2 -2
  17. package/dist/agent-src/commands/analyze/decision.md +2 -2
  18. package/dist/agent-src/commands/analyze/inbox.md +5 -5
  19. package/dist/agent-src/commands/analyze/incident.md +3 -3
  20. package/dist/agent-src/commands/analyze/near-miss.md +3 -3
  21. package/dist/agent-src/commands/analyze/postmortem.md +4 -4
  22. package/dist/agent-src/commands/analyze/premortem.md +2 -2
  23. package/dist/agent-src/commands/analyze.md +2 -2
  24. package/dist/agent-src/commands/bug/fix.md +1 -1
  25. package/dist/agent-src/commands/bug/investigate.md +2 -2
  26. package/dist/agent-src/commands/cost/profile.md +3 -3
  27. package/dist/agent-src/commands/design.md +1 -1
  28. package/dist/agent-src/commands/feature/explore.md +1 -1
  29. package/dist/agent-src/commands/feature/plan.md +2 -2
  30. package/dist/agent-src/commands/feature/roadmap.md +2 -2
  31. package/dist/agent-src/commands/ghostwriter/fetch.md +4 -4
  32. package/dist/agent-src/commands/ghostwriter/list.md +1 -1
  33. package/dist/agent-src/commands/ghostwriter/show.md +1 -1
  34. package/dist/agent-src/commands/ghostwriter/write.md +9 -9
  35. package/dist/agent-src/commands/humanize.md +6 -6
  36. package/dist/agent-src/commands/jira-ticket.md +1 -1
  37. package/dist/agent-src/commands/judge/solo.md +1 -1
  38. package/dist/agent-src/commands/judge.md +1 -1
  39. package/dist/agent-src/commands/knowledge/cross-repo.md +1 -1
  40. package/dist/agent-src/commands/knowledge/forget.md +1 -1
  41. package/dist/agent-src/commands/knowledge/ingest.md +2 -2
  42. package/dist/agent-src/commands/knowledge/list.md +1 -1
  43. package/dist/agent-src/commands/memory/add.md +3 -3
  44. package/dist/agent-src/commands/memory/load.md +4 -4
  45. package/dist/agent-src/commands/memory/mine-session.md +1 -1
  46. package/dist/agent-src/commands/memory/promote.md +4 -4
  47. package/dist/agent-src/commands/memory/propose.md +2 -2
  48. package/dist/agent-src/commands/mode.md +5 -5
  49. package/dist/agent-src/commands/post-as/ghostwriter.md +3 -3
  50. package/dist/agent-src/commands/post-as/me.md +7 -7
  51. package/dist/agent-src/commands/pr/create.md +1 -1
  52. package/dist/agent-src/commands/pr/merge.md +48 -39
  53. package/dist/agent-src/commands/profile/activate.md +1 -1
  54. package/dist/agent-src/commands/profile/show.md +1 -1
  55. package/dist/agent-src/commands/research/deep.md +2 -2
  56. package/dist/agent-src/commands/research/report.md +2 -2
  57. package/dist/agent-src/commands/review/changes.md +1 -1
  58. package/dist/agent-src/commands/review/routing.md +1 -1
  59. package/dist/agent-src/commands/roadmap/create.md +1 -1
  60. package/dist/agent-src/commands/roadmap/materialize.md +2 -2
  61. package/dist/agent-src/commands/roadmap/next.md +9 -1
  62. package/dist/agent-src/commands/roadmap/process-full.md +2 -2
  63. package/dist/agent-src/commands/security-audit-config.md +3 -3
  64. package/dist/agent-src/commands/skill/preview.md +1 -1
  65. package/dist/agent-src/commands/skills/discover.md +1 -1
  66. package/dist/agent-src/commands/sync/agent-settings.md +2 -2
  67. package/dist/agent-src/commands/sync/gitignore/fix.md +1 -1
  68. package/dist/agent-src/commands/tests/create.md +1 -1
  69. package/dist/agent-src/commands/tests/e2e-heal.md +1 -1
  70. package/dist/agent-src/commands/tests/e2e-plan.md +1 -1
  71. package/dist/agent-src/contexts/authority/kernel-rule-edits.md +6 -6
  72. package/dist/agent-src/contexts/communication/rules-auto/slash-command-routing-policy-mechanics.md +1 -2
  73. package/dist/agent-src/contexts/communication/rules-auto/think-before-action-mechanics.md +2 -2
  74. package/dist/agent-src/contexts/communication/rules-auto/token-efficiency-mechanics.md +88 -1
  75. package/dist/agent-src/contexts/communication/rules-auto/user-interaction-mechanics.md +1 -1
  76. package/dist/agent-src/contexts/contracts/frugality-charter.md +1 -1
  77. package/dist/agent-src/contexts/execution/auto-dispatch-classification.md +1 -1
  78. package/dist/agent-src/contexts/execution/auto-orchestration-activation.md +1 -1
  79. package/dist/agent-src/contexts/execution/autonomy-mechanics.md +13 -1
  80. package/dist/agent-src/contexts/execution/cheap-question-mechanics.md +1 -1
  81. package/dist/agent-src/contexts/execution/contract-decision-sheet.md +3 -3
  82. package/dist/agent-src/contexts/execution/mandated-lines.md +1 -1
  83. package/dist/agent-src/contexts/execution/non-interactive-contract.md +1 -1
  84. package/dist/agent-src/contexts/execution/orchestration-telemetry.md +1 -1
  85. package/dist/agent-src/contexts/execution/plan-confidence-gate.md +5 -5
  86. package/dist/agent-src/contexts/execution/roadmap-execution-contract.md +1 -1
  87. package/dist/agent-src/contexts/execution/subagent-modes-detail.md +1 -1
  88. package/dist/agent-src/contexts/execution/subagent-routing.md +2 -2
  89. package/dist/agent-src/contexts/execution/subagent-spawn-contract.md +1 -1
  90. package/dist/agent-src/contexts/execution/user-memory-channels.md +3 -3
  91. package/dist/agent-src/contexts/execution/verification-mechanics.md +1 -1
  92. package/dist/agent-src/contexts/judges/persona-voice-rubric.md +2 -2
  93. package/dist/agent-src/guidelines/abstraction-thresholds.md +88 -0
  94. package/dist/agent-src/guidelines/agent-infra/5w2h-analysis.md +260 -0
  95. package/dist/agent-src/guidelines/agent-infra/active-remediation-mechanics.md +99 -0
  96. package/dist/agent-src/guidelines/agent-infra/agent-interaction-and-decision-quality.md +325 -0
  97. package/dist/agent-src/guidelines/agent-infra/api-cost-levers.md +177 -0
  98. package/dist/agent-src/guidelines/agent-infra/artifact-drafting-protocol-mechanics.md +222 -0
  99. package/dist/agent-src/guidelines/agent-infra/ask-when-uncertain-demos.md +134 -0
  100. package/dist/agent-src/guidelines/agent-infra/asking-and-brevity-examples.md +198 -0
  101. package/dist/agent-src/guidelines/agent-infra/break-glass-usage.md +113 -0
  102. package/dist/agent-src/guidelines/agent-infra/carve-out-predicates.md +17 -0
  103. package/dist/agent-src/guidelines/agent-infra/comparison-matrix.md +179 -0
  104. package/dist/agent-src/guidelines/agent-infra/context-hygiene-mechanics.md +165 -0
  105. package/dist/agent-src/guidelines/agent-infra/corpus-grounding-authoring.md +91 -0
  106. package/dist/agent-src/guidelines/agent-infra/critical-thinking.md +156 -0
  107. package/dist/agent-src/guidelines/agent-infra/cross-source-consistency-mechanics.md +109 -0
  108. package/dist/agent-src/guidelines/agent-infra/developer-judgment.md +82 -0
  109. package/dist/agent-src/guidelines/agent-infra/direct-answers-demos.md +142 -0
  110. package/dist/agent-src/guidelines/agent-infra/domain-adoption-gates.md +112 -0
  111. package/dist/agent-src/guidelines/agent-infra/domain-eval-anti-pattern.md +53 -0
  112. package/dist/agent-src/guidelines/agent-infra/domain-pack-architecture.md +56 -0
  113. package/dist/agent-src/guidelines/agent-infra/downstream-changes-mechanics.md +91 -0
  114. package/dist/agent-src/guidelines/agent-infra/emphasis-budget.md +45 -0
  115. package/dist/agent-src/guidelines/agent-infra/engineering-memory-data-format.md +210 -0
  116. package/dist/agent-src/guidelines/agent-infra/evaluator-independence-mechanics.md +92 -0
  117. package/dist/agent-src/guidelines/agent-infra/existence-question-verdicts.md +80 -0
  118. package/dist/agent-src/guidelines/agent-infra/failure-signatures.md +70 -0
  119. package/dist/agent-src/guidelines/agent-infra/false-green.md +233 -0
  120. package/dist/agent-src/guidelines/agent-infra/first-principles.md +192 -0
  121. package/dist/agent-src/guidelines/agent-infra/framework-neutrality-patterns.md +66 -0
  122. package/dist/agent-src/guidelines/agent-infra/frontier-reasoning-operating-profile.md +169 -0
  123. package/dist/agent-src/guidelines/agent-infra/gate-authoring.md +177 -0
  124. package/dist/agent-src/guidelines/agent-infra/guarded-baseline.md +39 -0
  125. package/dist/agent-src/guidelines/agent-infra/installed-tools-manifest.md +142 -0
  126. package/dist/agent-src/guidelines/agent-infra/inversion-thinking.md +388 -0
  127. package/dist/agent-src/guidelines/agent-infra/ios-simulator-guide.md +378 -0
  128. package/dist/agent-src/guidelines/agent-infra/language-and-tone-examples.md +141 -0
  129. package/dist/agent-src/guidelines/agent-infra/layered-settings.md +317 -0
  130. package/dist/agent-src/guidelines/agent-infra/linked-projects-onboarding-gate.md +80 -0
  131. package/dist/agent-src/guidelines/agent-infra/mcp-request-signing.md +197 -0
  132. package/dist/agent-src/guidelines/agent-infra/memory-access.md +149 -0
  133. package/dist/agent-src/guidelines/agent-infra/mental-models.md +314 -0
  134. package/dist/agent-src/guidelines/agent-infra/minimal-safe-diff-mechanics.md +241 -0
  135. package/dist/agent-src/guidelines/agent-infra/missing-tool-handling.md +66 -0
  136. package/dist/agent-src/guidelines/agent-infra/model-recommendation.md +101 -0
  137. package/dist/agent-src/guidelines/agent-infra/naming.md +69 -0
  138. package/dist/agent-src/guidelines/agent-infra/notes-horizon-mechanics.md +90 -0
  139. package/dist/agent-src/guidelines/agent-infra/output-patterns.md +117 -0
  140. package/dist/agent-src/guidelines/agent-infra/recurring-criticism-mechanics.md +96 -0
  141. package/dist/agent-src/guidelines/agent-infra/reuse-verdict-mechanics.md +71 -0
  142. package/dist/agent-src/guidelines/agent-infra/review-routing-data-format.md +143 -0
  143. package/dist/agent-src/guidelines/agent-infra/roadmap-deferred-resolution-provenance.md +54 -0
  144. package/dist/agent-src/guidelines/agent-infra/roadmap-progress-mechanics.md +274 -0
  145. package/dist/agent-src/guidelines/agent-infra/role-contracts.md +209 -0
  146. package/dist/agent-src/guidelines/agent-infra/role-mode-router.md +89 -0
  147. package/dist/agent-src/guidelines/agent-infra/rule-body-migration-inventory.md +241 -0
  148. package/dist/agent-src/guidelines/agent-infra/rule-type-governance.md +73 -0
  149. package/dist/agent-src/guidelines/agent-infra/runtime-layer.md +89 -0
  150. package/dist/agent-src/guidelines/agent-infra/scqa-framework.md +526 -0
  151. package/dist/agent-src/guidelines/agent-infra/security-lint-containment.md +121 -0
  152. package/dist/agent-src/guidelines/agent-infra/self-improvement-pipeline.md +134 -0
  153. package/dist/agent-src/guidelines/agent-infra/simplicity-and-goal-demos.md +211 -0
  154. package/dist/agent-src/guidelines/agent-infra/six-hats.md +353 -0
  155. package/dist/agent-src/guidelines/agent-infra/size-and-scope.md +229 -0
  156. package/dist/agent-src/guidelines/agent-infra/skill-quality-checklist.md +157 -0
  157. package/dist/agent-src/guidelines/agent-infra/source-confidentiality-mechanics.md +109 -0
  158. package/dist/agent-src/guidelines/agent-infra/symptom-driven-harvest-loop.md +104 -0
  159. package/dist/agent-src/guidelines/agent-infra/systems-thinking.md +220 -0
  160. package/dist/agent-src/guidelines/agent-infra/tool-description-as-policy.md +35 -0
  161. package/dist/agent-src/guidelines/agent-infra/tool-integration.md +73 -0
  162. package/dist/agent-src/guidelines/agent-infra/traceability-field-mechanics.md +50 -0
  163. package/dist/agent-src/guidelines/agent-infra/untrusted-input-spotlighting.md +166 -0
  164. package/dist/agent-src/guidelines/agent-infra/verify-before-complete-demos.md +128 -0
  165. package/dist/agent-src/guidelines/augment-portability-patterns.md +70 -0
  166. package/dist/agent-src/guidelines/code-clarity.md +350 -0
  167. package/dist/agent-src/guidelines/component-oriented-and-oop-development.md +189 -0
  168. package/dist/agent-src/guidelines/cross-role-handoff.md +127 -0
  169. package/dist/agent-src/guidelines/design-antipatterns-triggers.json +18 -0
  170. package/dist/agent-src/guidelines/design-antipatterns.md +327 -0
  171. package/dist/agent-src/guidelines/design-asset-discipline.md +38 -0
  172. package/dist/agent-src/guidelines/design-canon.md +63 -0
  173. package/dist/agent-src/guidelines/design-fidelity-mechanics.md +293 -0
  174. package/dist/agent-src/guidelines/design-fidelity-routing.md +78 -0
  175. package/dist/agent-src/guidelines/design-handover-extraction.md +117 -0
  176. package/dist/agent-src/guidelines/design-modes.md +143 -0
  177. package/dist/agent-src/guidelines/design-reconciliation.md +172 -0
  178. package/dist/agent-src/guidelines/docs/readme-size-and-splitting.md +209 -0
  179. package/dist/agent-src/guidelines/e2e/playwright.md +363 -0
  180. package/dist/agent-src/guidelines/gtm-handoff.md +114 -0
  181. package/dist/agent-src/guidelines/monorepo-antipatterns.md +141 -0
  182. package/dist/agent-src/guidelines/php/api-design.md +115 -0
  183. package/dist/agent-src/guidelines/php/artisan-commands.md +81 -0
  184. package/dist/agent-src/guidelines/php/blade-ui.md +78 -0
  185. package/dist/agent-src/guidelines/php/controllers.md +90 -0
  186. package/dist/agent-src/guidelines/php/database.md +194 -0
  187. package/dist/agent-src/guidelines/php/eloquent.md +208 -0
  188. package/dist/agent-src/guidelines/php/flux.md +80 -0
  189. package/dist/agent-src/guidelines/php/general.md +207 -0
  190. package/dist/agent-src/guidelines/php/git.md +164 -0
  191. package/dist/agent-src/guidelines/php/jobs.md +111 -0
  192. package/dist/agent-src/guidelines/php/livewire.md +71 -0
  193. package/dist/agent-src/guidelines/php/logging.md +79 -0
  194. package/dist/agent-src/guidelines/php/naming.md +89 -0
  195. package/dist/agent-src/guidelines/php/patterns/dependency-injection.md +57 -0
  196. package/dist/agent-src/guidelines/php/patterns/dtos.md +199 -0
  197. package/dist/agent-src/guidelines/php/patterns/events.md +67 -0
  198. package/dist/agent-src/guidelines/php/patterns/factory.md +53 -0
  199. package/dist/agent-src/guidelines/php/patterns/pipelines.md +66 -0
  200. package/dist/agent-src/guidelines/php/patterns/policies.md +66 -0
  201. package/dist/agent-src/guidelines/php/patterns/repositories.md +122 -0
  202. package/dist/agent-src/guidelines/php/patterns/service-layer.md +64 -0
  203. package/dist/agent-src/guidelines/php/patterns/strategy.md +247 -0
  204. package/dist/agent-src/guidelines/php/patterns.md +28 -0
  205. package/dist/agent-src/guidelines/php/performance.md +92 -0
  206. package/dist/agent-src/guidelines/php/php-coding-patterns.md +80 -0
  207. package/dist/agent-src/guidelines/php/resources.md +100 -0
  208. package/dist/agent-src/guidelines/php/security.md +110 -0
  209. package/dist/agent-src/guidelines/php/sql.md +97 -0
  210. package/dist/agent-src/guidelines/php/validations.md +119 -0
  211. package/dist/agent-src/guidelines/php/websocket.md +100 -0
  212. package/dist/agent-src/guidelines/prompt-templates.md +189 -0
  213. package/dist/agent-src/guidelines/redundancy-taxonomy.md +208 -0
  214. package/dist/agent-src/guidelines/wing4-handoff.md +127 -0
  215. package/dist/agent-src/personas/README.md +1 -1
  216. package/dist/agent-src/personas/_template-specialist/persona.md +1 -1
  217. package/dist/agent-src/personas/product-owner.md +1 -1
  218. package/dist/agent-src/rules/active-remediation.md +1 -1
  219. package/dist/agent-src/rules/architecture.md +2 -2
  220. package/dist/agent-src/rules/artifact-drafting-protocol.md +1 -1
  221. package/dist/agent-src/rules/ask-when-uncertain.md +2 -2
  222. package/dist/agent-src/rules/brand-source-of-truth.md +9 -7
  223. package/dist/agent-src/rules/code-comment-discipline.md +2 -2
  224. package/dist/agent-src/rules/code-provenance.md +1 -1
  225. package/dist/agent-src/rules/content-quoting-floor.md +1 -1
  226. package/dist/agent-src/rules/context-hygiene.md +5 -5
  227. package/dist/agent-src/rules/cross-source-consistency.md +1 -1
  228. package/dist/agent-src/rules/design-fidelity.md +36 -46
  229. package/dist/agent-src/rules/direct-answers.md +2 -2
  230. package/dist/agent-src/rules/domain-adoption-policy.md +1 -1
  231. package/dist/agent-src/rules/evaluator-independence.md +1 -1
  232. package/dist/agent-src/rules/framework-neutrality-in-generic-skills.md +1 -1
  233. package/dist/agent-src/rules/icon-consistency.md +5 -1
  234. package/dist/agent-src/rules/improve-before-implement.md +1 -1
  235. package/dist/agent-src/rules/language-and-tone.md +3 -3
  236. package/dist/agent-src/rules/minimal-safe-diff.md +1 -1
  237. package/dist/agent-src/rules/notes-first-reasoning.md +36 -25
  238. package/dist/agent-src/rules/recurring-criticism.md +1 -1
  239. package/dist/agent-src/rules/roadmap-progress-sync.md +1 -1
  240. package/dist/agent-src/rules/role-mode-adherence.md +2 -2
  241. package/dist/agent-src/rules/security-sensitive-stop.md +1 -1
  242. package/dist/agent-src/rules/size-enforcement.md +1 -1
  243. package/dist/agent-src/rules/source-confidentiality.md +2 -2
  244. package/dist/agent-src/rules/think-before-action.md +1 -1
  245. package/dist/agent-src/rules/token-efficiency.md +7 -5
  246. package/dist/agent-src/rules/ui-audit-gate.md +3 -2
  247. package/dist/agent-src/rules/untrusted-input-defense.md +3 -3
  248. package/dist/agent-src/scripts/archive_completed_roadmaps.ts +106 -4
  249. package/dist/agent-src/skills/activation-design/SKILL.md +2 -2
  250. package/dist/agent-src/skills/adr-create/SKILL.md +5 -5
  251. package/dist/agent-src/skills/ai-council/SKILL.md +1 -1
  252. package/dist/agent-src/skills/ai-council/references/advanced-modes.md +1 -1
  253. package/dist/agent-src/skills/blade-ui/SKILL.md +1 -1
  254. package/dist/agent-src/skills/blast-radius-analyzer/SKILL.md +1 -1
  255. package/dist/agent-src/skills/brand-to-tokens/SKILL.md +24 -3
  256. package/dist/agent-src/skills/bug-analyzer/SKILL.md +1 -1
  257. package/dist/agent-src/skills/build-buy-partner/SKILL.md +1 -1
  258. package/dist/agent-src/skills/churn-prevention/SKILL.md +2 -2
  259. package/dist/agent-src/skills/code-intelligence/SKILL.md +57 -1
  260. package/dist/agent-src/skills/code-refactoring/SKILL.md +3 -3
  261. package/dist/agent-src/skills/code-review/SKILL.md +3 -3
  262. package/dist/agent-src/skills/command-writing/SKILL.md +3 -3
  263. package/dist/agent-src/skills/comp-banding/SKILL.md +1 -1
  264. package/dist/agent-src/skills/competitive-moat-analysis/SKILL.md +1 -1
  265. package/dist/agent-src/skills/competitive-positioning/SKILL.md +2 -2
  266. package/dist/agent-src/skills/complexity-first-planning/SKILL.md +1 -1
  267. package/dist/agent-src/skills/condense-memory/SKILL.md +1 -1
  268. package/dist/agent-src/skills/content-funnel-design/SKILL.md +2 -2
  269. package/dist/agent-src/skills/contracts-cognition/SKILL.md +1 -1
  270. package/dist/agent-src/skills/copilot-agents-optimization/SKILL.md +2 -2
  271. package/dist/agent-src/skills/corpus-grounding/SKILL.md +2 -2
  272. package/dist/agent-src/skills/customer-research/SKILL.md +2 -2
  273. package/dist/agent-src/skills/dashboard-design/SKILL.md +1 -1
  274. package/dist/agent-src/skills/data-handling-judgment/SKILL.md +1 -1
  275. package/dist/agent-src/skills/deal-qualification-meddic/SKILL.md +2 -2
  276. package/dist/agent-src/skills/decision-record/SKILL.md +3 -3
  277. package/dist/agent-src/skills/decision-review/SKILL.md +8 -8
  278. package/dist/agent-src/skills/deep-reading-analyst/SKILL.md +22 -22
  279. package/dist/agent-src/skills/design-intelligence/SKILL.md +4 -4
  280. package/dist/agent-src/skills/design-intelligence/references/context-and-registers.md +2 -2
  281. package/dist/agent-src/skills/design-review/SKILL.md +4 -4
  282. package/dist/agent-src/skills/design-system-capture/references/design-system-json.md +20 -1
  283. package/dist/agent-src/skills/design-tokens/SKILL.md +17 -2
  284. package/dist/agent-src/skills/design-variations/SKILL.md +2 -2
  285. package/dist/agent-src/skills/developer-like-execution/SKILL.md +2 -2
  286. package/dist/agent-src/skills/discovery-interview/SKILL.md +2 -2
  287. package/dist/agent-src/skills/doc-coauthoring/SKILL.md +3 -3
  288. package/dist/agent-src/skills/editorial-calendar/SKILL.md +2 -2
  289. package/dist/agent-src/skills/existing-ui-audit/SKILL.md +44 -1
  290. package/dist/agent-src/skills/existing-ui-audit/references/anti-slop-cross-reference.md +2 -2
  291. package/dist/agent-src/skills/expansion-playbook/SKILL.md +2 -2
  292. package/dist/agent-src/skills/experiment-loop/SKILL.md +1 -1
  293. package/dist/agent-src/skills/experiment-loop/references/protocol.md +1 -1
  294. package/dist/agent-src/skills/fe-design/SKILL.md +3 -3
  295. package/dist/agent-src/skills/fe-design/references/design-patterns.md +1 -1
  296. package/dist/agent-src/skills/fe-design/references/design-read-and-memory.md +3 -3
  297. package/dist/agent-src/skills/fe-design/references/source-led-port.md +1 -1
  298. package/dist/agent-src/skills/feature-planning/SKILL.md +1 -1
  299. package/dist/agent-src/skills/flux/SKILL.md +1 -1
  300. package/dist/agent-src/skills/forecast-accuracy/SKILL.md +2 -2
  301. package/dist/agent-src/skills/forecasting/SKILL.md +2 -2
  302. package/dist/agent-src/skills/fundraising-narrative/SKILL.md +2 -2
  303. package/dist/agent-src/skills/funnel-analysis/SKILL.md +2 -2
  304. package/dist/agent-src/skills/git-workflow/references/push-closes-its-loop.md +9 -0
  305. package/dist/agent-src/skills/gtm-launch/SKILL.md +2 -2
  306. package/dist/agent-src/skills/hiring-loop-design/SKILL.md +1 -1
  307. package/dist/agent-src/skills/html-deck/SKILL.md +5 -5
  308. package/dist/agent-src/skills/humanizer/SKILL.md +1 -1
  309. package/dist/agent-src/skills/humanizer/data/patterns.md +1 -1
  310. package/dist/agent-src/skills/iconography/SKILL.md +3 -3
  311. package/dist/agent-src/skills/judge-synthesis/SKILL.md +1 -1
  312. package/dist/agent-src/skills/laravel-api-endpoint/SKILL.md +2 -2
  313. package/dist/agent-src/skills/launch-readiness/SKILL.md +3 -3
  314. package/dist/agent-src/skills/learning-to-rule-or-skill/SKILL.md +6 -6
  315. package/dist/agent-src/skills/livewire/SKILL.md +1 -1
  316. package/dist/agent-src/skills/market-entry-analysis/SKILL.md +1 -1
  317. package/dist/agent-src/skills/mcp-builder/SKILL.md +1 -1
  318. package/dist/agent-src/skills/memory-consolidation/SKILL.md +3 -3
  319. package/dist/agent-src/skills/messaging-architecture/SKILL.md +2 -2
  320. package/dist/agent-src/skills/monorepo-workspace/SKILL.md +1 -1
  321. package/dist/agent-src/skills/onboarding-design/SKILL.md +2 -2
  322. package/dist/agent-src/skills/onboarding-program/SKILL.md +1 -1
  323. package/dist/agent-src/skills/one-on-one-cadence/SKILL.md +1 -1
  324. package/dist/agent-src/skills/org-design/SKILL.md +1 -1
  325. package/dist/agent-src/skills/overbuild-review-lens/SKILL.md +3 -3
  326. package/dist/agent-src/skills/override-management/SKILL.md +1 -1
  327. package/dist/agent-src/skills/perf-feedback-craft/SKILL.md +1 -1
  328. package/dist/agent-src/skills/persona-writing/SKILL.md +1 -1
  329. package/dist/agent-src/skills/php-coder/SKILL.md +1 -1
  330. package/dist/agent-src/skills/pipeline-strategy/SKILL.md +2 -2
  331. package/dist/agent-src/skills/playwright-testing/SKILL.md +3 -3
  332. package/dist/agent-src/skills/po-discovery/SKILL.md +1 -1
  333. package/dist/agent-src/skills/positioning-strategy/SKILL.md +2 -2
  334. package/dist/agent-src/skills/privacy-review/SKILL.md +1 -1
  335. package/dist/agent-src/skills/prompt-optimizer/SKILL.md +3 -3
  336. package/dist/agent-src/skills/react-shadcn-ui/SKILL.md +1 -1
  337. package/dist/agent-src/skills/reasoning-orchestrator/SKILL.md +2 -2
  338. package/dist/agent-src/skills/receiving-code-review/SKILL.md +1 -1
  339. package/dist/agent-src/skills/refine-ticket/SKILL.md +2 -2
  340. package/dist/agent-src/skills/release-comms/SKILL.md +2 -2
  341. package/dist/agent-src/skills/retention-loops/SKILL.md +2 -2
  342. package/dist/agent-src/skills/review-routing/SKILL.md +2 -2
  343. package/dist/agent-src/skills/roadmap-management/SKILL.md +1 -1
  344. package/dist/agent-src/skills/roadmap-management/references/archival.md +1 -1
  345. package/dist/agent-src/skills/roadmap-writing/SKILL.md +5 -5
  346. package/dist/agent-src/skills/rule-writing/SKILL.md +6 -6
  347. package/dist/agent-src/skills/runway-cognition/SKILL.md +2 -2
  348. package/dist/agent-src/skills/scenario-modeling/SKILL.md +2 -2
  349. package/dist/agent-src/skills/skill-writing/SKILL.md +2 -2
  350. package/dist/agent-src/skills/skill-writing/references/procedure.md +1 -1
  351. package/dist/agent-src/skills/skill-writing/references/section-patterns.md +3 -3
  352. package/dist/agent-src/skills/spreadsheet-authoring/SKILL.md +1 -1
  353. package/dist/agent-src/skills/stakeholder-tradeoff/SKILL.md +3 -3
  354. package/dist/agent-src/skills/subagent-orchestration/SKILL.md +1 -1
  355. package/dist/agent-src/skills/supply-chain-intake/SKILL.md +1 -1
  356. package/dist/agent-src/skills/systematic-debugging/SKILL.md +2 -2
  357. package/dist/agent-src/skills/tailwind-engineer/SKILL.md +60 -4
  358. package/dist/agent-src/skills/throughput-vs-morale-tradeoff/SKILL.md +1 -1
  359. package/dist/agent-src/skills/token-optimizer/SKILL.md +1 -1
  360. package/dist/agent-src/skills/typography-system/SKILL.md +2 -2
  361. package/dist/agent-src/skills/ui-component-architect/SKILL.md +28 -8
  362. package/dist/agent-src/skills/unit-economics-modeling/SKILL.md +2 -2
  363. package/dist/agent-src/skills/upstream-contribute/SKILL.md +1 -1
  364. package/dist/agent-src/skills/validate-feature-fit/SKILL.md +2 -2
  365. package/dist/agent-src/skills/verify-repair-loop/SKILL.md +2 -2
  366. package/dist/agent-src/skills/vision-articulation/SKILL.md +1 -1
  367. package/dist/agent-src/skills/voc-extract/SKILL.md +2 -2
  368. package/dist/agent-src/skills/voice-and-tone-design/SKILL.md +2 -2
  369. package/dist/agent-src/skills/wireframe/SKILL.md +1 -1
  370. package/dist/agent-src/skills/workspace-link/SKILL.md +1 -1
  371. package/dist/agent-src/templates/agent-settings.md +6 -6
  372. package/dist/agent-src/templates/agents/agent-project-settings.example.yml +1 -1
  373. package/dist/agent-src/templates/contexts/knowledge-pages.md +2 -3
  374. package/dist/agent-src/templates/copilot-instructions.md +1 -1
  375. package/dist/agent-src/templates/roadmaps.md +1 -1
  376. package/dist/agent-src/templates/scripts/README.md +2 -2
  377. package/dist/agent-src/user-types/_template/user-type.md +1 -1
  378. package/dist/cli/registry.js +1 -1
  379. package/dist/cli-delegate/archive_completed_roadmaps.js +52 -4
  380. package/dist/cli-delegate/check_update_banner.js +12 -1
  381. package/dist/cli-delegate/{chunk-TGIPZBCW.js → chunk-2KKXJW25.js} +22 -8
  382. package/dist/cli-delegate/{chunk-NLP54U3J.js → chunk-2XY7ILBT.js} +13 -1
  383. package/dist/cli-delegate/{chunk-4FS4WRXP.js → chunk-3FKR64R5.js} +34 -6
  384. package/dist/cli-delegate/{chunk-J3RFNKG4.js → chunk-55ACLDUE.js} +2 -2
  385. package/dist/cli-delegate/{chunk-PKOBQADP.js → chunk-57PN4GYJ.js} +1 -1
  386. package/dist/cli-delegate/{chunk-BIXEDQPZ.js → chunk-AK6HODNQ.js} +4 -4
  387. package/dist/cli-delegate/{chunk-2FTMENJL.js → chunk-GYC3VRYN.js} +1 -1
  388. package/dist/cli-delegate/{chunk-LWTYTX6F.js → chunk-HZ52BRGG.js} +24 -18
  389. package/dist/cli-delegate/{chunk-6RJA2F5A.js → chunk-IVBPRCQO.js} +1 -1
  390. package/dist/cli-delegate/{chunk-I6RHFQEG.js → chunk-YUM5KURG.js} +17 -1
  391. package/dist/cli-delegate/cmd_analyze_session.js +1 -1
  392. package/dist/cli-delegate/cmd_brand_status.js +1 -1
  393. package/dist/cli-delegate/cmd_conformance.js +3 -3
  394. package/dist/cli-delegate/cmd_doctor.js +3 -3
  395. package/dist/cli-delegate/cmd_explain.js +2 -2
  396. package/dist/cli-delegate/cmd_export.js +4 -4
  397. package/dist/cli-delegate/cmd_handoff.js +24 -16
  398. package/dist/cli-delegate/cmd_mcp_available.js +1 -1
  399. package/dist/cli-delegate/cmd_memory_get.js +1 -1
  400. package/dist/cli-delegate/cmd_migrate.js +2 -2
  401. package/dist/cli-delegate/cmd_packs_active.js +2 -2
  402. package/dist/cli-delegate/cmd_prune.js +4 -4
  403. package/dist/cli-delegate/cmd_route_audit.js +3 -3
  404. package/dist/cli-delegate/cmd_route_explain.js +1 -1
  405. package/dist/cli-delegate/cmd_session_recycle.js +4 -14
  406. package/dist/cli-delegate/cmd_settings_get.js +15 -9
  407. package/dist/cli-delegate/cmd_settings_migrate.js +4 -4
  408. package/dist/cli-delegate/cmd_settings_set.js +41 -11
  409. package/dist/cli-delegate/cmd_sync.js +4 -4
  410. package/dist/cli-delegate/cmd_uninstall.js +4 -4
  411. package/dist/cli-delegate/cmd_update.js +1 -1
  412. package/dist/cli-delegate/cmd_upgrade.js +1 -1
  413. package/dist/cli-delegate/cmd_validate.js +4 -4
  414. package/dist/cli-delegate/cmd_versions.js +1 -1
  415. package/dist/discovery/deprecation-report.md +1 -1
  416. package/dist/discovery/discovery-manifest.json +27 -80
  417. package/dist/discovery/discovery-manifest.json.sha256 +1 -1
  418. package/dist/discovery/discovery-manifest.summary.md +3 -3
  419. package/dist/discovery/dormancy-report.md +1 -1
  420. package/dist/discovery/orphan-report.md +1 -1
  421. package/dist/discovery/packs.json +5 -7
  422. package/dist/discovery/trust-report.md +2 -2
  423. package/dist/discovery/workspaces.json +4 -6
  424. package/dist/hooks/dispatch.js +9249 -8981
  425. package/dist/install/install.mjs +47 -8
  426. package/dist/mcp/registry-manifest.json +3 -3
  427. package/dist/mcp/server.json +2 -2
  428. package/dist/mcp/server.mjs +5496 -2400
  429. package/dist/router.json +1 -1
  430. package/dist/scripts/_lib/agent_settings.js +73 -0
  431. package/dist/scripts/_lib/agent_src.js +20 -0
  432. package/dist/scripts/_lib/guidelines_lane.js +158 -0
  433. package/dist/scripts/_lib/lean_projection_mode.js +108 -0
  434. package/dist/scripts/_lib/router_match.js +243 -0
  435. package/dist/scripts/_lib/rule_injection.js +216 -0
  436. package/dist/scripts/condense.js +22 -25
  437. package/dist/scripts/project_thin_rules.js +139 -19
  438. package/dist/server/io/yamlIO.js +125 -17
  439. package/dist/server/schemas/settings.js +32 -5
  440. package/dist/shared/settingsSurface.js +6 -1
  441. package/docs/MIGRATION.md +56 -0
  442. package/docs/contracts/settings-classes.md +48 -7
  443. package/docs/guidelines/abstraction-thresholds.md +35 -0
  444. package/docs/guidelines/agent-infra/active-remediation-mechanics.md +3 -3
  445. package/docs/guidelines/agent-infra/false-green.md +2 -2
  446. package/docs/guidelines/agent-infra/notes-horizon-mechanics.md +90 -0
  447. package/docs/guidelines/agent-infra/roadmap-progress-mechanics.md +1 -1
  448. package/docs/guidelines/agent-infra/skill-quality-checklist.md +1 -1
  449. package/docs/guidelines/design-fidelity-mechanics.md +4 -0
  450. package/docs/guidelines/design-fidelity-routing.md +78 -0
  451. package/docs/guidelines/design-reconciliation.md +172 -0
  452. package/docs/guidelines/gtm-handoff.md +2 -2
  453. package/docs/guidelines/wing4-handoff.md +2 -2
  454. package/llms.txt +1 -1
  455. package/package.json +7 -3
  456. package/src/agent-src/scripts/archive_completed_roadmaps.ts +106 -4
  457. package/src/config/agent-settings.template.yml +94 -1
  458. package/src/config/ci-local-parity.yml +34 -0
  459. package/src/config/continuity-surface.json +8 -29
  460. package/src/config/evaluator-budgets.json +4 -4
  461. package/src/config/gate-coverage.yml +31 -8
  462. package/src/config/gate-reachability-exemptions.json +1 -0
  463. package/src/config/gate-violation-baselines.json +3 -3
  464. package/src/config/hook-token-budget.json +8 -7
  465. package/src/config/host-injection-effect.json +2 -2
  466. package/src/config/pack-size-budget.json +55 -4
  467. package/src/config/packed-binary-manifest.json +1 -1
  468. package/src/config/platform-anchor.json +47 -0
  469. package/src/config/preamble-payload-budget.json +2 -3
  470. package/src/config/publish-surface.json +4 -2
  471. package/src/config/ratification-policy.json +7 -0
  472. package/src/config/routing-coverage-seed.json +9 -2
  473. package/src/config/rule-stub-ceilings.json +16 -9
  474. package/src/scripts/_cli/cmd_session_recycle.ts +2 -25
  475. package/src/scripts/_cli/cmd_settings_get.ts +22 -7
  476. package/src/scripts/_cli/handoff_sessions.ts +14 -44
  477. package/src/scripts/_lib/agent_settings.ts +85 -0
  478. package/src/scripts/_lib/agent_src.ts +21 -0
  479. package/src/scripts/_lib/artifact_maturity.ts +242 -0
  480. package/src/scripts/_lib/conformance_report.ts +403 -0
  481. package/src/scripts/_lib/continuity_slot.ts +295 -0
  482. package/src/scripts/_lib/continuity_writer.ts +312 -0
  483. package/src/scripts/_lib/design_system_import.ts +20 -0
  484. package/src/scripts/_lib/design_tolerance.ts +281 -0
  485. package/src/scripts/_lib/guidelines_lane.ts +164 -0
  486. package/src/scripts/_lib/hook_settings.ts +85 -0
  487. package/src/scripts/_lib/lean_projection_mode.ts +127 -0
  488. package/src/scripts/_lib/obligation_frequency.ts +2 -3
  489. package/src/scripts/_lib/packed_binary_predicate.ts +37 -0
  490. package/src/scripts/_lib/platform_anchor.ts +1053 -0
  491. package/src/scripts/_lib/ratification_artifact.ts +278 -0
  492. package/src/scripts/_lib/recycle_envelope_paths.ts +109 -21
  493. package/src/scripts/_lib/regression_neighbourhood.ts +159 -22
  494. package/src/scripts/_lib/security_lint.ts +23 -2
  495. package/src/scripts/_lib/self_repair.ts +21 -0
  496. package/src/scripts/_lib/session_eol.ts +44 -0
  497. package/src/scripts/_lib/session_index_trust.ts +387 -0
  498. package/src/scripts/_lib/standing_bound_ratchet.ts +220 -0
  499. package/src/scripts/_lib/tolerance_shadow.ts +177 -0
  500. package/src/scripts/_lib/vendored_grammar_upstream.ts +280 -0
  501. package/src/scripts/ai_council/redact_low_impact_entry.ts +8 -0
  502. package/src/scripts/audit_mcp_tools.ts +69 -25
  503. package/src/scripts/autonomy_friction_traffic.ts +379 -0
  504. package/src/scripts/build_mcp_catalog.ts +24 -1
  505. package/src/scripts/check_host_tree_parity.ts +291 -0
  506. package/src/scripts/check_kernel_edit_ratified.ts +462 -0
  507. package/src/scripts/check_loss_class_declared.ts +793 -46
  508. package/src/scripts/check_platform_anchor.ts +390 -0
  509. package/src/scripts/check_preamble_payload_budget.ts +424 -7
  510. package/src/scripts/check_projected_rule_routes.ts +395 -0
  511. package/src/scripts/check_rule_projection_integrity.ts +102 -2
  512. package/src/scripts/ci_settle.ts +205 -5
  513. package/src/scripts/code_graph/build.ts +79 -0
  514. package/src/scripts/code_graph/cli.ts +264 -7
  515. package/src/scripts/code_graph/detect.ts +38 -0
  516. package/src/scripts/code_graph/query.ts +25 -2
  517. package/src/scripts/code_graph/sqlite_store.ts +16 -1
  518. package/src/scripts/code_graph/types.ts +52 -3
  519. package/src/scripts/code_graph/validate.ts +3 -1
  520. package/src/scripts/code_graph/verbs.ts +590 -0
  521. package/src/scripts/condense.ts +22 -26
  522. package/src/scripts/generate_host_cost_table.ts +354 -0
  523. package/src/scripts/handoff_context_hook.ts +39 -1
  524. package/src/scripts/hook_manifest.json +1 -1
  525. package/src/scripts/hook_manifest.yaml +59 -28
  526. package/src/scripts/hooks/block_speaking_inbox_dir.ts +182 -16
  527. package/src/scripts/hooks/category_a.ts +1 -1
  528. package/src/scripts/hooks/chain_nudge_hook.ts +311 -25
  529. package/src/scripts/hooks/code_graph_context_hook.ts +7 -18
  530. package/src/scripts/hooks/push_settle_hook.ts +3 -2
  531. package/src/scripts/hooks/rule_inject_hook.ts +107 -20
  532. package/src/scripts/hooks/session_eol_hook.ts +177 -145
  533. package/src/scripts/hooks/source_first_gate_hook.ts +12 -0
  534. package/src/scripts/hooks/ui_route_nudge_hook.ts +52 -15
  535. package/src/scripts/hot_context_hook.ts +103 -408
  536. package/src/scripts/lint_evidence_artifacts.ts +1 -0
  537. package/src/scripts/lint_knowledge_scale.ts +0 -26
  538. package/src/scripts/lint_roadmap_blockers.ts +164 -1
  539. package/src/scripts/mcp_server/consumer_tool_catalog.json +152 -2
  540. package/src/scripts/mcp_server/graph_tools.ts +294 -0
  541. package/src/scripts/mcp_server/path_util.ts +31 -0
  542. package/src/scripts/mcp_server/tool_catalog_source.ts +7 -1
  543. package/src/scripts/mcp_server/tools.ts +3 -13
  544. package/src/scripts/memory_lookup.ts +9 -9
  545. package/src/scripts/model_rule_injection.ts +61 -4
  546. package/src/scripts/project_thin_rules.ts +149 -16
  547. package/src/scripts/report_standing_payload_by_host.ts +650 -0
  548. package/src/scripts/schemas/agent-settings.schema.json +7 -1
  549. package/src/scripts/session_memory_index.ts +86 -8
  550. package/src/scripts/sync_agent_settings.ts +270 -10
  551. package/src/server/io/yamlIO.ts +127 -17
  552. package/src/server/schemas/settings.ts +45 -6
  553. package/src/shared/settingsSurface.ts +13 -2
  554. package/src/templates/compact-instructions.md +8 -3
  555. package/src/vendor/grammars/README.md +27 -2
  556. package/dist/agent-src/commands/chat-history/import.md +0 -170
  557. package/dist/agent-src/commands/chat-history.md +0 -63
@@ -20,12 +20,25 @@
20
20
  * deterministic (real tokenizer, reported by `session_index_cost()`); the
21
21
  * hit-rate arm needs a live paired run and has NOT been executed — so the
22
22
  * default is off, per the roadmap's "off unless proven".
23
+ *
24
+ * loss_class: recoverable-lossy
25
+ * loss_recovery: agents/memory/ — this module renders the block and applies
26
+ * `capRows`, so it carries the same class as the cap it applies. Each dropped
27
+ * row names a curated entry that remains on disk and is fetchable by id.
23
28
  */
24
29
 
25
- import { CURATED_TYPES, retrieve_v1 } from './memory_lookup.js';
30
+ import { CURATED_TYPES, MEMORY_ROOT, retrieve_v1 } from './memory_lookup.js';
26
31
  import { load_agent_settings } from './_lib/agent_settings.js';
32
+ import {
33
+ capRows,
34
+ orderRows,
35
+ SESSION_INDEX_ROW_CAP,
36
+ verifyMemoryRoot,
37
+ type TrustGrant,
38
+ type TrustVerdict,
39
+ } from './_lib/session_index_trust.js';
27
40
 
28
- export const SESSION_INDEX_ROW_CAP = 30;
41
+ export { SESSION_INDEX_ROW_CAP };
29
42
 
30
43
  interface IndexRow {
31
44
  id: string;
@@ -54,21 +67,72 @@ export function session_index_enabled(root: string): boolean {
54
67
  * Expects the caller to have chdir'd to the workspace root — memory roots
55
68
  * are cwd-relative, same contract as the MCP tool handlers.
56
69
  */
70
+ /**
71
+ * How many entries retrieval is asked for before the declared order picks.
72
+ *
73
+ * R2 finding 2: asking retrieval for the CAP made P4 decorative. Every curated
74
+ * hit scores an identical 0.1 with an empty key set, so `retrieve` sliced the
75
+ * first 30 in STORE order at `memory_lookup.ts:995` and `orderRows` then sorted
76
+ * a set the cap had already chosen. Measured by the reviewer: a 40-entry corpus
77
+ * with the 5 cheapest last emitted zero cheap rows.
78
+ *
79
+ * The number must therefore exceed any realistic curated corpus, not merely the
80
+ * cap. 500 is a STATED DEFAULT, not a measured optimum — the cost of asking for
81
+ * more than exists is bounded by what exists, since `detail: 'index'` returns a
82
+ * row rather than a body. Its falsifier is recorded in the roadmap: a corpus
83
+ * larger than this bound makes store order decide again, silently.
84
+ */
85
+ export const SESSION_INDEX_RETRIEVAL_LIMIT = 500;
86
+
57
87
  export function session_index_rows(cap: number = SESSION_INDEX_ROW_CAP): IndexRow[] {
58
- const envelope = retrieve_v1([...CURATED_TYPES], [], cap, { detail: 'index' });
88
+ // Retrieval is asked for far more than the cap, so the declared order below
89
+ // is what chooses which rows survive truncation. P4 and P6 are one
90
+ // decision: retrieve wide, order, THEN cap — and never the reverse.
91
+ const envelope = retrieve_v1([...CURATED_TYPES], [], SESSION_INDEX_RETRIEVAL_LIMIT, { detail: 'index' });
59
92
  const entries = (envelope['entries'] as Array<Record<string, unknown>>) ?? [];
60
- return entries.slice(0, cap).map((e) => ({
93
+ const rows: IndexRow[] = entries.map((e) => ({
61
94
  id: String(e['id'] ?? ''),
62
95
  title: String(e['title'] ?? ''),
63
96
  tokens_estimate: Number(e['tokens_estimate'] ?? 0),
64
97
  }));
98
+ return capRows(orderRows(rows), cap);
65
99
  }
66
100
 
67
101
  /**
68
- * Render the injectable block, or `null` when the corpus is empty (no
69
- * block beats an empty scaffold).
102
+ * The trust gate, separated from the render so a caller can log WHY nothing
103
+ * was injected. `null` from {@link build_session_index_block} is ambiguous by
104
+ * construction — empty corpus and refused root look identical — and the
105
+ * distinction is the point of the contract, so it is available here.
106
+ *
107
+ * The workspace root is REQUIRED. `MEMORY_ROOT` is relative, so without a root
108
+ * to resolve it against there is nothing to check identity or containment
109
+ * against, and a default would be a bypass wearing a convenience.
70
110
  */
71
- export function build_session_index_block(cap: number = SESSION_INDEX_ROW_CAP): string | null {
111
+ export function session_index_trust(workspaceRoot: string, now?: Date): TrustVerdict {
112
+ // `exactOptionalPropertyTypes` refuses `now: undefined` on an optional
113
+ // field, so the key is omitted rather than passed as undefined.
114
+ return verifyMemoryRoot({
115
+ workspaceRoot,
116
+ relativeMemoryRoot: MEMORY_ROOT,
117
+ ...(now === undefined ? {} : { now }),
118
+ });
119
+ }
120
+
121
+ /**
122
+ * RENDER a block from whatever `MEMORY_ROOT` currently resolves to. Checks
123
+ * nothing, serves nothing.
124
+ *
125
+ * Split out on R2 findings 6 and 7. The trust check used to be an OPTIONAL
126
+ * parameter, which made the serving path opt-in — and `session_index_cost()`
127
+ * omitted it, so the one function whose name suggests it only measures was also
128
+ * the one that could render unchecked. Now the two operations have two names:
129
+ * this one renders (for cost measurement and for the unit seam that injects an
130
+ * absolute fixture root via `_setMemoryRoot`, which by construction sits
131
+ * outside any workspace), and {@link serve_session_index_block} is the only way
132
+ * to obtain a block for INJECTION — it takes a granted verdict, so there is no
133
+ * unchecked serving path to forget.
134
+ */
135
+ export function render_session_index_block(cap: number = SESSION_INDEX_ROW_CAP): string | null {
72
136
  const rows = session_index_rows(cap);
73
137
  if (rows.length === 0) return null;
74
138
  const lines = [
@@ -81,13 +145,27 @@ export function build_session_index_block(cap: number = SESSION_INDEX_ROW_CAP):
81
145
  return lines.join('\n');
82
146
  }
83
147
 
148
+ /**
149
+ * The only path to a block that will be INJECTED.
150
+ *
151
+ * Takes a granted verdict rather than a root, so the check happens once, at the
152
+ * caller, and cannot be skipped here — R2 finding 7 was that the hook and the
153
+ * builder each computed it, for a diagnostic the dispatcher does not forward.
154
+ * The type is `TrustGrant`, not `TrustVerdict`: a refusal cannot be passed in,
155
+ * so "did you check?" is answered by the signature instead of by a convention.
156
+ */
157
+ export function serve_session_index_block(_grant: TrustGrant, cap: number = SESSION_INDEX_ROW_CAP): string | null {
158
+ return render_session_index_block(cap);
159
+ }
160
+
84
161
  /**
85
162
  * Deterministic fixed cost of the block in real tokens (cl100k_base) —
86
163
  * the measurable arm of the roadmap's ship-criterion. Lazy-loads the
87
164
  * tokenizer so the enabled=off fast path never pays it.
88
165
  */
89
166
  export async function session_index_cost(cap: number = SESSION_INDEX_ROW_CAP): Promise<number> {
90
- const block = build_session_index_block(cap);
167
+ // Renders rather than serves: this measures a cost, it injects nothing.
168
+ const block = render_session_index_block(cap);
91
169
  if (block === null) return 0;
92
170
  const { gpt_tokens } = await import('./_lib/token_count.js');
93
171
  return gpt_tokens(block).tokens;
@@ -441,11 +441,201 @@ function loadTemplate(p: string, profileValues: Record<string, string>): string
441
441
  return renderTemplate(fs.readFileSync(p, 'utf-8'), profileValues);
442
442
  }
443
443
 
444
- function loadUser(p: string): Record<string, unknown> {
445
- if (!isFile(p)) {
446
- return {};
444
+ export interface DuplicateRepair {
445
+ /** The document with every safely-collapsible duplicate run reduced to one. */
446
+ text: string;
447
+ /** Keys that were collapsed, in first-seen order. */
448
+ collapsed: string[];
449
+ /** Duplicated keys this pass refuses to touch, with the reason. */
450
+ unsafe: string[];
451
+ }
452
+
453
+ /**
454
+ * Collapse duplicate TOP-LEVEL inline-scalar keys, keeping the last value.
455
+ *
456
+ * A strict YAML parser rejects a duplicate mapping key outright, so one such
457
+ * pair takes the whole file — and with it `task sync`, `task release-prepare`
458
+ * and every release — out of service with "Map keys must be unique". The
459
+ * duplicates in the wild were written by `mergeIntoTemplate`, whose flat
460
+ * `a.b: value` append was not idempotent before this change: each wizard save
461
+ * re-appended the entire "Wizard-added keys" block. That writer is fixed, but
462
+ * every file it already corrupted stays unreadable until something repairs
463
+ * it, and a release is the worst moment to discover that.
464
+ *
465
+ * Last-wins is value-neutral, not a guess: a lenient reader already resolved
466
+ * such a run that way, so collapsing changes what the file PARSES as in no
467
+ * way — it only changes whether it parses at all.
468
+ *
469
+ * Deliberately narrow, and the narrowness is what makes last-wins safe. A key
470
+ * is collapsible only when EVERY occurrence in the run is ONE-LINE-ONLY — its
471
+ * value sits on the key's own line and it owns no continuation lines at all.
472
+ * Anything else is reported in `unsafe` and left alone, because dropping the
473
+ * loser of a multi-line pair does not drop its body: the body survives, stops
474
+ * being that key's value, and attaches to whatever line precedes it. That
475
+ * turns a parse error into silent data corruption, which is strictly worse.
476
+ *
477
+ * Refused for that reason: a block key with indented children, a block scalar
478
+ * (`|`, `>`), a flow collection opened on one line and closed on another, and
479
+ * an anchor / alias / tag / merge value, whose meaning depends on which
480
+ * occurrence a reader binds rather than only on the value.
481
+ *
482
+ * A multi-document stream (`---` / `...`) is not repaired either. The reader
483
+ * takes a single document and rejects such a file whatever this pass does, so
484
+ * collapsing across a boundary could only merge two documents' keys into one.
485
+ */
486
+ export function collapseDuplicateFlatKeys(text: string): DuplicateRepair {
487
+ // Line endings are preserved: splitting on `\n` alone leaves a trailing
488
+ // `\r` on every CRLF line, which the key pattern then never matches — so a
489
+ // CRLF file reported zero duplicates and fell straight into the parse error
490
+ // this pass exists to prevent.
491
+ //
492
+ // The MAJORITY terminator, not "any CRLF present". A mostly-LF file with one
493
+ // stray CRLF line would otherwise be rewritten to CRLF throughout — a whole-
494
+ // file change nobody asked for, on a pass whose entire promise is that it
495
+ // touches only what it must. Matches `sync_yaml_rt.detectEol`.
496
+ const crlfCount = (text.match(/\r\n/g) ?? []).length;
497
+ const lfCount = (text.match(/(^|[^\r])\n/g) ?? []).length;
498
+ const eol = crlfCount > lfCount ? '\r\n' : '\n';
499
+ const lines = text.split(/\r?\n/);
500
+
501
+ // A document separator anywhere means more than one document may be in play.
502
+ if (lines.some((l) => /^(---|\.\.\.)\s*$/.test(l))) {
503
+ return { text, collapsed: [], unsafe: [] };
504
+ }
505
+
506
+ // A mapping key ends at a colon FOLLOWED BY SPACE OR END OF LINE — YAML's
507
+ // own plain-scalar rule. Stopping at the first colon instead reads `a:b: 1`
508
+ // as the key `a`, so a file carrying both `a:b:` and `a:` saw two `a` heads
509
+ // and collapsed them: the `a:b` entry vanished and the result parsed
510
+ // cleanly, so nothing downstream could notice. `sync_yaml_rt`'s own
511
+ // tokeniser already had this right; the two disagreeing was the defect.
512
+ //
513
+ // The value is captured with `[\s\S]` rather than `.`, which does not match
514
+ // a lone `\r`. A line holding one (a value like `"x\ry"`) was not recognised
515
+ // as a head at all, so a duplicate of it stayed invisible and the parse
516
+ // error this pass exists to remove survived untouched.
517
+ const HEAD_RE = /^([A-Za-z_][A-Za-z0-9_.-]*)[ \t]*:(?=[ \t]|$)([\s\S]*)$/;
518
+
519
+ /** Top-level key lines, in order. */
520
+ const heads: { key: string; at: number; rest: string }[] = [];
521
+ for (let i = 0; i < lines.length; i++) {
522
+ const line = lines[i];
523
+ if (line === undefined) continue;
524
+ if (line.trim() === '' || line.trimStart().startsWith('#')) continue;
525
+ // Top level only — an indented `a.b:` is somebody else's child key.
526
+ if (line.length !== line.trimStart().length) continue;
527
+ const m = HEAD_RE.exec(line);
528
+ if (m === null) continue;
529
+ heads.push({ key: m[1] as string, at: i, rest: (m[2] as string).trim() });
530
+ }
531
+
532
+ /**
533
+ * Why this key may NOT be collapsed, or null when it may.
534
+ *
535
+ * Collapsing is safe only for a key that occupies exactly its own line: its
536
+ * value is a plain inline scalar and nothing between here and the next
537
+ * top-level key belongs to it. Everything else keeps its real reason, so the
538
+ * refusal message names what actually blocked it rather than asserting
539
+ * "different children" over a null, an anchor or a block scalar.
540
+ */
541
+ const blocker = (h: { at: number; rest: string }, nextAt: number): string | null => {
542
+ const v = h.rest;
543
+ if (v === '' || v.startsWith('#')) return 'a block key or a null value';
544
+ if (/^[|>]/.test(v)) return 'a block scalar';
545
+ // An anchor, alias or tag ANYWHERE in the value, not merely at its start:
546
+ // `a: [&x 1]` fits on one line, and dropping it deletes an anchor that an
547
+ // alias on some other line binds to — turning a duplicate-key error into
548
+ // an unresolved-alias error. Token position only, so `a&b` in a plain
549
+ // scalar is left alone.
550
+ if (/(^|[\s[{,])[&*!]/.test(v)) return 'an anchor, alias or tag';
551
+ // A flow collection must open and close on this line.
552
+ const opens = (v.match(/[[{]/g) ?? []).length;
553
+ const closes = (v.match(/[\]}]/g) ?? []).length;
554
+ if (opens !== closes) return 'a flow collection spanning lines';
555
+ for (let i = h.at + 1; i < nextAt; i++) {
556
+ const line = lines[i];
557
+ if (line === undefined) continue;
558
+ if (line.trim() === '') continue;
559
+ if (line.trimStart().startsWith('#')) continue;
560
+ return 'a value spanning more than its own line';
561
+ }
562
+ return null;
563
+ };
564
+
565
+ const solo = new Set<number>();
566
+ const why = new Map<number, string>();
567
+ for (let n = 0; n < heads.length; n++) {
568
+ const h = heads[n] as { key: string; at: number; rest: string };
569
+ const nextAt = n + 1 < heads.length ? (heads[n + 1] as { at: number }).at : lines.length;
570
+ const reason = blocker(h, nextAt);
571
+ if (reason === null) solo.add(h.at);
572
+ else why.set(h.at, reason);
447
573
  }
448
- const data = parseYaml(fs.readFileSync(p, 'utf-8'), { version: '1.1' });
574
+
575
+ const seen = new Map<string, number[]>();
576
+ for (const h of heads) {
577
+ const at = seen.get(h.key);
578
+ if (at === undefined) seen.set(h.key, [h.at]);
579
+ else at.push(h.at);
580
+ }
581
+
582
+ const collapsed: string[] = [];
583
+ const unsafe: string[] = [];
584
+ const drop = new Set<number>();
585
+ for (const [key, hits] of seen) {
586
+ if (hits.length < 2) continue;
587
+ if (!hits.every((i) => solo.has(i))) {
588
+ const reason = hits.map((i) => why.get(i)).find((r) => r !== undefined) ?? 'not a plain one-line value';
589
+ unsafe.push(`${key} — ${reason}`);
590
+ continue;
591
+ }
592
+ collapsed.push(key);
593
+ // Keep the LAST occurrence's value at the FIRST occurrence's position, so
594
+ // the surrounding comments stay attached to the line they document.
595
+ lines[hits[0] as number] = lines[hits[hits.length - 1] as number] as string;
596
+ for (let i = 1; i < hits.length; i++) drop.add(hits[i] as number);
597
+ }
598
+
599
+ // Duplicates this pass cannot even SEE, reported rather than skipped.
600
+ //
601
+ // `HEAD_RE` recognises the key shapes the repair understands. A key starting
602
+ // with a digit (`2fa.on`) or a quoted key (`"a:b"`) matches nothing, so a
603
+ // file duplicating one used to come back with zero collapses AND zero
604
+ // refusals — and then died on the strict parse with the same opaque "Map keys
605
+ // must be unique" the whole repair exists to remove, giving the operator no
606
+ // hint that their key was the part being skipped. Shipping a repair pass
607
+ // creates the expectation that it either fixes the file or says why it
608
+ // cannot; silence is neither.
609
+ //
610
+ // Detection only. These keys are never collapsed — the shapes are exactly the
611
+ // ones whose tokenisation this pass does not model, which is why it declines
612
+ // to rewrite them.
613
+ const loose = new Map<string, number>();
614
+ for (let i = 0; i < lines.length; i++) {
615
+ const line = lines[i];
616
+ if (line === undefined) continue;
617
+ if (line.trim() === '' || line.trimStart().startsWith('#')) continue;
618
+ if (line.length !== line.trimStart().length) continue;
619
+ if (HEAD_RE.test(line)) continue;
620
+ const m = /^("[^"]*"|'[^']*'|[^\s:#][^:]*?)[ \t]*:(?=[ \t]|$)/.exec(line);
621
+ if (m === null) continue;
622
+ const key = m[1] as string;
623
+ loose.set(key, (loose.get(key) ?? 0) + 1);
624
+ }
625
+ for (const [key, count] of loose) {
626
+ if (count < 2) continue;
627
+ unsafe.push(`${key} — a key shape this repair does not model (quoted, or not starting with a letter)`);
628
+ }
629
+
630
+ if (collapsed.length === 0) {
631
+ return { text, collapsed, unsafe };
632
+ }
633
+ const kept = lines.filter((_, i) => !drop.has(i));
634
+ return { text: kept.join(eol), collapsed, unsafe };
635
+ }
636
+
637
+ function loadUserText(raw: string): Record<string, unknown> {
638
+ const data = parseYaml(raw, { version: '1.1' });
449
639
  if (data === null || data === undefined) {
450
640
  return {};
451
641
  }
@@ -655,10 +845,61 @@ export function main(argv: readonly string[] = process.argv.slice(2)): number {
655
845
  const templatePath = args.template;
656
846
  const profileDir = args.profile_dir;
657
847
 
848
+ // Repair BEFORE the first parse. A duplicate mapping key makes the whole
849
+ // file unreadable, so without this the run dies here and takes the release
850
+ // with it — see `collapseDuplicateFlatKeys` for why last-wins is safe.
851
+ const rawText = isFile(target) ? fs.readFileSync(target, 'utf-8') : '';
852
+ const repair = collapseDuplicateFlatKeys(rawText);
853
+ if (repair.unsafe.length > 0) {
854
+ // Each entry carries the reason that key was refused. A single blanket
855
+ // sentence used to claim "different children" for every refusal — over a
856
+ // null value, a block scalar or an anchor that has none — which sends the
857
+ // reader looking for children to merge that are not there.
858
+ process.stderr.write(
859
+ `error: ${target} has duplicate mapping keys this tool will not collapse:\n` +
860
+ repair.unsafe.map((u) => ` - ${u}\n`).join('') +
861
+ `Keeping one occurrence would change what the file means. Merge them by hand, then re-run.\n`,
862
+ );
863
+ return 2;
864
+ }
865
+ if (repair.collapsed.length > 0) {
866
+ // NOT gated on --quiet, and not on stdout. Both callers that matter run
867
+ // quiet — the release's `sync-agent-settings` step and the pre-release
868
+ // probe — so gating this would let a release rewrite the operator's own
869
+ // settings file with no trace. `--quiet` suppresses routine success
870
+ // chatter; "I modified your file" is not that.
871
+ //
872
+ // The tense follows the mode. `--check` and `--dry-run` never write, so
873
+ // announcing a completed collapse there describes a write that did not
874
+ // happen — and the pre-release probe runs `--dry-run`, which made that the
875
+ // FIRST thing an operator saw on every release with a corrupted file.
876
+ const writes = !args.check && !args.dry_run;
877
+ const verb = writes
878
+ ? `collapsed ${repair.collapsed.length} duplicate key(s), last value kept`
879
+ : `would collapse ${repair.collapsed.length} duplicate key(s), keeping the last value (no write in this mode)`;
880
+ process.stderr.write(`🔧 ${target}: ${verb} — ${repair.collapsed.join(', ')}\n`);
881
+
882
+ // The collapse promises to change only WHETHER the file parses, never what
883
+ // it parses as — and that promise is only meaningful over an input that had
884
+ // a reading to preserve. If the original was unparseable for some further
885
+ // reason (an unterminated quote, say), the repaired file may parse to
886
+ // structure no human wrote, and the next `git diff` shows keys nobody
887
+ // added. Cheap to say, and the operator cannot recover it from the output.
888
+ try {
889
+ parseYaml(rawText, { version: '1.1', uniqueKeys: false });
890
+ } catch {
891
+ process.stderr.write(
892
+ `⚠️ ${target}: the original had a syntax error beyond the duplicate keys, so the ` +
893
+ `result may differ in structure, not only in duplicates — read the diff before trusting it.\n`,
894
+ );
895
+ }
896
+ }
897
+ const sourceText = repair.text;
898
+
658
899
  let profile: string;
659
900
  let templateBody: string;
660
901
  try {
661
- const userData = loadUser(target);
902
+ const userData = loadUserText(sourceText);
662
903
  const personalRaw = userData['personal'];
663
904
  const personal =
664
905
  personalRaw !== null && typeof personalRaw === 'object' && !Array.isArray(personalRaw)
@@ -706,7 +947,12 @@ export function main(argv: readonly string[] = process.argv.slice(2)): number {
706
947
  throw err;
707
948
  }
708
949
 
709
- const existingText = isFile(target) ? fs.readFileSync(target, 'utf-8') : '';
950
+ // The merge reads the REPAIRED text; every "did anything change" decision
951
+ // below compares against `rawText`, what is actually on disk. Comparing
952
+ // against the repaired text instead would report "already in sync" on a
953
+ // file whose collapse never reached disk — the repair would run on every
954
+ // invocation and fix nothing.
955
+ const existingText = sourceText;
710
956
 
711
957
  let newText: string;
712
958
  if (existingText) {
@@ -724,7 +970,7 @@ export function main(argv: readonly string[] = process.argv.slice(2)): number {
724
970
  newText = templateBody;
725
971
  }
726
972
 
727
- if (newText === existingText) {
973
+ if (newText === rawText) {
728
974
  if (!args.quiet) {
729
975
  process.stdout.write(`✅ ${target}: already in sync (profile=${profile})\n`);
730
976
  }
@@ -732,14 +978,28 @@ export function main(argv: readonly string[] = process.argv.slice(2)): number {
732
978
  }
733
979
 
734
980
  if (args.check) {
735
- const diff = renderDiff(existingText, newText, String(target));
981
+ const diff = renderDiff(rawText, newText, String(target));
736
982
  process.stdout.write(diff);
737
- process.stderr.write(`\n❌ ${target}: drift detected (profile=${profile})\n`);
983
+ // Name WHICH problem, because the two have different remedies and a CI job
984
+ // reading only "drift detected" cannot tell them apart. A repair-only diff
985
+ // means the file is corrupt, not out of step with the template; the
986
+ // discriminator is whether the merge changed anything beyond the collapse.
987
+ //
988
+ // Still exit 2 in both cases: the file needs a write either way, and a
989
+ // `--check` that returned 0 over a file the reader cannot parse would hide
990
+ // exactly the breakage this whole change exists to surface.
991
+ const repairOnly = repair.collapsed.length > 0 && newText === sourceText;
992
+ process.stderr.write(
993
+ repairOnly
994
+ ? `\n❌ ${target}: duplicate keys need collapsing — no template drift (profile=${profile})\n` +
995
+ ` Run \`sync_agent_settings\` without --check to apply the repair.\n`
996
+ : `\n❌ ${target}: drift detected (profile=${profile})\n`,
997
+ );
738
998
  return 2;
739
999
  }
740
1000
 
741
1001
  if (args.dry_run) {
742
- const diff = renderDiff(existingText, newText, String(target));
1002
+ const diff = renderDiff(rawText, newText, String(target));
743
1003
  process.stdout.write(diff);
744
1004
  if (!args.quiet) {
745
1005
  process.stderr.write(`\n(dry-run) would update ${target} (profile=${profile})\n`);
@@ -40,6 +40,25 @@ function formatScalar(value: unknown): string {
40
40
  return yamlDump(value, { lineWidth: -1, flowLevel: 0 }).replace(/\n$/, '').trim();
41
41
  }
42
42
 
43
+ /**
44
+ * A NESTED mapping key, as both the presence probe and the insertion walk read
45
+ * one. Shared so the writer and its own existence-probe cannot drift apart —
46
+ * that drift is the shape this file has now hit four times.
47
+ *
48
+ * `-` is in the class because a dashed key is ordinary YAML (`first-name`,
49
+ * `dry-run`, `retry-count`) and `detectIndentWidth` already accepted one.
50
+ * While this pattern did not, `findScalarLine` could never find such a key:
51
+ * `mergeIntoTemplate` then wrote a bogus top-level `x.first-principles:` line
52
+ * beside the real nested one and the intended value was never written — a save
53
+ * that silently did nothing — and `upsertScalar` appended another child line on
54
+ * EVERY call, growing without bound until the file stopped parsing.
55
+ *
56
+ * `.` stays OUT, deliberately. A dotted key is the FLAT form, which
57
+ * `replaceFlatDottedKey` owns; letting this pattern match `a.b:` would make the
58
+ * nested probe claim a line that is one key literally named "a.b".
59
+ */
60
+ const NESTED_KEY_RE = /^([A-Za-z_][A-Za-z0-9_-]*)\s*:/;
61
+
43
62
  interface FlatEntry {
44
63
  path: string[];
45
64
  value: unknown;
@@ -68,6 +87,25 @@ export function replaceScalar(template: string, dottedPath: string[], value: unk
68
87
  const key = dottedPath[dottedPath.length - 1];
69
88
 
70
89
  const lines = template.split('\n');
90
+ const at = findScalarLine(lines, sections, key);
91
+ if (at === -1) return template;
92
+ const width = detectIndentWidth(lines);
93
+ lines[at] = `${' '.repeat(width * sections.length)}${key}: ${formatScalar(value)}`;
94
+ return lines.join('\n');
95
+ }
96
+
97
+ /**
98
+ * Index of the line holding the scalar at `sections`/`key`, or -1.
99
+ *
100
+ * Split out of `replaceScalar` so that "is this path present?" is answerable
101
+ * WITHOUT writing. `mergeIntoTemplate` used to infer presence from
102
+ * `replaceScalar` returning a changed string, which conflates "absent" with
103
+ * "present and already equal to the value being written" — under that test a
104
+ * no-op write reads as a miss and the caller appends a duplicate mapping key.
105
+ * A presence question answered by a mutation's side effect is the shape to
106
+ * look for here; the file already carries two earlier rounds of it.
107
+ */
108
+ function findScalarLine(lines: readonly string[], sections: readonly string[], key: string | undefined): number {
71
109
  // The document's own width, the same read `upsertScalar` performs.
72
110
  //
73
111
  // R2 round 5, finding 1, and it is the second half of round 4's finding 7.
@@ -84,9 +122,7 @@ export function replaceScalar(template: string, dottedPath: string[], value: unk
84
122
  // A writer and its own existence-probe disagreeing about the format is the
85
123
  // shape to look for whenever one of a pair is taught something new.
86
124
  const width = detectIndentWidth(lines);
87
- const targetIndent = ' '.repeat(width * sections.length);
88
125
  const currentPath: (string | null)[] = new Array<string | null>(sections.length).fill(null);
89
- const formatted = formatScalar(value);
90
126
 
91
127
  for (let i = 0; i < lines.length; i++) {
92
128
  const line = lines[i];
@@ -98,7 +134,7 @@ export function replaceScalar(template: string, dottedPath: string[], value: unk
98
134
  const level = indentLen / width;
99
135
  if (level > sections.length) continue;
100
136
 
101
- const m = /^([A-Za-z_][A-Za-z0-9_]*)\s*:/.exec(stripped);
137
+ const m = NESTED_KEY_RE.exec(stripped);
102
138
  if (!m) continue;
103
139
  const lineKey = m[1];
104
140
  if (lineKey === undefined) continue;
@@ -115,10 +151,9 @@ export function replaceScalar(template: string, dottedPath: string[], value: unk
115
151
  const parentsMatch = sections.every((s, idx) => currentPath[idx] === s);
116
152
  if (!parentsMatch) continue;
117
153
  if (lineKey !== key) continue;
118
- lines[i] = `${targetIndent}${key}: ${formatted}`;
119
- return lines.join('\n');
154
+ return i;
120
155
  }
121
- return template;
156
+ return -1;
122
157
  }
123
158
 
124
159
  /**
@@ -182,11 +217,21 @@ export function detectIndentWidth(lines: readonly string[]): number {
182
217
  */
183
218
  export function upsertScalar(template: string, dottedPath: string[], value: unknown): string {
184
219
  if (dottedPath.length === 0) return template;
185
- const replaced = replaceScalar(template, dottedPath, value);
186
- if (replaced !== template) return replaced;
187
-
188
220
  const sections = dottedPath.slice(0, -1);
189
221
  const key = dottedPath[dottedPath.length - 1] as string;
222
+
223
+ // Presence is asked, never inferred from the write changing the text. The
224
+ // string compare that used to stand here read "key already holds this
225
+ // value" as "key absent" and fell through to the append below — so saving
226
+ // the wizard's council page twice WITHOUT changing a toggle wrote a second
227
+ // `api_on_quota:` into the same block, over two `{ok: true}` responses, and
228
+ // left `.ai-council.yml` rejected by a strict parser. Third instance of one
229
+ // shape in this file; `findScalarLine` exists to end it, and a fix that
230
+ // taught only `mergeIntoTemplate` would have left the live path here.
231
+ if (findScalarLine(template.split('\n'), sections, key) !== -1) {
232
+ return replaceScalar(template, dottedPath, value);
233
+ }
234
+
190
235
  const formatted = formatScalar(value);
191
236
  const lines = template.split('\n');
192
237
  // The document's OWN indent width, not an assumed two.
@@ -228,7 +273,7 @@ export function upsertScalar(template: string, dottedPath: string[], value: unkn
228
273
  if (line.trim() === '' || line.trim().startsWith('#')) continue;
229
274
  const indentLen = line.length - line.trimStart().length;
230
275
  if (indentLen !== indent.length) continue;
231
- const m = /^([A-Za-z_][A-Za-z0-9_]*)\s*:/.exec(line.trim());
276
+ const m = NESTED_KEY_RE.exec(line.trim());
232
277
  if (m !== null && m[1] === want) {
233
278
  found = i;
234
279
  break;
@@ -269,23 +314,88 @@ export function upsertScalar(template: string, dottedPath: string[], value: unkn
269
314
  return lines.join('\n');
270
315
  }
271
316
 
317
+ /**
318
+ * Rewrite an existing top-level `a.b.c: value` line — the FLAT form
319
+ * `mergeIntoTemplate` itself appends when a path has no template entry.
320
+ * Returns the template unchanged when no such line exists.
321
+ *
322
+ * `replaceScalar` cannot see this line: its key pattern is
323
+ * `[A-Za-z_][A-Za-z0-9_]*`, with no dot, so a path this module appended on
324
+ * one pass is invisible to the probe on the next and `mergeIntoTemplate`
325
+ * appends a SECOND copy. Two wizard saves therefore produce a file carrying
326
+ * duplicate mapping keys — a strict parser rejects it outright, which is what
327
+ * wedged `task release`: its `sync` step exits 2 with "Map keys must be
328
+ * unique" before the release does any git work.
329
+ *
330
+ * A pre-existing duplicate run is COLLAPSED, not merely updated: the first
331
+ * occurrence keeps its position and takes the new value, every later one is
332
+ * dropped. Collapsing is value-neutral — a lenient reader already resolved
333
+ * such a run last-wins — and it is the only way a file corrupted by the old
334
+ * behaviour becomes parseable again through the ordinary write path.
335
+ *
336
+ * Deliberately NOT folded into `replaceScalar`: `upsertScalar` calls that
337
+ * probe and documents the flat form as the WRONG shape for a nested key (a
338
+ * reader sees one key literally named "a.b"). Teaching the shared probe to
339
+ * match it would make `upsertScalar` write into that broken line instead of
340
+ * creating the nesting. The flat form is legitimate only here, where this
341
+ * function is the one that wrote it.
342
+ */
343
+ function replaceFlatDottedKey(
344
+ template: string,
345
+ dottedPath: string[],
346
+ value: unknown,
347
+ ): { found: boolean; body: string } {
348
+ if (dottedPath.length < 2) return { found: false, body: template };
349
+ const flat = dottedPath.join('.');
350
+ const lines = template.split('\n');
351
+ const hits: number[] = [];
352
+ for (let i = 0; i < lines.length; i++) {
353
+ const line = lines[i];
354
+ if (line === undefined) continue;
355
+ // Top level only — an indented `a.b:` is somebody else's child key.
356
+ if (line.length !== line.trimStart().length) continue;
357
+ const m = /^([A-Za-z_][A-Za-z0-9_.-]*)\s*:/.exec(line);
358
+ if (m === null || m[1] !== flat) continue;
359
+ hits.push(i);
360
+ }
361
+ if (hits.length === 0) return { found: false, body: template };
362
+ lines[hits[0] as number] = `${flat}: ${formatScalar(value)}`;
363
+ // Drop the later copies back-to-front so the earlier indices stay valid.
364
+ for (let i = hits.length - 1; i >= 1; i--) lines.splice(hits[i] as number, 1);
365
+ return { found: true, body: lines.join('\n') };
366
+ }
367
+
272
368
  /**
273
369
  * Apply every leaf change from `newValues` to `templateBody`. Paths that
274
370
  * are not present in the template are appended at the end. Comments are
275
371
  * preserved for every key that already exists in the template.
372
+ *
373
+ * Idempotent: applying the same values twice yields the same document, so a
374
+ * second wizard save re-writes the flat block instead of duplicating it.
276
375
  */
277
376
  export function mergeIntoTemplate(templateBody: string, newValues: Record<string, unknown>): string {
278
377
  let body = templateBody;
279
378
  const appended: string[] = [];
280
379
  for (const entry of flatten(newValues)) {
281
- const before = body;
282
- body = replaceScalar(body, entry.path, entry.value);
283
- if (body === before) {
284
- // Path is not in the template — append a flat key=value at EOF.
285
- // Form coverage is asserted by parity test, so this branch only
286
- // fires on hand-edited templates.
287
- appended.push(`${entry.path.join('.')}: ${formatScalar(entry.value)}`);
380
+ const sections = entry.path.slice(0, -1);
381
+ const leaf = entry.path[entry.path.length - 1];
382
+ // Presence is asked, never inferred from the write changing the text —
383
+ // see `findScalarLine`. Writing the value it already holds is a hit.
384
+ if (findScalarLine(body.split('\n'), sections, leaf) !== -1) {
385
+ body = replaceScalar(body, entry.path, entry.value);
386
+ continue;
387
+ }
388
+ // Not in the template's nested form. It may still be here as a flat
389
+ // dotted line this function appended on an earlier pass.
390
+ const flat = replaceFlatDottedKey(body, entry.path, entry.value);
391
+ if (flat.found) {
392
+ body = flat.body;
393
+ continue;
288
394
  }
395
+ // Genuinely absent — append a flat key=value at EOF. Form coverage is
396
+ // asserted by parity test, so this branch only fires on hand-edited
397
+ // templates.
398
+ appended.push(`${entry.path.join('.')}: ${formatScalar(entry.value)}`);
289
399
  }
290
400
  if (appended.length > 0) {
291
401
  if (!body.endsWith('\n')) body += '\n';