agentic-workflow-manager 3.0.1 → 3.2.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 (103) 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/export.js +48 -0
  6. package/dist/src/commands/hooks/claude.js +192 -0
  7. package/dist/src/commands/hooks/codex.js +191 -0
  8. package/dist/src/commands/hooks/index.js +23 -8
  9. package/dist/src/commands/hooks/install.js +11 -107
  10. package/dist/src/commands/hooks/resync.js +28 -18
  11. package/dist/src/commands/hooks/shared.js +94 -0
  12. package/dist/src/commands/hooks/status.js +13 -64
  13. package/dist/src/commands/hooks/uninstall.js +11 -47
  14. package/dist/src/commands/init.js +90 -28
  15. package/dist/src/commands/registry/index.js +1 -1
  16. package/dist/src/commands/sync.js +113 -0
  17. package/dist/src/commands/update.js +107 -0
  18. package/dist/src/core/agent-targets.js +50 -0
  19. package/dist/src/core/artifact-state.js +65 -0
  20. package/dist/src/core/atomic-file.js +72 -0
  21. package/dist/src/core/bundle-install.js +60 -38
  22. package/dist/src/core/bundles.js +21 -0
  23. package/dist/src/core/context/managed-block.js +190 -0
  24. package/dist/src/core/context/orchestrator.js +3 -1
  25. package/dist/src/core/context/project-constitution-inject.js +1 -1
  26. package/dist/src/core/context/regenerate.js +3 -3
  27. package/dist/src/core/context/strategies/codex-agents.js +100 -0
  28. package/dist/src/core/diagnostics/checks.js +16 -0
  29. package/dist/src/core/diagnostics/context.js +37 -8
  30. package/dist/src/core/diagnostics/provider-checks.js +219 -0
  31. package/dist/src/core/discovery.js +9 -3
  32. package/dist/src/core/executor.js +30 -9
  33. package/dist/src/core/export/index.js +51 -0
  34. package/dist/src/core/export/pack.js +58 -0
  35. package/dist/src/core/export/resolve.js +77 -0
  36. package/dist/src/core/export/transform.js +57 -0
  37. package/dist/src/core/export/types.js +2 -0
  38. package/dist/src/core/init/mutation-targets.js +139 -0
  39. package/dist/src/core/init/provider-facts.js +164 -0
  40. package/dist/src/core/init/steps.js +65 -9
  41. package/dist/src/core/install-planner.js +206 -0
  42. package/dist/src/core/install-transaction.js +405 -0
  43. package/dist/src/core/profile.js +1 -1
  44. package/dist/src/core/provider-artifacts.js +54 -0
  45. package/dist/src/core/provider-version.js +36 -0
  46. package/dist/src/core/reconciliation.js +61 -0
  47. package/dist/src/core/registries.js +10 -0
  48. package/dist/src/core/renderers/canonical-agent.js +27 -0
  49. package/dist/src/core/renderers/codex-agent.js +60 -0
  50. package/dist/src/core/skill-integrity.js +4 -3
  51. package/dist/src/index.js +99 -289
  52. package/dist/src/providers/index.js +143 -39
  53. package/dist/src/ui/provider-preflight.js +26 -0
  54. package/dist/src/utils/config.js +101 -9
  55. package/dist/tests/commands/agent.test.js +103 -0
  56. package/dist/tests/commands/backup.test.js +109 -0
  57. package/dist/tests/commands/doctor.test.js +65 -4
  58. package/dist/tests/commands/export.test.js +64 -0
  59. package/dist/tests/commands/hooks/codex.test.js +230 -0
  60. package/dist/tests/commands/hooks/install.test.js +20 -1
  61. package/dist/tests/commands/hooks/resync.test.js +53 -6
  62. package/dist/tests/commands/hooks/status.test.js +10 -0
  63. package/dist/tests/commands/hooks/uninstall.test.js +12 -0
  64. package/dist/tests/commands/init.test.js +195 -8
  65. package/dist/tests/commands/multi-agent-targeting.test.js +231 -0
  66. package/dist/tests/core/agent-targets.test.js +45 -0
  67. package/dist/tests/core/artifact-state.test.js +144 -0
  68. package/dist/tests/core/atomic-file.test.js +96 -0
  69. package/dist/tests/core/bundle-install.test.js +208 -17
  70. package/dist/tests/core/bundles.test.js +12 -0
  71. package/dist/tests/core/context/managed-block.test.js +90 -0
  72. package/dist/tests/core/context/orchestrator.test.js +48 -3
  73. package/dist/tests/core/context/regenerate.test.js +1 -1
  74. package/dist/tests/core/context/strategies/codex-agents.test.js +185 -0
  75. package/dist/tests/core/context/strategies/config-instructions.test.js +1 -1
  76. package/dist/tests/core/diagnostics/checks.test.js +19 -0
  77. package/dist/tests/core/diagnostics/context.test.js +74 -0
  78. package/dist/tests/core/diagnostics/provider-checks.test.js +252 -0
  79. package/dist/tests/core/executor.test.js +20 -0
  80. package/dist/tests/core/export/engine.test.js +104 -0
  81. package/dist/tests/core/export/pack.test.js +122 -0
  82. package/dist/tests/core/export/resolve.test.js +78 -0
  83. package/dist/tests/core/export/transform.test.js +74 -0
  84. package/dist/tests/core/init/mutation-targets.test.js +235 -0
  85. package/dist/tests/core/init/orchestrator.test.js +1 -1
  86. package/dist/tests/core/init/provider-facts.test.js +129 -0
  87. package/dist/tests/core/init/steps.test.js +106 -2
  88. package/dist/tests/core/install-planner.test.js +224 -0
  89. package/dist/tests/core/install-transaction.test.js +257 -0
  90. package/dist/tests/core/provider-artifacts.test.js +64 -0
  91. package/dist/tests/core/provider-version.test.js +53 -0
  92. package/dist/tests/core/reconciliation.test.js +165 -0
  93. package/dist/tests/core/registries-sync.test.js +4 -4
  94. package/dist/tests/core/renderers/canonical-agent.test.js +58 -0
  95. package/dist/tests/core/renderers/codex-agent.test.js +128 -0
  96. package/dist/tests/core/versioning.test.js +1 -1
  97. package/dist/tests/integration/codex-provider-isolated.test.js +206 -0
  98. package/dist/tests/providers/hooks-config.test.js +43 -25
  99. package/dist/tests/providers/index.test.js +144 -39
  100. package/dist/tests/structural/codex-agent-escaping-completeness.test.js +77 -0
  101. package/dist/tests/ui/provider-preflight.test.js +25 -0
  102. package/dist/tests/utils/config.test.js +178 -18
  103. package/package.json +1 -1
