agentic-workflow-manager 3.1.0 → 3.2.1

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 (92) hide show
  1. package/dist/src/commands/add.js +84 -0
  2. package/dist/src/commands/agent.js +95 -0
  3. package/dist/src/commands/backup.js +46 -0
  4. package/dist/src/commands/doctor.js +80 -4
  5. package/dist/src/commands/hooks/claude.js +192 -0
  6. package/dist/src/commands/hooks/codex.js +191 -0
  7. package/dist/src/commands/hooks/index.js +23 -8
  8. package/dist/src/commands/hooks/install.js +11 -107
  9. package/dist/src/commands/hooks/resync.js +28 -18
  10. package/dist/src/commands/hooks/shared.js +94 -0
  11. package/dist/src/commands/hooks/status.js +13 -64
  12. package/dist/src/commands/hooks/uninstall.js +11 -47
  13. package/dist/src/commands/init.js +90 -28
  14. package/dist/src/commands/registry/index.js +1 -1
  15. package/dist/src/commands/sync.js +113 -0
  16. package/dist/src/commands/update.js +107 -0
  17. package/dist/src/core/agent-targets.js +50 -0
  18. package/dist/src/core/artifact-state.js +65 -0
  19. package/dist/src/core/atomic-file.js +72 -0
  20. package/dist/src/core/bundle-install.js +60 -38
  21. package/dist/src/core/bundles.js +21 -0
  22. package/dist/src/core/context/managed-block.js +190 -0
  23. package/dist/src/core/context/orchestrator.js +3 -1
  24. package/dist/src/core/context/project-constitution-inject.js +1 -1
  25. package/dist/src/core/context/regenerate.js +3 -3
  26. package/dist/src/core/context/strategies/codex-agents.js +100 -0
  27. package/dist/src/core/diagnostics/checks.js +16 -0
  28. package/dist/src/core/diagnostics/context.js +37 -8
  29. package/dist/src/core/diagnostics/provider-checks.js +219 -0
  30. package/dist/src/core/executor.js +30 -9
  31. package/dist/src/core/init/mutation-targets.js +139 -0
  32. package/dist/src/core/init/provider-facts.js +179 -0
  33. package/dist/src/core/init/steps.js +65 -9
  34. package/dist/src/core/install-planner.js +206 -0
  35. package/dist/src/core/install-transaction.js +405 -0
  36. package/dist/src/core/profile.js +1 -1
  37. package/dist/src/core/provider-artifacts.js +54 -0
  38. package/dist/src/core/provider-version.js +36 -0
  39. package/dist/src/core/reconciliation.js +61 -0
  40. package/dist/src/core/registries.js +10 -0
  41. package/dist/src/core/renderers/canonical-agent.js +27 -0
  42. package/dist/src/core/renderers/codex-agent.js +60 -0
  43. package/dist/src/core/skill-integrity.js +4 -3
  44. package/dist/src/index.js +97 -289
  45. package/dist/src/providers/index.js +143 -39
  46. package/dist/src/ui/provider-preflight.js +26 -0
  47. package/dist/src/utils/config.js +101 -9
  48. package/dist/tests/commands/agent.test.js +103 -0
  49. package/dist/tests/commands/backup.test.js +109 -0
  50. package/dist/tests/commands/doctor.test.js +65 -4
  51. package/dist/tests/commands/hooks/codex.test.js +230 -0
  52. package/dist/tests/commands/hooks/install.test.js +20 -1
  53. package/dist/tests/commands/hooks/resync.test.js +53 -6
  54. package/dist/tests/commands/hooks/status.test.js +10 -0
  55. package/dist/tests/commands/hooks/uninstall.test.js +12 -0
  56. package/dist/tests/commands/init.test.js +195 -8
  57. package/dist/tests/commands/multi-agent-targeting.test.js +231 -0
  58. package/dist/tests/core/agent-targets.test.js +45 -0
  59. package/dist/tests/core/artifact-state.test.js +144 -0
  60. package/dist/tests/core/atomic-file.test.js +96 -0
  61. package/dist/tests/core/bundle-install.test.js +208 -17
  62. package/dist/tests/core/bundles.test.js +12 -0
  63. package/dist/tests/core/context/managed-block.test.js +90 -0
  64. package/dist/tests/core/context/orchestrator.test.js +48 -3
  65. package/dist/tests/core/context/regenerate.test.js +1 -1
  66. package/dist/tests/core/context/strategies/codex-agents.test.js +185 -0
  67. package/dist/tests/core/context/strategies/config-instructions.test.js +1 -1
  68. package/dist/tests/core/diagnostics/checks.test.js +19 -0
  69. package/dist/tests/core/diagnostics/context.test.js +74 -0
  70. package/dist/tests/core/diagnostics/provider-checks.test.js +252 -0
  71. package/dist/tests/core/executor.test.js +20 -0
  72. package/dist/tests/core/export/engine.test.js +3 -2
  73. package/dist/tests/core/init/mutation-targets.test.js +235 -0
  74. package/dist/tests/core/init/orchestrator.test.js +1 -1
  75. package/dist/tests/core/init/provider-facts.test.js +177 -0
  76. package/dist/tests/core/init/steps.test.js +106 -2
  77. package/dist/tests/core/install-planner.test.js +224 -0
  78. package/dist/tests/core/install-transaction.test.js +257 -0
  79. package/dist/tests/core/provider-artifacts.test.js +64 -0
  80. package/dist/tests/core/provider-version.test.js +53 -0
  81. package/dist/tests/core/reconciliation.test.js +165 -0
  82. package/dist/tests/core/registries-sync.test.js +4 -4
  83. package/dist/tests/core/renderers/canonical-agent.test.js +58 -0
  84. package/dist/tests/core/renderers/codex-agent.test.js +128 -0
  85. package/dist/tests/core/versioning.test.js +1 -1
  86. package/dist/tests/integration/codex-provider-isolated.test.js +235 -0
  87. package/dist/tests/providers/hooks-config.test.js +43 -25
  88. package/dist/tests/providers/index.test.js +144 -39
  89. package/dist/tests/structural/codex-agent-escaping-completeness.test.js +77 -0
  90. package/dist/tests/ui/provider-preflight.test.js +25 -0
  91. package/dist/tests/utils/config.test.js +178 -18
  92. package/package.json +1 -1
