@open-agent-toolkit/cli 0.2.36 → 0.2.37

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 (183) hide show
  1. package/assets/bundle-metadata.json +1 -1
  2. package/assets/docs/cli-utilities/backlog-lifecycle.md +18 -0
  3. package/assets/docs/cli-utilities/bootstrap.md +6 -3
  4. package/assets/docs/cli-utilities/config-and-local-state.md +26 -12
  5. package/assets/docs/cli-utilities/configuration.md +36 -13
  6. package/assets/docs/cli-utilities/tool-packs.md +423 -51
  7. package/assets/docs/provider-sync/scope-and-surface.md +4 -1
  8. package/assets/docs/reference/cli-reference.md +5 -3
  9. package/assets/docs/reference/file-locations.md +28 -4
  10. package/assets/docs/reference/troubleshooting.md +132 -2
  11. package/assets/docs/workflows/projects/lifecycle.md +21 -2
  12. package/assets/public-package-versions.json +4 -4
  13. package/assets/scripts/generate-oat-state.sh +441 -0
  14. package/assets/scripts/generate-thin-index.sh +200 -0
  15. package/assets/skills/oat-agent-instructions-analyze/SKILL.md +10 -5
  16. package/assets/skills/oat-agent-instructions-apply/SKILL.md +30 -5
  17. package/assets/skills/oat-brainstorm/SKILL.md +34 -8
  18. package/assets/skills/oat-brainstorm/references/destinations.md +6 -1
  19. package/assets/skills/oat-docs-analyze/SKILL.md +9 -4
  20. package/assets/skills/oat-docs-apply/SKILL.md +6 -3
  21. package/assets/skills/oat-pjm-add-backlog-item/SKILL.md +14 -1
  22. package/assets/skills/oat-pjm-decision/SKILL.md +22 -10
  23. package/assets/skills/oat-pjm-review-backlog/SKILL.md +20 -3
  24. package/assets/skills/oat-pjm-update-repo-reference/SKILL.md +20 -10
  25. package/assets/skills/oat-project-autonomous/references/gate-inventory.md +1 -1
  26. package/assets/skills/oat-project-document/SKILL.md +21 -3
  27. package/assets/skills/oat-project-document/references/docs/autonomy-contract.md +1 -1
  28. package/assets/skills/oat-project-implement/references/docs/autonomy-contract.md +1 -1
  29. package/assets/skills/oat-project-pr-final/references/docs/autonomy-contract.md +1 -1
  30. package/assets/skills/oat-project-quick-start/references/docs/autonomy-contract.md +1 -1
  31. package/assets/skills/oat-project-summary/SKILL.md +23 -11
  32. package/assets/skills/oat-repo-knowledge-index/SKILL.md +6 -3
  33. package/dist/commands/backlog/index.d.ts +2 -0
  34. package/dist/commands/backlog/index.d.ts.map +1 -1
  35. package/dist/commands/backlog/index.js +67 -23
  36. package/dist/commands/backlog/new.d.ts +3 -0
  37. package/dist/commands/backlog/new.d.ts.map +1 -1
  38. package/dist/commands/backlog/new.js +8 -21
  39. package/dist/commands/decision/agents-guidance.d.ts.map +1 -1
  40. package/dist/commands/decision/agents-guidance.js +2 -1
  41. package/dist/commands/decision/index.d.ts +2 -0
  42. package/dist/commands/decision/index.d.ts.map +1 -1
  43. package/dist/commands/decision/index.js +19 -3
  44. package/dist/commands/decision/new.d.ts +4 -0
  45. package/dist/commands/decision/new.d.ts.map +1 -1
  46. package/dist/commands/decision/new.js +11 -30
  47. package/dist/commands/doctor/index.d.ts +4 -0
  48. package/dist/commands/doctor/index.d.ts.map +1 -1
  49. package/dist/commands/doctor/index.js +254 -9
  50. package/dist/commands/init/tools/brainstorm/index.d.ts.map +1 -1
  51. package/dist/commands/init/tools/brainstorm/index.js +20 -6
  52. package/dist/commands/init/tools/docs/index.d.ts.map +1 -1
  53. package/dist/commands/init/tools/docs/index.js +4 -1
  54. package/dist/commands/init/tools/ideas/index.d.ts.map +1 -1
  55. package/dist/commands/init/tools/ideas/index.js +4 -1
  56. package/dist/commands/init/tools/index.d.ts +11 -2
  57. package/dist/commands/init/tools/index.d.ts.map +1 -1
  58. package/dist/commands/init/tools/index.js +459 -351
  59. package/dist/commands/init/tools/install-state.d.ts +2 -0
  60. package/dist/commands/init/tools/install-state.d.ts.map +1 -1
  61. package/dist/commands/init/tools/install-state.js +11 -0
  62. package/dist/commands/init/tools/project-management/agents-guidance.d.ts.map +1 -1
  63. package/dist/commands/init/tools/project-management/agents-guidance.js +3 -1
  64. package/dist/commands/init/tools/project-management/index.d.ts +1 -0
  65. package/dist/commands/init/tools/project-management/index.d.ts.map +1 -1
  66. package/dist/commands/init/tools/project-management/index.js +24 -55
  67. package/dist/commands/init/tools/project-management/install-project-management.d.ts +4 -0
  68. package/dist/commands/init/tools/project-management/install-project-management.d.ts.map +1 -1
  69. package/dist/commands/init/tools/project-management/install-project-management.js +4 -1
  70. package/dist/commands/init/tools/research/index.d.ts.map +1 -1
  71. package/dist/commands/init/tools/research/index.js +4 -1
  72. package/dist/commands/init/tools/shared/copy-helpers.d.ts.map +1 -1
  73. package/dist/commands/init/tools/shared/copy-helpers.js +10 -0
  74. package/dist/commands/init/tools/shared/skill-manifest.d.ts +15 -15
  75. package/dist/commands/init/tools/shared/skill-manifest.d.ts.map +1 -1
  76. package/dist/commands/init/tools/shared/skill-manifest.js +17 -122
  77. package/dist/commands/init/tools/utility/index.d.ts.map +1 -1
  78. package/dist/commands/init/tools/utility/index.js +4 -1
  79. package/dist/commands/init/tools/workflows/index.d.ts.map +1 -1
  80. package/dist/commands/init/tools/workflows/index.js +4 -1
  81. package/dist/commands/init/tools/workflows/install-workflows.d.ts.map +1 -1
  82. package/dist/commands/init/tools/workflows/install-workflows.js +2 -4
  83. package/dist/commands/pjm/adoption.d.ts +12 -0
  84. package/dist/commands/pjm/adoption.d.ts.map +1 -0
  85. package/dist/commands/pjm/adoption.js +47 -0
  86. package/dist/commands/pjm/doctor.d.ts +3 -2
  87. package/dist/commands/pjm/doctor.d.ts.map +1 -1
  88. package/dist/commands/pjm/doctor.js +28 -13
  89. package/dist/commands/pjm/index.d.ts +2 -0
  90. package/dist/commands/pjm/index.d.ts.map +1 -1
  91. package/dist/commands/pjm/index.js +11 -3
  92. package/dist/commands/pjm/init.d.ts +3 -0
  93. package/dist/commands/pjm/init.d.ts.map +1 -1
  94. package/dist/commands/pjm/init.js +37 -32
  95. package/dist/commands/pjm/migrate.d.ts +1 -0
  96. package/dist/commands/pjm/migrate.d.ts.map +1 -1
  97. package/dist/commands/pjm/migrate.js +1 -0
  98. package/dist/commands/pjm/template-source.d.ts +14 -0
  99. package/dist/commands/pjm/template-source.d.ts.map +1 -0
  100. package/dist/commands/pjm/template-source.js +56 -0
  101. package/dist/commands/remove/skills/remove-skills.d.ts.map +1 -1
  102. package/dist/commands/remove/skills/remove-skills.js +10 -19
  103. package/dist/commands/status/index.d.ts +5 -0
  104. package/dist/commands/status/index.d.ts.map +1 -1
  105. package/dist/commands/status/index.js +203 -2
  106. package/dist/commands/sync/index.d.ts.map +1 -1
  107. package/dist/commands/sync/index.js +40 -7
  108. package/dist/commands/sync/sync.types.d.ts +4 -0
  109. package/dist/commands/sync/sync.types.d.ts.map +1 -1
  110. package/dist/commands/tools/has/has-pack.d.ts +11 -2
  111. package/dist/commands/tools/has/has-pack.d.ts.map +1 -1
  112. package/dist/commands/tools/has/has-pack.js +33 -20
  113. package/dist/commands/tools/index.d.ts.map +1 -1
  114. package/dist/commands/tools/index.js +3 -1
  115. package/dist/commands/tools/info/info-tool.d.ts +3 -0
  116. package/dist/commands/tools/info/info-tool.d.ts.map +1 -1
  117. package/dist/commands/tools/info/info-tool.js +29 -4
  118. package/dist/commands/tools/list/list-tools.d.ts +3 -0
  119. package/dist/commands/tools/list/list-tools.d.ts.map +1 -1
  120. package/dist/commands/tools/list/list-tools.js +31 -4
  121. package/dist/commands/tools/migrate/index.d.ts +26 -0
  122. package/dist/commands/tools/migrate/index.d.ts.map +1 -0
  123. package/dist/commands/tools/migrate/index.js +312 -0
  124. package/dist/commands/tools/migrate/migrate-pack.d.ts +81 -0
  125. package/dist/commands/tools/migrate/migrate-pack.d.ts.map +1 -0
  126. package/dist/commands/tools/migrate/migrate-pack.js +324 -0
  127. package/dist/commands/tools/outdated/outdated-tools.d.ts +11 -0
  128. package/dist/commands/tools/outdated/outdated-tools.d.ts.map +1 -1
  129. package/dist/commands/tools/outdated/outdated-tools.js +33 -5
  130. package/dist/commands/tools/remove/index.d.ts.map +1 -1
  131. package/dist/commands/tools/remove/index.js +73 -20
  132. package/dist/commands/tools/remove/remove-tools.d.ts +28 -0
  133. package/dist/commands/tools/remove/remove-tools.d.ts.map +1 -1
  134. package/dist/commands/tools/remove/remove-tools.js +156 -26
  135. package/dist/commands/tools/shared/apply-pack-reconcile.d.ts +41 -0
  136. package/dist/commands/tools/shared/apply-pack-reconcile.d.ts.map +1 -0
  137. package/dist/commands/tools/shared/apply-pack-reconcile.js +126 -0
  138. package/dist/commands/tools/shared/auto-sync.d.ts +5 -2
  139. package/dist/commands/tools/shared/auto-sync.d.ts.map +1 -1
  140. package/dist/commands/tools/shared/auto-sync.js +1 -0
  141. package/dist/commands/tools/shared/content-digest.d.ts +3 -0
  142. package/dist/commands/tools/shared/content-digest.d.ts.map +1 -0
  143. package/dist/commands/tools/shared/content-digest.js +57 -0
  144. package/dist/commands/tools/shared/format-pack-inventory.d.ts +3 -0
  145. package/dist/commands/tools/shared/format-pack-inventory.d.ts.map +1 -0
  146. package/dist/commands/tools/shared/format-pack-inventory.js +21 -0
  147. package/dist/commands/tools/shared/install-sync-context.d.ts.map +1 -1
  148. package/dist/commands/tools/shared/install-sync-context.js +2 -25
  149. package/dist/commands/tools/shared/pack-inventory.d.ts +46 -0
  150. package/dist/commands/tools/shared/pack-inventory.d.ts.map +1 -0
  151. package/dist/commands/tools/shared/pack-inventory.js +207 -0
  152. package/dist/commands/tools/shared/pack-lifecycle.d.ts +34 -0
  153. package/dist/commands/tools/shared/pack-lifecycle.d.ts.map +1 -0
  154. package/dist/commands/tools/shared/pack-lifecycle.js +67 -0
  155. package/dist/commands/tools/shared/pack-manifest.d.ts +11 -0
  156. package/dist/commands/tools/shared/pack-manifest.d.ts.map +1 -0
  157. package/dist/commands/tools/shared/pack-manifest.js +384 -0
  158. package/dist/commands/tools/shared/pack-paths.d.ts +21 -0
  159. package/dist/commands/tools/shared/pack-paths.d.ts.map +1 -0
  160. package/dist/commands/tools/shared/pack-paths.js +35 -0
  161. package/dist/commands/tools/shared/pack-reconcile.d.ts +64 -0
  162. package/dist/commands/tools/shared/pack-reconcile.d.ts.map +1 -0
  163. package/dist/commands/tools/shared/pack-reconcile.js +158 -0
  164. package/dist/commands/tools/shared/project-tools-config.d.ts.map +1 -1
  165. package/dist/commands/tools/shared/project-tools-config.js +12 -38
  166. package/dist/commands/tools/shared/scan-tools.js +2 -2
  167. package/dist/commands/tools/shared/scoped-pack-intent.d.ts +34 -0
  168. package/dist/commands/tools/shared/scoped-pack-intent.d.ts.map +1 -0
  169. package/dist/commands/tools/shared/scoped-pack-intent.js +116 -0
  170. package/dist/commands/tools/shared/types.d.ts +22 -0
  171. package/dist/commands/tools/shared/types.d.ts.map +1 -1
  172. package/dist/commands/tools/update/index.d.ts.map +1 -1
  173. package/dist/commands/tools/update/index.js +30 -5
  174. package/dist/commands/tools/update/update-tools.d.ts +9 -1
  175. package/dist/commands/tools/update/update-tools.d.ts.map +1 -1
  176. package/dist/commands/tools/update/update-tools.js +176 -64
  177. package/dist/config/oat-config.d.ts +6 -0
  178. package/dist/config/oat-config.d.ts.map +1 -1
  179. package/dist/config/oat-config.js +49 -20
  180. package/dist/fs/paths.d.ts +12 -0
  181. package/dist/fs/paths.d.ts.map +1 -1
  182. package/dist/fs/paths.js +87 -1
  183. package/package.json +2 -2