@@ -0,0 +1,2 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
@@ -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
+ }
@@ -0,0 +1,164 @@
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.gatherProviderFacts = gatherProviderFacts;
7
+ exports.assertClaudeBaselinePreserved = assertClaudeBaselinePreserved;
8
+ // src/core/init/provider-facts.ts
9
+ //
10
+ // R19's "Claude baseline is read-only" guarantee: when `awm init` targets a
11
+ // non-Claude agent (today: Codex), nothing this run does may change what's
12
+ // already managed for claude-code. `gatherProviderFacts` snapshots every path
13
+ // Claude's own init run would touch (hooks, skill/agent dirs, injection);
14
+ // `assertClaudeBaselinePreserved` compares a before/after pair and throws if
15
+ // anything moved. Directories are hashed by recursively walking the full tree
16
+ // and folding in every nested file's own content hash — this is an absolute
17
+ // safety invariant (R19), so a mutation nested arbitrarily deep under a
18
+ // managed directory (e.g. a `copy`-mode global install materializing a
19
+ // nested source tree) must still be detected, not just a top-level stat
20
+ // summary. These are small, provider-managed directories (skills/agents/
21
+ // hook scripts), never arbitrary user content, so full content hashing stays
22
+ // cheap.
23
+ const fs_1 = __importDefault(require("fs"));
24
+ const path_1 = __importDefault(require("path"));
25
+ const crypto_1 = __importDefault(require("crypto"));
26
+ const providers_1 = require("../../providers");
27
+ function hashFileContent(target) {
28
+ return crypto_1.default.createHash('sha256').update(fs_1.default.readFileSync(target)).digest('hex');
29
+ }
30
+ /**
31
+ * Recursively walks `dir`, producing a digest that changes if ANY nested
32
+ * file's content changes at ANY depth, or any entry is added/removed/renamed/
33
+ * changes kind (file/dir/symlink) — not just the top-level entries.
34
+ *
35
+ * `exclude` names absolute paths to omit entirely — not just their content,
36
+ * but their very presence as an entry — from the hash. This exists because
37
+ * Codex's hook scripts directory (`~/.awm/hooks/codex/`) is physically NESTED
38
+ * inside Claude's own hook scripts directory (`~/.awm/hooks/` —
39
+ * providers/index.ts), so a naive full recursive hash of Claude's scriptsDir
40
+ * would flag a REAL Codex hook install as "Claude's baseline changed" — first
41
+ * by content, and (if merely masked to a placeholder rather than fully
42
+ * dropped) still by the sibling directory's mere appearance/disappearance as
43
+ * an entry — either way making R19's guard permanently unsatisfiable for any
44
+ * Codex init that actually installs its hook (caught via Task 9's real
45
+ * end-to-end init test). Fully omitting another agent's own managed paths
46
+ * keeps the guard scoped to what it's meant to protect — content Claude
47
+ * itself owns — without weakening it for genuine Claude-owned changes.
48
+ */
49
+ function hashDirectoryTree(dir, exclude) {
50
+ let entries;
51
+ try {
52
+ entries = fs_1.default.readdirSync(dir).sort();
53
+ }
54
+ catch {
55
+ return 'unreadable';
56
+ }
57
+ const parts = entries
58
+ .filter((name) => !exclude.has(path_1.default.resolve(path_1.default.join(dir, name))))
59
+ .map((name) => {
60
+ const p = path_1.default.join(dir, name);
61
+ let s;
62
+ try {
63
+ s = fs_1.default.lstatSync(p);
64
+ }
65
+ catch {
66
+ return `${name}:missing`;
67
+ }
68
+ if (s.isSymbolicLink()) {
69
+ try {
70
+ return `${name}:l:${fs_1.default.readlinkSync(p)}`;
71
+ }
72
+ catch {
73
+ return `${name}:l:unreadable`;
74
+ }
75
+ }
76
+ if (s.isDirectory()) {
77
+ return `${name}:d:${hashDirectoryTree(p, exclude)}`;
78
+ }
79
+ try {
80
+ return `${name}:f:${hashFileContent(p)}`;
81
+ }
82
+ catch {
83
+ return `${name}:f:unreadable`;
84
+ }
85
+ });
86
+ return parts.join('|');
87
+ }
88
+ function hashPath(target, exclude) {
89
+ let stat;
90
+ try {
91
+ stat = fs_1.default.lstatSync(target);
92
+ }
93
+ catch {
94
+ return 'absent';
95
+ }
96
+ if (stat.isSymbolicLink()) {
97
+ return `symlink:${fs_1.default.readlinkSync(target)}`;
98
+ }
99
+ if (stat.isDirectory()) {
100
+ return `dir:${hashDirectoryTree(target, exclude)}`;
101
+ }
102
+ return `file:${hashFileContent(target)}`;
103
+ }
104
+ /** Paths that materially represent `agent`'s managed AWM state (hook, injection, artifact dirs). */
105
+ function providerManagedPaths(agent) {
106
+ const provider = (0, providers_1.providerFor)(agent);
107
+ const paths = new Set();
108
+ if (provider.hooks) {
109
+ paths.add(provider.hooks.settingsPath);
110
+ paths.add(provider.hooks.scriptsDir);
111
+ }
112
+ if (provider.injection) {
113
+ if (provider.injection.type === 'config-instructions')
114
+ paths.add(provider.injection.configPath);
115
+ if (provider.injection.type === 'managed-agents-md')
116
+ paths.add(provider.injection.globalPath);
117
+ }
118
+ if (provider.skill)
119
+ paths.add(provider.skill.global);
120
+ if (provider.workflow)
121
+ paths.add(provider.workflow.global);
122
+ if (provider.agent)
123
+ paths.add(provider.agent.global);
124
+ return Array.from(paths).sort();
125
+ }
126
+ /** Every OTHER agent's own managed paths, resolved — see hashDirectoryTree's docstring:
127
+ * these get excluded from `agent`'s recursive hash so a sibling agent's writes to a
128
+ * path nested inside `agent`'s own managed directory (Codex's scriptsDir under
129
+ * Claude's, today) don't register as a change to `agent`'s baseline. */
130
+ function otherAgentManagedPaths(agent) {
131
+ const out = new Set();
132
+ for (const other of providers_1.AGENT_TARGETS) {
133
+ if (other === agent)
134
+ continue;
135
+ for (const p of providerManagedPaths(other))
136
+ out.add(path_1.default.resolve(p));
137
+ }
138
+ return out;
139
+ }
140
+ /** Snapshots `agent`'s managed AWM state. Pure read — never writes. */
141
+ function gatherProviderFacts(agent) {
142
+ const paths = providerManagedPaths(agent);
143
+ const exclude = otherAgentManagedPaths(agent);
144
+ const hash = crypto_1.default.createHash('sha256');
145
+ for (const p of paths) {
146
+ hash.update(p).update('\0').update(hashPath(p, exclude)).update('\0');
147
+ }
148
+ return { agent, paths, hash: hash.digest('hex') };
149
+ }
150
+ /**
151
+ * Throws when `before`/`after` diverge — i.e. Claude's managed state changed
152
+ * during an init run that targeted a different agent. `before`/`after` must
153
+ * both be for the SAME agent (mismatched agents is a caller bug, not a
154
+ * baseline violation, so it throws a distinct message).
155
+ */
156
+ function assertClaudeBaselinePreserved(before, after) {
157
+ if (before.agent !== after.agent) {
158
+ throw new Error(`assertClaudeBaselinePreserved: mismatched agents (${before.agent} vs ${after.agent})`);
159
+ }
160
+ if (before.hash !== after.hash) {
161
+ throw new Error(`Claude Code baseline changed during a non-Claude init — this must never happen (R19). ` +
162
+ `Inspected: ${before.paths.join(', ')}`);
163
+ }
164
+ }
@@ -29,9 +29,11 @@ const context_1 = require("../diagnostics/context");
29
29
  const detector_1 = require("./detector");