@@ -16,6 +16,7 @@ const profile_1 = require("../profile");
16
16
  const bundles_1 = require("../bundles");
17
17
  const skill_integrity_1 = require("../skill-integrity");
18
18
  const paths_1 = require("../paths");
19
+ const provider_checks_1 = require("./provider-checks");
19
20
  // Estado de un artefacto en <dir>/<skill>: link vivo / symlink colgante / ausente.
20
21
  function linkState(dir, skill) {
21
22
  const p = path_1.default.join(dir, skill);
@@ -44,12 +45,12 @@ function classifyLinks(skillNames, dir) {
44
45
  }
45
46
  function detectGitState(repoDir) {
46
47
  try {
47
- const porcelain = (0, child_process_1.execSync)('git status --porcelain', { cwd: repoDir, stdio: ['ignore', 'pipe', 'ignore'] })
48
+ const porcelain = (0, child_process_1.execFileSync)('git', ['status', '--porcelain'], { cwd: repoDir, stdio: ['ignore', 'pipe', 'ignore'], encoding: 'utf8' })
48
49
  .toString().trim();
49
50
  if (porcelain.length > 0)
50
51
  return 'dirty';
51
52
  try {
52
- const behind = (0, child_process_1.execSync)('git rev-list --count HEAD..@{u}', { cwd: repoDir, stdio: ['ignore', 'pipe', 'ignore'], timeout: 3000 })
53
+ const behind = (0, child_process_1.execFileSync)('git', ['rev-list', '--count', 'HEAD..@{u}'], { cwd: repoDir, stdio: ['ignore', 'pipe', 'ignore'], timeout: 3000, encoding: 'utf8' })
53
54
  .toString().trim();
54
55
  if (behind !== '' && behind !== '0')
55
56
  return 'behind';
@@ -67,8 +68,8 @@ function detectGitState(repoDir) {
67
68
  function gatherContextInjection() {
68
69
  const out = [];
69
70
  const orch = new orchestrator_1.InjectionOrchestrator();
70
- for (const agent of Object.keys(providers_1.PROVIDERS)) {
71
- const inj = providers_1.PROVIDERS[agent].injection;
71
+ for (const agent of providers_1.AGENT_TARGETS) {
72
+ const inj = (0, providers_1.providerFor)(agent).injection;
72
73
  if (!inj || inj.type !== 'config-instructions')
73
74
  continue;
74
75
  if (!fs_1.default.existsSync(inj.configPath))
@@ -96,17 +97,22 @@ function gatherMachine(bundles, agent = 'claude-code') {
96
97
  const first = regs[0];
97
98
  const cachePresent = !!first && fs_1.default.existsSync(path_1.default.join(first.contentRoot, '.git'));
98
99
  const gitState = cachePresent ? detectGitState(first.contentRoot) : undefined;
99
- // hook (reutiliza computeHookStatus)
100
+ // hook (reutiliza computeHookStatus) — Task 9 fix: this used to be hardcoded to
101
+ // 'claude-code' regardless of `agent`, so `awm init --agent codex`'s own hook
102
+ // precondition silently read CLAUDE's hook status instead of Codex's. Whenever
103
+ // Claude's hook was already installed, stepHook would then wrongly conclude
104
+ // Codex's hook was "already present" and skip installing it (R18 regression —
105
+ // caught while building this task's real end-to-end init test).
100
106
  let hookPresent = false;
101
107
  let hookDegraded = false;
102
108
  try {
103
- const hs = (0, status_1.computeHookStatus)('claude-code');
109
+ const hs = (0, status_1.computeHookStatus)(agent);
104
110
  hookPresent = hs.checks.settingsEntry.ok;
105
111
  hookDegraded = hs.overall === 'DEGRADED';
106
112
  }
107
113
  catch { /* sin soporte de hooks → ausente */ }
108
114
  // devCore (bundle baseline)
109
- const skillsDir = providers_1.PROVIDERS[agent].skill.global;
115
+ const skillsDir = (0, providers_1.providerFor)(agent).skill.global;
110
116
  const baseline = bundles.find((b) => b.scope === 'baseline');
111
117
  let devCorePresent = false;
112
118
  let brokenLinks = [];
@@ -116,6 +122,26 @@ function gatherMachine(bundles, agent = 'claude-code') {
116
122
  const absent = skillNames.filter((s) => !linked.includes(s) && !broken.includes(s));
117
123
  devCorePresent = skillNames.length > 0 && (linked.length + broken.length) > 0;
118
124
  brokenLinks = [...broken, ...absent];
125
+ // Agent-type artifacts are never shared across agents (R12/R13 —
126
+ // install-planner.ts — unlike skills, where OpenCode and Codex both
127
+ // resolve to ~/.agents/skills). A shared skill directory already
128
+ // linked by a co-owner agent must not make THIS agent's own native
129
+ // agent artifacts look "present" when they were never installed for
130
+ // it specifically — otherwise stepDevCore (init/steps.ts) would skip
131
+ // re-running and Codex would never get its .toml rendered on a
132
+ // machine where OpenCode was initialized first. Found while building
133
+ // the real Codex+OpenCode coexistence E2E test
134
+ // (tests/integration/codex-provider-isolated.test.ts).
135
+ const agentConfig = (0, providers_1.providerFor)(agent).agent;
136
+ if (agentConfig) {
137
+ const agentNames = (0, bundles_1.resolveBundleAgents)(baseline.name, bundles);
138
+ if (agentNames.length > 0) {
139
+ const filenames = agentNames.map((n) => agentConfig.renderer === 'codex-agent-toml' ? `${n}.toml` : n);
140
+ const { linked: agentLinked, broken: agentBroken } = classifyLinks(filenames, agentConfig.global);
141
+ const agentAbsent = filenames.filter((f) => !agentLinked.includes(f) && !agentBroken.includes(f));
142
+ brokenLinks = [...brokenLinks, ...agentBroken, ...agentAbsent];
143
+ }
144
+ }
119
145
  }
120
146
  // ambient (deseados desde ~/.awm/config.json)
121
147
  let wanted = [];
@@ -145,7 +171,7 @@ function gatherMachine(bundles, agent = 'claude-code') {
145
171
  function gatherProject(root, bundles, agent = 'claude-code') {
146
172
  const profile = (0, profile_1.readProfile)(root);
147
173
  const profilePresent = fs_1.default.existsSync(path_1.default.join(root, '.awm', 'profile.json'));
148
- const localSkillsDir = path_1.default.join(root, providers_1.PROVIDERS[agent].skill.local);
174
+ const localSkillsDir = path_1.default.join(root, (0, providers_1.providerFor)(agent).skill.local);
149
175
  const expected = [];
150
176
  for (const ext of profile.extensions) {
151
177
  for (const s of (0, bundles_1.resolveBundleSkills)(ext, bundles))
@@ -172,8 +198,11 @@ function gatherContext(opts = {}) {
172
198
  const bundles = opts.bundles ?? (0, bundles_1.discoverAllBundles)();
173
199
  const agent = opts.agent ?? 'claude-code';
174
200
  const root = (0, profile_1.findProjectRoot)(cwd);
201
+ const agents = opts.agents ?? [agent];
202
+ const scanSkills = opts.scanSkills ?? ((dir) => (0, skill_integrity_1.classifyGlobalSkills)(dir, (0, registries_1.contentRoots)()));
175
203
  return {
176
204
  machine: gatherMachine(bundles, agent),
177
205
  project: root ? gatherProject(root, bundles, agent) : null,
206
+ providers: (0, provider_checks_1.gatherProviderChecks)(agents, scanSkills),
178
207
  };
179
208
  }
@@ -0,0 +1,219 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.gatherProviderChecks = gatherProviderChecks;
7
+ // src/core/diagnostics/provider-checks.ts
8
+ //
9
+ // Builds the per-provider diagnostic matrix consumed by `awm doctor`
10
+ // (Task 9). Kept in its own module (rather than growing context.ts or
11
+ // checks.ts) because it has a distinct concern: turning raw provider state
12
+ // (binary version, skill/agent dirs, hook trust, context injection) into a
13
+ // small set of stable, JSON-friendly `ProviderCheck` rows — one per
14
+ // provider, deduplicated for physical directories shared between providers
15
+ // (today: OpenCode and Codex both read/write ~/.agents/skills).
16
+ const fs_1 = __importDefault(require("fs"));
17
+ const path_1 = __importDefault(require("path"));
18
+ const providers_1 = require("../../providers");
19
+ const provider_version_1 = require("../provider-version");
20
+ const status_1 = require("../../commands/hooks/status");
21
+ const orchestrator_1 = require("../context/orchestrator");
22
+ const registries_1 = require("../registries");
23
+ function binaryVersionCheck(agent) {
24
+ const provider = (0, providers_1.providerFor)(agent);
25
+ if (!provider.versionCommand || !provider.minimumVersion) {
26
+ // No version gate for this agent — nothing to enforce, treat as supported.
27
+ return { id: 'binary.version', state: 'supported' };
28
+ }
29
+ try {
30
+ const { version } = (0, provider_version_1.assertProviderSupported)(agent);
31
+ return { id: 'binary.version', state: 'supported', target: version ?? undefined };
32
+ }
33
+ catch (err) {
34
+ const message = err.message;
35
+ const missing = /not installed|not available on PATH/i.test(message);
36
+ return {
37
+ id: 'binary.version',
38
+ state: missing ? 'missing' : 'unsupported',
39
+ detail: message,
40
+ remediationCode: missing ? 'install-provider-binary' : 'upgrade-provider-binary',
41
+ };
42
+ }
43
+ }
44
+ function skillsGlobalCheck(dir, owners, integrity) {
45
+ const shared = owners.length > 1;
46
+ const broken = integrity.repairable.length + integrity.dead.length;
47
+ // Broken links are checked BEFORE shared: 'shared' is a non-degrading/OK state
48
+ // (see checks.ts's DEGRADING_PROVIDER_STATES), so if it were set unconditionally
49
+ // for a shared dir it would silently mask real broken/dead symlinks — a green
50
+ // checkmark next to "N broken links → repair-global-skills" would contradict its
51
+ // own trailing text, and `overall` would never degrade despite real breakage.
52
+ let state;
53
+ if (broken > 0) {
54
+ state = 'broken';
55
+ }
56
+ else if (shared) {
57
+ state = 'shared';
58
+ }
59
+ else if (!fs_1.default.existsSync(dir)) {
60
+ state = 'absent';
61
+ }
62
+ else {
63
+ state = 'healthy';
64
+ }
65
+ return {
66
+ id: 'skills.global',
67
+ state,
68
+ target: dir,
69
+ owners: shared ? owners : undefined,
70
+ detail: broken > 0 ? `${broken} broken links` : undefined,
71
+ remediationCode: broken > 0 ? 'repair-global-skills' : undefined,
72
+ };
73
+ }
74
+ /** R8: verify the Codex `.toml` agents this run's renderer would have produced still parse. */
75
+ function tomlAgentsHealthy(dir, entries) {
76
+ const tomls = entries.filter((e) => e.endsWith('.toml'));
77
+ const broken = tomls.filter((file) => {
78
+ try {
79
+ const content = fs_1.default.readFileSync(path_1.default.join(dir, file), 'utf8');
80
+ // renderCodexAgent (Task 4) always emits this key — its absence means the
81
+ // file was hand-edited or truncated, not a shape `awm` would have written.
82
+ return !content.includes('developer_instructions = ');
83
+ }
84
+ catch {
85
+ return true;
86
+ }
87
+ }).length;
88
+ return { broken };
89
+ }
90
+ /**
91
+ * Returns `null` when a check doesn't structurally apply to `agent` (e.g. Antigravity
92
+ * has no `agent`/`hooks`/`injection` config; OpenCode has no hooks; Claude Code's global
93
+ * context rides its SessionStart hook, already covered by hook.trust). `null` rows are
94
+ * dropped by `gatherProviderChecks` — omitted entirely, not rendered as a failure. This
95
+ * is deliberately distinct from `state: 'unsupported'`, which `binaryVersionCheck` still
96
+ * uses for a genuine failure (an installed CLI version below the required minimum) — that
97
+ * state correctly degrades `overall` (see checks.ts's DEGRADING_PROVIDER_STATES) and must
98
+ * never be reused to mean "not applicable", or a fully-healthy single-provider `awm doctor`
99
+ * run (e.g. claude-code-only, opencode-only, antigravity-only) would always render
100
+ * inapplicable rows as red ✖ and `overall` could never be 'healthy'.
101
+ */
102
+ function agentsNativeCheck(agent) {
103
+ const provider = (0, providers_1.providerFor)(agent);
104
+ if (!provider.agent)
105
+ return null;
106
+ const dir = provider.agent.global;
107
+ let entries;
108
+ try {
109
+ entries = fs_1.default.readdirSync(dir);
110
+ }
111
+ catch {
112
+ return { id: 'agents.native', state: 'absent', target: dir };
113
+ }
114
+ if (entries.length === 0)
115
+ return { id: 'agents.native', state: 'absent', target: dir };
116
+ if (provider.agent.renderer === 'codex-agent-toml') {
117
+ const { broken } = tomlAgentsHealthy(dir, entries);
118
+ return {
119
+ id: 'agents.native',
120
+ state: broken > 0 ? 'broken' : 'healthy',
121
+ target: dir,
122
+ detail: broken > 0 ? `${broken} malformed .toml` : undefined,
123
+ remediationCode: broken > 0 ? 'reinstall-native-agents' : undefined,
124
+ };
125
+ }
126
+ return { id: 'agents.native', state: 'healthy', target: dir };
127
+ }
128
+ function hookTrustCheck(agent) {
129
+ const provider = (0, providers_1.providerFor)(agent);
130
+ if (!provider.hooks)
131
+ return null;
132
+ let status;
133
+ try {
134
+ status = (0, status_1.computeHookStatus)(agent);
135
+ }
136
+ catch {
137
+ return { id: 'hook.trust', state: 'absent', remediationCode: 'awm-init' };
138
+ }
139
+ if (status.overall === 'NOT_INSTALLED') {
140
+ return { id: 'hook.trust', state: 'absent', remediationCode: 'awm-init' };
141
+ }
142
+ if (status.trust) {
143
+ // Codex: 'pending-trust' | 'healthy' | 'stale' map directly onto ProviderCheckState.
144
+ return {
145
+ id: 'hook.trust',
146
+ state: status.trust,
147
+ remediationCode: status.trust === 'pending-trust' ? 'open-hooks-trust' : undefined,
148
+ };
149
+ }
150
+ return { id: 'hook.trust', state: status.overall === 'HEALTHY' ? 'healthy' : 'broken' };
151
+ }
152
+ /** R7: reflects the provider's global context-delivery mechanism (config-instructions /
153
+ * managed-agents-md). claude-code's context rides the SessionStart hook — already
154
+ * covered by hook.trust, so this check is OMITTED (returns null) rather than reported,
155
+ * to avoid double-reporting the same fact as a separate, redundant row. */
156
+ function contextGlobalCheck(agent) {
157
+ const injection = (0, providers_1.providerFor)(agent).injection;
158
+ if (!injection || injection.type === 'cc-settings-merge') {
159
+ return null;
160
+ }
161
+ let state = 'absent';
162
+ try {
163
+ const orchestrator = new orchestrator_1.InjectionOrchestrator();
164
+ const result = orchestrator.contextStatus({
165
+ agent,
166
+ scope: 'global',
167
+ registryRoot: (0, registries_1.capabilityRoot)('skills') ?? '',
168
+ installMethod: 'symlink',
169
+ profileExtensions: [],
170
+ });
171
+ state = result === 'injected' ? 'delivered' : result === 'stale' ? 'stale' : 'absent';
172
+ }
173
+ catch {
174
+ state = 'absent';
175
+ }
176
+ return {
177
+ id: 'context.global',
178
+ state,
179
+ remediationCode: state === 'delivered' ? undefined : 'awm-init',
180
+ };
181
+ }
182
+ /**
183
+ * Builds one `ProviderFacts` row per requested agent. Physical skill directories shared
184
+ * between providers (OpenCode + Codex both use `~/.agents/skills`) are scanned exactly
185
+ * once via `scanSkills` and every owner's `skills.global` check is marked `state: 'shared'`
186
+ * — mirrors the dedup principle `install-planner.ts` (Task 5) already applies to writes.
187
+ *
188
+ * Named `gatherProviderChecks` (not `gatherProviderFacts`) to avoid colliding with the
189
+ * unrelated `gatherProviderFacts` in `core/init/provider-facts.ts` (Task 8, baseline-hash
190
+ * snapshot for rollback comparison — a different shape, a different purpose). No file
191
+ * currently imports both, but the names are close enough to trip up a future reader/editor.
192
+ */
193
+ function gatherProviderChecks(agents, scanSkills) {
194
+ const ownersByDir = new Map();
195
+ for (const agent of agents) {
196
+ const dir = (0, providers_1.providerFor)(agent).skill.global;
197
+ const owners = ownersByDir.get(dir) ?? [];
198
+ owners.push(agent);
199
+ ownersByDir.set(dir, owners);
200
+ }
201
+ const scansByDir = new Map();
202
+ for (const dir of ownersByDir.keys()) {
203
+ scansByDir.set(dir, scanSkills(dir));
204
+ }
205
+ return agents.map((agent) => {
206
+ const provider = (0, providers_1.providerFor)(agent);
207
+ const dir = provider.skill.global;
208
+ const owners = ownersByDir.get(dir) ?? [agent];
209
+ const integrity = scansByDir.get(dir) ?? { valid: [], repairable: [], dead: [] };
210
+ const checks = [
211
+ binaryVersionCheck(agent),
212
+ skillsGlobalCheck(dir, owners, integrity),
213
+ agentsNativeCheck(agent),
214
+ hookTrustCheck(agent),
215
+ contextGlobalCheck(agent),
216
+ ].filter((check) => check !== null);
217
+ return { id: agent, label: provider.label, checks };
218
+ });
219
+ }
@@ -4,6 +4,8 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
4
4
  };
5
5
  Object.defineProperty(exports, "__esModule", { value: true });
6
6
  exports.removeArtifact = removeArtifact;
7
+ exports.stageArtifact = stageArtifact;
8
+ exports.replaceArtifact = replaceArtifact;
7
9
  exports.installArtifact = installArtifact;
8
10
  // src/core/executor.ts
9
11
  const fs_1 = __importDefault(require("fs"));
@@ -22,20 +24,39 @@ function removeArtifact(targetPath) {
22
24
  }
23
25
  fs_1.default.rmSync(targetPath, { recursive: true, force: true });
24
26
  }
25
- function installArtifact(sourcePath, targetPath, method) {
27
+ /**
28
+ * Stages `sourcePath` next to `targetPath` (same parent directory) WITHOUT
29
+ * touching `targetPath` itself (R17): the live target is never removed until
30
+ * a stage has already succeeded. Returns the staged path, ready to be swapped
31
+ * in via `replaceArtifact`.
32
+ */
33
+ function stageArtifact(sourcePath, targetPath, method) {
26
34
  if (!fs_1.default.existsSync(sourcePath)) {
27
35
  throw new Error(`Source path does not exist: ${sourcePath}`);
28
36
  }
29
- const parentDir = path_1.default.dirname(targetPath);
30
- if (!fs_1.default.existsSync(parentDir)) {
31
- fs_1.default.mkdirSync(parentDir, { recursive: true });
32
- }
33
- // Clean up existing if it exists
34
- fs_1.default.rmSync(targetPath, { recursive: true, force: true });
37
+ const parent = path_1.default.dirname(targetPath);
38
+ fs_1.default.mkdirSync(parent, { recursive: true });
39
+ const staged = path_1.default.join(parent, `.${path_1.default.basename(targetPath)}.${process.pid}.staged`);
40
+ fs_1.default.rmSync(staged, { recursive: true, force: true });
35
41
  if (method === 'symlink') {
36
- fs_1.default.symlinkSync(sourcePath, targetPath, 'dir');
42
+ fs_1.default.symlinkSync(sourcePath, staged, 'dir');
37
43
  }
38
44
  else {
39
- fs_1.default.cpSync(sourcePath, targetPath, { recursive: true });
45
+ fs_1.default.cpSync(sourcePath, staged, { recursive: true });
40
46
  }
47
+ return staged;
48
+ }
49
+ /** Atomically swaps a staged artifact into `targetPath`, replacing whatever is there. */
50
+ function replaceArtifact(staged, targetPath) {
51
+ fs_1.default.rmSync(targetPath, { recursive: true, force: true });
52
+ fs_1.default.renameSync(staged, targetPath);
53
+ }
54
+ /**
55
+ * Convenience wrapper over `stageArtifact` + `replaceArtifact` for callers
56
+ * that don't need transactional multi-target coordination (see
57
+ * install-transaction.ts's `applyInstallPlan` for the transactional path).
58
+ */
59
+ function installArtifact(sourcePath, targetPath, method) {
60
+ const staged = stageArtifact(sourcePath, targetPath, method);
61
+ replaceArtifact(staged, targetPath);
41
62
  }
@@ -0,0 +1,139 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.planInitMutationTargets = planInitMutationTargets;
7
+ // src/core/init/mutation-targets.ts
8
+ //
9
+ // Enumerates, WITHOUT writing anything, every filesystem path a real `awm
10
+ // init` run for a given agent is about to touch. `runInit` feeds this list to
11
+ // `beginBackupSession` (install-transaction.ts) BEFORE any step runs, so a
12
+ // failed init can roll every one of them back to its pre-run state.
13
+ //
14
+ // Deliberately generous rather than exhaustively precise: a path that ends up
15
+ // unused this run (e.g. an ambient bundle whose signal isn't present) is
16
+ // harmless to include — beginBackupSession just snapshots it (a no-op backup
17
+ // entry when nothing exists there yet). The one failure mode that matters is
18
+ // under-enumeration: a real write to a path NOT in this list would land
19
+ // outside the backup session and survive a rollback.
20
+ const path_1 = __importDefault(require("path"));
21
+ const providers_1 = require("../../providers");
22
+ const bundles_1 = require("../bundles");
23
+ const bundle_install_1 = require("../bundle-install");
24
+ const install_planner_1 = require("../install-planner");
25
+ const artifact_state_1 = require("../artifact-state");
26
+ const paths_1 = require("../paths");
27
+ const profile_1 = require("../profile");
28
+ const registries_1 = require("../registries");
29
+ // Thin wrapper over install-planner.ts's `physicalTarget` (the single source
30
+ // of truth for the dir+filename computation, including the `.toml` rename for
31
+ // codex-agent-toml) — adapts its throw-on-unsupported contract to this
32
+ // module's null-on-unsupported one, and drops the `renderer` field this
33
+ // module's callers never needed.
34
+ function physicalTarget(type, agent, scope, installName, projectRoot) {
35
+ try {
36
+ return (0, install_planner_1.physicalTarget)({ name: installName, installName, type, sourcePath: '' }, agent, scope, projectRoot ?? '').targetPath;
37
+ }
38
+ catch {
39
+ return null;
40
+ }
41
+ }
42
+ function addBundleTargets(targets, bundleName, bundles, agent, scope, projectRoot, contentDir) {
43
+ let intents;
44
+ try {
45
+ intents = (0, bundle_install_1.expandBundleArtifacts)({
46
+ bundleName,
47
+ bundles,
48
+ agents: [agent],
49
+ method: 'symlink',
50
+ projectRoot,
51
+ contentDir,
52
+ });
53
+ }
54
+ catch {
55
+ // Bundle not resolvable (e.g. missing dependency in a partially-seeded
56
+ // test registry) — nothing to enumerate for it, not a hard failure.
57
+ return;
58
+ }
59
+ for (const intent of intents) {
60
+ const p = physicalTarget(intent.type, agent, scope, intent.installName, projectRoot);
61
+ if (p)
62
+ targets.add(p);
63
+ }
64
+ }
65
+ /**
66
+ * Enumerates every path `runInitSteps` may write to for `agent`, given the
67
+ * discovered bundle catalog. Driven entirely by `agent` (the single target
68
+ * being initialized this run) and `bundles` — not by preferences, since
69
+ * enumeration never needs to know about any OTHER enabled agent's targets.
70
+ * Pure / read-only (does hit the filesystem to discover project root and
71
+ * read the project profile, but never writes).
72
+ */
73
+ function planInitMutationTargets(params) {
74
+ const { cwd, agent, bundles } = params;
75
+ const targets = new Set();
76
+ // preferences.json + the artifact ownership ledger (state/artifacts.json)
77
+ targets.add(path_1.default.join((0, paths_1.awmHome)(), 'preferences.json'));
78
+ targets.add((0, artifact_state_1.artifactStateFile)());
79
+ const provider = (0, providers_1.providerFor)(agent);
80
+ // hook: settings file (or hooks.json for codex) + the scripts directory it points at
81
+ if (provider.hooks) {
82
+ targets.add(provider.hooks.settingsPath);
83
+ targets.add(provider.hooks.scriptsDir);
84
+ }
85
+ // Global skills directory itself, not just the bundle-derived skill
86
+ // subdirectories enumerated below. stepGlobalSkillsRepair (steps.ts) calls
87
+ // repairGlobalSkills, which readdirSync's the WHOLE skills dir and
88
+ // rm/symlinks individual entries classified as repairable/dead — i.e.
89
+ // entries that, by definition, don't belong to any currently-known
90
+ // bundle, so they can never be enumerated by addBundleTargets below.
91
+ // Adding the parent directory here makes beginBackupSession snapshot the
92
+ // whole tree (backupEntryFor does a recursive fs.cpSync for directory
93
+ // targets — verified in install-transaction.ts), covering any entry that
94
+ // repair might mutate. Broad-but-safe, per this module's own philosophy.
95
+ targets.add(provider.skill.global);
96
+ // global context / AGENTS.md injection (covered by the hook for claude-code)
97
+ const injection = provider.injection;
98
+ if (injection) {
99
+ if (injection.type === 'config-instructions')
100
+ targets.add(injection.configPath);
101
+ if (injection.type === 'managed-agents-md')
102
+ targets.add(injection.globalPath);
103
+ }
104
+ // machine-level bundle targets: baseline (dev-core) + ambient, global scope
105
+ const roots = (0, registries_1.contentRoots)();
106
+ const contentDir = roots[0] ?? '';
107
+ const machineBundles = bundles.filter((b) => b.scope === 'baseline' || b.scope === 'ambient');
108
+ for (const b of machineBundles) {
109
+ addBundleTargets(targets, b.name, bundles, agent, 'global', cwd, contentDir);
110
+ }
111
+ // project-level: profile, sensors manifest, project injection, and every
112
+ // extension currently recorded in .awm/profile.json
113
+ const projectRoot = (0, profile_1.findProjectRoot)(cwd);
114
+ if (projectRoot) {
115
+ targets.add(path_1.default.join(projectRoot, '.awm', 'profile.json'));
116
+ targets.add(path_1.default.join(projectRoot, '.awm', 'sensors.json'));
117
+ if (injection?.type === 'config-instructions') {
118
+ targets.add(path_1.default.join(projectRoot, path_1.default.basename(injection.configPath)));
119
+ }
120
+ if (injection?.type === 'managed-agents-md') {
121
+ targets.add(path_1.default.join(projectRoot, path_1.default.basename(injection.localFile)));
122
+ }
123
+ let profile;
124
+ try {
125
+ profile = (0, profile_1.readProfile)(projectRoot);
126
+ }
127
+ catch {
128
+ profile = { extensions: [] };
129
+ }
130
+ for (const ext of profile.extensions) {
131
+ const extBundle = bundles.find((b) => b.name === ext);
132
+ if (!extBundle)
133
+ continue;
134
+ const scope = (0, bundles_1.defaultScopeForBundle)(extBundle.scope);
135
+ addBundleTargets(targets, ext, bundles, agent, scope, projectRoot, contentDir);
136
+ }
137
+ }
138
+ return Array.from(targets);
139
+ }