@@ -1,14 +1,16 @@
1
- import { rm, unlink } from 'node:fs/promises';
2
1
  import { join } from 'node:path';
3
2
  import { buildCommandContext, } from '../../../app/command-context.js';
4
- import { buildDecisionAgentsSectionBody, DECISION_AGENTS_SECTION_KEY, } from '../../decision/agents-guidance.js';
5
3
  import { copyDirWithStatus } from '../../init/tools/shared/copy-helpers.js';
6
4
  import { applyGitignore } from '../../local/apply.js';
7
5
  import { addLocalPaths } from '../../local/manage.js';
8
6
  import { removeAgentsMdSection, upsertAgentsMdSection, } from '../../shared/agents-md.js';
7
+ import { withScopeOption } from '../../shared/scope-option.js';
9
8
  import { selectManyWithAbort, selectWithAbort, } from '../../shared/shared.prompts.js';
10
9
  import { readGlobalOptions } from '../../shared/shared.utils.js';
11
10
  import { canonicalPathsForPacks, getInstalledCanonicalPaths, setInstalledCanonicalPaths, } from '../../tools/shared/install-sync-context.js';
11
+ import { inventoryPack, } from '../../tools/shared/pack-inventory.js';
12
+ import { reconcilePackLifecycles, } from '../../tools/shared/pack-lifecycle.js';
13
+ import { getPackDefinition } from '../../tools/shared/pack-manifest.js';
12
14
  import { reconcileProjectToolsConfig } from '../../tools/shared/project-tools-config.js';
13
15
  import { scanTools } from '../../tools/shared/scan-tools.js';
14
16
  import { readOatConfig, resolveLocalPaths, writeOatConfig, } from '../../../config/oat-config.js';
@@ -23,13 +25,12 @@ import { createInitToolsDocsCommand } from './docs/index.js';
23
25
  import { installDocs as defaultInstallDocs, } from './docs/install-docs.js';
