universal-dev-standards 6.7.5 → 6.9.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 (213) hide show
  1. package/bin/uds.js +19 -2
  2. package/bundled/ai/standards/acceptance-criteria-traceability.ai.yaml +14 -2
  3. package/bundled/ai/standards/adr-standards.ai.yaml +14 -2
  4. package/bundled/ai/standards/ai-instruction-standards.ai.yaml +6 -6
  5. package/bundled/ai/standards/code-review.ai.yaml +13 -3
  6. package/bundled/ai/standards/commit-message.ai.yaml +8 -4
  7. package/bundled/ai/standards/deferred-item-exit.ai.yaml +225 -0
  8. package/bundled/ai/standards/feature-discovery-standards.ai.yaml +14 -2
  9. package/bundled/ai/standards/governance-layer.ai.yaml +128 -2
  10. package/bundled/ai/standards/logging.ai.yaml +2 -2
  11. package/bundled/ai/standards/retrospective-standards.ai.yaml +14 -2
  12. package/bundled/ai/standards/reverse-engineering-standards.ai.yaml +73 -2
  13. package/bundled/ai/standards/security-standards.ai.yaml +2 -2
  14. package/bundled/ai/standards/spec-driven-development.ai.yaml +14 -2
  15. package/bundled/ai/standards/tech-debt-standards.ai.yaml +87 -3
  16. package/bundled/ai/standards/turn-completion-integrity.ai.yaml +131 -0
  17. package/bundled/core/acceptance-criteria-traceability.md +5 -2
  18. package/bundled/core/adr-standards.md +26 -2
  19. package/bundled/core/ai-instruction-standards.md +9 -7
  20. package/bundled/core/code-review-checklist.md +5 -2
  21. package/bundled/core/context-aware-loading.md +1 -1
  22. package/bundled/core/deferred-item-exit.md +254 -0
  23. package/bundled/core/feature-discovery-standards.md +5 -1
  24. package/bundled/core/governance-layer.md +114 -2
  25. package/bundled/core/retrospective-standards.md +4 -2
  26. package/bundled/core/reverse-engineering-standards.md +81 -2
  27. package/bundled/core/spec-driven-development.md +8 -2
  28. package/bundled/core/tech-debt-standards.md +67 -8
  29. package/bundled/core/turn-completion-integrity.md +196 -0
  30. package/bundled/hooks/check-dangerous-cmd.mjs +60 -0
  31. package/bundled/hooks/check-logging-standard.mjs +59 -0
  32. package/bundled/hooks/check-turn-completion.mjs +233 -0
  33. package/bundled/hooks/inject-standards.mjs +183 -0
  34. package/bundled/hooks/telemetry-wrapper.mjs +77 -0
  35. package/bundled/hooks/turn-completion/detect.mjs +99 -0
  36. package/bundled/hooks/turn-completion/locales/en.mjs +159 -0
  37. package/bundled/hooks/turn-completion/locales/zh-TW.mjs +166 -0
  38. package/bundled/hooks/validate-commit-msg.mjs +104 -0
  39. package/bundled/locales/zh-CN/CHANGELOG.md +65 -3
  40. package/bundled/locales/zh-CN/CLAUDE.md +1 -1
  41. package/bundled/locales/zh-CN/README.md +7 -7
  42. package/bundled/locales/zh-CN/SECURITY.md +1 -1
  43. package/bundled/locales/zh-CN/core/adr-standards.md +1 -1
  44. package/bundled/locales/zh-CN/core/ai-instruction-standards.md +10 -8
  45. package/bundled/locales/zh-CN/core/governance-layer.md +118 -6
  46. package/bundled/locales/zh-CN/core/retrospective-standards.md +1 -1
  47. package/bundled/locales/zh-CN/core/tech-debt-standards.md +71 -4
  48. package/bundled/locales/zh-CN/core/turn-completion-integrity.md +190 -0
  49. package/bundled/locales/zh-CN/docs/CHEATSHEET.md +27 -6
  50. package/bundled/locales/zh-CN/docs/CLI-INIT-OPTIONS.md +29 -68
  51. package/bundled/locales/zh-CN/docs/FEATURE-REFERENCE.md +172 -24
  52. package/bundled/locales/zh-CN/docs/USAGE-MODES-COMPARISON.md +1 -2
  53. package/bundled/locales/zh-CN/integrations/google-antigravity/{INSTRUCTIONS.md → AGENTS.md} +1 -1
  54. package/bundled/locales/zh-CN/integrations/google-antigravity/README.md +3 -3
  55. package/bundled/locales/zh-CN/skills/agents/README.md +1 -1
  56. package/bundled/locales/zh-CN/skills/atdd-assistant/SKILL.md +2 -0
  57. package/bundled/locales/zh-CN/skills/bdd-assistant/SKILL.md +2 -0
  58. package/bundled/locales/zh-CN/skills/brainstorm-assistant/SKILL.md +22 -12
  59. package/bundled/locales/zh-CN/skills/brainstorm-assistant/guide.md +12 -9
  60. package/bundled/locales/zh-CN/skills/code-review-assistant/SKILL.md +1 -0
  61. package/bundled/locales/zh-CN/skills/commands/brainstorm.md +17 -13
  62. package/bundled/locales/zh-CN/skills/commands/config.md +0 -1
  63. package/bundled/locales/zh-CN/skills/commands/init.md +1 -2
  64. package/bundled/locales/zh-CN/skills/commit-standards/SKILL.md +2 -0
  65. package/bundled/locales/zh-CN/skills/contract-test-assistant/SKILL.md +1 -0
  66. package/bundled/locales/zh-CN/skills/dev-methodology/SKILL.md +2 -0
  67. package/bundled/locales/zh-CN/skills/observability-assistant/SKILL.md +1 -0
  68. package/bundled/locales/zh-CN/skills/project-structure-guide/SKILL.md +1 -0
  69. package/bundled/locales/zh-CN/skills/release-standards/SKILL.md +3 -0
  70. package/bundled/locales/zh-CN/skills/requirement-assistant/SKILL.md +2 -0
  71. package/bundled/locales/zh-CN/skills/reverse-engineer/SKILL.md +3 -0
  72. package/bundled/locales/zh-CN/skills/reverse-engineer/tdd-analysis.md +13 -23
  73. package/bundled/locales/zh-CN/skills/runbook-assistant/SKILL.md +1 -0
  74. package/bundled/locales/zh-CN/skills/slo-assistant/SKILL.md +1 -0
  75. package/bundled/locales/zh-CN/skills/tdd-assistant/SKILL.md +2 -0
  76. package/bundled/locales/zh-CN/skills/workflows/README.md +2 -11
  77. package/bundled/locales/zh-TW/CHANGELOG.md +65 -3
  78. package/bundled/locales/zh-TW/CLAUDE.md +1 -1
  79. package/bundled/locales/zh-TW/README.md +7 -7
  80. package/bundled/locales/zh-TW/SECURITY.md +1 -1
  81. package/bundled/locales/zh-TW/core/acceptance-criteria-traceability.md +2 -0
  82. package/bundled/locales/zh-TW/core/adr-standards.md +26 -5
  83. package/bundled/locales/zh-TW/core/ai-instruction-standards.md +10 -8
  84. package/bundled/locales/zh-TW/core/code-review-checklist.md +2 -0
  85. package/bundled/locales/zh-TW/core/container-image-standards.md +2 -2
  86. package/bundled/locales/zh-TW/core/contract-testing-standards.md +2 -2
  87. package/bundled/locales/zh-TW/core/cross-flow-regression.md +8 -7
  88. package/bundled/locales/zh-TW/core/data-contract.md +2 -2
  89. package/bundled/locales/zh-TW/core/data-migration-testing.md +2 -2
  90. package/bundled/locales/zh-TW/core/data-pipeline.md +2 -2
  91. package/bundled/locales/zh-TW/core/deferred-item-exit.md +251 -0
  92. package/bundled/locales/zh-TW/core/documentation-writing-standards.md +228 -3
  93. package/bundled/locales/zh-TW/core/full-coverage-testing.md +15 -2
  94. package/bundled/locales/zh-TW/core/governance-layer.md +118 -5
  95. package/bundled/locales/zh-TW/core/iac-design-principles.md +2 -2
  96. package/bundled/locales/zh-TW/core/incident-response.md +2 -2
  97. package/bundled/locales/zh-TW/core/model-provenance.md +4 -2
  98. package/bundled/locales/zh-TW/core/pii-classification.md +42 -6
  99. package/bundled/locales/zh-TW/core/prd-standards.md +4 -2
  100. package/bundled/locales/zh-TW/core/product-metrics-standards.md +4 -2
  101. package/bundled/locales/zh-TW/core/release-readiness-gate.md +2 -2
  102. package/bundled/locales/zh-TW/core/resource-cost-boundary.md +2 -2
  103. package/bundled/locales/zh-TW/core/retrospective-standards.md +5 -3
  104. package/bundled/locales/zh-TW/core/reverse-engineering-standards.md +83 -5
  105. package/bundled/locales/zh-TW/core/runbook.md +2 -2
  106. package/bundled/locales/zh-TW/core/schema-evolution.md +2 -2
  107. package/bundled/locales/zh-TW/core/secret-management-standards.md +2 -2
  108. package/bundled/locales/zh-TW/core/slo-sli.md +2 -2
  109. package/bundled/locales/zh-TW/core/spec-driven-development.md +2 -0
  110. package/bundled/locales/zh-TW/core/tech-debt-standards.md +71 -4
  111. package/bundled/locales/zh-TW/core/turn-completion-integrity.md +190 -0
  112. package/bundled/locales/zh-TW/core/user-journey-testing.md +2 -2
  113. package/bundled/locales/zh-TW/core/user-story-mapping.md +2 -2
  114. package/bundled/locales/zh-TW/core/verification-oracle.md +2 -2
  115. package/bundled/locales/zh-TW/docs/CHEATSHEET.md +27 -6
  116. package/bundled/locales/zh-TW/docs/CLI-INIT-OPTIONS.md +29 -68
  117. package/bundled/locales/zh-TW/docs/FEATURE-REFERENCE.md +172 -24
  118. package/bundled/locales/zh-TW/docs/USAGE-MODES-COMPARISON.md +1 -2
  119. package/bundled/locales/zh-TW/integrations/google-antigravity/{INSTRUCTIONS.md → AGENTS.md} +1 -1
  120. package/bundled/locales/zh-TW/integrations/google-antigravity/README.md +3 -3
  121. package/bundled/locales/zh-TW/skills/adr-assistant/SKILL.md +1 -1
  122. package/bundled/locales/zh-TW/skills/agents/README.md +1 -1
  123. package/bundled/locales/zh-TW/skills/atdd-assistant/SKILL.md +2 -0
  124. package/bundled/locales/zh-TW/skills/bdd-assistant/SKILL.md +2 -0
  125. package/bundled/locales/zh-TW/skills/brainstorm-assistant/SKILL.md +22 -12
  126. package/bundled/locales/zh-TW/skills/brainstorm-assistant/guide.md +12 -9
  127. package/bundled/locales/zh-TW/skills/code-review-assistant/SKILL.md +1 -0
  128. package/bundled/locales/zh-TW/skills/commands/brainstorm.md +17 -13
  129. package/bundled/locales/zh-TW/skills/commands/config.md +0 -1
  130. package/bundled/locales/zh-TW/skills/commands/init.md +1 -2
  131. package/bundled/locales/zh-TW/skills/commit-standards/SKILL.md +2 -0
  132. package/bundled/locales/zh-TW/skills/contract-test-assistant/SKILL.md +2 -1
  133. package/bundled/locales/zh-TW/skills/dev-methodology/SKILL.md +2 -0
  134. package/bundled/locales/zh-TW/skills/dev-workflow-guide/SKILL.md +1 -1
  135. package/bundled/locales/zh-TW/skills/knowledge-graph/guide.md +2 -2
  136. package/bundled/locales/zh-TW/skills/migration-assistant/SKILL.md +1 -1
  137. package/bundled/locales/zh-TW/skills/observability-assistant/SKILL.md +1 -0
  138. package/bundled/locales/zh-TW/skills/project-discovery/SKILL.md +1 -0
  139. package/bundled/locales/zh-TW/skills/project-structure-guide/SKILL.md +1 -0
  140. package/bundled/locales/zh-TW/skills/release-standards/SKILL.md +3 -0
  141. package/bundled/locales/zh-TW/skills/requirement-assistant/SKILL.md +2 -0
  142. package/bundled/locales/zh-TW/skills/reverse-engineer/SKILL.md +3 -0
  143. package/bundled/locales/zh-TW/skills/reverse-engineer/tdd-analysis.md +13 -23
  144. package/bundled/locales/zh-TW/skills/runbook-assistant/SKILL.md +1 -0
  145. package/bundled/locales/zh-TW/skills/slo-assistant/SKILL.md +1 -0
  146. package/bundled/locales/zh-TW/skills/tdd-assistant/SKILL.md +2 -0
  147. package/bundled/locales/zh-TW/skills/workflows/README.md +2 -11
  148. package/bundled/skills/agents/README.md +1 -1
  149. package/bundled/skills/atdd-assistant/SKILL.md +2 -0
  150. package/bundled/skills/bdd-assistant/SKILL.md +2 -0
  151. package/bundled/skills/brainstorm-assistant/SKILL.md +31 -13
  152. package/bundled/skills/brainstorm-assistant/guide.md +9 -6
  153. package/bundled/skills/code-review-assistant/SKILL.md +1 -0
  154. package/bundled/skills/commands/brainstorm.md +12 -9
  155. package/bundled/skills/commands/config.md +0 -1
  156. package/bundled/skills/commands/init.md +2 -3
  157. package/bundled/skills/commit-standards/SKILL.md +2 -0
  158. package/bundled/skills/contract-test-assistant/SKILL.md +1 -0
  159. package/bundled/skills/dev-methodology/SKILL.md +4 -0
  160. package/bundled/skills/observability-assistant/SKILL.md +1 -0
  161. package/bundled/skills/project-discovery/SKILL.md +1 -0
  162. package/bundled/skills/project-structure-guide/SKILL.md +1 -0
  163. package/bundled/skills/release-standards/SKILL.md +3 -0
  164. package/bundled/skills/requirement-assistant/SKILL.md +2 -0
  165. package/bundled/skills/reverse-engineer/SKILL.md +3 -0
  166. package/bundled/skills/reverse-engineer/tdd-analysis.md +16 -23
  167. package/bundled/skills/runbook-assistant/SKILL.md +1 -0
  168. package/bundled/skills/slo-assistant/SKILL.md +1 -0
  169. package/bundled/skills/tdd-assistant/SKILL.md +2 -0
  170. package/bundled/skills/workflows/README.md +2 -11
  171. package/bundled/templates/.ai-context.yaml.template +194 -0
  172. package/bundled/templates/CLAUDE.md.template +145 -0
  173. package/bundled/templates/DESIGN.md +237 -0
  174. package/bundled/templates/SKILL-BRIEF-TEMPLATE.md +57 -0
  175. package/bundled/templates/SKILL-CANDIDATES.md +39 -0
  176. package/bundled/templates/gates/check-error-exit.mjs +309 -0
  177. package/bundled/templates/mcp-config.json +10 -0
  178. package/bundled/templates/methodology-template.yaml +209 -0
  179. package/bundled/templates/migration-template.md +408 -0
  180. package/bundled/templates/requirement-checklist.md +410 -0
  181. package/bundled/templates/requirement-document-template.md +591 -0
  182. package/bundled/templates/requirement-template.md +881 -0
  183. package/bundled/templates/reverse-spec-template.md +409 -0
  184. package/bundled/templates/test-case-template.md +74 -0
  185. package/bundled/templates/test-plan-template.md +74 -0
  186. package/package.json +9 -5
  187. package/src/commands/audit.js +82 -0
  188. package/src/commands/check.js +66 -10
  189. package/src/commands/init.js +161 -16
  190. package/src/commands/lint.js +96 -0
  191. package/src/commands/quickstart.js +16 -13
  192. package/src/commands/update.js +286 -14
  193. package/src/compilers/claude-code-compiler.js +4 -1
  194. package/src/config/ai-agent-paths.js +62 -17
  195. package/src/core/constants.js +42 -11
  196. package/src/core/manifest.js +201 -3
  197. package/src/core/paths.js +2 -2
  198. package/src/i18n/messages.js +9 -32
  199. package/src/installers/hooks-installer.js +167 -75
  200. package/src/installers/integration-installer.js +9 -5
  201. package/src/prompts/init.js +14 -14
  202. package/src/reconciler/actual-state-scanner.js +14 -3
  203. package/src/utils/detector.js +21 -1
  204. package/src/utils/effect-boundary.js +1093 -0
  205. package/src/utils/hasher.js +229 -9
  206. package/src/utils/hook-stats.js +1 -1
  207. package/src/utils/integration-generator.js +100 -4
  208. package/src/utils/reference-sync.js +4 -1
  209. package/src/utils/skills-installer.js +17 -3
  210. package/src/utils/spec-linter.js +35 -76
  211. package/src/utils/yaml-generator.js +51 -9
  212. package/standards-registry.json +31 -8
  213. package/src/commands/sync.js +0 -133
