@sabaiway/agent-workflow-kit 1.15.1 → 1.16.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 (55) hide show
  1. package/CHANGELOG.md +57 -0
  2. package/README.md +2 -1
  3. package/SKILL.md +91 -15
  4. package/capability.json +1 -1
  5. package/package.json +5 -2
  6. package/tools/commands.mjs +206 -0
  7. package/tools/delegation.mjs +8 -0
  8. package/tools/family-registry.mjs +263 -13
  9. package/tools/procedures.mjs +3 -1
  10. package/bin/install.test.mjs +0 -347
  11. package/tools/delegation.test.mjs +0 -116
  12. package/tools/detect-backends.test.mjs +0 -444
  13. package/tools/engine-source.test.mjs +0 -280
  14. package/tools/family-registry.test.mjs +0 -279
  15. package/tools/fs-safe.test.mjs +0 -346
  16. package/tools/hide-footprint.integration.test.mjs +0 -168
  17. package/tools/hide-footprint.test.mjs +0 -463
  18. package/tools/inject-methodology.test.mjs +0 -689
  19. package/tools/known-footprint.test.mjs +0 -271
  20. package/tools/manifest/fixtures/bad-available/SKILL.md +0 -7
  21. package/tools/manifest/fixtures/bad-available/capability.json +0 -10
  22. package/tools/manifest/fixtures/detect-array/SKILL.md +0 -7
  23. package/tools/manifest/fixtures/detect-array/capability.json +0 -10
  24. package/tools/manifest/fixtures/malformed-json/capability.json +0 -1
  25. package/tools/manifest/fixtures/metadata-version/SKILL.md +0 -10
  26. package/tools/manifest/fixtures/metadata-version/capability.json +0 -9
  27. package/tools/manifest/fixtures/missing-key/SKILL.md +0 -7
  28. package/tools/manifest/fixtures/missing-key/capability.json +0 -8
  29. package/tools/manifest/fixtures/missing-source/SKILL.md +0 -7
  30. package/tools/manifest/fixtures/missing-source/capability.json +0 -11
  31. package/tools/manifest/fixtures/nested-version-decoy/SKILL.md +0 -10
  32. package/tools/manifest/fixtures/nested-version-decoy/capability.json +0 -9
  33. package/tools/manifest/fixtures/null-root/capability.json +0 -1
  34. package/tools/manifest/fixtures/provides-roles-mismatch/SKILL.md +0 -7
  35. package/tools/manifest/fixtures/provides-roles-mismatch/bin/run.sh +0 -2
  36. package/tools/manifest/fixtures/provides-roles-mismatch/capability.json +0 -11
  37. package/tools/manifest/fixtures/stub/capability.json +0 -10
  38. package/tools/manifest/fixtures/traversal-source/SKILL.md +0 -7
  39. package/tools/manifest/fixtures/traversal-source/capability.json +0 -11
  40. package/tools/manifest/fixtures/unknown-schema/capability.json +0 -9
  41. package/tools/manifest/fixtures/valid/SKILL.md +0 -10
  42. package/tools/manifest/fixtures/valid/bin/run.sh +0 -3
  43. package/tools/manifest/fixtures/valid/capability.json +0 -18
  44. package/tools/manifest/fixtures/version-mismatch/SKILL.md +0 -7
  45. package/tools/manifest/fixtures/version-mismatch/capability.json +0 -9
  46. package/tools/manifest/fixtures/win-absolute-source/SKILL.md +0 -7
  47. package/tools/manifest/fixtures/win-absolute-source/capability.json +0 -11
  48. package/tools/manifest/validate.test.mjs +0 -73
  49. package/tools/procedures.test.mjs +0 -303
  50. package/tools/recipes.test.mjs +0 -538
  51. package/tools/release-scan.test.mjs +0 -41
  52. package/tools/setup-backends.test.mjs +0 -500
  53. package/tools/uninstall.integration.test.mjs +0 -144
  54. package/tools/uninstall.test.mjs +0 -401
  55. package/tools/velocity-profile.test.mjs +0 -496