24
26
  import { createInitToolsIdeasCommand } from './ideas/index.js';
25
27
  import { installIdeas as defaultInstallIdeas, } from './ideas/install-ideas.js';
26
- import { buildPackInstallStateMap, } from './install-state.js';
28
+ import { buildPackInstallStateMap, buildPackInstallStateMapFromInventory, } from './install-state.js';
27
29
  import { createInitToolsProjectManagementCommand } from './project-management/index.js';
28
- import { buildProjectManagementAgentsSectionBody, PROJECT_MANAGEMENT_AGENTS_SECTION_KEY, } from './project-management/agents-guidance.js';
29
30
  import { installProjectManagement as defaultInstallProjectManagement, } from './project-management/install-project-management.js';
30
31
  import { createInitToolsResearchCommand } from './research/index.js';
31
32
  import { installResearch as defaultInstallResearch, } from './research/install-research.js';
32
- import { BRAINSTORM_SKILLS, DOCS_SKILLS, IDEA_SKILLS, RESEARCH_AGENTS, RESEARCH_SKILLS, UTILITY_SKILLS, WORKFLOW_AGENTS, WORKFLOW_SCRIPTS, WORKFLOW_SKILLS, WORKFLOW_TEMPLATES, resolvePackDefaultScope, } from './shared/skill-manifest.js';
33
+ import { DOCS_SKILLS, RESEARCH_SKILLS, UTILITY_SKILLS, resolvePackDefaultScope, } from './shared/skill-manifest.js';
33
34
  import { createInitToolsUtilityCommand } from './utility/index.js';
34
35
  import { installUtility as defaultInstallUtility, } from './utility/install-utility.js';
35
36
  import { createInitToolsWorkflowsCommand } from './workflows/index.js';
@@ -47,33 +48,8 @@ const ALL_TOOL_PACKS = [
47
48
  'research',
48
49
  'brainstorm',
49
50
  ];
50
- const USER_ELIGIBLE_PACK_MEMBERS = {
51
- ideas: {
52
- skills: IDEA_SKILLS,
53
- agents: [],
54
- },
55
- docs: {
56
- skills: DOCS_SKILLS,
57
- agents: [],
58
- },
59
- workflows: {
60
- skills: WORKFLOW_SKILLS,
61
- agents: WORKFLOW_AGENTS,
62
- },
63
- utility: {
64
- skills: UTILITY_SKILLS,
65
- agents: [],
66
- },
67
- research: {
68
- skills: RESEARCH_SKILLS,
69
- agents: RESEARCH_AGENTS,
70
- },
71
- brainstorm: {
72
- skills: BRAINSTORM_SKILLS,
73
- agents: [],
74
- },
75
- };
76
51
  let lastRunInitToolsMetadata = null;