@@ -2,11 +2,11 @@ import chalk from 'chalk';
2
2
  import ora from 'ora';
3
3
  import { select, confirm as inquirerConfirm, checkbox, Separator } from '@inquirer/prompts';
4
4
  import { execSync } from 'child_process';
5
- import { existsSync, readFileSync, unlinkSync } from 'fs';
6
- import { join, basename } from 'path';
7
- import { readManifest, writeManifest, copyStandard, isInitialized } from '../utils/copier.js';
5
+ import { existsSync, mkdirSync, readFileSync, unlinkSync, writeFileSync } from 'fs';
6
+ import { join, basename, dirname, relative } from 'path';
7
+ import { readManifest, writeManifest, copyStandard, isInitialized, getRepoRoot } from '../utils/copier.js';
8
8
  import { getRepositoryInfo, getAllStandards, getStandardSource } from '../utils/registry.js';
9
- import { computeFileHash, scanForUntrackedFiles, refreshIntegrationBlockHashes } from '../utils/hasher.js';
9
+ import { computeFileHash, planStandardsRemovals, refreshIntegrationBlockHashes } from '../utils/hasher.js';
10
10
  import {
11
11
  writeIntegrationFile,
12
12
  getToolFilePath,
@@ -42,6 +42,7 @@ import {
42
42
  getCommandsDirForAgent
43
43
  } from '../config/ai-agent-paths.js';
44
44
  import { getMarketplaceSkillsInfo } from '../utils/github.js';
45
+ import { detectAITools } from '../utils/detector.js';
45
46
  import {
46
47
  promptSkillsInstallLocation,
47
48
  promptCommandsInstallation
@@ -56,7 +57,110 @@ import {
56
57
  import { restoreSingleFile } from './check.js';
57
58
  import { guardAgainstSelfAdoption } from '../utils/detect-self-adoption.js';
58
59
  import { resolveIntegrationFile } from '../core/constants.js';
59
- import { mergeInstalledNames } from '../core/manifest.js';
60
+ import {
61
+ mergeInstalledNames,
62
+ recordFileProvenance,
63
+ forgetFileProvenance,
64
+ establishProvenance,
65
+ isProvenanceEstablished
66
+ } from '../core/manifest.js';
67
+
68
+ /**
69
+ * How aggressive `uds update` is about deleting standards it no longer ships.
70
+ *
71
+ * XSPEC-384 §4 leaves the staging strategy for R2 undecided, so this is the
72
+ * most conservative of the three options on the table and it lives in one named
73
+ * place rather than being spread through the deletion path. Changing this
74
+ * constant is the whole change of default; the alternatives are:
75
+ *
76
+ * 'report-only' list what would go, delete nothing without --prune (current)
77
+ * 'ask' list, and prompt in interactive sessions only
78
+ * 'auto' delete as part of a normal update
79
+ *
80
+ * It starts at 'report-only' because the change is adopter-visible in the
81
+ * dangerous direction: files that survive today start disappearing. A default
82
+ * that deletes must not arrive in a patch release, and must not arrive at all
83
+ * before someone decides it should.
84
+ */
85
+ export const PRUNE_POLICY = 'report-only';
86
+
87
+ /**
88
+ * Decide whether this run is allowed to delete.
89
+ *
90
+ * `--prune` is deliberately its own flag rather than a use of `--yes`. `--yes`
91
+ * means "don't ask me about the thing I already asked for"; it is set in CI and
92
+ * in scripts, and letting it also authorise deletion would enable the
93
+ * destructive path silently in exactly the unattended environments that cannot
94
+ * notice it. Non-interactive runs therefore never delete unless --prune is
95
+ * explicit. (XSPEC-384 R3)
96
+ *
97
+ * @param {Object} plan - Result of planStandardsRemovals
98
+ * @param {Object} options - Command options
99
+ * @returns {boolean}
100
+ */
101
+ export function shouldPrune(plan, options = {}) {
102
+ if (plan.remove.length === 0) return false;
103
+ if (options.prune) return true;
104
+ return PRUNE_POLICY === 'auto';
105
+ }
106
+
107
+ /**
108
+ * Print the removal plan, including its denominator. (XSPEC-384 R3)
109
+ *
110
+ * The counts are not decoration. A report that lists only the files it intends
111
+ * to touch is bit-for-bit identical to a report from a scan that only *saw*
112
+ * those files — which is the failure the previous code shipped for months,
113
+ * silently, because nothing ever printed how many files it had considered.
114
+ *
115
+ * @param {Object} plan - Result of planStandardsRemovals
116
+ * @param {boolean} provenanceWasEstablished - Whether ownership was already known
117
+ * @param {boolean} willDelete - Whether this run is going to act on the plan
118
+ */
119
+ export function reportRemovalPlan(plan, provenanceWasEstablished, willDelete = false) {
120
+ if (plan.scanned === 0) return;
121
+
122
+ console.log();
123
+ console.log(chalk.gray(
124
+ ` .standards/ examined: ${plan.scanned} file(s) — ` +
125
+ `${plan.census.udsOwned} written by UDS, ` +
126
+ `${plan.census.foreign} not ours, ` +
127
+ `${plan.census.unknown} ownership unknown, ` +
128
+ `${plan.excluded.length} excluded`
129
+ ));
130
+ for (const { path: p, reason } of plan.excluded) {
131
+ console.log(chalk.gray(` excluded ${p} (${reason})`));
132
+ }
133
+
134
+ if (!provenanceWasEstablished) {
135
+ console.log(chalk.gray(
136
+ ' first run with ownership tracking: files installed before this ' +
137
+ 'release cannot be attributed yet, so none will be removed.'
138
+ ));
139
+ }
140
+
141
+ if (plan.remove.length === 0) {
142
+ if (plan.census.foreign > 0 || plan.census.unknown > 0) {
143
+ console.log(chalk.gray(
144
+ ` kept ${plan.census.foreign + plan.census.unknown} file(s) UDS did not write`
145
+ ));
146
+ }
147
+ return;
148
+ }
149
+
150
+ console.log();
151
+ console.log(chalk.yellow(
152
+ ` ${plan.remove.length} file(s) are no longer shipped by UDS` +
153
+ (willDelete ? ' and will be removed:' : ':')
154
+ ));
155
+ for (const { path: p, reason } of plan.remove) {
156
+ console.log(chalk.yellow(` - ${p} (${reason})`));
157
+ }
158
+ console.log(chalk.gray(
159
+ willDelete
160
+ ? ' Removing now (--prune). Files UDS did not write are never touched.'
161
+ : ' Not deleted. Re-run with --prune to remove them.'
162
+ ));
163
+ }
60
164
 
61
165
  /**
62
166
  * Determine the correct target directory for a standard file.
@@ -546,6 +650,21 @@ export async function updateCommand(options) {
546
650
  errors: []
547
651
  };
548
652
 
653
+ // Paths UDS actually wrote this run, taken from the copy result rather than
654
+ // recomputed from the source name. Recomputing is how `fileHashes` ended up
655
+ // with five `.standards/options/<name>.ai.yaml` entries for files that live
656
+ // at `.standards/options/<category>/<name>.ai.yaml`; a provenance record
657
+ // built the same way would claim files that do not exist and disown ones
658
+ // that do. (XSPEC-384 R1)
659
+ const writtenPaths = [];
660
+ const recordWritten = (result) => {
661
+ if (!result?.success || !result.data) return;
662
+ const abs = typeof result.data === 'string' ? result.data : null;
663
+ if (!abs) return;
664
+ const rel = relative(projectPath, abs).replace(/\\/g, '/');
665
+ if (rel && !rel.startsWith('..')) writtenPaths.push(rel);
666
+ };
667
+
549
668
  // Update standards
550
669
  const updateFormat = manifest.format || 'ai';
551
670
  const allStdsUpdate = getAllStandards();
@@ -561,6 +680,7 @@ export async function updateCommand(options) {
561
680
  const result = await copyStandard(sourcePath, getStandardTargetDir(sourcePath), projectPath);
562
681
  if (result.success) {
563
682
  results.updated.push(std);
683
+ recordWritten(result);
564
684
  } else {
565
685
  results.errors.push(`${std}: ${result.error}`);
566
686
  }
@@ -572,6 +692,7 @@ export async function updateCommand(options) {
572
692
  const result = await copyStandard(ext, '.standards', projectPath);
573
693
  if (result.success) {
574
694
  results.updated.push(ext);
695
+ recordWritten(result);
575
696
  } else {
576
697
  results.errors.push(`${ext}: ${result.error}`);
577
698
  }
@@ -601,6 +722,7 @@ export async function updateCommand(options) {
601
722
  if (result.success) {
602
723
  manifest.standards.push(ns.source);
603
724
  results.updated.push(ns.source);
725
+ recordWritten(result);
604
726
  newCount++;
605
727
  } else {
606
728
  results.errors.push(`${ns.source}: ${result.error}`);
@@ -751,6 +873,20 @@ export async function updateCommand(options) {
751
873
  manifest.fileHashes = {};
752
874
  }
753
875
 
876
+ // The set of `.standards/` files the *current* registry says should exist.
877
+ //
878
+ // Built by accumulating inside the loops that already resolve each entry,
879
+ // rather than by a second function that re-derives the same paths. A second
880
+ // derivation is a second copy, and the two would answer "where does this
881
+ // standard live" differently the moment either is edited — which is the
882
+ // failure mode XSPEC-384 is about, not a shape to reproduce. (XSPEC-384 R2)
883
+ const desiredStandardsFiles = new Set();
884
+
885
+ // Standards whose ID no longer resolves against the registry. Collected here
886
+ // instead of being silently `continue`d, because "skipped" and "deleted
887
+ // upstream" were previously indistinguishable — issue #165. (XSPEC-384 R2)
888
+ const unresolvedStandardIds = [];
889
+
754
890
  // Update hashes for standards (handles both ID format and legacy path format)
755
891
  const hashFormat = manifest.format || 'ai';
756
892
  const allStdsHash = getAllStandards();
@@ -759,7 +895,13 @@ export async function updateCommand(options) {
759
895
  let resolvedPath = std;
760
896
  if (!std.includes('/') && !std.includes('.')) {
761
897
  const entry = allStdsHash.find(r => r.id === std);
762
- if (!entry) continue; // Skip unrecognized IDs (e.g. stale AI tool names)
898
+ if (!entry) {
899
+ // Unrecognized ID: either a standard removed from the registry, or a
900
+ // stale AI tool name an old CLI wrote into `standards`. Either way it
901
+ // is no longer a thing we ship, so it must not enter the desired set.
902
+ unresolvedStandardIds.push(std);
903
+ continue;
904
+ }
763
905
  const src = getStandardSource(entry, hashFormat);
764
906
  if (src) resolvedPath = src;
765
907
  }
@@ -767,6 +909,10 @@ export async function updateCommand(options) {
767
909
  const relativePath = (resolvedPath.includes('options/')
768
910
  ? join('.standards', 'options', fileName)
769
911
  : join('.standards', fileName)).replace(/\\/g, '/');
912
+ // Added before the hash check: a file that exists but failed to hash is
913
+ // still wanted. Nominating it for deletion because hashing failed would
914
+ // turn a transient read error into data loss.
915
+ desiredStandardsFiles.add(relativePath);
770
916
  const fullPath = join(projectPath, relativePath);
771
917
  const hashInfo = computeFileHash(fullPath);
772
918
  if (hashInfo) {
@@ -779,6 +925,7 @@ export async function updateCommand(options) {
779
925
  if (typeof ext !== 'string') continue;
780
926
  const fileName = basename(ext);
781
927
  const relativePath = join('.standards', fileName).replace(/\\/g, '/');
928
+ desiredStandardsFiles.add(relativePath);
782
929
  const fullPath = join(projectPath, relativePath);
783
930
  const hashInfo = computeFileHash(fullPath);
784
931
  if (hashInfo) {
@@ -795,21 +942,63 @@ export async function updateCommand(options) {
795
942
  }
796
943
  }
797
944
 
798
- // Clean up orphan files in .standards/ that are no longer tracked
799
- // This handles files renamed or removed between versions
800
- const orphanFiles = scanForUntrackedFiles(projectPath, manifest);
801
- const standardsOrphans = orphanFiles.filter(f => f.startsWith('.standards/'));
802
- if (standardsOrphans.length > 0) {
803
- for (const orphan of standardsOrphans) {
945
+ // Record what UDS wrote this run, then decide what (if anything) may go.
946
+ // (XSPEC-384 R1/R2/R3)
947
+ const provenanceWasEstablished = isProvenanceEstablished(manifest);
948
+ let provenanceCarrier = manifest;
949
+ for (const written of [...writtenPaths, ...results.integrations]) {
950
+ provenanceCarrier = recordFileProvenance(provenanceCarrier, written, 'update');
951
+ }
952
+ manifest.provenance = provenanceCarrier.provenance;
953
+
954
+ // Drop `standards` entries the registry no longer knows about. Previously
955
+ // these were `continue`d in three separate loops and left in the manifest
956
+ // forever, so `upstream.version` advanced to "latest" while the manifest
957
+ // still listed standards that had not existed for releases. (issue #165)
958
+ if (unresolvedStandardIds.length > 0) {
959
+ manifest.standards = manifest.standards.filter(s => !unresolvedStandardIds.includes(s));
960
+ console.log();
961
+ console.log(chalk.yellow(
962
+ `⚠ ${unresolvedStandardIds.length} manifest entr${unresolvedStandardIds.length === 1 ? 'y is' : 'ies are'} no longer in the registry and ${unresolvedStandardIds.length === 1 ? 'was' : 'were'} dropped:`
963
+ ));
964
+ for (const id of unresolvedStandardIds) {
965
+ console.log(chalk.gray(` - ${id}`));
966
+ }
967
+ }
968
+
969
+ const removalPlan = planStandardsRemovals(projectPath, manifest, desiredStandardsFiles);
970
+ // Decide before reporting, so the report states what this run will actually
971
+ // do. Reporting first meant printing "Not deleted. Re-run with --prune" on a
972
+ // run that was about to delete — a message contradicting the very action it
973
+ // preceded, which is worse than no message.
974
+ const mayPrune = shouldPrune(removalPlan, options);
975
+ reportRemovalPlan(removalPlan, provenanceWasEstablished, mayPrune);
976
+
977
+ if (mayPrune && removalPlan.remove.length > 0) {
978
+ for (const { path: orphan } of removalPlan.remove) {
804
979
  try {
805
980
  unlinkSync(join(projectPath, orphan));
806
981
  results.updated.push(`removed: ${orphan}`);
982
+ delete manifest.fileHashes[orphan];
983
+ // Deleting the file is how we give up the claim on that path, so a
984
+ // file the user later creates under the same name is not inherited.
985
+ manifest.provenance = forgetFileProvenance(manifest, orphan).provenance;
807
986
  } catch {
808
987
  // Ignore removal errors
809
988
  }
810
989
  }
811
990
  }
812
991
 
992
+ // Provenance is only a complete account of what we own once a run has
993
+ // written the whole installed set. Until then every unrecorded file is
994
+ // `unknown` rather than `foreign`, and nothing gets deleted on the strength
995
+ // of an absent record. Establishing it here — after the writes, not before —
996
+ // is what makes the first upgrade conservative by construction rather than
997
+ // by a flag someone has to remember. (XSPEC-384 §4)
998
+ if (results.errors.length === 0) {
999
+ manifest.provenance = establishProvenance(manifest).provenance;
1000
+ }
1001
+
813
1002
  // Migrate display_language: if manifest options is missing display_language,
814
1003
  // attempt to backfill from ~/.udsrc ui.language so the project honours the
815
1004
  // language the user already configured globally.
@@ -1366,11 +1555,58 @@ export async function updateCommand(options) {
1366
1555
 
1367
1556
  console.log();
1368
1557
 
1558
+ // 錯誤訊息單一出口閘門:問過才寫,而且從不覆寫。
1559
+ await offerErrorExitGate(projectPath, options);
1560
+
1369
1561
  // Exit explicitly to prevent hanging.
1370
1562
  // T11: a partial update exits non-zero so CI / scripts can detect the failure.
1371
1563
  process.exit(updateIncomplete ? 1 : 0);
1372
1564
  }
1373
1565
 
1566
+ /**
1567
+ * 提供錯誤訊息單一出口閘門——**顯示它是什麼、問過、才寫,而且絕不覆寫既有檔案。**
1568
+ *
1569
+ * 🔴 為什麼不自動寫:`uds` 是別人裝進自己 repo 的工具。未經同意往陌生人的專案
1570
+ * 寫一支可執行腳本,比它要修的那個問題更糟——那會變成信任問題,而不是 bug。
1571
+ * 採用者說不要,我們就不寫,而且下次 `uds check` 只會再提一次,不會偷偷放進去。
1572
+ *
1573
+ * ⚠️ 已存在就直接跳過,連問都不問:那個檔案裡的 CONFIG 是採用者自己填的,
1574
+ * 我們沒有立場拿範本蓋掉它。要更新內容是另一件事,不在這裡做。
1575
+ */
1576
+ async function offerErrorExitGate(projectPath, options) {
1577
+ const dest = join(projectPath, 'scripts', 'check-error-exit.mjs');
1578
+ if (existsSync(dest)) return; // 他們自己的檔案
1579
+ if (!existsSync(join(projectPath, 'src'))) return; // 沒有原始碼就沒有這個問題
1580
+
1581
+ const src = join(getRepoRoot(), 'templates', 'gates', 'check-error-exit.mjs');
1582
+ if (!existsSync(src)) return; // 範本沒出貨——沉默,不要假裝提供了
1583
+
1584
+ console.log(chalk.bold(' 錯誤訊息單一出口閘門'));
1585
+ console.log(chalk.gray(' 防的是:每個呼叫端各自把錯誤回應拼成給人看的字串。'));
1586
+ console.log(chalk.gray(' 第一處是實作,第二處開始就會各寫各的,而畫面上只剩一句 Bad Request。'));
1587
+ console.log(chalk.gray(` 要寫入:scripts/check-error-exit.mjs(純 Node、零相依、${Math.round(readFileSync(src, 'utf-8').length / 1024)}KB)`));
1588
+ console.log(chalk.gray(' 寫入後它會 exit 2 直到你填好 CONFIG——那是刻意的,'));
1589
+ console.log(chalk.gray(' 一支在新 repo 裡靜靜回綠的閘門跟一支沒裝的無從分辨。'));
1590
+ console.log();
1591
+
1592
+ const ok = await confirmOrFail({
1593
+ message: '要把這道閘門寫進 scripts/check-error-exit.mjs 嗎?',
1594
+ defaultValue: false, // 🔴 預設不寫。沉默不是同意。
1595
+ options
1596
+ });
1597
+ if (!ok) {
1598
+ console.log(chalk.gray(' 跳過。`uds check` 之後會再提醒,但不會自己寫進去。'));
1599
+ console.log();
1600
+ return;
1601
+ }
1602
+
1603
+ mkdirSync(dirname(dest), { recursive: true });
1604
+ writeFileSync(dest, readFileSync(src, 'utf-8'), 'utf-8');
1605
+ console.log(chalk.green(' ✓ 已寫入 scripts/check-error-exit.mjs'));
1606
+ console.log(chalk.gray(' 下一步:填好檔頭的 CONFIG,然後把它接進你的 CI 或 pre-commit。'));
1607
+ console.log();
1608
+ }
1609
+
1374
1610
  /**
1375
1611
  * Regenerate integration files for all configured AI tools
1376
1612
  * Reusable core logic that can be called from both updateIntegrationsOnly and configureCommand
@@ -1845,7 +2081,35 @@ async function updateSkillsOnly(projectPath, manifest, options) {
1845
2081
  // Convert legacy format to new format
1846
2082
  const legacyLocation = manifest.skills.location;
1847
2083
  if (legacyLocation === 'user' || legacyLocation === 'project') {
1848
- skillsInstallations.push({ agent: 'claude-code', level: legacyLocation });
2084
+ // 🔴 A legacy manifest records a LOCATION, never a tool — and this used to
2085
+ // fill the gap with `claude-code`, unconditionally. For everyone who ran the
2086
+ // old `uds init -y` in a codex or opencode repo, that guess is wrong and
2087
+ // self-perpetuating: the fix to `init.js` would never reach them, because
2088
+ // update would keep re-installing to `.claude/skills/` on the strength of a
2089
+ // tool name nobody ever recorded. (XSPEC-408 §18.3 — Codex does not read
2090
+ // that directory; measured, two arms, 2026-09-08.)
2091
+ //
2092
+ // So re-derive from what is actually detected in the repo, and keep
2093
+ // `claude-code` only as the fallback for when nothing is detected — which is
2094
+ // exactly the case the old line was written for.
2095
+ let detected = {};
2096
+ try {
2097
+ detected = detectAITools(projectPath) || {};
2098
+ } catch {
2099
+ // Detection failing puts us in the "we do not know" case, which is exactly
2100
+ // what the old unconditional `claude-code` line was for. Never let a probe
2101
+ // for extra information break the update that would have worked without it.
2102
+ detected = {};
2103
+ }
2104
+ const detectedSkillsTools = Object.entries(detected)
2105
+ .filter(([, present]) => present)
2106
+ .map(([key]) => (key === 'claudeCode' ? 'claude-code' : key === 'geminiCli' ? 'gemini-cli' : key))
2107
+ .filter(agent => {
2108
+ const config = getAgentConfig(agent);
2109
+ return Boolean(config?.supportsSkills && config?.skills);
2110
+ });
2111
+ const agents = detectedSkillsTools.length > 0 ? detectedSkillsTools : ['claude-code'];
2112
+ for (const agent of agents) skillsInstallations.push({ agent, level: legacyLocation });
1849
2113
  } else if (legacyLocation === 'marketplace') {
1850
2114
  console.log(chalk.yellow(msg.skillsViaMarketplace || 'Skills installed via Marketplace'));
1851
2115
  console.log(chalk.gray(` ${msg.updateViaMarketplace || 'Update through Plugin Marketplace'}`));
@@ -1913,8 +2177,16 @@ async function updateSkillsOnly(projectPath, manifest, options) {
1913
2177
  }).join(', ');
1914
2178
 
1915
2179
  if (result.totalErrors === 0) {
2180
+ // 🔴 `{count}` is worded "AI tools" in every locale, and this passed
2181
+ // `totalInstalled` — the number of skill FILES written, summed across targets.
2182
+ // A two-tool update of 55 skills printed "Updated Skills for 165 AI tools".
2183
+ // It was invisible while this branch only ran from the interactive flow; the
2184
+ // fix to `init.js` makes it the common path, so it stops being invisible.
1916
2185
  spinner.succeed((msg.skillsUpdated || 'Updated Skills in {count} locations: {locations}')
1917
- .replace('{count}', result.totalInstalled)
2186
+ // Counted from the same list that builds `locations`, so the number and the
2187
+ // names can never disagree — and it does not depend on the shape of the
2188
+ // installer's return value.
2189
+ .replace('{count}', fileBasedInstallations.length)
1918
2190
  .replace('{locations}', locations));
1919
2191
  } else {
1920
2192
  spinner.warn((msg.skillsUpdatedWithErrors || 'Updated Skills with {errors} errors')
@@ -33,9 +33,12 @@ export class ClaudeCodeCompiler extends BaseCompiler {
33
33
  hooks[trigger] = [];
34
34
  }
35
35
 
36
+ // Claude Code dispatches on `type`; an entry without one is skipped
37
+ // silently. Measured 2026-09-08 with both shapes in the same event of the
38
+ // same session: the object ran, the bare string did not.
36
39
  hooks[trigger].push({
37
40
  matcher,
38
- hooks: [`node ${hook_script}`],
41
+ hooks: [{ type: 'command', command: `node ${hook_script}` }],
39
42
  });
40
43
  }
41
44
 
@@ -139,7 +139,19 @@ export const AI_AGENT_PATHS = {
139
139
  user: join(homedir(), '.roo', 'workflows')
140
140
  },
141
141
  supportsMarketplace: false,
142
- fallbackSkillsPath: '.claude/skills/',
142
+ // 🔴 Was '.claude/skills/', with no provenance. Roo Code's docs ENUMERATE where it
143
+ // looks for skills — `.roo/skills/`, `.agents/skills/`, and their `~` twins — and
144
+ // `.claude/skills/` is not among them. An enumerated discovery list that omits a
145
+ // path is a statement, not silence (same shape as the Codex list that this field
146
+ // was falsified against by measurement).
147
+ //
148
+ // ⚠️ Docs only, 2026-09-08: NOT measured, because there is no Roo Code CLI on this
149
+ // machine. Acting on it anyway because the asymmetry is one-sided — wrong here
150
+ // means skills land in `.roo/skills/`, which the docs confirm is read; leaving it
151
+ // meant `uds init` routed Roo Code to Claude's marketplace and installed **zero**
152
+ // skills, which is what it actually did until this change.
153
+ // (roocodeinc.github.io/Roo-Code/features/skills)
154
+ fallbackSkillsPath: null,
143
155
  supportsSkills: true,
144
156
  supportsTask: true,
145
157
  supportsAgents: true
@@ -169,7 +181,14 @@ export const AI_AGENT_PATHS = {
169
181
  },
170
182
  workflows: null,
171
183
  supportsMarketplace: false,
172
- fallbackSkillsPath: '.claude/skills/',
184
+ // 🔴 Was '.claude/skills/'. MEASURED FALSE 2026-09-08 (XSPEC-408 §18.3): two probe
185
+ // arms differing only in install path. At `.agents/skills` Codex reported "Skill
186
+ // descriptions were shortened to fit the 2% skills context budget — Codex can
187
+ // still see every skill", 18,096 input tokens. At `.claude/skills` it said
188
+ // nothing, 15,235. The discovery list documented above never included that
189
+ // directory; this field asserted otherwise with no provenance, and `check.js`
190
+ // printed it to adopters as "Can use fallback".
191
+ fallbackSkillsPath: null,
173
192
  supportsSkills: true,
174
193
  supportsTask: false,
175
194
  supportsAgents: true
@@ -251,28 +270,54 @@ export const AI_AGENT_PATHS = {
251
270
  },
252
271
  'antigravity': {
253
272
  name: 'Google Antigravity',
254
- tier: 'minimal',
255
- // Skills install path is UNVERIFIED against a real Antigravity CLI, so it is null:
256
- // `supportsSkills && skills` is the install guard everywhere (init.js, init-flow.js,
257
- // update.js, config.js), and a null `skills` makes it decline rather than write to a
258
- // path the tool may never read.
273
+ // Raised from 'minimal' 2026-09-08. It was minimal because UDS installed
274
+ // nothing for it — the tier was describing our own gap, not the tool. It now
275
+ // gets 55 skills and an instruction file, the same as the other 'partial'
276
+ // tools, and passes all six of that tier's rules.
277
+ tier: 'partial',
278
+ // Skills path CONFIRMED 2026-09-08, three independent ways:
279
+ // 1. The agy 1.0.14 binary carries the literal template
280
+ // `{workspace}/.agents/skills/{skill_name}/SKILL.md`.
281
+ // 2. The guide shipped with the tool
282
+ // (~/.gemini/antigravity-cli/builtin/skills/antigravity_guide/references/ide.md)
283
+ // says `<project-root>/.agents/` holds "project-specific rules, custom skills,
284
+ // and plugins".
285
+ // 3. A live two-arm run: a skill at `.agents/skills/` was listed back with its
286
+ // description verbatim; a decoy at `.agent/skills/` was not listed at all.
287
+ //
288
+ // 🔴 BOTH candidates previously recorded here were wrong — the official-plugin path
289
+ // and `.agent/skills/` (singular). Neither had been tested; declining to install was
290
+ // the right call for six months, and the answer came from the tool's own binary, not
291
+ // from either document.
259
292
  //
260
- // Two candidates conflict, and neither has been tested:
261
- // a) ~/.gemini/antigravity-cli/plugins/<name>/skills/ -- official plugin docs
262
- // b) .agent/skills/ + ~/.gemini/antigravity/skills -- UDS's own 2026-02 spec,
263
- // written while Gemini CLI was still the product; Antigravity replaced it on
264
- // 2026-06-18, so (b) inherits an assumption that may no longer hold.
293
+ // 🔴 PRECONDITION the adopter must meet: agy only discovers `.agents/` when the
294
+ // directory is a registered Antigravity project (~/.gemini/projects.json). Measured:
295
+ // the same probe repo returned only the builtin skill before `agy --new-project` and
296
+ // both skills after. Installing here is correct, but a user who never opens the
297
+ // project in Antigravity will see nothing — that is the tool's condition, not ours.
265
298
  //
266
- // Installing to the wrong path fails SILENTLY -- the user sees a successful init and
267
- // an assistant that never picks the skills up. Declining is the safer default until
268
- // one candidate is confirmed. Tracked as XSPEC-355 OQ6.
269
- skills: null,
299
+ // No `user` path: the binary has exactly one skills template and it is
300
+ // workspace-scoped. A user-level path would be invented, which is how the two wrong
301
+ // candidates above got written down in the first place.
302
+ skills: {
303
+ project: '.agents/skills/'
304
+ },
305
+ // Shown to the adopter after a successful install. A field rather than a
306
+ // special case for this tool: the next tool with a precondition fills it in
307
+ // instead of adding a branch. Without it the condition lives only in a spec,
308
+ // and the adopter — who reads neither — sees a successful install and an
309
+ // assistant that picked nothing up.
310
+ installNote:
311
+ 'Antigravity only discovers .agents/ once the directory is a registered '
312
+ + 'Antigravity project. Open it in the IDE, or run `agy --new-project` here '
313
+ + 'once. Measured 2026-09-08: the same repo reported only the built-in skill '
314
+ + 'before that and all of them after.',
270
315
  commands: null,
271
316
  agents: null,
272
317
  workflows: null,
273
318
  supportsMarketplace: false,
274
319
  fallbackSkillsPath: null,
275
- supportsSkills: true, // The tool does support skills; only our path for it is unverified.
320
+ supportsSkills: true,
276
321
  supportsTask: false,
277
322
  supportsAgents: false
278
323
  }
@@ -94,16 +94,25 @@ export const SUPPORTED_AI_TOOLS = {
94
94
  category: 'secondary',
95
95
  supports: ['skills']
96
96
  },
97
- 'roo': {
98
- name: 'Roo',
99
- file: 'ROO.md',
97
+ // 🔴 Key was 'roo' and the file was 'ROO.md'. Every other module calls this tool
98
+ // `roo-code` (`ai-agent-paths.js`, `REGISTRY.json`, `agent-adapter.js`,
99
+ // `agents-installer.js`), so every lookup here MISSED — and the miss was silent:
100
+ // `getToolFileName()` ends in `return \`${tool}.md\``, so a real `uds init` in a Roo
101
+ // Code repo wrote its instructions to `roo-code.md` in the repo root. Roo Code's
102
+ // docs name `.roo/rules/`, `.roorules`, and `AGENTS.md`; `roo-code.md` is read by
103
+ // nothing, and `ROO.md` was never written by anything either.
104
+ // File value matches REGISTRY.json's `instructionFile` for this agent.
105
+ // (roocodeinc.github.io/Roo-Code/features/custom-instructions, read 2026-09-08)
106
+ 'roo-code': {
107
+ name: 'Roo Code',
108
+ file: '.roo/rules/uds-standards.md',
100
109
  format: 'markdown',
101
110
  category: 'secondary',
102
111
  supports: ['skills', 'workflows']
103
112
  },
104
113
  'antigravity': {
105
114
  name: 'Antigravity',
106
- file: 'INSTRUCTIONS.md',
115
+ file: '.agents/AGENTS.md',
107
116
  format: 'markdown',
108
117
  category: 'secondary',
109
118
  supports: ['skills', 'workflows']
@@ -122,7 +131,12 @@ export const SUPPORTED_AI_TOOLS = {
122
131
  */
123
132
  export const LEGACY_TOOL_MAPPINGS = {
124
133
  'codex': 'opencode',
125
- 'copilot': 'github-copilot'
134
+ 'copilot': 'github-copilot',
135
+ // Maps the OLD key to the CORRECT one, never the reverse — an alias pointing at
136
+ // the wrong name would make the wrong name authoritative. No manifest in any of
137
+ // the five adopting repos records 'roo' (checked 2026-09-08); this exists for
138
+ // manifests written before the rename.
139
+ 'roo': 'roo-code'
126
140
  };
127
141
 
128
142
  /**
@@ -141,10 +155,13 @@ export const FILE_EXTENSIONS = {
141
155
  /**
142
156
  * Content modes for integration files
143
157
  */
158
+ // `FULL: 'full'` was removed here (XSPEC-357 R7). It never produced different
159
+ // output from INDEX — the generator's only test on the mode is `=== 'minimal'`
160
+ // — so it was a second name for one behaviour, offered to adopters as a
161
+ // distinct choice. See SUPPORTED_CONTENT_MODES in core/manifest.js.
144
162
  export const CONTENT_MODES = {
145
163
  MINIMAL: 'minimal', // Reference-only content
146
- INDEX: 'index', // Standard index with descriptions
147
- FULL: 'full' // Full standard content embedded
164
+ INDEX: 'index' // Standard index with descriptions + compliance instructions
148
165
  };
149
166
 
150
167
  /**
@@ -541,13 +558,27 @@ export function resolveToolKey(entry) {
541
558
  if (typeof entry !== 'string' || entry.length === 0) return null;
542
559
  if (SUPPORTED_AI_TOOLS[entry]) return entry;
543
560
  const norm = entry.replace(/\\/g, '/');
561
+
562
+ // Exact or path-suffix only. A bare basename match would let `MY-CLAUDE.md`
563
+ // or `CLAUDE.md.bak` pass as the managed integration file.
564
+ //
565
+ // 🔴 Two passes, and the order matters. Antigravity's file is
566
+ // `.agents/AGENTS.md` while OpenCode's is `AGENTS.md`, so a single pass
567
+ // resolved `.agents/AGENTS.md` to OpenCode — whichever tool happened to be
568
+ // declared first won a path the other had spelled out in full. An exactly
569
+ // declared path must beat another tool's suffix, and among suffixes the
570
+ // longest (most specific) one wins.
571
+ for (const [key, cfg] of Object.entries(SUPPORTED_AI_TOOLS)) {
572
+ if (cfg?.file && norm === cfg.file) return key;
573
+ }
574
+ let best = null;
544
575
  for (const [key, cfg] of Object.entries(SUPPORTED_AI_TOOLS)) {
545
576
  if (!cfg?.file) continue;
546
- // Exact or path-suffix only. A bare basename match would let `MY-CLAUDE.md`
547
- // or `CLAUDE.md.bak` pass as the managed integration file.
548
- if (norm === cfg.file || norm.endsWith(`/${cfg.file}`)) return key;
577
+ if (norm.endsWith(`/${cfg.file}`) && (!best || cfg.file.length > best.len)) {
578
+ best = { key, len: cfg.file.length };
579
+ }
549
580
  }
550
- return null;
581
+ return best ? best.key : null;
551
582
  }
552
583
 
553
584
  /** Entry (tool key OR file path) → repo-relative integration file, or null. */