30
30
  const orchestrator_1 = require("../context/orchestrator");
31
31
  const providers_1 = require("../../providers");
32
+ const install_planner_1 = require("../install-planner");
32
33
  const skill_integrity_1 = require("../skill-integrity");
33
34
  const registries_2 = require("../registries");
34
35
  const project_constitution_inject_1 = require("../context/project-constitution-inject");
36
+ const codex_agents_1 = require("../context/strategies/codex-agents");
35
37
  // ---------------------------------------------------------------------------
36
38
  // defaultActions — bridges the real functions to the InitActions interface
37
39
  // ---------------------------------------------------------------------------
@@ -43,6 +45,9 @@ exports.defaultActions = {
43
45
  registryRoot: o.registryRoot,
44
46
  installMethod: o.installMethod,
45
47
  }),
48
+ // No `applyPlan` override here — installBundle defaults to the real
49
+ // applyInstallPlan (install-transaction.ts, Task 6), so this materializes
50
+ // for real on the `awm init` code path.
46
51
  installBundle: (o) => (0, bundle_install_1.installBundle)({
47
52
  bundleName: o.bundleName,
48
53
  bundles: o.bundles,
@@ -51,6 +56,7 @@ exports.defaultActions = {
51
56
  projectRoot: o.projectRoot,
52
57
  contentDir: o.contentDir,
53
58
  }),
59
+ // Same as installBundle above — defaults to the real applyInstallPlan.
54
60
  syncProfile: (o) => (0, bundle_install_1.syncProfile)({
55
61
  projectRoot: o.projectRoot,
56
62
  bundles: o.bundles,
@@ -68,7 +74,12 @@ exports.defaultActions = {
68
74
  contextStatus: (op) => realInjectionOrchestrator.contextStatus(op),
69
75
  installContext: (op) => { realInjectionOrchestrator.installContext(op); },
70
76
  repairGlobalSkills: (skillsDir, registryContentDirs) => (0, skill_integrity_1.repairGlobalSkills)(skillsDir, registryContentDirs),
71
- injectProjectConstitution: (o) => (0, project_constitution_inject_1.injectProjectConstitution)(o.projectRoot, o.agent),
77
+ injectProjectConstitution: (o) => {
78
+ if ((0, providers_1.getInjection)(o.agent)?.type === 'managed-agents-md') {
79
+ return new codex_agents_1.CodexAgentsStrategy().injectProject(o.projectRoot) === 'injected' ? 'injected' : 'already';
80
+ }
81
+ return (0, project_constitution_inject_1.injectProjectConstitution)(o.projectRoot, o.agent);
82
+ },
72
83
  };
73
84
  // ---------------------------------------------------------------------------
74
85
  // Step helpers
@@ -82,6 +93,42 @@ function failed(id, level, error) {
82
93
  // ---------------------------------------------------------------------------
83
94
  // Machine-level steps
84
95
  // ---------------------------------------------------------------------------
96
+ /**
97
+ * Agents to pass as `agents` when auto-installing a global-scope bundle for
98
+ * `d.agent`: `d.agent` itself, plus every other currently-enabled agent that
99
+ * shares `d.agent`'s physical skill directory (today: OpenCode and Codex
100
+ * both resolve to `~/.agents/skills`, see providers/index.ts). Baseline/
101
+ * ambient bundles always install at 'global' scope (bundles.ts's
102
+ * `defaultScopeForBundle` — 'baseline'/'ambient' never map to 'local'), so
103
+ * scope is fixed here rather than threaded through InitDeps.
104
+ *
105
+ * Without this, a `[d.agent]` singleton would make install-planner.ts's
106
+ * `assertCompleteSharedGroup` (R14) refuse the install outright whenever a
107
+ * co-owner is independently enabled (e.g. `awm init --agent codex` on a
108
+ * machine with OpenCode already enabled) — that was a BLOCKER: it broke the
109
+ * exact "multiple providers coexist on one machine" scenario this plan
110
+ * exists to deliver. Including the co-owner here is safe: it already has
111
+ * this exact skill installed from its own prior init (same source, same
112
+ * target), so `planInstall`'s dedup (R15/R15.1) collapses it into the same
113
+ * physical operation and simply re-confirms ownership — it does not change
114
+ * the co-owner's installed content.
115
+ */
116
+ function sharedInstallAgents(d) {
117
+ const group = (0, install_planner_1.agentsSharingSkillTarget)(d.agent, d.enabledAgents, 'global', d.cwd);
118
+ return group.includes(d.agent) ? group : [d.agent, ...group];
119
+ }
120
+ /**
121
+ * Same reasoning as `sharedInstallAgents`, but for LOCAL-scope project
122
+ * extension bundles (`stepActivation`'s `syncProfile` call). OpenCode and
123
+ * Codex share both their global AND local skill directories
124
+ * (`providers/index.ts`), so a project with a skill-bearing extension hits
125
+ * the identical R14 refusal `sharedInstallAgents` was added to avoid — just
126
+ * scoped to `proj.root` instead of the machine-global directory.
127
+ */
128
+ function sharedActivationAgents(d, projectRoot) {
129
+ const group = (0, install_planner_1.agentsSharingSkillTarget)(d.agent, d.enabledAgents, 'local', projectRoot);
130
+ return group.includes(d.agent) ? group : [d.agent, ...group];
131
+ }
85
132
  /** Step 1 – Sync the registry cache (clone / pull). */
86
133
  async function stepCache(d) {
87
134
  const { registryCache } = d.ctx.machine;
@@ -99,6 +146,16 @@ async function stepCache(d) {
99
146
  }
100
147
  /** Step 2 – Install the session-start hook for the target agent. */
101
148
  function stepHook(d) {
149
+ // Not every agent has a hook mechanism (today: OpenCode, Antigravity —
150
+ // providers/index.ts's `hooks` is optional). Mirrors provider-checks.ts's
151
+ // `hookTrustCheck`, which treats a missing `hooks` config as "not
152
+ // applicable" rather than a failure. Without this guard, a real
153
+ // (unstubbed) `awm init --agent opencode` throws "hooks not supported for
154
+ // agent target: opencode" from `installHook` — a real, previously-latent
155
+ // bug caught while building this plan's real end-to-end init test
156
+ // (tests/integration/codex-provider-isolated.test.ts).
157
+ if (!(0, providers_1.providerFor)(d.agent).hooks)
158
+ return ok('machine.hook', 'machine', 'skipped', 'no hook mechanism for this agent');
102
159
  const { hook } = d.ctx.machine;
103
160
  if (hook.present && !hook.degraded)
104
161
  return ok('machine.hook', 'machine', 'skipped');
@@ -121,7 +178,7 @@ function stepDevCore(d) {
121
178
  d.actions.installBundle({
122
179
  bundleName,
123
180
  bundles: d.bundles,
124
- agents: [d.agent],
181
+ agents: sharedInstallAgents(d),
125
182
  method: d.installMethod,
126
183
  projectRoot: d.cwd,
127
184
  contentDir: d.contentDir,
@@ -134,7 +191,7 @@ function stepGlobalSkillsRepair(d) {
134
191
  const broken = globalSkills.repairable.length + globalSkills.dead.length;
135
192
  if (broken === 0)
136
193
  return ok('machine.globalSkills', 'machine', 'skipped');
137
- const skillsDir = providers_1.PROVIDERS[d.agent].skill.global;
194
+ const skillsDir = (0, providers_1.providerFor)(d.agent).skill.global;
138
195
  const r = d.actions.repairGlobalSkills(skillsDir, (0, registries_2.contentRoots)());
139
196
  return ok('machine.globalSkills', 'machine', 'applied', `re-linked ${r.relinked.length}, pruned ${r.pruned.length}`);
140
197
  }
@@ -150,7 +207,7 @@ function stepAmbient(d) {
150
207
  d.actions.installBundle({
151
208
  bundleName,
152
209
  bundles: d.bundles,
153
- agents: [d.agent],
210
+ agents: sharedInstallAgents(d),
154
211
  method: d.installMethod,
155
212
  projectRoot: d.cwd,
156
213
  contentDir: d.contentDir,
@@ -206,7 +263,7 @@ function stepActivation(d) {
206
263
  d.actions.syncProfile({
207
264
  projectRoot: proj.root,
208
265
  bundles: d.bundles,
209
- agents: [d.agent],
266
+ agents: sharedActivationAgents(d, proj.root),
210
267
  method: d.installMethod,
211
268
  contentDir: d.contentDir,
212
269
  });
@@ -231,17 +288,16 @@ function stepConstitution(d) {
231
288
  return ok('project.constitution', 'project', 'skipped');
232
289
  return ok('project.constitution', 'project', 'pending', 'skill: project-constitution');
233
290
  }
234
- /** Step 8b – Entregar CONSTITUTION.md a agentes con inyección config-instructions
235
- * (opencode) vía un opencode.json local del proyecto. Claude lo recibe por el hook. */
291
+ /** Step 8b – Deliver project guidance via the provider-specific context mechanism. */
236
292
  function stepConstitutionInjection(d) {
237
293
  const proj = d.ctx.project;
238
294
  if (!proj)
239
295
  return ok('project.constitutionInjection', 'project', 'skipped', 'no project');
240
296
  const inj = (0, providers_1.getInjection)(d.agent);
241
- if (!inj || inj.type !== 'config-instructions') {
297
+ if (!inj || inj.type === 'cc-settings-merge') {
242
298
  return ok('project.constitutionInjection', 'project', 'skipped', 'covered by hook');
243
299
  }
244
- if (!proj.constitution.present) {
300
+ if (inj.type === 'config-instructions' && !proj.constitution.present) {
245
301
  return ok('project.constitutionInjection', 'project', 'skipped', 'no CONSTITUTION.md');
246
302
  }
247
303
  const res = d.actions.injectProjectConstitution({ projectRoot: proj.root, agent: d.agent });
@@ -0,0 +1,206 @@
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.physicalTarget = physicalTarget;
7
+ exports.skillTargetDir = skillTargetDir;
8
+ exports.agentsSharingSkillTarget = agentsSharingSkillTarget;
9
+ exports.planInstall = planInstall;
10
+ exports.planRemoval = planRemoval;
11
+ // src/core/install-planner.ts
12
+ //
13
+ // Pure planning layer for artifact install/removal: no filesystem writes.
14
+ // Given artifact intents and a set of agent targets, computes the physical
15
+ // operations to perform, deduping targets that multiple agents' provider
16
+ // configs happen to resolve to the same directory (e.g. OpenCode and Codex
17
+ // both point their `skill` config at ~/.agents/skills), and tracks ownership
18
+ // so a later removal can tell whether a shared target is still needed by
19
+ // another agent before deleting it.
20
+ //
21
+ // Task 6 (applyInstallPlan) consumes InstallPlan/RemovalPlan to perform the
22
+ // actual transactional filesystem writes, backups, and ManagedArtifactRecord
23
+ // persistence (artifact-state.ts). This module only computes the plan.
24
+ const path_1 = __importDefault(require("path"));
25
+ const providers_1 = require("../providers");
26
+ /** Resolves the single physical filesystem location an intent renders to for one agent. */
27
+ /**
28
+ * Resolves the physical target path + renderer for one artifact intent on one
29
+ * agent (dir + filename, applying the `.toml` rename for `codex-agent-toml`).
30
+ * Shared with `core/init/mutation-targets.ts`, which needs the exact same
31
+ * dir/filename computation to enumerate paths before a real `awm init` run —
32
+ * duplicating this logic there would let the two silently diverge.
33
+ */
34
+ function physicalTarget(intent, agent, scope, projectRoot) {
35
+ const config = (0, providers_1.providerFor)(agent)[intent.type];
36
+ if (!config)
37
+ throw new Error(`${intent.type}s are not supported by ${(0, providers_1.providerFor)(agent).label}`);
38
+ const dir = scope === 'local' ? path_1.default.join(projectRoot, config.local) : config.global;
39
+ const filename = config.renderer === 'codex-agent-toml'
40
+ ? `${path_1.default.parse(intent.installName).name}.toml`
41
+ : intent.installName;
42
+ return { targetPath: path_1.default.join(dir, filename), renderer: config.renderer };
43
+ }
44
+ /**
45
+ * The physical directory `agent`'s skill artifacts resolve to at `scope` —
46
+ * independent of any specific artifact/intent, since every skill intent for
47
+ * a given agent+scope shares the same directory and only the filename
48
+ * inside it varies (`physicalTarget`'s `dir` component, without the
49
+ * filename). Exposed so callers that need to answer "does agent X share
50
+ * agent Y's skill target?" purely structurally — e.g. deciding which agents
51
+ * to proactively include in a `selectedAgents` set — don't need to invent a
52
+ * throwaway `ArtifactIntent` just to ask a directory-equality question.
53
+ */
54
+ function skillTargetDir(agent, scope, projectRoot) {
55
+ const config = (0, providers_1.providerFor)(agent).skill;
56
+ return scope === 'local' ? path_1.default.join(projectRoot, config.local) : config.global;
57
+ }
58
+ /**
59
+ * Of `candidates`, the ones that share `agent`'s skill physical target at
60
+ * `scope` (includes `agent` itself when it appears in `candidates`). Used by
61
+ * `core/init/steps.ts` to compute the correct `selectedAgents` for an
62
+ * automatic baseline/ambient bundle install: `stepDevCore`/`stepAmbient`
63
+ * used to pass a `[agent]` singleton, which `assertCompleteSharedGroup`
64
+ * below (R14) then refused whenever a co-owner (e.g. OpenCode alongside
65
+ * Codex) was independently enabled — a BLOCKER that made `awm init --agent
66
+ * codex` structurally fail once OpenCode was already enabled, and vice
67
+ * versa. Passing the complete shared group up front avoids tripping R14 in
68
+ * the first place, without weakening the assertion itself.
69
+ */
70
+ function agentsSharingSkillTarget(agent, candidates, scope, projectRoot) {
71
+ const target = skillTargetDir(agent, scope, projectRoot);
72
+ return candidates.filter((candidate) => skillTargetDir(candidate, scope, projectRoot) === target);
73
+ }
74
+ /**
75
+ * Skills are, today, the only artifact type where two agents' provider
76
+ * configs physically resolve to the exact same directory (OpenCode and Codex
77
+ * both use `~/.agents/skills` globally). Because that target is shared,
78
+ * selecting only part of the group sharing it would either strand the
79
+ * unselected agent's install or silently delete on its behalf. Refuse the
80
+ * whole change instead (R14): every enabled agent that shares a given
81
+ * skill's physical target with a selected agent must itself be selected.
82
+ */
83
+ function assertCompleteSharedGroup(intent, selected, enabled, scope, projectRoot) {
84
+ if (intent.type !== 'skill')
85
+ return;
86
+ for (const agent of selected) {
87
+ const target = physicalTarget(intent, agent, scope, projectRoot).targetPath;
88
+ const group = enabled.filter((candidate) => physicalTarget(intent, candidate, scope, projectRoot).targetPath === target);
89
+ if (group.some((candidate) => !selected.includes(candidate))) {
90
+ throw new Error(`Shared skill target cannot diverge; select the complete shared target group: ${group.join(',')}`);
91
+ }
92
+ }
93
+ }
94
+ /**
95
+ * Plans installation of `artifacts` for `selectedAgents`. Pure — no
96
+ * filesystem access. Groups by physical location (targetPath + renderer) —
97
+ * NOT sourcePath: a target path is one filesystem location, so at most one
98
+ * source may back it. When two selected agents resolve an intent to the
99
+ * identical physical location with the SAME sourcePath, they collapse into a
100
+ * single PlannedOperation with both agents listed as owners (R15/R15.1), so
101
+ * the operation is only ever performed once. When two intents resolve to the
102
+ * same physical location with DIFFERENT sourcePaths (e.g. same skill name
103
+ * shipped by two different registries), that is a genuine conflict — one
104
+ * target can't be backed by two different sources — and planInstall throws
105
+ * before producing any operations, in the same "abort before writes" spirit
106
+ * as assertCompleteSharedGroup's shared-group error. Independently addressed
107
+ * artifact types (workflow, agent) never collapse across agents, since each
108
+ * agent's provider config points at its own directory (R12/R13). An artifact
109
+ * type unsupported by a given agent (provider config is null for that type)
110
+ * is silently skipped for that agent, mirroring the legacy installBundle
111
+ * skip semantics — it is not an error.
112
+ *
113
+ * Within a shared group, the first owner (in selection order) is reported
114
+ * with action 'install' (it is the one that causes the operation) and any
115
+ * additional co-owners are reported 'retain' (the write already covers
116
+ * them; they simply gain ownership of it).
117
+ */
118
+ function planInstall(params) {
119
+ const { artifacts, selectedAgents, enabledAgents, scope, projectRoot, method } = params;
120
+ for (const intent of artifacts) {
121
+ assertCompleteSharedGroup(intent, selectedAgents, enabledAgents, scope, projectRoot);
122
+ }
123
+ const groups = new Map();
124
+ for (const intent of artifacts) {
125
+ for (const agent of selectedAgents) {
126
+ const config = (0, providers_1.providerFor)(agent)[intent.type];
127
+ if (!config)
128
+ continue;
129
+ const { targetPath, renderer } = physicalTarget(intent, agent, scope, projectRoot);
130
+ // Key on the physical location alone (targetPath + renderer), NOT
131
+ // sourcePath: a target path is one filesystem location, so it can
132
+ // only ever be backed by one source. Including sourcePath in the
133
+ // key would let two intents with different sources but the same
134
+ // target silently produce two operations for the same physical
135
+ // location — see the conflict check just below.
136
+ const key = `${targetPath}\0${renderer}`;
137
+ let group = groups.get(key);
138
+ if (!group) {
139
+ group = {
140
+ name: intent.name,
141
+ type: intent.type,
142
+ sourcePath: intent.sourcePath,
143
+ targetPath,
144
+ renderer,
145
+ owners: [],
146
+ };
147
+ groups.set(key, group);
148
+ }
149
+ else if (group.sourcePath !== intent.sourcePath) {
150
+ throw new Error(`physical target already claimed by a different source: ${targetPath} (${group.sourcePath} vs ${intent.sourcePath})`);
151
+ }
152
+ if (!group.owners.includes(agent))
153
+ group.owners.push(agent);
154
+ }
155
+ }
156
+ const operations = [];
157
+ const records = [];
158
+ const reports = [];
159
+ for (const group of groups.values()) {
160
+ const record = {
161
+ name: group.name,
162
+ type: group.type,
163
+ scope,
164
+ targetPath: group.targetPath,
165
+ sourcePath: group.sourcePath,
166
+ renderer: group.renderer,
167
+ owners: group.owners,
168
+ };
169
+ records.push(record);
170
+ operations.push({ ...record, method, output: group.renderer });
171
+ group.owners.forEach((owner, index) => {
172
+ reports.push({ owner, targetPath: group.targetPath, action: index === 0 ? 'install' : 'retain' });
173
+ });
174
+ }
175
+ return { operations, records, reports };
176
+ }
177
+ /**
178
+ * Plans removal of `artifactNames` for `selectedAgents` against the
179
+ * currently persisted `records`. For each matching record, owners being
180
+ * removed drop out of its owner list; if any remaining owner is still in
181
+ * `enabledAgents`, the physical target is retained (R16) — no unlink is
182
+ * produced, and the record is carried forward with the reduced owner list.
183
+ * Only when no enabled owner remains does the record become an unlink
184
+ * operation, dropping out of the returned records. Records for artifacts not
185
+ * named in `artifactNames` pass through unchanged.
186
+ */
187
+ function planRemoval(params) {
188
+ const { records, selectedAgents, enabledAgents, artifactNames } = params;
189
+ const operations = [];
190
+ const resultRecords = [];
191
+ for (const record of records) {
192
+ if (!artifactNames.includes(record.name)) {
193
+ resultRecords.push(record);
194
+ continue;
195
+ }
196
+ const remainingOwners = record.owners.filter((owner) => !selectedAgents.includes(owner));
197
+ const activeOwners = remainingOwners.filter((owner) => enabledAgents.includes(owner));
198
+ if (activeOwners.length > 0) {
199
+ resultRecords.push({ ...record, owners: remainingOwners });
200
+ }
201
+ else {
202
+ operations.push({ ...record, owners: remainingOwners, action: 'unlink' });
203
+ }
204
+ }
205
+ return { operations, records: resultRecords };
206
+ }