52
+ const appliedScopesByCommand = new WeakMap();
77
53
  const DEFAULT_DEPENDENCIES = {
78
54
  buildCommandContext,
79
55
  resolveProjectRoot,
@@ -91,21 +67,6 @@ const DEFAULT_DEPENDENCIES = {
91
67
  installResearch: defaultInstallResearch,
92
68
  installBrainstorm: defaultInstallBrainstorm,
93
69
  copyDirWithStatus,
94
- removeDirectory: async (target) => {
95
- await rm(target, { recursive: true, force: true });
96
- },
97
- removeFile: async (target) => {
98
- try {
99
- await unlink(target);
100
- }
101
- catch (error) {
102
- if (!(error instanceof Error) ||
103
- !('code' in error) ||
104
- error.code !== 'ENOENT') {
105
- throw error;
106
- }
107
- }
108
- },
109
70
  addLocalPaths,
110
71
  applyGitignore,
111
72
  readOatConfig,
@@ -113,12 +74,15 @@ const DEFAULT_DEPENDENCIES = {
113
74
  resolveLocalPaths,
114
75
  upsertAgentsMdSection,
115
76
  removeAgentsMdSection,
77
+ reconcilePacks: reconcilePackLifecycles,
78
+ inventoryPack,
116
79
  };
117
80
  const USER_ELIGIBLE_PACKS = new Set([
118
81
  'ideas',
119
82
  'docs',
120
83
  'workflows',
121
84
  'utility',
85
+ 'project-management',
122
86
  'research',
123
87
  'brainstorm',
124
88
  ]);
@@ -163,8 +127,7 @@ function reconcilePackScope(current, desired) {
163
127
  const currentScopes = new Set(scopesForLocation(current));
164
128
  const desiredScopes = new Set(scopesForEndState(desired));
165
129
  const adds = [...desiredScopes].filter((scope) => !currentScopes.has(scope));
166
- const removes = [...currentScopes].filter((scope) => !desiredScopes.has(scope));
167
- return { adds, removes };
130
+ return { adds };
168
131
  }
169
132
  /**
170
133
  * Render a batch change summary (`+ pack@scope` adds / `- pack@scope` removes)
@@ -197,12 +160,23 @@ function unionScopeWithCurrent(current, requested) {
197
160
  return scopes.has('user') ? 'user' : 'project';
198
161
  }
199
162
  async function loadInstalledPackStates(projectRoot, userRoot, assetsRoot, dependencies) {
200
- const [projectTools, userTools] = await Promise.all([
201
- dependencies.scanTools({
202
- scope: 'project',
203
- scopeRoot: projectRoot,
163
+ if (dependencies.reconcilePacks && dependencies.inventoryPack) {
164
+ const inventories = await Promise.all(ALL_TOOL_PACKS.map((pack) => dependencies.inventoryPack({
165
+ pack,
204
166
  assetsRoot,
205
- }),
167
+ ...(projectRoot ? { projectRoot } : {}),
168
+ userRoot,
169
+ })));
170
+ return buildPackInstallStateMapFromInventory(ALL_TOOL_PACKS, inventories);
171
+ }
172
+ const [projectTools, userTools] = await Promise.all([
173
+ projectRoot
174
+ ? dependencies.scanTools({
175
+ scope: 'project',
176
+ scopeRoot: projectRoot,
177
+ assetsRoot,
178
+ })
179
+ : Promise.resolve([]),
206
180
  dependencies.scanTools({
207
181
  scope: 'user',
208
182
  scopeRoot: userRoot,
@@ -244,7 +218,7 @@ function buildPackChoices(installedPackStates) {
244
218
  checked: true,
245
219
  },
246
220
  {
247
- label: `Project Management [project]${installedPackStates['project-management'].location === 'not-installed' ? '' : ` (installed: ${formatInstalledLocation(installedPackStates['project-management'].location)})`}`,
221
+ label: `Project Management [project|user]${installedPackStates['project-management'].location === 'not-installed' ? '' : ` (installed: ${formatInstalledLocation(installedPackStates['project-management'].location)})`}`,
248
222
  value: 'project-management',
249
223
  checked: false,
250
224
  },
@@ -275,23 +249,6 @@ export function consumeInitToolsRunMetadata() {
275
249
  lastRunInitToolsMetadata = null;
276
250
  return metadata;
277
251
  }
278
- async function removePackFromScope(pack, root, dependencies) {
279
- const members = USER_ELIGIBLE_PACK_MEMBERS[pack];
280
- for (const skill of members.skills) {
281
- await dependencies.removeDirectory(join(root, '.agents', 'skills', skill));
282
- }
283
- for (const agent of members.agents) {
284
- await dependencies.removeFile(join(root, '.agents', 'agents', agent));
285
- }
286
- if (pack === 'workflows') {
287
- for (const template of WORKFLOW_TEMPLATES) {
288
- await dependencies.removeFile(join(root, '.oat', 'templates', template));
289
- }
290
- for (const script of WORKFLOW_SCRIPTS) {
291
- await dependencies.removeFile(join(root, '.oat', 'scripts', script));
292
- }
293
- }
294
- }
295
252
  /**
296
253
  * Resolve a possibly-deferred scope-selection signal into a concrete mode.
297
254
  *
@@ -327,9 +284,9 @@ async function resolveDeferredGate(scopeSelection, interactive, dependencies) {
327
284
  }
328
285
  async function resolvePackScopes(context, selections, installedPackStates, dependencies) {
329
286
  const scopes = {};
330
- // Packs outside the user-eligible set are project-only.
287
+ // Scope eligibility is release-owned by the canonical manifest.
331
288
  for (const pack of selections) {
332
- if (!USER_ELIGIBLE_PACKS.has(pack)) {
289
+ if (!getPackDefinition(pack).allowedScopes.includes('user')) {
333
290
  scopes[pack] = 'project';
334
291
  }
335
292
  }
@@ -389,14 +346,23 @@ async function resolvePackScopes(context, selections, installedPackStates, depen
389
346
  const currentLocation = installedPackStates[pack].location;
390
347
  const defaultEndState = resolvePackDefaultEndState(pack, currentLocation);
391
348
  const selection = await dependencies.selectWithAbort(`Where should ${pack} install?`, buildPackEndStateChoices(pack, currentLocation, defaultEndState), { interactive: context.interactive });
392
- scopes[pack] = selection ?? defaultEndState;
349
+ const requested = selection ?? defaultEndState;
350
+ scopes[pack] =
351
+ requested === 'both'
352
+ ? 'both'
353
+ : unionScopeWithCurrent(currentLocation, requested);
393
354
  }
394
355
  return scopes;
395
356
  }
396
357
  /**
397
- * Default end-state offered for a pack in the interactive selector: its
398
- * current placement when installed, or its configured default scope when not
399
- * yet present.
358
+ * Existing-placement precedence: a pack that is already installed keeps the
359
+ * placement it has, and only a pack that is not present anywhere falls back to
360
+ * its configured default scope.
361
+ *
362
+ * This is the single definition of that rule. It backs the interactive
363
+ * selector's offered default, non-interactive aggregate resolution, and the
364
+ * per-pack `oat tools install <pack>` subcommands, so a bare re-install can
365
+ * never silently migrate or duplicate an existing install across scopes.
400
366
  */
401
367
  function resolvePackDefaultEndState(pack, currentLocation) {
402
368
  switch (currentLocation) {
@@ -523,9 +489,16 @@ export function buildToolPacksSectionBody(packs) {
523
489
  export async function runInitTools(context, dependencies) {
524
490
  lastRunInitToolsMetadata = null;
525
491
  try {
526
- const projectRoot = await dependencies.resolveProjectRoot(context.cwd);
527
492
  const userRoot = dependencies.resolveScopeRoot('user', context.cwd, context.home);
528
493
  const assetsRoot = await dependencies.resolveAssetsRoot();
494
+ let projectRoot = null;
495
+ try {
496
+ projectRoot = await dependencies.resolveProjectRoot(context.cwd);
497
+ }
498
+ catch (error) {
499
+ if (context.scope === 'project')
500
+ throw error;
501
+ }
529
502
  const initialPackStates = await loadInstalledPackStates(projectRoot, userRoot, assetsRoot, dependencies);
530
503
  const selectedPacks = context.interactive
531
504
  ? ((await dependencies.selectManyWithAbort('Select tool packs to install', buildPackChoices(initialPackStates), { interactive: context.interactive })) ?? [])
@@ -542,7 +515,7 @@ export async function runInitTools(context, dependencies) {
542
515
  selectedPacks.push('project-management');
543
516
  }
544
517
  if (selectedPacks.length === 0) {
545
- lastRunInitToolsMetadata = { affectedScopes: [] };
518
+ lastRunInitToolsMetadata = { affectedScopes: [], appliedScopes: [] };
546
519
  if (!context.json) {
547
520
  context.logger.info('No tool packs selected.');
548
521
  }
@@ -551,285 +524,278 @@ export async function runInitTools(context, dependencies) {
551
524
  }
552
525
  const packScopes = await resolvePackScopes(context, selectedPacks, initialPackStates, dependencies);
553
526
  function scopeRoot(scope) {
554
- return scope === 'user' ? userRoot : projectRoot;
555
- }
556
- // Reconcile current placement vs the desired end-state per user-eligible
557
- // pack into adds/removes. Installs copy idempotently into the full desired
558
- // end-state, but only the scopes that actually changed (an add or a
559
- // confirmed remove) are recorded in `affectedScopes`, so auto-sync never
560
- // prunes a preserved scope.
561
- const reconciliationByPack = new Map();
562
- for (const pack of selectedPacks) {
563
- if (!isUserEligiblePack(pack)) {
564
- continue;
527
+ if (scope === 'user')
528
+ return userRoot;
529
+ if (!projectRoot) {
530
+ throw new Error('Project scope is unavailable outside a repository');
565
531
  }
566
- reconciliationByPack.set(pack, reconcilePackScope(initialPackStates[pack].location, packScopes[pack]));
567
- }
568
- // Scopes a pack should install into (its full desired end-state for
569
- // user-eligible packs; the resolved scope otherwise). Re-installing an
570
- // already-present scope is an idempotent copy.
571
- function packTargets(pack) {
572
- return packScopes[pack] === 'both'
573
- ? [projectRoot, userRoot]
574
- : [packScopes[pack] === 'user' ? userRoot : projectRoot];
575
- }
576
- // Only scopes that received a new add for this pack should be auto-synced.
577
- function addedScopes(pack) {
578
- return new Set(reconciliationByPack.get(pack)?.adds);
532
+ return projectRoot;
579
533
  }
580
534
  const outdatedSkills = [];
581
535
  const affectedScopes = new Set();
582
- // Collect all staged removals across packs. Removals are interactive-only
583
- // and gated behind a single batch confirmation (added in p01-t03);
584
- // non-interactive paths are strictly additive and can never remove.
585
- const stagedRemovals = [];
586
- for (const [pack, reconciliation] of reconciliationByPack) {
587
- for (const scope of reconciliation.removes) {
588
- stagedRemovals.push({ pack, scope });
589
- }
590
- }
591
- if (stagedRemovals.length > 0 && !context.interactive) {
592
- // Strictly-additive guard: removals must never be applied
593
- // non-interactively. This should be unreachable because non-interactive
594
- // resolution unions with current placement, but we fail loud rather than
595
- // silently delete a user's install.
596
- throw new Error('Non-interactive install attempted to remove a pack from a scope; ' +
597
- 'install is strictly additive in non-interactive mode.');
598
- }
599
- // Interactive removal gate: if any pack would lose a scope, surface one
600
- // batch change summary and require a single confirmation before mutating
601
- // anything. Declining aborts with zero changes (no installs, no removals).
602
- if (stagedRemovals.length > 0 && context.interactive) {
603
- const stagedAdds = [];
604
- for (const [pack, reconciliation] of reconciliationByPack) {
605
- for (const scope of reconciliation.adds) {
606
- stagedAdds.push({ pack, scope });
536
+ if (dependencies.reconcilePacks) {
537
+ const requests = [];
538
+ for (const pack of selectedPacks) {
539
+ const targets = pack === 'core'
540
+ ? [{ scope: 'user', root: userRoot }]
541
+ : scopesForEndState(packScopes[pack]).map((scope) => ({
542
+ scope,
543
+ root: scopeRoot(scope),
544
+ }));
545
+ for (const target of targets) {
546
+ requests.push({
547
+ pack,
548
+ scope: target.scope,
549
+ scopeRoot: target.root,
550
+ assetsRoot,
551
+ action: 'install',
552
+ });
607
553
  }
608
554
  }
609
- context.logger.info(formatReconcileSummary(stagedAdds, stagedRemovals));
610
- const confirmation = await dependencies.selectWithAbort('Apply these changes? Removals will delete the listed scoped installs.', [
611
- {
612
- label: 'No, cancel (recommended)',
613
- value: 'no',
614
- description: 'Make no changes',
615
- },
616
- {
617
- label: 'Yes, apply adds and removals',
618
- value: 'yes',
619
- description: 'Install adds and delete the listed removals',
620
- },
621
- ], { interactive: context.interactive });
622
- if (confirmation !== 'yes') {
623
- lastRunInitToolsMetadata = { affectedScopes: [] };
624
- if (!context.json) {
625
- context.logger.info('No changes applied.');
626
- }
627
- process.exitCode = 0;
628
- return [];
555
+ const lifecycle = await dependencies.reconcilePacks(requests);
556
+ for (const { plan } of lifecycle) {
557
+ if (plan.operations.length > 0)
558
+ affectedScopes.add(plan.scope);
629
559
  }
630
- }
631
- // Removals are deferred until after the add phase below: for a confirmed
632
- // move (e.g. user -> project), the replacement install must succeed before
633
- // the preserved scope is deleted. If any installer throws, the catch
634
- // handler aborts and these removals never run, so the pack is never left
635
- // installed in neither scope (review I1; design.md:86/114).
636
- if (selectedPacks.includes('core')) {
637
- // Core pack always installs at user scope, regardless of userEligibleScope
638
- affectedScopes.add('user');
639
- const coreResult = await dependencies.installCore({
640
- assetsRoot,
641
- targetRoot: userRoot,
642
- });
643
- for (const skill of coreResult.outdatedSkills) {
644
- outdatedSkills.push({
645
- ...skill,
646
- targetRoot: userRoot,
647
- selectionKey: `${skill.name}:${userRoot}`,
648
- });
560
+ if (requests.some(({ pack, scope }) => pack === 'workflows' && scope === 'project')) {
561
+ const repoRoot = scopeRoot('project');
562
+ const existingConfig = await dependencies.readOatConfig(repoRoot);
563
+ const paths = [
564
+ '.oat/projects/**/pr',
565
+ '.oat/projects/**/reviews/archived',
566
+ ];
567
+ const existing = new Set(dependencies.resolveLocalPaths(existingConfig));
568
+ if (!paths.every((path) => existing.has(path))) {
569
+ const addResult = await dependencies.addLocalPaths(repoRoot, paths);
570
+ if (addResult.added.length > 0) {
571
+ const config = await dependencies.readOatConfig(repoRoot);
572
+ await dependencies.applyGitignore(repoRoot, dependencies.resolveLocalPaths(config));
573
+ }
574
+ }
649
575
  }
650
576
  }
651
- if (selectedPacks.includes('ideas')) {
652
- const ideasAdded = addedScopes('ideas');
653
- for (const targetRoot of packTargets('ideas')) {
654
- const targetScope = targetRoot === userRoot ? 'user' : 'project';
655
- if (ideasAdded.has(targetScope)) {
656
- affectedScopes.add(targetScope);
577
+ else {
578
+ // Reconcile current placement vs the desired end-state per user-eligible
579
+ // pack into adds/removes. Installs copy idempotently into the full desired
580
+ // end-state, but only the scopes that actually changed (an add or a
581
+ // confirmed remove) are recorded in `affectedScopes`, so auto-sync never
582
+ // prunes a preserved scope.
583
+ const reconciliationByPack = new Map();
584
+ for (const pack of selectedPacks) {
585
+ if (!isUserEligiblePack(pack)) {
586
+ continue;
657
587
  }
658
- const ideasResult = await dependencies.installIdeas({
588
+ reconciliationByPack.set(pack, reconcilePackScope(initialPackStates[pack].location, packScopes[pack]));
589
+ }
590
+ // Scopes a pack should install into (its full desired end-state for
591
+ // user-eligible packs; the resolved scope otherwise). Re-installing an
592
+ // already-present scope is an idempotent copy.
593
+ function packTargets(pack) {
594
+ return packScopes[pack] === 'both'
595
+ ? [scopeRoot('project'), userRoot]
596
+ : [scopeRoot(packScopes[pack] === 'user' ? 'user' : 'project')];
597
+ }
598
+ // Only scopes that received a new add for this pack should be auto-synced.
599
+ function addedScopes(pack) {
600
+ return new Set(reconciliationByPack.get(pack)?.adds);
601
+ }
602
+ if (selectedPacks.includes('core')) {
603
+ // Core pack always installs at user scope, regardless of userEligibleScope
604
+ affectedScopes.add('user');
605
+ const coreResult = await dependencies.installCore({
659
606
  assetsRoot,
660
- targetRoot,
607
+ targetRoot: userRoot,
661
608
  });
662
- for (const skill of ideasResult.outdatedSkills) {
609
+ for (const skill of coreResult.outdatedSkills) {
663
610
  outdatedSkills.push({
664
611
  ...skill,
665
- targetRoot,
666
- selectionKey: `${skill.name}:${targetRoot}`,
612
+ targetRoot: userRoot,
613
+ selectionKey: `${skill.name}:${userRoot}`,
667
614
  });
668
615
  }
669
616
  }
670
- }
671
- if (selectedPacks.includes('docs')) {
672
- const docsAdded = addedScopes('docs');
673
- for (const targetRoot of packTargets('docs')) {
674
- const targetScope = targetRoot === userRoot ? 'user' : 'project';
675
- if (docsAdded.has(targetScope)) {
676
- affectedScopes.add(targetScope);
677
- }
678
- const docsResult = await dependencies.installDocs({
679
- assetsRoot,
680
- targetRoot,
681
- skills: [...DOCS_SKILLS],
682
- });
683
- for (const skill of docsResult.outdatedSkills) {
684
- outdatedSkills.push({
685
- ...skill,
617
+ if (selectedPacks.includes('ideas')) {
618
+ const ideasAdded = addedScopes('ideas');
619
+ for (const targetRoot of packTargets('ideas')) {
620
+ const targetScope = targetRoot === userRoot ? 'user' : 'project';
621
+ if (ideasAdded.has(targetScope)) {
622
+ affectedScopes.add(targetScope);
623
+ }
624
+ const ideasResult = await dependencies.installIdeas({
625
+ assetsRoot,
686
626
  targetRoot,
687
- selectionKey: `${skill.name}:${targetRoot}`,
688
627
  });
628
+ for (const skill of ideasResult.outdatedSkills) {
629
+ outdatedSkills.push({
630
+ ...skill,
631
+ targetRoot,
632
+ selectionKey: `${skill.name}:${targetRoot}`,
633
+ });
634
+ }
689
635
  }
690
636
  }
691
- }
692
- if (selectedPacks.includes('workflows')) {
693
- const workflowsAdded = addedScopes('workflows');
694
- for (const targetRoot of packTargets('workflows')) {
695
- const targetScope = targetRoot === userRoot ? 'user' : 'project';
696
- if (workflowsAdded.has(targetScope)) {
697
- affectedScopes.add(targetScope);
698
- }
699
- const workflowsResult = await dependencies.installWorkflows({
700
- assetsRoot,
701
- targetRoot,
702
- scope: targetScope,
703
- });
704
- for (const skill of workflowsResult.outdatedSkills) {
705
- outdatedSkills.push({
706
- ...skill,
637
+ if (selectedPacks.includes('docs')) {
638
+ const docsAdded = addedScopes('docs');
639
+ for (const targetRoot of packTargets('docs')) {
640
+ const targetScope = targetRoot === userRoot ? 'user' : 'project';
641
+ if (docsAdded.has(targetScope)) {
642
+ affectedScopes.add(targetScope);
643
+ }
644
+ const docsResult = await dependencies.installDocs({
645
+ assetsRoot,
707
646
  targetRoot,
708
- selectionKey: `${skill.name}:${targetRoot}`,
647
+ skills: [...DOCS_SKILLS],
709
648
  });
649
+ for (const skill of docsResult.outdatedSkills) {
650
+ outdatedSkills.push({
651
+ ...skill,
652
+ targetRoot,
653
+ selectionKey: `${skill.name}:${targetRoot}`,
654
+ });
655
+ }
710
656
  }
711
- if (targetScope === 'project') {
712
- const resolvedRoot = workflowsResult.resolvedProjectsRoot || '.oat/projects/shared';
713
- const projectsBase = resolvedRoot.replace(/\/[^/]+$/, '');
714
- const PR_ARCHIVE_LOCAL_PATHS = [
715
- `${projectsBase}/**/pr`,
716
- `${projectsBase}/**/reviews/archived`,
717
- ];
718
- const existingConfig = await dependencies.readOatConfig(projectRoot);
719
- const existingLocalPaths = new Set(dependencies.resolveLocalPaths(existingConfig));
720
- const alreadyConfigured = PR_ARCHIVE_LOCAL_PATHS.every((p) => existingLocalPaths.has(p));
721
- if (!alreadyConfigured) {
722
- let makeLocal = true;
723
- if (context.interactive) {
724
- const selected = await dependencies.selectWithAbort('Should shared-project PR directories and archived review history be local-only (gitignored) or version-controlled?', [
725
- {
726
- label: 'Local only (recommended)',
727
- value: 'local',
728
- description: 'PR artifacts and archived reviews stay local; active reviews remain tracked until received',
729
- },
730
- {
731
- label: 'Version controlled',
732
- value: 'tracked',
733
- description: 'PR artifacts and archived reviews are committed to the repo too',
734
- },
735
- ], { interactive: context.interactive });
736
- makeLocal = selected !== 'tracked';
737
- }
738
- if (makeLocal) {
739
- const addResult = await dependencies.addLocalPaths(projectRoot, PR_ARCHIVE_LOCAL_PATHS);
740
- if (addResult.added.length > 0) {
741
- const config = await dependencies.readOatConfig(projectRoot);
742
- const allPaths = dependencies.resolveLocalPaths(config);
743
- await dependencies.applyGitignore(projectRoot, allPaths);
657
+ }
658
+ if (selectedPacks.includes('workflows')) {
659
+ const workflowsAdded = addedScopes('workflows');
660
+ for (const targetRoot of packTargets('workflows')) {
661
+ const targetScope = targetRoot === userRoot ? 'user' : 'project';
662
+ if (workflowsAdded.has(targetScope)) {
663
+ affectedScopes.add(targetScope);
664
+ }
665
+ const workflowsResult = await dependencies.installWorkflows({
666
+ assetsRoot,
667
+ targetRoot,
668
+ scope: targetScope,
669
+ });
670
+ for (const skill of workflowsResult.outdatedSkills) {
671
+ outdatedSkills.push({
672
+ ...skill,
673
+ targetRoot,
674
+ selectionKey: `${skill.name}:${targetRoot}`,
675
+ });
676
+ }
677
+ if (targetScope === 'project') {
678
+ const resolvedRoot = workflowsResult.resolvedProjectsRoot || '.oat/projects/shared';
679
+ const projectsBase = resolvedRoot.replace(/\/[^/]+$/, '');
680
+ const PR_ARCHIVE_LOCAL_PATHS = [
681
+ `${projectsBase}/**/pr`,
682
+ `${projectsBase}/**/reviews/archived`,
683
+ ];
684
+ const existingConfig = await dependencies.readOatConfig(scopeRoot('project'));
685
+ const existingLocalPaths = new Set(dependencies.resolveLocalPaths(existingConfig));
686
+ const alreadyConfigured = PR_ARCHIVE_LOCAL_PATHS.every((p) => existingLocalPaths.has(p));
687
+ if (!alreadyConfigured) {
688
+ let makeLocal = true;
689
+ if (context.interactive) {
690
+ const selected = await dependencies.selectWithAbort('Should shared-project PR directories and archived review history be local-only (gitignored) or version-controlled?', [
691
+ {
692
+ label: 'Local only (recommended)',
693
+ value: 'local',
694
+ description: 'PR artifacts and archived reviews stay local; active reviews remain tracked until received',
695
+ },
696
+ {
697
+ label: 'Version controlled',
698
+ value: 'tracked',
699
+ description: 'PR artifacts and archived reviews are committed to the repo too',
700
+ },
701
+ ], { interactive: context.interactive });
702
+ makeLocal = selected !== 'tracked';
703
+ }
704
+ if (makeLocal) {
705
+ const addResult = await dependencies.addLocalPaths(scopeRoot('project'), PR_ARCHIVE_LOCAL_PATHS);
706
+ if (addResult.added.length > 0) {
707
+ const config = await dependencies.readOatConfig(scopeRoot('project'));
708
+ const allPaths = dependencies.resolveLocalPaths(config);
709
+ await dependencies.applyGitignore(scopeRoot('project'), allPaths);
710
+ }
744
711
  }
745
712
  }
746
713
  }
747
714
  }
748
715
  }
749
- }
750
- if (selectedPacks.includes('utility')) {
751
- const utilityAdded = addedScopes('utility');
752
- for (const targetRoot of packTargets('utility')) {
753
- const targetScope = targetRoot === userRoot ? 'user' : 'project';
754
- if (utilityAdded.has(targetScope)) {
755
- affectedScopes.add(targetScope);
756
- }
757
- const utilityResult = await dependencies.installUtility({
758
- assetsRoot,
759
- targetRoot,
760
- skills: [...UTILITY_SKILLS],
761
- });
762
- for (const skill of utilityResult.outdatedSkills) {
763
- outdatedSkills.push({
764
- ...skill,
716
+ if (selectedPacks.includes('utility')) {
717
+ const utilityAdded = addedScopes('utility');
718
+ for (const targetRoot of packTargets('utility')) {
719
+ const targetScope = targetRoot === userRoot ? 'user' : 'project';
720
+ if (utilityAdded.has(targetScope)) {
721
+ affectedScopes.add(targetScope);
722
+ }
723
+ const utilityResult = await dependencies.installUtility({
724
+ assetsRoot,
765
725
  targetRoot,
766
- selectionKey: `${skill.name}:${targetRoot}`,
726
+ skills: [...UTILITY_SKILLS],
767
727
  });
728
+ for (const skill of utilityResult.outdatedSkills) {
729
+ outdatedSkills.push({
730
+ ...skill,
731
+ targetRoot,
732
+ selectionKey: `${skill.name}:${targetRoot}`,
733
+ });
734
+ }
768
735
  }
769
736
  }
770
- }
771
- if (selectedPacks.includes('project-management')) {
772
- const targetRoot = projectRoot;
773
- affectedScopes.add('project');
774
- const projectManagementResult = await dependencies.installProjectManagement({
775
- assetsRoot,
776
- targetRoot,
777
- });
778
- for (const skill of projectManagementResult.outdatedSkills) {
779
- outdatedSkills.push({
780
- ...skill,
781
- targetRoot,
782
- selectionKey: `${skill.name}:${targetRoot}`,
783
- });
784
- }
785
- }
786
- if (selectedPacks.includes('research')) {
787
- const researchAdded = addedScopes('research');
788
- for (const targetRoot of packTargets('research')) {
789
- const targetScope = targetRoot === userRoot ? 'user' : 'project';
790
- if (researchAdded.has(targetScope)) {
791
- affectedScopes.add(targetScope);
792
- }
793
- const researchResult = await dependencies.installResearch({
794
- assetsRoot,
795
- targetRoot,
796
- skills: [...RESEARCH_SKILLS],
797
- });
798
- for (const skill of researchResult.outdatedSkills) {
799
- outdatedSkills.push({
800
- ...skill,
737
+ if (selectedPacks.includes('project-management')) {
738
+ const projectManagementAdded = addedScopes('project-management');
739
+ for (const targetRoot of packTargets('project-management')) {
740
+ const targetScope = targetRoot === userRoot ? 'user' : 'project';
741
+ if (projectManagementAdded.has(targetScope)) {
742
+ affectedScopes.add(targetScope);
743
+ }
744
+ const projectManagementResult = await dependencies.installProjectManagement({
745
+ assetsRoot,
801
746
  targetRoot,
802
- selectionKey: `${skill.name}:${targetRoot}`,
803
747
  });
748
+ for (const skill of projectManagementResult.outdatedSkills) {
749
+ outdatedSkills.push({
750
+ ...skill,
751
+ targetRoot,
752
+ selectionKey: `${skill.name}:${targetRoot}`,
753
+ });
754
+ }
804
755
  }
805
756
  }
806
- }
807
- if (selectedPacks.includes('brainstorm')) {
808
- const brainstormAdded = addedScopes('brainstorm');
809
- for (const targetRoot of packTargets('brainstorm')) {
810
- const targetScope = targetRoot === userRoot ? 'user' : 'project';
811
- if (brainstormAdded.has(targetScope)) {
812
- affectedScopes.add(targetScope);
757
+ if (selectedPacks.includes('research')) {
758
+ const researchAdded = addedScopes('research');
759
+ for (const targetRoot of packTargets('research')) {
760
+ const targetScope = targetRoot === userRoot ? 'user' : 'project';
761
+ if (researchAdded.has(targetScope)) {
762
+ affectedScopes.add(targetScope);
763
+ }
764
+ const researchResult = await dependencies.installResearch({
765
+ assetsRoot,
766
+ targetRoot,
767
+ skills: [...RESEARCH_SKILLS],
768
+ });
769
+ for (const skill of researchResult.outdatedSkills) {
770
+ outdatedSkills.push({
771
+ ...skill,
772
+ targetRoot,
773
+ selectionKey: `${skill.name}:${targetRoot}`,
774
+ });
775
+ }
813
776
  }
814
- const brainstormResult = await dependencies.installBrainstorm({
815
- assetsRoot,
816
- targetRoot,
817
- });
818
- for (const skill of brainstormResult.outdatedSkills) {
819
- outdatedSkills.push({
820
- ...skill,
777
+ }
778
+ if (selectedPacks.includes('brainstorm')) {
779
+ const brainstormAdded = addedScopes('brainstorm');
780
+ for (const targetRoot of packTargets('brainstorm')) {
781
+ const targetScope = targetRoot === userRoot ? 'user' : 'project';
782
+ if (brainstormAdded.has(targetScope)) {
783
+ affectedScopes.add(targetScope);
784
+ }
785
+ const brainstormResult = await dependencies.installBrainstorm({
786
+ assetsRoot,
821
787
  targetRoot,
822
- selectionKey: `${skill.name}:${targetRoot}`,
823
788
  });
789
+ for (const skill of brainstormResult.outdatedSkills) {
790
+ outdatedSkills.push({
791
+ ...skill,
792
+ targetRoot,
793
+ selectionKey: `${skill.name}:${targetRoot}`,
794
+ });
795
+ }
824
796
  }
825
797
  }
826
798
  }
827
- // Apply confirmed removals only after every add has succeeded, so a failed
828
- // replacement install can never leave a pack uninstalled in both scopes.
829
- for (const { pack, scope } of stagedRemovals) {
830
- await removePackFromScope(pack, scopeRoot(scope), dependencies);
831
- affectedScopes.add(scope);
832
- }
833
799
  if (outdatedSkills.length > 0) {
834
800
  reportOutdatedSkills(context, outdatedSkills);
835
801
  if (context.interactive) {
@@ -854,32 +820,27 @@ export async function runInitTools(context, dependencies) {
854
820
  pack,
855
821
  scope: packScopes[pack],
856
822
  }));
823
+ const adoptsProject = packScopeInfo.some(({ scope }) => scope !== 'user');
857
824
  const sectionBody = buildToolPacksSectionBody(packScopeInfo);
858
- const sectionResult = await dependencies.upsertAgentsMdSection(projectRoot, 'tools', sectionBody);
859
- const projectManagementSectionResult = selectedPacks.includes('project-management')
860
- ? await dependencies.upsertAgentsMdSection(projectRoot, PROJECT_MANAGEMENT_AGENTS_SECTION_KEY, buildProjectManagementAgentsSectionBody())
861
- : null;
862
- const decisionSectionResult = selectedPacks.includes('project-management')
863
- ? await dependencies.upsertAgentsMdSection(projectRoot, DECISION_AGENTS_SECTION_KEY, buildDecisionAgentsSectionBody())
864
- : null;
865
- // Migrate: remove legacy <!-- OAT workflows --> section if present
866
- await dependencies.removeAgentsMdSection(projectRoot, 'workflows');
825
+ const sectionResult = adoptsProject
826
+ ? await dependencies.upsertAgentsMdSection(scopeRoot('project'), 'tools', sectionBody)
827
+ : { action: 'no-change' };
828
+ // Pack placement never writes the project-management or decisions AGENTS
829
+ // sections. Those belong to explicit repository adoption (`oat pjm init`,
830
+ // via `initializeRepoReference`), which owns the `pjm.initialized` marker.
831
+ if (adoptsProject) {
832
+ await dependencies.removeAgentsMdSection(scopeRoot('project'), 'workflows');
833
+ }
867
834
  if (!context.json && sectionResult.action !== 'no-change') {
868
835
  context.logger.info(`AGENTS.md tool packs section ${sectionResult.action}.`);
869
836
  }
870
- if (!context.json &&
871
- projectManagementSectionResult !== null &&
872
- projectManagementSectionResult.action !== 'no-change') {
873
- context.logger.info(`AGENTS.md project-management section ${projectManagementSectionResult.action}.`);
874
- }
875
- if (!context.json &&
876
- decisionSectionResult !== null &&
877
- decisionSectionResult.action !== 'no-change') {
878
- context.logger.info(`AGENTS.md decisions section ${decisionSectionResult.action}.`);
879
- }
880
837
  const affectedScopesList = [...affectedScopes];
838
+ const appliedScopes = [
839
+ ...new Set(packScopeInfo.flatMap(({ scope }) => scopesForEndState(scope))),
840
+ ];
881
841
  lastRunInitToolsMetadata = {
882
842
  affectedScopes: affectedScopesList,
843
+ appliedScopes,
883
844
  };
884
845
  reportSuccess(context, packScopeInfo, affectedScopesList);
885
846
  process.exitCode = 0;
@@ -901,11 +862,138 @@ export async function runInitTools(context, dependencies) {
901
862
  export async function runInitToolsWithDefaults(context) {
902
863
  return runInitTools(context, { ...DEFAULT_DEPENDENCIES });
903
864
  }
865
+ /**
866
+ * Scopes a bare `oat tools install <pack>` (no explicit `--scope`) should
867
+ * target.
868
+ *
869
+ * Delegates to `resolvePackDefaultEndState`, the same existing-placement rule
870
+ * the aggregate installer uses, so a pack already installed at project scope
871
+ * stays at project scope instead of gaining a second copy at the pack's
872
+ * `defaultScope`. Falls back to the configured default only when placement
873
+ * cannot be observed (no inventory dependency, or an inventory failure).
874
+ */
875
+ async function resolvePackCommandScopes(pack, context, assetsRoot, dependencies) {
876
+ const definition = getPackDefinition(pack);
877
+ const inventory = dependencies.inventoryPack;
878
+ if (!inventory)
879
+ return [definition.defaultScope];
880
+ try {
881
+ const userRoot = dependencies.resolveScopeRoot('user', context.cwd, context.home);
882
+ const projectRoot = definition.allowedScopes.includes('project')
883
+ ? await dependencies
884
+ .resolveProjectRoot(context.cwd)
885
+ .catch(() => undefined)
886
+ : undefined;
887
+ const packInventory = await inventory({
888
+ pack,
889
+ assetsRoot,
890
+ ...(projectRoot ? { projectRoot } : {}),
891
+ userRoot,
892
+ });
893
+ const states = buildPackInstallStateMapFromInventory([pack], [packInventory]);
894
+ return scopesForEndState(resolvePackDefaultEndState(pack, states[pack].location)).filter((scope) => definition.allowedScopes.includes(scope));
895
+ }
896
+ catch {
897
+ return [definition.defaultScope];
898
+ }
899
+ }
900
+ function createReconciledPackCommand(pack, dependencies) {
901
+ const definition = getPackDefinition(pack);
902
+ const descriptions = {
903
+ core: 'Install OAT core skills (diagnostics, docs)',
904
+ ideas: 'Install OAT ideas skills, templates, and idea workflow files',
905
+ docs: 'Install OAT docs workflow skills',
906
+ 'project-management': 'Install OAT project-management skills and templates',
907
+ workflows: 'Install OAT workflows skills, agents, templates, and scripts',
908
+ utility: 'Install OAT utility skills',
909
+ research: 'Install OAT research skills',
910
+ brainstorm: 'Install OAT brainstorm skill (always-on entry point with visual companion)',
911
+ };
912
+ const base = new Command(pack).description(descriptions[pack]);
913
+ const packCommand = pack === 'core' ? base : withScopeOption(base);
914
+ return packCommand
915
+ .option('--force', 'Reconcile managed assets to bundled content')
916
+ .action(async (_options, command) => {
917
+ const context = dependencies.buildCommandContext(readGlobalOptions(command));
918
+ try {
919
+ const assetsRoot = await dependencies.resolveAssetsRoot();
920
+ const explicitScope = command.getOptionValueSourceWithGlobals('scope') === 'cli';
921
+ const requestedScope = context.scope === 'project' || context.scope === 'user'
922
+ ? context.scope
923
+ : null;
924
+ const scopes = explicitScope && context.scope === 'all'
925
+ ? [...definition.allowedScopes]
926
+ : requestedScope
927
+ ? [requestedScope]
928
+ : await resolvePackCommandScopes(pack, context, assetsRoot, dependencies);
929
+ for (const scope of scopes) {
930
+ if (!definition.allowedScopes.includes(scope)) {
931
+ throw new Error(`Pack ${pack} does not allow ${scope} scope`);
932
+ }
933
+ }
934
+ const requests = await Promise.all(scopes.map(async (scope) => ({
935
+ pack,
936
+ scope,
937
+ scopeRoot: scope === 'project'
938
+ ? await dependencies.resolveProjectRoot(context.cwd)
939
+ : dependencies.resolveScopeRoot('user', context.cwd, context.home),
940
+ assetsRoot,
941
+ action: 'install',
942
+ })));
943
+ const results = await dependencies.reconcilePacks(requests);
944
+ appliedScopesByCommand.set(command, results.map(({ request }) => request.scope));
945
+ setInstalledCanonicalPaths(command, canonicalPathsForPacks([pack]));
946
+ // Project-scope placement installs capability only. The
947
+ // project-management and decisions AGENTS sections are written by the
948
+ // explicit adoption action (`oat pjm init`), never by pack placement.
949
+ if (context.json) {
950
+ context.logger.json({
951
+ status: 'ok',
952
+ pack,
953
+ scopes,
954
+ results,
955
+ });
956
+ }
957
+ else {
958
+ context.logger.info(`Installed ${pack} tool pack.`);
959
+ for (const result of results) {
960
+ context.logger.info(`Scope: ${result.request.scope}`);
961
+ context.logger.info(`Target root: ${result.request.scopeRoot}`);
962
+ context.logger.info(`Reconciled operations: ${result.apply.applied.length}`);
963
+ context.logger.info(`Run: oat sync --scope ${result.request.scope}`);
964
+ }
965
+ }
966
+ process.exitCode = 0;
967
+ }
968
+ catch (error) {
969
+ const message = error instanceof Error ? error.message : String(error);
970
+ if (context.json)
971
+ context.logger.json({ status: 'error', message });
972
+ else
973
+ context.logger.error(message);
974
+ process.exitCode = 1;
975
+ }
976
+ });
977
+ }
904
978
  export function createInitToolsCommand(overrides = {}) {
905
979
  const dependencies = {
906
980
  ...DEFAULT_DEPENDENCIES,
907
981
  ...overrides,
908
982
  };
983
+ const hasLegacyInstallerOverrides = [
984
+ 'installCore',
985
+ 'installIdeas',
986
+ 'installDocs',
987
+ 'installWorkflows',
988
+ 'installUtility',
989
+ 'installProjectManagement',
990
+ 'installResearch',
991
+ 'installBrainstorm',
992
+ ].some((key) => Object.prototype.hasOwnProperty.call(overrides, key));
993
+ if (hasLegacyInstallerOverrides &&
994
+ !Object.prototype.hasOwnProperty.call(overrides, 'reconcilePacks')) {
995
+ dependencies.reconcilePacks = undefined;
996
+ }
909
997
  async function reconcileAfterInstall(actionCommand) {
910
998
  if (process.exitCode !== undefined && process.exitCode !== 0) {
911
999
  return;
@@ -914,6 +1002,14 @@ export function createInitToolsCommand(overrides = {}) {
914
1002
  return;
915
1003
  }
916
1004
  const context = dependencies.buildCommandContext(readGlobalOptions(actionCommand));
1005
+ const appliedScopes = appliedScopesByCommand.get(actionCommand) ??
1006
+ (dependencies.reconcilePacks
1007
+ ? []
1008
+ : context.scope === 'user'
1009
+ ? ['user']
1010
+ : ['project']);
1011
+ if (!appliedScopes.includes('project'))
1012
+ return;
917
1013
  const repoRoot = await dependencies.resolveProjectRoot(context.cwd);
918
1014
  await reconcileProjectToolsConfig({
919
1015
  repoRoot,
@@ -921,23 +1017,34 @@ export function createInitToolsCommand(overrides = {}) {
921
1017
  home: context.home,
922
1018
  }, dependencies);
923
1019
  }
924
- const packCommands = [
925
- createInitToolsCoreCommand(),
926
- createInitToolsIdeasCommand(),
927
- createInitToolsDocsCommand(),
928
- createInitToolsProjectManagementCommand(),
929
- createInitToolsWorkflowsCommand(),
930
- createInitToolsUtilityCommand(),
931
- createInitToolsResearchCommand(),
932
- createInitToolsBrainstormCommand({
933
- buildCommandContext: dependencies.buildCommandContext,
934
- resolveProjectRoot: dependencies.resolveProjectRoot,
935
- resolveScopeRoot: dependencies.resolveScopeRoot,
936
- resolveAssetsRoot: dependencies.resolveAssetsRoot,
937
- installBrainstorm: dependencies.installBrainstorm,
938
- scanTools: dependencies.scanTools,
939
- }),
940
- ];
1020
+ const packCommands = dependencies.reconcilePacks
1021
+ ? [
1022
+ 'core',
1023
+ 'ideas',
1024
+ 'docs',
1025
+ 'project-management',
1026
+ 'workflows',
1027
+ 'utility',
1028
+ 'research',
1029
+ 'brainstorm',
1030
+ ].map((pack) => createReconciledPackCommand(pack, dependencies))
1031
+ : [
1032
+ createInitToolsCoreCommand(),
1033
+ createInitToolsIdeasCommand(),
1034
+ createInitToolsDocsCommand(),
1035
+ createInitToolsProjectManagementCommand(),
1036
+ createInitToolsWorkflowsCommand(),
1037
+ createInitToolsUtilityCommand(),
1038
+ createInitToolsResearchCommand(),
1039
+ createInitToolsBrainstormCommand({
1040
+ buildCommandContext: dependencies.buildCommandContext,
1041
+ resolveProjectRoot: dependencies.resolveProjectRoot,
1042
+ resolveScopeRoot: dependencies.resolveScopeRoot,
1043
+ resolveAssetsRoot: dependencies.resolveAssetsRoot,
1044
+ installBrainstorm: dependencies.installBrainstorm,
1045
+ scanTools: dependencies.scanTools,
1046
+ }),
1047
+ ];
941
1048
  for (const packCommand of packCommands) {
942
1049
  packCommand.hook('postAction', async (_thisCommand, actionCommand) => {
943
1050
  await reconcileAfterInstall(actionCommand);
@@ -950,6 +1057,7 @@ export function createInitToolsCommand(overrides = {}) {
950
1057
  command.action(async (_options, actionCommand) => {
951
1058
  const context = dependencies.buildCommandContext(readGlobalOptions(actionCommand));
952
1059
  const selectedPacks = await runInitTools(context, dependencies);
1060
+ appliedScopesByCommand.set(actionCommand, lastRunInitToolsMetadata?.appliedScopes ?? []);
953
1061
  setInstalledCanonicalPaths(actionCommand, canonicalPathsForPacks(selectedPacks));
954
1062
  await reconcileAfterInstall(actionCommand);
955
1063
  });