agentic-workflow-manager 3.1.0 → 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.
- package/dist/src/commands/add.js +84 -0
- package/dist/src/commands/agent.js +95 -0
- package/dist/src/commands/backup.js +46 -0
- package/dist/src/commands/doctor.js +80 -4
- package/dist/src/commands/hooks/claude.js +192 -0
- package/dist/src/commands/hooks/codex.js +191 -0
- package/dist/src/commands/hooks/index.js +23 -8
- package/dist/src/commands/hooks/install.js +11 -107
- package/dist/src/commands/hooks/resync.js +28 -18
- package/dist/src/commands/hooks/shared.js +94 -0
- package/dist/src/commands/hooks/status.js +13 -64
- package/dist/src/commands/hooks/uninstall.js +11 -47
- package/dist/src/commands/init.js +90 -28
- package/dist/src/commands/registry/index.js +1 -1
- package/dist/src/commands/sync.js +113 -0
- package/dist/src/commands/update.js +107 -0
- package/dist/src/core/agent-targets.js +50 -0
- package/dist/src/core/artifact-state.js +65 -0
- package/dist/src/core/atomic-file.js +72 -0
- package/dist/src/core/bundle-install.js +60 -38
- package/dist/src/core/bundles.js +21 -0
- package/dist/src/core/context/managed-block.js +190 -0
- package/dist/src/core/context/orchestrator.js +3 -1
- package/dist/src/core/context/project-constitution-inject.js +1 -1
- package/dist/src/core/context/regenerate.js +3 -3
- package/dist/src/core/context/strategies/codex-agents.js +100 -0
- package/dist/src/core/diagnostics/checks.js +16 -0
- package/dist/src/core/diagnostics/context.js +37 -8
- package/dist/src/core/diagnostics/provider-checks.js +219 -0
- package/dist/src/core/executor.js +30 -9
- package/dist/src/core/init/mutation-targets.js +139 -0
- package/dist/src/core/init/provider-facts.js +164 -0
- package/dist/src/core/init/steps.js +65 -9
- package/dist/src/core/install-planner.js +206 -0
- package/dist/src/core/install-transaction.js +405 -0
- package/dist/src/core/profile.js +1 -1
- package/dist/src/core/provider-artifacts.js +54 -0
- package/dist/src/core/provider-version.js +36 -0
- package/dist/src/core/reconciliation.js +61 -0
- package/dist/src/core/registries.js +10 -0
- package/dist/src/core/renderers/canonical-agent.js +27 -0
- package/dist/src/core/renderers/codex-agent.js +60 -0
- package/dist/src/core/skill-integrity.js +4 -3
- package/dist/src/index.js +97 -289
- package/dist/src/providers/index.js +143 -39
- package/dist/src/ui/provider-preflight.js +26 -0
- package/dist/src/utils/config.js +101 -9
- package/dist/tests/commands/agent.test.js +103 -0
- package/dist/tests/commands/backup.test.js +109 -0
- package/dist/tests/commands/doctor.test.js +65 -4
- package/dist/tests/commands/hooks/codex.test.js +230 -0
- package/dist/tests/commands/hooks/install.test.js +20 -1
- package/dist/tests/commands/hooks/resync.test.js +53 -6
- package/dist/tests/commands/hooks/status.test.js +10 -0
- package/dist/tests/commands/hooks/uninstall.test.js +12 -0
- package/dist/tests/commands/init.test.js +195 -8
- package/dist/tests/commands/multi-agent-targeting.test.js +231 -0
- package/dist/tests/core/agent-targets.test.js +45 -0
- package/dist/tests/core/artifact-state.test.js +144 -0
- package/dist/tests/core/atomic-file.test.js +96 -0
- package/dist/tests/core/bundle-install.test.js +208 -17
- package/dist/tests/core/bundles.test.js +12 -0
- package/dist/tests/core/context/managed-block.test.js +90 -0
- package/dist/tests/core/context/orchestrator.test.js +48 -3
- package/dist/tests/core/context/regenerate.test.js +1 -1
- package/dist/tests/core/context/strategies/codex-agents.test.js +185 -0
- package/dist/tests/core/context/strategies/config-instructions.test.js +1 -1
- package/dist/tests/core/diagnostics/checks.test.js +19 -0
- package/dist/tests/core/diagnostics/context.test.js +74 -0
- package/dist/tests/core/diagnostics/provider-checks.test.js +252 -0
- package/dist/tests/core/executor.test.js +20 -0
- package/dist/tests/core/export/engine.test.js +3 -2
- package/dist/tests/core/init/mutation-targets.test.js +235 -0
- package/dist/tests/core/init/orchestrator.test.js +1 -1
- package/dist/tests/core/init/provider-facts.test.js +129 -0
- package/dist/tests/core/init/steps.test.js +106 -2
- package/dist/tests/core/install-planner.test.js +224 -0
- package/dist/tests/core/install-transaction.test.js +257 -0
- package/dist/tests/core/provider-artifacts.test.js +64 -0
- package/dist/tests/core/provider-version.test.js +53 -0
- package/dist/tests/core/reconciliation.test.js +165 -0
- package/dist/tests/core/registries-sync.test.js +4 -4
- package/dist/tests/core/renderers/canonical-agent.test.js +58 -0
- package/dist/tests/core/renderers/codex-agent.test.js +128 -0
- package/dist/tests/core/versioning.test.js +1 -1
- package/dist/tests/integration/codex-provider-isolated.test.js +206 -0
- package/dist/tests/providers/hooks-config.test.js +43 -25
- package/dist/tests/providers/index.test.js +144 -39
- package/dist/tests/structural/codex-agent-escaping-completeness.test.js +77 -0
- package/dist/tests/ui/provider-preflight.test.js +25 -0
- package/dist/tests/utils/config.test.js +178 -18
- package/package.json +1 -1
|
@@ -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) =>
|
|
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:
|
|
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.
|
|
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:
|
|
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:
|
|
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 –
|
|
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
|
|
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
|
+
}
|