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
@@ -0,0 +1,405 @@
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.applyInstallPlan = applyInstallPlan;
7
+ exports.defaultTransactionDeps = defaultTransactionDeps;
8
+ exports.beginBackupSession = beginBackupSession;
9
+ exports.restoreBackup = restoreBackup;
10
+ exports.listBackups = listBackups;
11
+ // src/core/install-transaction.ts
12
+ //
13
+ // Transactional filesystem apply for InstallPlan (install-planner.ts):
14
+ // validates every target, backs up every existing target BEFORE any
15
+ // replacement happens (R24.1), replaces targets, verifies, and — on any
16
+ // failure — rolls back every target already replaced, in reverse order
17
+ // (R25). Never removes a live target before its staged replacement exists
18
+ // (R17, enforced by executor.ts's stageArtifact/replaceArtifact split).
19
+ //
20
+ // Also exports a general-purpose `beginBackupSession`, for callers outside
21
+ // the planner (e.g. preferences/provider-config mutations during `init`)
22
+ // that want the same backup/restore guarantees without going through a full
23
+ // InstallPlan. `applyInstallPlan` and `beginBackupSession` write independent
24
+ // backup directories under ~/.awm/backups/<transactionId>/, each with a
25
+ // manifest.json (mode 0600) inside a 0700 directory — paths and existence
26
+ // flags only, never file contents or environment variables.
27
+ const fs_1 = __importDefault(require("fs"));
28
+ const path_1 = __importDefault(require("path"));
29
+ const crypto_1 = __importDefault(require("crypto"));
30
+ const artifact_state_1 = require("./artifact-state");
31
+ const paths_1 = require("./paths");
32
+ const atomic_file_1 = require("./atomic-file");
33
+ const codex_agent_1 = require("./renderers/codex-agent");
34
+ const executor_1 = require("./executor");
35
+ /**
36
+ * The single timestamp-sanitization rule for transaction IDs, shared by
37
+ * `applyInstallPlan` and `beginBackupSession` so both always produce IDs
38
+ * `restoreBackup`'s validation regex (`^\d{4}-\d{2}-\d{2}T[0-9A-Za-z.-]+$`)
39
+ * accepts. Strips both `:` and `.` (an ISO timestamp has both) so the result
40
+ * is filesystem- and regex-safe either way.
41
+ */
42
+ function sanitizeTransactionTimestamp() {
43
+ return new Date().toISOString().replace(/[:.]/g, '-');
44
+ }
45
+ function ensureBackupDir(backupDir) {
46
+ fs_1.default.mkdirSync(backupDir, { recursive: true });
47
+ fs_1.default.chmodSync(backupDir, 0o700);
48
+ }
49
+ function writeBackupManifest(backupDir, manifest) {
50
+ ensureBackupDir(backupDir);
51
+ (0, atomic_file_1.writeFileAtomic)(path_1.default.join(backupDir, 'manifest.json'), JSON.stringify(manifest, null, 2) + '\n', 0o600);
52
+ }
53
+ function readBackupManifest(manifestPath) {
54
+ if (!fs_1.default.existsSync(manifestPath)) {
55
+ throw new Error(`backup manifest not found: ${manifestPath}`);
56
+ }
57
+ let value;
58
+ try {
59
+ value = JSON.parse(fs_1.default.readFileSync(manifestPath, 'utf8'));
60
+ }
61
+ catch {
62
+ throw new Error(`${manifestPath} is not valid JSON`);
63
+ }
64
+ const manifest = value;
65
+ if (!manifest || typeof manifest.id !== 'string' || !Array.isArray(manifest.entries)) {
66
+ throw new Error(`${manifestPath} is not a valid backup manifest`);
67
+ }
68
+ return manifest;
69
+ }
70
+ function targetExists(targetPath) {
71
+ try {
72
+ fs_1.default.lstatSync(targetPath);
73
+ return true;
74
+ }
75
+ catch {
76
+ return false;
77
+ }
78
+ }
79
+ /**
80
+ * sha256 of the target's content, for the manifest's integrity record.
81
+ * Files hash their bytes directly; directories and symlinks hash a stable
82
+ * summary (entry name + kind + size + mtime, or the symlink's own target)
83
+ * rather than walking and hashing every file individually — enough to detect
84
+ * "the backup no longer matches what's on disk" without the cost of a full
85
+ * recursive content hash.
86
+ */
87
+ function computeContentHash(targetPath) {
88
+ const hash = crypto_1.default.createHash('sha256');
89
+ const stat = fs_1.default.lstatSync(targetPath);
90
+ if (stat.isSymbolicLink()) {
91
+ hash.update('symlink:').update(fs_1.default.readlinkSync(targetPath));
92
+ }
93
+ else if (stat.isDirectory()) {
94
+ hash.update('dir:');
95
+ for (const name of fs_1.default.readdirSync(targetPath).sort()) {
96
+ const entryStat = fs_1.default.lstatSync(path_1.default.join(targetPath, name));
97
+ const kind = entryStat.isDirectory() ? 'd' : entryStat.isSymbolicLink() ? 'l' : 'f';
98
+ hash.update(`${name}:${kind}:${entryStat.size}:${entryStat.mtimeMs}\n`);
99
+ }
100
+ }
101
+ else {
102
+ hash.update(fs_1.default.readFileSync(targetPath));
103
+ }
104
+ return hash.digest('hex');
105
+ }
106
+ /** Copies `targetPath` (if it exists) into `backupDir/relName` and returns the manifest entry. */
107
+ function backupEntryFor(targetPath, backupDir, relName) {
108
+ if (!targetExists(targetPath)) {
109
+ return { targetPath, existed: false, backupRelPath: null };
110
+ }
111
+ ensureBackupDir(backupDir);
112
+ // dereference:false (fs.cpSync default) — a symlinked target is backed up
113
+ // as a symlink, not by copying whatever it points at.
114
+ fs_1.default.cpSync(targetPath, path_1.default.join(backupDir, relName), { recursive: true });
115
+ return { targetPath, existed: true, backupRelPath: relName, contentHash: computeContentHash(targetPath) };
116
+ }
117
+ /** Restores `targetPath` from its backup, or removes it if it didn't exist before. */
118
+ function restoreTargetFrom(targetPath, backupPath) {
119
+ fs_1.default.rmSync(targetPath, { recursive: true, force: true });
120
+ if (backupPath !== null) {
121
+ fs_1.default.mkdirSync(path_1.default.dirname(targetPath), { recursive: true });
122
+ fs_1.default.cpSync(backupPath, targetPath, { recursive: true });
123
+ }
124
+ }
125
+ function restoreManifestEntry(backupDir, entry) {
126
+ // Only ever operates on targetPath/backupRelPath literally enumerated in
127
+ // the validated manifest — never on a path derived from user input.
128
+ const backupPath = entry.existed && entry.backupRelPath ? path_1.default.join(backupDir, entry.backupRelPath) : null;
129
+ restoreTargetFrom(entry.targetPath, backupPath);
130
+ }
131
+ // ---------------------------------------------------------------------------
132
+ // applyInstallPlan
133
+ // ---------------------------------------------------------------------------
134
+ /**
135
+ * Real transactional apply for an InstallPlan. Order (per target, across the
136
+ * whole plan):
137
+ * 1. validate every op (no writes)
138
+ * 2. backup every op's existing target (before ANY replace — R24.1)
139
+ * 3. stage every op's replacement (source untouched, target untouched — R17)
140
+ * 4. replace every op's target with its staged replacement
141
+ * 5. verify every op
142
+ * 6. persist artifact-state records — merged into the existing ledger
143
+ * (upsert by targetPath, see mergeArtifactRecords), not a wholesale
144
+ * overwrite, so earlier applyInstallPlan calls' records survive
145
+ * Any failure from step 4 onward rolls back every already-replaced target, in
146
+ * reverse order, using the backups captured in step 2 (R25). Rollback is
147
+ * best-effort: a rollback failure for one target does not stop the others
148
+ * from being attempted, and never masks the original failure.
149
+ */
150
+ function applyInstallPlan(plan, deps = defaultTransactionDeps()) {
151
+ for (const op of plan.operations)
152
+ deps.validate(op);
153
+ const backupDir = path_1.default.join((0, paths_1.awmHome)(), 'backups', sanitizeTransactionTimestamp());
154
+ const transactionId = path_1.default.basename(backupDir);
155
+ const backups = new Map();
156
+ for (const op of plan.operations)
157
+ backups.set(op, deps.backup(op, backupDir));
158
+ const staged = new Map();
159
+ try {
160
+ for (const op of plan.operations)
161
+ staged.set(op, deps.stage(op));
162
+ }
163
+ catch (error) {
164
+ // Best-effort: clean up any temp files earlier iterations already
165
+ // staged before this one threw — none of them have been swapped in
166
+ // yet (replace() hasn't run), so there's nothing to roll back, just
167
+ // orphaned staging artifacts to remove.
168
+ for (const stagedPath of staged.values()) {
169
+ try {
170
+ fs_1.default.rmSync(stagedPath, { recursive: true, force: true });
171
+ }
172
+ catch {
173
+ // best-effort: retain the original failure
174
+ }
175
+ }
176
+ throw error;
177
+ }
178
+ const replaced = [];
179
+ try {
180
+ for (const op of plan.operations) {
181
+ deps.replace(op, staged.get(op));
182
+ replaced.push(op);
183
+ }
184
+ for (const op of plan.operations)
185
+ deps.verify(op);
186
+ // Upsert this plan's records into the EXISTING persisted ledger,
187
+ // keyed by targetPath — a real `awm init` run makes several separate
188
+ // applyInstallPlan calls (one per bundle), and each one only knows
189
+ // about the artifacts IT touched. Writing plan.records wholesale
190
+ // would silently discard every earlier call's ownership records
191
+ // (BLOCKER found in post-implementation QA); merging preserves them.
192
+ (0, artifact_state_1.writeArtifactState)((0, artifact_state_1.mergeArtifactRecords)((0, artifact_state_1.readArtifactState)(), plan.records));
193
+ }
194
+ catch (error) {
195
+ for (const op of [...replaced].reverse()) {
196
+ try {
197
+ deps.rollback(op, backups.get(op) ?? null);
198
+ }
199
+ catch {
200
+ // best-effort: retain the original failure; the backup path remains available for manual recovery
201
+ }
202
+ }
203
+ throw error;
204
+ }
205
+ return {
206
+ // Every owner in plan.reports — 'install' (the owner whose selection
207
+ // caused the physical write) AND 'retain' (co-owners of the same
208
+ // shared target, e.g. OpenCode+Codex sharing one skill directory) —
209
+ // gained ownership of this target as a result of THIS run. The
210
+ // user-facing summary must list all of them, not just the first
211
+ // (R15/R15.1): a single physical write can still mean N providers now
212
+ // own the artifact.
213
+ installed: plan.reports
214
+ .map((report) => `${path_1.default.basename(report.targetPath)} → ${report.owner}`),
215
+ skipped: [],
216
+ transactionId,
217
+ modifiedFiles: plan.operations.map((op) => op.targetPath),
218
+ };
219
+ }
220
+ function stageRenderedFile(content, targetPath) {
221
+ const parent = path_1.default.dirname(targetPath);
222
+ fs_1.default.mkdirSync(parent, { recursive: true });
223
+ const staged = path_1.default.join(parent, `.${path_1.default.basename(targetPath)}.${process.pid}.staged`);
224
+ fs_1.default.rmSync(staged, { recursive: true, force: true });
225
+ fs_1.default.writeFileSync(staged, content, 'utf8');
226
+ return staged;
227
+ }
228
+ /**
229
+ * The real, filesystem-touching TransactionDeps used by applyInstallPlan by
230
+ * default. Renders `codex-agent-toml` targets from the canonical agent
231
+ * Markdown source at stage time; every other renderer uses the plain
232
+ * symlink/copy staging from executor.ts. Never logs target contents or
233
+ * environment variables.
234
+ */
235
+ function defaultTransactionDeps() {
236
+ let index = 0;
237
+ let createdAt = null;
238
+ const manifestEntries = [];
239
+ return {
240
+ validate(op) {
241
+ if (!fs_1.default.existsSync(op.sourcePath)) {
242
+ throw new Error(`Source path does not exist: ${op.sourcePath}`);
243
+ }
244
+ if (op.renderer === 'codex-agent-toml') {
245
+ // Renders without writing anything, purely to surface parse errors
246
+ // before any backup/replace happens.
247
+ (0, codex_agent_1.renderCodexAgent)(fs_1.default.readFileSync(op.sourcePath, 'utf8'));
248
+ }
249
+ },
250
+ backup(op, backupDir) {
251
+ if (createdAt === null)
252
+ createdAt = new Date().toISOString();
253
+ const entry = backupEntryFor(op.targetPath, backupDir, String(index));
254
+ index += 1;
255
+ manifestEntries.push(entry);
256
+ writeBackupManifest(backupDir, {
257
+ id: path_1.default.basename(backupDir),
258
+ createdAt,
259
+ committed: false,
260
+ entries: manifestEntries,
261
+ });
262
+ return entry.existed ? path_1.default.join(backupDir, entry.backupRelPath) : null;
263
+ },
264
+ stage(op) {
265
+ if (op.renderer === 'codex-agent-toml') {
266
+ const rendered = (0, codex_agent_1.renderCodexAgent)(fs_1.default.readFileSync(op.sourcePath, 'utf8'));
267
+ return stageRenderedFile(rendered, op.targetPath);
268
+ }
269
+ return (0, executor_1.stageArtifact)(op.sourcePath, op.targetPath, op.method);
270
+ },
271
+ replace(op, staged) {
272
+ (0, executor_1.replaceArtifact)(staged, op.targetPath);
273
+ },
274
+ verify(op) {
275
+ let stat;
276
+ try {
277
+ stat = fs_1.default.lstatSync(op.targetPath);
278
+ }
279
+ catch {
280
+ throw new Error(`verification failed: ${op.targetPath} is missing after install`);
281
+ }
282
+ if (op.renderer === 'codex-agent-toml') {
283
+ if (!stat.isFile()) {
284
+ throw new Error(`verification failed: ${op.targetPath} is not a regular file`);
285
+ }
286
+ const content = fs_1.default.readFileSync(op.targetPath, 'utf8');
287
+ if (!content.startsWith('name = ')) {
288
+ throw new Error(`verification failed: ${op.targetPath} does not look like rendered TOML`);
289
+ }
290
+ return;
291
+ }
292
+ if (op.method === 'symlink' && !stat.isSymbolicLink()) {
293
+ throw new Error(`verification failed: ${op.targetPath} is not a symlink`);
294
+ }
295
+ if (op.method === 'copy' && stat.isSymbolicLink()) {
296
+ throw new Error(`verification failed: ${op.targetPath} is unexpectedly a symlink`);
297
+ }
298
+ },
299
+ rollback(op, backup) {
300
+ restoreTargetFrom(op.targetPath, backup);
301
+ },
302
+ };
303
+ }
304
+ // ---------------------------------------------------------------------------
305
+ // beginBackupSession — general-purpose backup wrapper for mutations outside
306
+ // the planner (preferences, provider configs, etc.)
307
+ // ---------------------------------------------------------------------------
308
+ function createBackupManifest(targets, backupDir) {
309
+ const entries = targets.map((target, i) => backupEntryFor(target, backupDir, String(i)));
310
+ return {
311
+ id: path_1.default.basename(backupDir),
312
+ createdAt: new Date().toISOString(),
313
+ committed: false,
314
+ entries,
315
+ };
316
+ }
317
+ function markBackupCommitted(backupDir) {
318
+ const manifest = readBackupManifest(path_1.default.join(backupDir, 'manifest.json'));
319
+ writeBackupManifest(backupDir, { ...manifest, committed: true });
320
+ }
321
+ /**
322
+ * Backs up every (currently existing) path in `targetPaths` before the
323
+ * caller mutates them. `commit()` marks the backup as no-longer-needed
324
+ * (kept for manual recovery, never auto-deleted); `rollback()` restores
325
+ * every target to its pre-session state (or removes it, if it did not exist
326
+ * before the session began).
327
+ */
328
+ function beginBackupSession(targetPaths) {
329
+ const unique = Array.from(new Set(targetPaths.map((target) => path_1.default.resolve(target))));
330
+ if (unique.some((target) => target === path_1.default.parse(target).root)) {
331
+ throw new Error('refusing to back up a filesystem root');
332
+ }
333
+ const transactionId = sanitizeTransactionTimestamp();
334
+ const backupDir = path_1.default.join((0, paths_1.awmHome)(), 'backups', transactionId);
335
+ const manifest = createBackupManifest(unique, backupDir);
336
+ writeBackupManifest(backupDir, manifest);
337
+ return {
338
+ transactionId,
339
+ targetPaths: unique,
340
+ commit: () => markBackupCommitted(backupDir),
341
+ rollback: () => {
342
+ restoreBackup(transactionId);
343
+ },
344
+ };
345
+ }
346
+ // ---------------------------------------------------------------------------
347
+ // restoreBackup — explicit, ID-scoped restore (also used by `awm backup restore`)
348
+ // ---------------------------------------------------------------------------
349
+ /**
350
+ * Restores every target recorded in the given transaction's manifest.
351
+ * `transactionId` is untrusted input (it reaches here from CLI args) — it is
352
+ * validated against a strict format AND the resolved backup directory is
353
+ * checked to stay inside ~/.awm/backups before any read, to rule out path
354
+ * traversal. Only targets literally enumerated in the manifest are touched.
355
+ */
356
+ function restoreBackup(transactionId) {
357
+ if (!/^\d{4}-\d{2}-\d{2}T[0-9A-Za-z.-]+$/.test(transactionId)) {
358
+ throw new Error('invalid backup transaction id');
359
+ }
360
+ const root = path_1.default.join((0, paths_1.awmHome)(), 'backups');
361
+ const dir = path_1.default.join(root, transactionId);
362
+ if (!dir.startsWith(`${root}${path_1.default.sep}`)) {
363
+ throw new Error('backup path escapes AWM backup root');
364
+ }
365
+ const manifest = readBackupManifest(path_1.default.join(dir, 'manifest.json'));
366
+ const restored = [];
367
+ for (const entry of manifest.entries) {
368
+ restoreManifestEntry(dir, entry);
369
+ restored.push(entry.targetPath);
370
+ }
371
+ return { restored };
372
+ }
373
+ /** Lists every recorded backup transaction under ~/.awm/backups, newest first. Corrupt manifests are skipped. */
374
+ function listBackups() {
375
+ const root = path_1.default.join((0, paths_1.awmHome)(), 'backups');
376
+ if (!fs_1.default.existsSync(root))
377
+ return [];
378
+ const ids = fs_1.default.readdirSync(root).filter((name) => {
379
+ try {
380
+ return fs_1.default.statSync(path_1.default.join(root, name)).isDirectory();
381
+ }
382
+ catch {
383
+ return false;
384
+ }
385
+ });
386
+ const summaries = [];
387
+ for (const id of ids) {
388
+ const manifestPath = path_1.default.join(root, id, 'manifest.json');
389
+ if (!fs_1.default.existsSync(manifestPath))
390
+ continue;
391
+ try {
392
+ const manifest = readBackupManifest(manifestPath);
393
+ summaries.push({
394
+ id: manifest.id,
395
+ createdAt: manifest.createdAt,
396
+ committed: manifest.committed,
397
+ targets: manifest.entries.map((entry) => entry.targetPath),
398
+ });
399
+ }
400
+ catch {
401
+ // corrupt manifest — skip rather than fail the whole listing
402
+ }
403
+ }
404
+ return summaries.sort((a, b) => b.id.localeCompare(a.id));
405
+ }
@@ -120,7 +120,7 @@ function ensureSkillsGitignored(root, agents) {
120
120
  const existingLines = existing.split(/\r?\n/).map((l) => l.trim());
121
121
  const toIgnore = [];
122
122
  for (const agent of agents) {
123
- const provider = providers_1.PROVIDERS[agent];
123
+ const provider = (0, providers_1.providerFor)(agent);
124
124
  for (const type of ['skill', 'workflow', 'agent']) {
125
125
  const config = provider[type];
126
126
  if (!config)
@@ -0,0 +1,54 @@
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.preflightLinkArtifactPairs = preflightLinkArtifactPairs;
7
+ exports.scanLegacyArtifacts = scanLegacyArtifacts;
8
+ const fs_1 = __importDefault(require("fs"));
9
+ const path_1 = __importDefault(require("path"));
10
+ const providers_1 = require("../providers");
11
+ const ARTIFACT_TYPES = ['skill', 'workflow', 'agent'];
12
+ function preflightLinkArtifactPairs(pairs) {
13
+ for (const { agent, artifact } of pairs) {
14
+ (0, providers_1.assertLinkRenderer)(artifact.type, agent);
15
+ }
16
+ }
17
+ function scanLegacyArtifacts(agents, scope) {
18
+ const artifacts = new Map();
19
+ for (const agent of agents) {
20
+ for (const type of ARTIFACT_TYPES) {
21
+ let config;
22
+ try {
23
+ config = (0, providers_1.assertLinkRenderer)(type, agent);
24
+ }
25
+ catch (error) {
26
+ if (error instanceof providers_1.UnsupportedRendererError)
27
+ continue;
28
+ throw error;
29
+ }
30
+ if (!config)
31
+ continue;
32
+ const dir = config[scope];
33
+ if (!fs_1.default.existsSync(dir))
34
+ continue;
35
+ for (const entry of fs_1.default.readdirSync(dir, { withFileTypes: true })) {
36
+ const fullPath = path_1.default.join(dir, entry.name);
37
+ const existing = artifacts.get(entry.name);
38
+ if (existing) {
39
+ existing.installedIn.push(agent);
40
+ existing.fullPaths.push(fullPath);
41
+ }
42
+ else {
43
+ artifacts.set(entry.name, {
44
+ name: entry.name,
45
+ type,
46
+ installedIn: [agent],
47
+ fullPaths: [fullPath],
48
+ });
49
+ }
50
+ }
51
+ }
52
+ }
53
+ return Array.from(artifacts.values());
54
+ }
@@ -0,0 +1,36 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.assertProviderSupported = assertProviderSupported;
4
+ const child_process_1 = require("child_process");
5
+ const providers_1 = require("../providers");
6
+ const versioning_1 = require("./versioning");
7
+ function assertProviderSupported(agent, exec = child_process_1.execFileSync) {
8
+ const provider = (0, providers_1.providerFor)(agent);
9
+ if (!provider.versionCommand || !provider.minimumVersion) {
10
+ return { provider: agent, version: null };
11
+ }
12
+ let output;
13
+ try {
14
+ output = exec(provider.versionCommand.command, provider.versionCommand.args, {
15
+ encoding: 'utf8',
16
+ stdio: ['ignore', 'pipe', 'pipe'],
17
+ timeout: 5000,
18
+ }).toString();
19
+ }
20
+ catch (error) {
21
+ const code = error.code;
22
+ if (code === 'ENOENT') {
23
+ throw new Error('Codex is not installed or not available on PATH. ' +
24
+ 'Install the current stable @openai/codex release, then re-run.');
25
+ }
26
+ throw new Error(`Codex version probe failed: ${error.message}`);
27
+ }
28
+ const match = output.trim().match(/^codex-cli (\d+\.\d+\.\d+)$/);
29
+ if (!match) {
30
+ throw new Error(`could not parse Codex version from: ${output.trim()}`);
31
+ }
32
+ if ((0, versioning_1.compareSemver)(match[1], provider.minimumVersion) < 0) {
33
+ throw new Error(`requires Codex >= ${provider.minimumVersion}; found ${match[1]}`);
34
+ }
35
+ return { provider: agent, version: match[1] };
36
+ }
@@ -0,0 +1,61 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.planReconciliation = planReconciliation;
4
+ const bundles_1 = require("./bundles");
5
+ const install_planner_1 = require("./install-planner");
6
+ const bundle_install_1 = require("./bundle-install");
7
+ const config_1 = require("../utils/config");
8
+ function intentKey(intent) {
9
+ return `${intent.type}\0${intent.installName}\0${intent.sourcePath}`;
10
+ }
11
+ /**
12
+ * Computes the InstallPlan that reconciles every machine-scope (baseline +
13
+ * ambient) artifact for `targets` against the currently synced registries at
14
+ * `roots`. Pure — no filesystem writes; callers apply the result with
15
+ * `applyInstallPlan` (install-transaction.ts).
16
+ */
17
+ function planReconciliation(params) {
18
+ const { targets, roots } = params;
19
+ const bundles = params.bundles ?? (0, bundles_1.discoverAllBundles)(roots);
20
+ const contentDir = roots[0] ?? '';
21
+ const machineBundles = bundles.filter((b) => b.scope === 'baseline' || b.scope === 'ambient');
22
+ const seen = new Set();
23
+ const artifacts = [];
24
+ for (const b of machineBundles) {
25
+ let intents;
26
+ try {
27
+ intents = (0, bundle_install_1.expandBundleArtifacts)({
28
+ bundleName: b.name,
29
+ bundles,
30
+ agents: targets,
31
+ method: 'symlink',
32
+ projectRoot: '',
33
+ contentDir,
34
+ });
35
+ }
36
+ catch {
37
+ // A plain dangling `dependsOn` name doesn't reach here — resolveBundleClosure
38
+ // (bundles.ts) silently drops it before this point. This only fires on a
39
+ // genuinely malformed bundle definition (e.g. `dependsOn` not an array), which
40
+ // throws while walking the closure. Skip that one bundle rather than aborting
41
+ // reconciliation for every other bundle (see tests/core/reconciliation.test.ts).
42
+ continue;
43
+ }
44
+ for (const intent of intents) {
45
+ const key = intentKey(intent);
46
+ if (seen.has(key))
47
+ continue;
48
+ seen.add(key);
49
+ artifacts.push(intent);
50
+ }
51
+ }
52
+ const enabledAgents = params.enabledAgents ?? (0, config_1.getPreferences)().enabledAgents;
53
+ return (0, install_planner_1.planInstall)({
54
+ artifacts,
55
+ selectedAgents: targets,
56
+ enabledAgents,
57
+ scope: 'global',
58
+ projectRoot: '',
59
+ method: 'symlink',
60
+ });
61
+ }
@@ -16,6 +16,7 @@ exports.syncRegistries = syncRegistries;
16
16
  exports.readRegistryManifest = readRegistryManifest;
17
17
  exports.registryNameForPath = registryNameForPath;
18
18
  exports.verifyMinCliVersions = verifyMinCliVersions;
19
+ exports.assertRegistryGates = assertRegistryGates;
19
20
  // src/core/registries.ts
20
21
  const fs_1 = __importDefault(require("fs"));
21
22
  const path_1 = __importDefault(require("path"));
@@ -203,3 +204,12 @@ function verifyMinCliVersions(current = (0, cli_version_1.cliVersion)()) {
203
204
  }
204
205
  return failures;
205
206
  }
207
+ /** Throwing form of `verifyMinCliVersions`'s failure list — lets callers treat the
208
+ * min-CLI-version gate uniformly with other gates (version-gate, backup session, …)
209
+ * in a single try/catch rather than a bespoke console.warn loop. */
210
+ function assertRegistryGates(failures) {
211
+ if (failures.length === 0)
212
+ return;
213
+ const detail = failures.map((f) => `${f.name} requires CLI >= ${f.min}`).join('; ');
214
+ throw new Error(`Registry version gate failed: ${detail}. Run: npm i -g agentic-workflow-manager`);
215
+ }
@@ -0,0 +1,27 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.parseCanonicalAgent = parseCanonicalAgent;
4
+ function parseCanonicalAgent(source) {
5
+ const match = source.match(/^---\r?\n([\s\S]*?)\r?\n---\r?\n([\s\S]*)$/);
6
+ if (!match)
7
+ throw new Error('canonical agent requires YAML frontmatter');
8
+ const fields = new Map();
9
+ for (const line of match[1].split(/\r?\n/)) {
10
+ const field = line.match(/^([A-Za-z][A-Za-z0-9_-]*):\s*(.*)$/);
11
+ if (!field)
12
+ throw new Error(`invalid canonical agent frontmatter line: ${line}`);
13
+ if (fields.has(field[1]))
14
+ throw new Error(`duplicate canonical agent frontmatter key: ${field[1]}`);
15
+ fields.set(field[1], field[2].replace(/^(['"])(.*)\1$/, '$2').trim());
16
+ }
17
+ const name = fields.get('name') ?? '';
18
+ const description = fields.get('description') ?? '';
19
+ const instructions = match[2].trim();
20
+ if (!/^[a-z0-9]+(?:-[a-z0-9]+)*$/.test(name))
21
+ throw new Error('invalid agent name');
22
+ if (!description)
23
+ throw new Error('canonical agent requires a non-empty description');
24
+ if (!instructions)
25
+ throw new Error('canonical agent requires a non-empty instruction body');
26
+ return { name, description, instructions };
27
+ }
@@ -0,0 +1,60 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.renderCodexAgent = renderCodexAgent;
4
+ const canonical_agent_1 = require("./canonical-agent");
5
+ // TOML requires control characters other than tab (U+0009) and newline
6
+ // (U+000A) to be escaped in both basic and multi-line basic strings,
7
+ // including U+007F (DEL) and the rest of the C0 range (U+0000-U+0008,
8
+ // U+000B-U+001F). JSON.stringify (used for single-line strings) already
9
+ // escapes U+0000-U+001F, but not U+007F. A plain backslash/quote replace
10
+ // (used for multi-line strings) escapes neither. escapeControlChars covers
11
+ // both: for tomlString it only ever finds a literal U+007F left over from
12
+ // JSON.stringify (the rest were already turned into escape sequences by
13
+ // JSON.stringify, so they are no longer literal control characters); for
14
+ // tomlMultiline it finds every raw control character in the value.
15
+ //
16
+ // The character class below is built from numeric code points rather than
17
+ // written as literal escape sequences in source, to avoid ambiguity between
18
+ // "the two characters backslash and u" and "the control character itself".
19
+ const CONTROL_CHAR_RANGES = [
20
+ [0x00, 0x08],
21
+ [0x0b, 0x1f],
22
+ [0x7f, 0x7f],
23
+ ];
24
+ function buildControlCharsRegex() {
25
+ const classBody = CONTROL_CHAR_RANGES.map(([start, end]) => {
26
+ const from = String.fromCharCode(start);
27
+ const to = String.fromCharCode(end);
28
+ return start === end ? from : `${from}-${to}`;
29
+ }).join('');
30
+ return new RegExp(`[${classBody}]`, 'g');
31
+ }
32
+ const CONTROL_CHARS = buildControlCharsRegex();
33
+ function escapeControlChars(value) {
34
+ return value.replace(CONTROL_CHARS, (ch) => `\\u${ch.charCodeAt(0).toString(16).padStart(4, '0')}`);
35
+ }
36
+ function tomlString(value) {
37
+ // JSON.stringify escapes backslashes, double quotes, and control characters
38
+ // U+0000-U+001F, but TOML also requires U+007F (DEL) to be escaped.
39
+ return escapeControlChars(JSON.stringify(value));
40
+ }
41
+ function tomlMultiline(value) {
42
+ // Escape every backslash and every double quote individually (not just
43
+ // triple-quote runs) so no run of 3+ unescaped quotes can ever appear in
44
+ // the output, regardless of how many consecutive quotes are embedded.
45
+ // Order matters: backslashes first (so escaping a control character does
46
+ // not introduce a backslash that gets doubled), then control characters,
47
+ // then quotes. Tab and newline are left raw; they're legitimately
48
+ // allowed unescaped in a TOML multi-line basic string.
49
+ const escaped = escapeControlChars(value.replace(/\\/g, '\\\\')).replace(/"/g, '\\"');
50
+ return `"""\n${escaped}\n"""`;
51
+ }
52
+ function renderCodexAgent(source) {
53
+ const agent = (0, canonical_agent_1.parseCanonicalAgent)(source);
54
+ return [
55
+ `name = ${tomlString(agent.name)}`,
56
+ `description = ${tomlString(agent.description)}`,
57
+ `developer_instructions = ${tomlMultiline(agent.instructions)}`,
58
+ '',
59
+ ].join('\n');
60
+ }