agentic-workflow-manager 9.2.0 → 9.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.
@@ -21,6 +21,7 @@ exports.runUpdateCore = runUpdateCore;
21
21
  // instead (which already imports clack at the top and is never `require()`d
22
22
  // by tests).
23
23
  const picocolors_1 = __importDefault(require("picocolors"));
24
+ const versioning_1 = require("../core/versioning");
24
25
  const paths_1 = require("../core/paths");
25
26
  const config_1 = require("../utils/config");
26
27
  const agent_targets_1 = require("../core/agent-targets");
@@ -89,8 +90,29 @@ async function runUpdateCore(options = {}, deps = {}) {
89
90
  for (const r of registryResults) {
90
91
  if (r.action === 'error')
91
92
  console.warn(picocolors_1.default.yellow(` ⚠ registry ${r.name}: ${r.error}`));
92
- else
93
- console.log(picocolors_1.default.green(` ✓ Registry ${r.name} ${r.action === 'pulled' ? 'updated' : 're-cloned'} @ ${r.version}`));
93
+ else if (r.disposition === 'regressed') {
94
+ const latest = r.availableVersion && r.availableVersion !== r.version
95
+ ? ` Latest available: ${r.availableVersion}.`
96
+ : '';
97
+ console.warn(picocolors_1.default.yellow(` ⚠ Registry ${r.name} regressed ${r.previousVersion ?? '(previous version)'} → ${r.version} `
98
+ + `because it is pinned to v${(0, versioning_1.normalizePin)(r.pin ?? '')}.${latest} Run \`awm unpin ${r.name}\` to move forward.`));
99
+ }
100
+ else if (r.pin && r.availableVersion !== r.version) {
101
+ console.log(picocolors_1.default.yellow(` ✓ Registry ${r.name} resolved @ ${r.version} (pinned to v${(0, versioning_1.normalizePin)(r.pin)}; `
102
+ + `latest available: ${r.availableVersion ?? 'unknown'}). Run \`awm unpin ${r.name}\` to move forward.`));
103
+ }
104
+ else if (r.disposition === 'unchanged') {
105
+ console.log(picocolors_1.default.dim(` ✓ Registry ${r.name} already at ${r.version}`));
106
+ }
107
+ else if (r.disposition === 'advanced') {
108
+ console.log(picocolors_1.default.green(` ✓ Registry ${r.name} updated @ ${r.version}`));
109
+ }
110
+ else if (r.disposition === 'installed') {
111
+ console.log(picocolors_1.default.green(` ✓ Registry ${r.name} installed @ ${r.version}`));
112
+ }
113
+ else {
114
+ console.log(picocolors_1.default.green(` ✓ Registry ${r.name} resolved @ ${r.version}`));
115
+ }
94
116
  }
95
117
  const registries = {
96
118
  configured: registryResults.length,
@@ -251,9 +251,11 @@ async function syncRegistries() {
251
251
  await (0, simple_git_1.default)(reg.contentRoot).reset(['--hard']);
252
252
  }
253
253
  const git = (0, simple_git_1.default)(reg.contentRoot);
254
+ const previousVersion = freshClone ? null : await (0, versioning_1.currentVersion)(reg.contentRoot);
255
+ const versionOpts = (0, versioning_1.machineVersionOpts)(reg.name);
254
256
  let resolved;
255
257
  try {
256
- resolved = await (0, versioning_1.resolveTargetRef)(reg.contentRoot, (0, versioning_1.machineVersionOpts)(reg.name));
258
+ resolved = await (0, versioning_1.resolveTargetRef)(reg.contentRoot, versionOpts);
257
259
  await git.checkout(resolved.ref);
258
260
  if (resolved.kind !== 'tag')
259
261
  await git.pull('origin', resolved.ref);
@@ -266,10 +268,24 @@ async function syncRegistries() {
266
268
  fs_1.default.rmSync(reg.contentRoot, { recursive: true, force: true });
267
269
  throw e;
268
270
  }
271
+ const version = resolved.kind === 'tag' ? `v${resolved.version}` : 'HEAD';
272
+ let disposition;
273
+ if (freshClone)
274
+ disposition = 'installed';
275
+ else if (resolved.kind !== 'tag' || previousVersion === null)
276
+ disposition = 'resolved';
277
+ else {
278
+ const direction = (0, versioning_1.compareSemver)(resolved.version, previousVersion);
279
+ disposition = direction > 0 ? 'advanced' : direction < 0 ? 'regressed' : 'unchanged';
280
+ }
269
281
  results.push({
270
282
  name: reg.name,
271
283
  action: freshClone ? 'recloned' : 'pulled',
272
- version: resolved.kind === 'tag' ? `v${resolved.version}` : 'HEAD',
284
+ version,
285
+ disposition,
286
+ ...(versionOpts.pin ? { pin: (0, versioning_1.normalizePin)(versionOpts.pin) } : {}),
287
+ ...(previousVersion ? { previousVersion: `v${previousVersion}` } : {}),
288
+ ...(resolved.kind === 'tag' ? { availableVersion: `v${resolved.latestVersion}` } : {}),
273
289
  });
274
290
  }
275
291
  catch (e) {
@@ -69,7 +69,7 @@ async function resolveTargetRef(repoDir, opts) {
69
69
  : 'the registry has no version tags';
70
70
  throw new Error(`Pinned version ${want} not found in ${repoDir} — ${available}`);
71
71
  }
72
- return { kind: 'tag', ref: want, version: normalizePin(opts.pin) };
72
+ return { kind: 'tag', ref: want, version: normalizePin(opts.pin), latestVersion: tags[tags.length - 1].slice(1) };
73
73
  }
74
74
  const branch = await defaultBranch(git);
75
75
  if (opts.channel === 'dev')
@@ -77,7 +77,7 @@ async function resolveTargetRef(repoDir, opts) {
77
77
  if (tags.length === 0)
78
78
  return { kind: 'head-fallback', ref: branch };
79
79
  const latest = tags[tags.length - 1];
80
- return { kind: 'tag', ref: latest, version: latest.slice(1) };
80
+ return { kind: 'tag', ref: latest, version: latest.slice(1), latestVersion: latest.slice(1) };
81
81
  }
82
82
  /** Versión checkouteada actual ("X.Y.Z") si HEAD coincide exactamente con un tag semver; null si sigue un branch. */
83
83
  async function currentVersion(repoDir) {
@@ -125,6 +125,37 @@ describe('awm update — honest outcome', () => {
125
125
  expect(result.registries).toEqual({ configured: 2, synced: 2, failed: [] });
126
126
  expect(updateOutro(result)).toBe('✅ 2 registries, skills and hooks updated.');
127
127
  });
128
+ it('una regresión causada por pin se advierte y explica cómo volver a avanzar', async () => {
129
+ const { runUpdateCore } = require('../../src/commands/update');
130
+ await runUpdateCore({}, deps({
131
+ syncRegistries: async () => [{
132
+ name: 'baseline',
133
+ action: 'pulled',
134
+ version: 'v1.0.0',
135
+ disposition: 'regressed',
136
+ pin: '1.0.0',
137
+ availableVersion: 'v1.1.0',
138
+ }],
139
+ }));
140
+ const output = [...logSpy.mock.calls, ...warnSpy.mock.calls].flat().join('\n');
141
+ expect(output).toMatch(/regress/i);
142
+ expect(output).toMatch(/v1\.0\.0/);
143
+ expect(output).toMatch(/v1\.1\.0/);
144
+ expect(output).toMatch(/awm unpin baseline/);
145
+ expect(output).not.toMatch(/Registry baseline updated/i);
146
+ });
147
+ it('un pin que ya coincide con el último release no recomienda unpin', async () => {
148
+ const { runUpdateCore } = require('../../src/commands/update');
149
+ await runUpdateCore({}, deps({
150
+ syncRegistries: async () => [{
151
+ name: 'baseline', action: 'pulled', version: 'v1.1.0', disposition: 'unchanged',
152
+ pin: 'v1.1.0', availableVersion: 'v1.1.0',
153
+ }],
154
+ }));
155
+ const output = [...logSpy.mock.calls, ...warnSpy.mock.calls].flat().join('\n');
156
+ expect(output).toMatch(/already at v1\.1\.0/i);
157
+ expect(output).not.toMatch(/awm unpin baseline/);
158
+ });
128
159
  it('un registry que falló y NO dejó contenido en disco falla cerrado', async () => {
129
160
  const { runUpdateCore } = require('../../src/commands/update');
130
161
  const result = await runUpdateCore({}, deps({
@@ -79,7 +79,7 @@ describe('syncRegistries (git fixtures locales)', () => {
79
79
  const source = makeSourceRepo(tmpWork, 'alpha');
80
80
  m.writeRegistriesConfig([{ name: 'personal', remote: source }]);
81
81
  const results = await m.syncRegistries();
82
- expect(results).toEqual([{ name: 'personal', action: 'recloned', version: 'HEAD' }]);
82
+ expect(results).toEqual([{ name: 'personal', action: 'recloned', version: 'HEAD', disposition: 'installed' }]);
83
83
  expect(fs_1.default.existsSync(path_1.default.join(tmpHome, '.awm/registries/personal/skills/alpha/SKILL.md'))).toBe(true);
84
84
  });
85
85
  it('pulls an existing clone and reports non-fatal errors per registry', async () => {
@@ -95,7 +95,7 @@ describe('syncRegistries (git fixtures locales)', () => {
95
95
  GIT(source, 'add -A');
96
96
  GIT(source, 'commit -qm v2');
97
97
  const results = await m.syncRegistries();
98
- expect(results[0]).toEqual({ name: 'personal', action: 'pulled', version: 'HEAD' });
98
+ expect(results[0]).toEqual({ name: 'personal', action: 'pulled', version: 'HEAD', disposition: 'resolved' });
99
99
  expect(results[1].name).toBe('broken');
100
100
  expect(results[1].action).toBe('error');
101
101
  const synced = fs_1.default.readFileSync(path_1.default.join(tmpHome, '.awm/registries/personal/skills/alpha/SKILL.md'), 'utf-8');
@@ -127,7 +127,7 @@ describe('syncRegistries (git fixtures locales)', () => {
127
127
  GIT(source, 'commit -qm unreleased');
128
128
  m.writeRegistriesConfig([{ name: 'personal', remote: source }]);
129
129
  const results = await m.syncRegistries();
130
- expect(results).toEqual([{ name: 'personal', action: 'recloned', version: 'v1.0.0' }]);
130
+ expect(results).toEqual([{ name: 'personal', action: 'recloned', version: 'v1.0.0', disposition: 'installed', availableVersion: 'v1.0.0' }]);
131
131
  const synced = fs_1.default.readFileSync(path_1.default.join(tmpHome, '.awm/registries/personal/skills/alpha/SKILL.md'), 'utf-8');
132
132
  expect(synced).toContain('test skill'); // contenido del tag, no del HEAD
133
133
  });
@@ -157,7 +157,9 @@ describe('syncRegistries (git fixtures locales)', () => {
157
157
  fs_1.default.mkdirSync(awmDir, { recursive: true });
158
158
  fs_1.default.writeFileSync(path_1.default.join(awmDir, 'preferences.json'), JSON.stringify({ defaultAgent: 'claude-code', installMethod: 'symlink', defaultScope: 'local', pins: { personal: '1.0.0' } }));
159
159
  const results = await m.syncRegistries();
160
- expect(results).toEqual([{ name: 'personal', action: 'recloned', version: 'v1.0.0' }]);
160
+ expect(results).toEqual([{
161
+ name: 'personal', action: 'recloned', version: 'v1.0.0', disposition: 'installed', pin: '1.0.0', availableVersion: 'v1.1.0',
162
+ }]);
161
163
  });
162
164
  it('baseline sembrado se sincroniza por el mismo loop que cualquier registry', async () => {
163
165
  const m = require('../../src/core/registries');
@@ -167,7 +169,7 @@ describe('syncRegistries (git fixtures locales)', () => {
167
169
  m.seedBaselineRegistry();
168
170
  delete process.env.AWM_BASE_REMOTE;
169
171
  const results = await m.syncRegistries();
170
- expect(results).toEqual([{ name: 'baseline', action: 'recloned', version: 'v1.0.0' }]);
172
+ expect(results).toEqual([{ name: 'baseline', action: 'recloned', version: 'v1.0.0', disposition: 'installed', availableVersion: 'v1.0.0' }]);
171
173
  expect(fs_1.default.existsSync(path_1.default.join(tmpHome, '.awm/registries/baseline/skills/alpha/SKILL.md'))).toBe(true);
172
174
  });
173
175
  it('verifyMinCliVersions reporta registries que exigen CLI más nuevo', async () => {
@@ -193,7 +195,7 @@ describe('syncRegistries (git fixtures locales)', () => {
193
195
  m.writeRegistriesConfig([{ name: 'personal', remote: source }]);
194
196
  // primera sync: queda en v1.0.0
195
197
  const r1 = await m.syncRegistries();
196
- expect(r1).toEqual([{ name: 'personal', action: 'recloned', version: 'v1.0.0' }]);
198
+ expect(r1).toEqual([{ name: 'personal', action: 'recloned', version: 'v1.0.0', disposition: 'installed', availableVersion: 'v1.0.0' }]);
197
199
  // nueva release en el remote
198
200
  fs_1.default.writeFileSync(path_1.default.join(source, 'skills', 'alpha', 'SKILL.md'), '---\nname: alpha\ndescription: v2\n---\n');
199
201
  GIT(source, 'add -A');
@@ -201,9 +203,15 @@ describe('syncRegistries (git fixtures locales)', () => {
201
203
  GIT(source, 'tag v1.1.0');
202
204
  // segunda sync: avanza al nuevo tag
203
205
  const r2 = await m.syncRegistries();
204
- expect(r2).toEqual([{ name: 'personal', action: 'pulled', version: 'v1.1.0' }]);
206
+ expect(r2).toEqual([{
207
+ name: 'personal', action: 'pulled', version: 'v1.1.0', disposition: 'advanced', previousVersion: 'v1.0.0', availableVersion: 'v1.1.0',
208
+ }]);
205
209
  const content = fs_1.default.readFileSync(path_1.default.join(tmpHome, '.awm/registries/personal/skills/alpha/SKILL.md'), 'utf-8');
206
210
  expect(content).toContain('v2');
211
+ const r3 = await m.syncRegistries();
212
+ expect(r3).toEqual([{
213
+ name: 'personal', action: 'pulled', version: 'v1.1.0', disposition: 'unchanged', previousVersion: 'v1.1.0', availableVersion: 'v1.1.0',
214
+ }]);
207
215
  });
208
216
  it('rollback tag→tag: clone en v1.1.0 retrocede a v1.0.0 al establecer pin', async () => {
209
217
  const m = require('../../src/core/registries');
@@ -216,13 +224,21 @@ describe('syncRegistries (git fixtures locales)', () => {
216
224
  m.writeRegistriesConfig([{ name: 'personal', remote: source }]);
217
225
  // primera sync sin pin: queda en v1.1.0
218
226
  const r1 = await m.syncRegistries();
219
- expect(r1).toEqual([{ name: 'personal', action: 'recloned', version: 'v1.1.0' }]);
227
+ expect(r1).toEqual([{ name: 'personal', action: 'recloned', version: 'v1.1.0', disposition: 'installed', availableVersion: 'v1.1.0' }]);
220
228
  // establece pin a la versión anterior
221
229
  const awmDir = path_1.default.join(tmpHome, '.awm');
222
230
  fs_1.default.writeFileSync(path_1.default.join(awmDir, 'preferences.json'), JSON.stringify({ defaultAgent: 'claude-code', installMethod: 'symlink', defaultScope: 'local', pins: { personal: '1.0.0' } }));
223
231
  // segunda sync con pin: retrocede a v1.0.0
224
232
  const r2 = await m.syncRegistries();
225
- expect(r2).toEqual([{ name: 'personal', action: 'pulled', version: 'v1.0.0' }]);
233
+ expect(r2).toEqual([{
234
+ name: 'personal',
235
+ action: 'pulled',
236
+ version: 'v1.0.0',
237
+ disposition: 'regressed',
238
+ pin: '1.0.0',
239
+ previousVersion: 'v1.1.0',
240
+ availableVersion: 'v1.1.0',
241
+ }]);
226
242
  const content = fs_1.default.readFileSync(path_1.default.join(tmpHome, '.awm/registries/personal/skills/alpha/SKILL.md'), 'utf-8');
227
243
  expect(content).toContain('test skill'); // contenido original del tag v1.0.0
228
244
  });
@@ -236,14 +252,16 @@ describe('syncRegistries (git fixtures locales)', () => {
236
252
  fs_1.default.writeFileSync(path_1.default.join(awmDir, 'preferences.json'), JSON.stringify({ defaultAgent: 'claude-code', installMethod: 'symlink', defaultScope: 'local', channel: 'dev' }));
237
253
  // primera sync en canal dev: queda en HEAD (no en el tag)
238
254
  const r1 = await m.syncRegistries();
239
- expect(r1).toEqual([{ name: 'personal', action: 'recloned', version: 'HEAD' }]);
255
+ expect(r1).toEqual([{ name: 'personal', action: 'recloned', version: 'HEAD', disposition: 'installed' }]);
240
256
  // nuevo commit en el remote (post-tag, no release)
241
257
  fs_1.default.writeFileSync(path_1.default.join(source, 'skills', 'alpha', 'SKILL.md'), '---\nname: alpha\ndescription: head-2\n---\n');
242
258
  GIT(source, 'add -A');
243
259
  GIT(source, 'commit -qm head-2');
244
260
  // segunda sync: recibe el nuevo commit
245
261
  const r2 = await m.syncRegistries();
246
- expect(r2).toEqual([{ name: 'personal', action: 'pulled', version: 'HEAD' }]);
262
+ expect(r2).toEqual([{
263
+ name: 'personal', action: 'pulled', version: 'HEAD', disposition: 'resolved', previousVersion: 'v1.0.0',
264
+ }]);
247
265
  const content = fs_1.default.readFileSync(path_1.default.join(tmpHome, '.awm/registries/personal/skills/alpha/SKILL.md'), 'utf-8');
248
266
  expect(content).toContain('head-2');
249
267
  });
@@ -62,16 +62,16 @@ describe('versioning core', () => {
62
62
  const clone = cloneOf(source, tmpWork, 'clone');
63
63
  const { resolveTargetRef } = require('../../src/core/versioning');
64
64
  const r = await resolveTargetRef(clone, { channel: 'stable' });
65
- expect(r).toEqual({ kind: 'tag', ref: 'v1.10.0', version: '1.10.0' });
65
+ expect(r).toEqual({ kind: 'tag', ref: 'v1.10.0', version: '1.10.0', latestVersion: '1.10.0' });
66
66
  });
67
67
  it('pin exacto gana, con y sin prefijo v', async () => {
68
68
  const source = makeTaggedRepo(tmpWork, 'src', ['1.0.0', '1.1.0']);
69
69
  const clone = cloneOf(source, tmpWork, 'clone');
70
70
  const { resolveTargetRef } = require('../../src/core/versioning');
71
71
  expect(await resolveTargetRef(clone, { pin: '1.0.0', channel: 'stable' }))
72
- .toEqual({ kind: 'tag', ref: 'v1.0.0', version: '1.0.0' });
72
+ .toEqual({ kind: 'tag', ref: 'v1.0.0', version: '1.0.0', latestVersion: '1.1.0' });
73
73
  expect(await resolveTargetRef(clone, { pin: 'v1.0.0', channel: 'stable' }))
74
- .toEqual({ kind: 'tag', ref: 'v1.0.0', version: '1.0.0' });
74
+ .toEqual({ kind: 'tag', ref: 'v1.0.0', version: '1.0.0', latestVersion: '1.1.0' });
75
75
  });
76
76
  it('pin inexistente → error que lista las versiones disponibles', async () => {
77
77
  const source = makeTaggedRepo(tmpWork, 'src', ['1.0.0']);
@@ -102,7 +102,7 @@ describe('versioning core', () => {
102
102
  const clone = cloneOf(source, tmpWork, 'clone');
103
103
  const { resolveTargetRef } = require('../../src/core/versioning');
104
104
  const r = await resolveTargetRef(clone, { channel: 'stable' });
105
- expect(r).toEqual({ kind: 'tag', ref: 'v1.0.0', version: '1.0.0' });
105
+ expect(r).toEqual({ kind: 'tag', ref: 'v1.0.0', version: '1.0.0', latestVersion: '1.0.0' });
106
106
  });
107
107
  it('hace fetch: ve tags creados en el remote después del clone', async () => {
108
108
  const source = makeTaggedRepo(tmpWork, 'src', ['1.0.0']);
@@ -114,7 +114,7 @@ describe('versioning core', () => {
114
114
  GIT(source, 'tag v1.1.0');
115
115
  const { resolveTargetRef } = require('../../src/core/versioning');
116
116
  const r = await resolveTargetRef(clone, { channel: 'stable' });
117
- expect(r).toEqual({ kind: 'tag', ref: 'v1.1.0', version: '1.1.0' });
117
+ expect(r).toEqual({ kind: 'tag', ref: 'v1.1.0', version: '1.1.0', latestVersion: '1.1.0' });
118
118
  });
119
119
  });
120
120
  describe('currentVersion', () => {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "agentic-workflow-manager",
3
- "version": "9.2.0",
3
+ "version": "9.2.1",
4
4
  "main": "dist/src/index.js",
5
5
  "bin": {
6
6
  "awm": "./dist/src/index.js"