@@ -1,303 +0,0 @@
1
- import { describe, it, beforeEach, afterEach } from 'node:test';
2
- import assert from 'node:assert/strict';
3
- import { mkdtempSync, mkdirSync, rmSync, writeFileSync, symlinkSync } from 'node:fs';
4
- import { tmpdir } from 'node:os';
5
- import { dirname, join } from 'node:path';
6
- import { fileURLToPath } from 'node:url';
7
- import { main, extractSection, CONFIG_REL } from './procedures.mjs';
8
- import { READY, NEEDS_SKILL } from './detect-backends.mjs';
9
-
10
- // Host-independent fixtures: a temp cwd for the config + the REPO's OWN engine via
11
- // AGENT_WORKFLOW_ENGINE_DIR (it ships references/procedures.md, so the live read is deterministic and
12
- // needs no separate engine fixture) + an INJECTED synthetic detection (ctx.detect) so the resolved
13
- // recipe never depends on which backends the test host happens to have installed.
14
- const HERE = dirname(fileURLToPath(import.meta.url));
15
- const ENGINE_DIR = join(HERE, '..', '..', 'agent-workflow-engine');
16
- const CODEX = 'codex-cli-bridge';
17
- const AGY = 'antigravity-cli-bridge';
18
- const detect = (codex, agy) => () => [
19
- { name: CODEX, readiness: codex },
20
- { name: AGY, readiness: agy },
21
- ];
22
-
23
- let cwd;
24
- beforeEach(() => {
25
- cwd = mkdtempSync(join(tmpdir(), 'procedures-cwd-'));
26
- mkdirSync(join(cwd, 'docs', 'ai'), { recursive: true });
27
- });
28
- afterEach(() => {
29
- rmSync(cwd, { recursive: true, force: true });
30
- });
31
-
32
- const writeConfig = (json) => writeFileSync(join(cwd, CONFIG_REL), json);
33
- // Run main() with the repo engine + an injected detection; config comes from the temp cwd.
34
- const run = (argv, { codex = READY, agy = READY } = {}) =>
35
- main(argv, { cwd, env: { AGENT_WORKFLOW_ENGINE_DIR: ENGINE_DIR }, detect: detect(codex, agy) });
36
-
37
- describe('procedures CLI — happy path (section verbatim + resolved recipe)', () => {
38
- it('plan-authoring prints the canon section + the resolved review recipe, exit 0', () => {
39
- const r = run(['plan-authoring'], { codex: READY, agy: NEEDS_SKILL });
40
- assert.equal(r.code, 0, r.stderr);
41
- assert.match(r.stdout, /## plan-authoring/);
42
- assert.match(r.stdout, /Slots: review/);
43
- assert.match(r.stdout, /resolved recipes for "plan-authoring"/);
44
- assert.match(r.stdout, /review: reviewed — computed default/);
45
- });
46
-
47
- it('plan-execution resolves BOTH slots (execute then review)', () => {
48
- const r = run(['plan-execution'], { codex: READY, agy: NEEDS_SKILL });
49
- assert.equal(r.code, 0, r.stderr);
50
- assert.match(r.stdout, /## plan-execution/);
51
- assert.match(r.stdout, /Slots: execute, review/);
52
- assert.match(r.stdout, /execute: solo — computed default/);
53
- assert.match(r.stdout, /review: reviewed — computed default/);
54
- });
55
-
56
- it('section extraction is scoped to the requested activity (no sibling section bleeds in)', () => {
57
- const r = run(['plan-execution'], { codex: READY, agy: READY });
58
- assert.ok(r.stdout.includes('## plan-execution'));
59
- assert.ok(!r.stdout.includes('## plan-authoring'), 'only the requested activity section is printed');
60
- });
61
- });
62
-
63
- describe('procedures CLI — config IO (§2.2)', () => {
64
- it('absent config → computed defaults, stated as configSource:none', () => {
65
- const r = run(['plan-authoring', '--json'], { codex: NEEDS_SKILL, agy: NEEDS_SKILL });
66
- assert.equal(r.code, 0, r.stderr);
67
- const j = JSON.parse(r.stdout);
68
- assert.equal(j.configSource, 'none');
69
- assert.equal(j.slots.review.source, 'default');
70
- assert.equal(j.slots.review.recipe, 'solo', 'no ready backend → review defaults to solo');
71
- });
72
-
73
- it('a valid config drives the slot (execute=delegated honoured when codex is ready)', () => {
74
- writeConfig(JSON.stringify({ _README: 'composition-root config', 'plan-execution': { execute: 'delegated' } }));
75
- const r = run(['plan-execution', '--json'], { codex: READY, agy: NEEDS_SKILL });
76
- assert.equal(r.code, 0, r.stderr);
77
- const j = JSON.parse(r.stdout);
78
- assert.equal(j.configSource, CONFIG_REL);
79
- assert.equal(j.slots.execute.recipe, 'delegated');
80
- assert.equal(j.slots.execute.source, 'config');
81
- });
82
-
83
- it('malformed JSON → loud `path: malformed JSON …`, exit 1', () => {
84
- writeConfig('{ not valid json');
85
- const r = run(['plan-authoring']);
86
- assert.equal(r.code, 1);
87
- assert.match(r.stderr, new RegExp(`${CONFIG_REL}: malformed JSON`));
88
- });
89
-
90
- it('schema-invalid (recipe not allowed for the slot) → loud `path: invalid recipe …`, exit 1', () => {
91
- writeConfig(JSON.stringify({ 'plan-authoring': { review: 'delegated' } }));
92
- const r = run(['plan-authoring']);
93
- assert.equal(r.code, 1);
94
- assert.match(r.stderr, /invalid recipe "delegated" for review slot of "plan-authoring"/);
95
- });
96
-
97
- it('schema-invalid (unknown activity) → exit 1', () => {
98
- writeConfig(JSON.stringify({ 'plan-foo': { review: 'reviewed' } }));
99
- const r = run(['plan-authoring']);
100
- assert.equal(r.code, 1);
101
- assert.match(r.stderr, /unknown activity "plan-foo"/);
102
- });
103
-
104
- it('schema-invalid (unknown slot) → exit 1', () => {
105
- writeConfig(JSON.stringify({ 'plan-authoring': { execute: 'solo' } }));
106
- const r = run(['plan-authoring']);
107
- assert.equal(r.code, 1);
108
- assert.match(r.stderr, /unknown slot "execute" for activity "plan-authoring"/);
109
- });
110
-
111
- it('unreadable config (a directory in its place → EISDIR) → loud `path: unreadable …`, exit 1', () => {
112
- mkdirSync(join(cwd, CONFIG_REL)); // orchestration.json IS a dir → readFileSync throws
113
- const r = run(['plan-authoring']);
114
- assert.equal(r.code, 1);
115
- assert.match(r.stderr, new RegExp(`${CONFIG_REL}: unreadable`));
116
- });
117
-
118
- it('a DANGLING symlink at the config path is unreadable (exit 1), NOT silently treated as absent', () => {
119
- // A broken config symlink is a present-but-broken config — surface it loudly, never fall through to
120
- // defaults (no-silent-failures). lstat sees the link; readFileSync follows it to a missing target.
121
- symlinkSync(join(cwd, 'nowhere.json'), join(cwd, CONFIG_REL));
122
- const r = run(['plan-authoring']);
123
- assert.equal(r.code, 1);
124
- assert.match(r.stderr, new RegExp(`${CONFIG_REL}: unreadable`));
125
- });
126
- });
127
-
128
- describe('procedures CLI — usage errors → exit 2', () => {
129
- it('unknown <activity> → exit 2', () => {
130
- const r = run(['plan-foo']);
131
- assert.equal(r.code, 2);
132
- assert.match(r.stderr, /unknown activity "plan-foo"/);
133
- });
134
-
135
- it('missing <activity> → exit 2', () => {
136
- const r = run([]);
137
- assert.equal(r.code, 2);
138
- assert.match(r.stderr, /missing <activity>/);
139
- });
140
-
141
- it('a bare --override <recipe> (no slot) → exit 2', () => {
142
- const r = run(['plan-authoring', '--override', 'council']);
143
- assert.equal(r.code, 2);
144
- assert.match(r.stderr, /--override must be <slot>=<recipe>/);
145
- });
146
-
147
- it('--override with an unknown slot for the activity → exit 2', () => {
148
- const r = run(['plan-authoring', '--override', 'execute=delegated']);
149
- assert.equal(r.code, 2);
150
- assert.match(r.stderr, /unknown slot "execute" for activity "plan-authoring"/);
151
- });
152
-
153
- it('--override with a recipe invalid for the slot → exit 2', () => {
154
- const r = run(['plan-authoring', '--override', 'review=delegated']);
155
- assert.equal(r.code, 2);
156
- assert.match(r.stderr, /invalid recipe "delegated" for review slot/);
157
- });
158
-
159
- it('a duplicate --override for the same slot → exit 2', () => {
160
- const r = run(['plan-execution', '--override', 'review=council', '--override', 'review=solo']);
161
- assert.equal(r.code, 2);
162
- assert.match(r.stderr, /duplicate override for slot "review"/);
163
- });
164
- });
165
-
166
- describe('procedures CLI — override resolution (degrades loudly, still exit 0)', () => {
167
- it('an UNSATISFIABLE explicit override degrades loudly and exits 0 with a warning', () => {
168
- // council needs two ready reviewers; only codex is ready → degrade to reviewed, flagged loud.
169
- const r = run(['plan-authoring', '--override', 'review=council', '--json'], { codex: READY, agy: NEEDS_SKILL });
170
- assert.equal(r.code, 0, r.stderr);
171
- const j = JSON.parse(r.stdout);
172
- assert.equal(j.slots.review.recipe, 'reviewed');
173
- assert.equal(j.slots.review.degradedFrom, 'council');
174
- assert.equal(j.slots.review.source, 'override');
175
- assert.equal(j.warnings.length, 1, 'an unsatisfiable override is surfaced as a loud warning');
176
- assert.match(j.warnings[0], /could not be satisfied/);
177
- });
178
-
179
- it('the same override in human mode prints a ⚠ warning line', () => {
180
- const r = run(['plan-authoring', '--override', 'review=council'], { codex: READY, agy: NEEDS_SKILL });
181
- assert.equal(r.code, 0);
182
- assert.match(r.stdout, /warnings:/);
183
- assert.match(r.stdout, /⚠/);
184
- });
185
-
186
- it('a satisfiable override holds with no warning (exit 0)', () => {
187
- const r = run(['plan-authoring', '--override', 'review=council', '--json'], { codex: READY, agy: READY });
188
- assert.equal(r.code, 0);
189
- const j = JSON.parse(r.stdout);
190
- assert.equal(j.slots.review.recipe, 'council');
191
- assert.equal(j.warnings.length, 0);
192
- });
193
- });
194
-
195
- describe('procedures CLI — a backend-detection failure does NOT break activity resolution', () => {
196
- // A corrupt / unreadable bridge can make the detector throw. Detection is a SECONDARY input (it only
197
- // refines the recipe), so a throw must NOT surface as a config/engine error (exit 1) — resolution
198
- // floors at Solo and the failure is a loud warning, exit 0.
199
- const throwingDetect = () => {
200
- throw Object.assign(new Error('corrupt bridge manifest (EISDIR)'), { code: 'EISDIR' });
201
- };
202
-
203
- it('detect() throwing → exit 0, a warning, and every slot floors at solo', () => {
204
- const r = main(['plan-execution', '--json'], { cwd, env: { AGENT_WORKFLOW_ENGINE_DIR: ENGINE_DIR }, detect: throwingDetect });
205
- assert.equal(r.code, 0, r.stderr);
206
- const j = JSON.parse(r.stdout);
207
- assert.equal(j.slots.execute.recipe, 'solo');
208
- assert.equal(j.slots.review.recipe, 'solo');
209
- assert.ok(j.warnings.some((w) => /backend detection failed/.test(w)), 'the detection failure is surfaced as a warning');
210
- });
211
-
212
- it('the same failure in human mode prints a ⚠ warning, still exit 0', () => {
213
- const r = main(['plan-authoring'], { cwd, env: { AGENT_WORKFLOW_ENGINE_DIR: ENGINE_DIR }, detect: throwingDetect });
214
- assert.equal(r.code, 0);
215
- assert.match(r.stdout, /backend detection failed/);
216
- assert.match(r.stdout, /review: solo/);
217
- });
218
- });
219
-
220
- describe('procedures CLI — --json schema (§2.0)', () => {
221
- it('emits activity, section, per-slot resolution, configSource, warnings', () => {
222
- const r = run(['plan-execution', '--json'], { codex: READY, agy: NEEDS_SKILL });
223
- assert.equal(r.code, 0, r.stderr);
224
- const j = JSON.parse(r.stdout);
225
- assert.deepEqual(Object.keys(j).sort(), ['activity', 'configSource', 'section', 'slots', 'warnings'].sort());
226
- assert.equal(j.activity, 'plan-execution');
227
- assert.match(j.section, /## plan-execution/);
228
- for (const slot of ['execute', 'review']) {
229
- assert.ok(j.slots[slot], `slot ${slot} present`);
230
- assert.deepEqual(Object.keys(j.slots[slot]).sort(), ['degradedFrom', 'reason', 'recipe', 'source'].sort());
231
- }
232
- assert.ok(Array.isArray(j.warnings));
233
- });
234
- });
235
-
236
- describe('procedures CLI — --help is read-only and exits 0', () => {
237
- it('prints usage naming both activities and exits 0', () => {
238
- const r = run(['--help']);
239
- assert.equal(r.code, 0);
240
- assert.match(r.stdout, /plan-authoring/);
241
- assert.match(r.stdout, /plan-execution/);
242
- assert.match(r.stdout, /never commits/);
243
- });
244
- });
245
-
246
- // §4.0 — an installed engine too old to ship references/procedures.md must FAIL LOUDLY (exit 1 with a
247
- // clear "upgrade the engine" message), never a cryptic read error. A temp fixture models a VALID
248
- // methodology-engine that ships every fragment EXCEPT procedures.md (i.e. engine < 1.3.0).
249
- describe('procedures CLI — engine too old (no procedures.md) → loud exit 1', () => {
250
- const makeOldEngine = () => {
251
- const dir = mkdtempSync(join(tmpdir(), 'old-engine-'));
252
- const manifest = {
253
- family: 'agent-workflow',
254
- schema: 1,
255
- name: 'agent-workflow-engine',
256
- kind: 'methodology-engine',
257
- version: '1.2.0',
258
- available: true,
259
- provides: ['plan'],
260
- roles: {},
261
- };
262
- writeFileSync(join(dir, 'capability.json'), JSON.stringify(manifest, null, 2));
263
- writeFileSync(join(dir, 'SKILL.md'), "---\nname: agent-workflow-engine\nmetadata:\n version: '1.2.0'\n---\n# engine\n");
264
- mkdirSync(join(dir, 'references'), { recursive: true });
265
- writeFileSync(join(dir, 'references', 'methodology-slot.md'), '> methodology fragment\n');
266
- // deliberately NO references/procedures.md
267
- return dir;
268
- };
269
-
270
- it('exits 1 with an upgrade-the-engine message (not a cryptic fs error)', () => {
271
- const oldEngine = makeOldEngine();
272
- try {
273
- const r = main(['plan-authoring'], { cwd, env: { AGENT_WORKFLOW_ENGINE_DIR: oldEngine }, detect: detect(READY, READY) });
274
- assert.equal(r.code, 1);
275
- assert.match(r.stderr, /procedures\.md/, 'the error names the missing fragment');
276
- assert.match(r.stderr, /upgrade the engine|@latest init/i, 'the error tells the user to upgrade the engine');
277
- } finally {
278
- rmSync(oldEngine, { recursive: true, force: true });
279
- }
280
- });
281
- });
282
-
283
- describe('extractSection (unit) — boundary + verbatim', () => {
284
- const FIXTURE = ['# Title', '', '## plan-authoring', '', 'Slots: review', '', 'step one', '', '## plan-execution', '', 'Slots: execute, review', '', 'step two', ''].join('\n');
285
-
286
- it('returns the requested section, heading-to-next-heading', () => {
287
- const sec = extractSection(FIXTURE, 'plan-authoring');
288
- assert.match(sec, /## plan-authoring/);
289
- assert.match(sec, /Slots: review/);
290
- assert.match(sec, /step one/);
291
- assert.ok(!sec.includes('plan-execution'), 'stops before the next ## heading');
292
- });
293
-
294
- it('extracts the LAST section to EOF', () => {
295
- const sec = extractSection(FIXTURE, 'plan-execution');
296
- assert.match(sec, /step two/);
297
- assert.ok(!sec.includes('plan-authoring'));
298
- });
299
-
300
- it('throws (engine-too-old) when the activity section is absent', () => {
301
- assert.throws(() => extractSection(FIXTURE, 'plan-nope'), /has no "## plan-nope" section/);
302
- });
303
- });