@agentworkforce/cli 0.14.0 → 0.15.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.
- package/CHANGELOG.md +8 -0
- package/README.md +109 -70
- package/dist/cli.d.ts.map +1 -1
- package/dist/cli.js +68 -20
- package/dist/cli.js.map +1 -1
- package/dist/cli.test.js +126 -33
- package/dist/cli.test.js.map +1 -1
- package/dist/local-personas.d.ts +4 -2
- package/dist/local-personas.d.ts.map +1 -1
- package/dist/local-personas.js +129 -26
- package/dist/local-personas.js.map +1 -1
- package/dist/local-personas.test.js +292 -54
- package/dist/local-personas.test.js.map +1 -1
- package/package.json +4 -4
|
@@ -22,34 +22,33 @@ function withLayers(fn) {
|
|
|
22
22
|
function writeJson(path, value) {
|
|
23
23
|
writeFileSync(path, JSON.stringify(value));
|
|
24
24
|
}
|
|
25
|
-
test('user layer extends library and merges env', () => {
|
|
25
|
+
test('user layer extends internal library and merges env', () => {
|
|
26
26
|
withLayers(({ cwd, homeDir }) => {
|
|
27
|
-
writeJson(join(homeDir, 'my-
|
|
28
|
-
id: 'my-
|
|
29
|
-
extends: '
|
|
30
|
-
env: {
|
|
27
|
+
writeJson(join(homeDir, 'my-persona.json'), {
|
|
28
|
+
id: 'my-persona',
|
|
29
|
+
extends: 'persona-maker',
|
|
30
|
+
env: { API_TOKEN: '$API_TOKEN', EXTRA: 'literal' }
|
|
31
31
|
});
|
|
32
32
|
const loaded = loadLocalPersonas({ cwd, homeDir });
|
|
33
33
|
assert.deepEqual(loaded.warnings, []);
|
|
34
|
-
const spec = loaded.byId.get('my-
|
|
34
|
+
const spec = loaded.byId.get('my-persona');
|
|
35
35
|
assert.ok(spec);
|
|
36
|
-
assert.equal(loaded.sources.get('my-
|
|
37
|
-
assert.equal(spec.intent, '
|
|
38
|
-
assert.equal(spec.env?.
|
|
36
|
+
assert.equal(loaded.sources.get('my-persona'), 'user');
|
|
37
|
+
assert.equal(spec.intent, 'persona-authoring');
|
|
38
|
+
assert.equal(spec.env?.API_TOKEN, '$API_TOKEN');
|
|
39
39
|
assert.equal(spec.env?.EXTRA, 'literal');
|
|
40
|
-
assert.ok(spec.mcpServers?.posthog);
|
|
41
40
|
});
|
|
42
41
|
});
|
|
43
42
|
test('cwd layer overrides user layer for the same id', () => {
|
|
44
43
|
withLayers(({ cwd, homeDir, pwdDir }) => {
|
|
45
44
|
writeJson(join(homeDir, 'ph.json'), {
|
|
46
45
|
id: 'ph',
|
|
47
|
-
extends: '
|
|
46
|
+
extends: 'persona-maker',
|
|
48
47
|
env: { POSTHOG_API_KEY: 'home-value', FROM_HOME: 'yes' }
|
|
49
48
|
});
|
|
50
49
|
writeJson(join(pwdDir, 'ph.json'), {
|
|
51
50
|
id: 'ph',
|
|
52
|
-
extends: '
|
|
51
|
+
extends: 'persona-maker',
|
|
53
52
|
env: { POSTHOG_API_KEY: 'pwd-value' }
|
|
54
53
|
});
|
|
55
54
|
const loaded = loadLocalPersonas({ cwd, homeDir });
|
|
@@ -57,25 +56,25 @@ test('cwd layer overrides user layer for the same id', () => {
|
|
|
57
56
|
const spec = loaded.byId.get('ph');
|
|
58
57
|
assert.equal(loaded.sources.get('ph'), 'cwd');
|
|
59
58
|
// cwd's env wins; note user is NOT layered here (cwd overrides user as a whole,
|
|
60
|
-
// not merges). Base is
|
|
59
|
+
// not merges). Base is persona-maker directly via cwd's own `extends`.
|
|
61
60
|
assert.equal(spec?.env?.POSTHOG_API_KEY, 'pwd-value');
|
|
62
61
|
assert.equal(spec?.env?.FROM_HOME, undefined);
|
|
63
62
|
});
|
|
64
63
|
});
|
|
65
|
-
test('implicit same-id extends: cwd file with id=
|
|
64
|
+
test('implicit same-id extends: cwd file with id=persona-maker inherits from library persona-maker', () => {
|
|
66
65
|
withLayers(({ cwd, homeDir, pwdDir }) => {
|
|
67
|
-
writeJson(join(pwdDir, '
|
|
68
|
-
id: '
|
|
66
|
+
writeJson(join(pwdDir, 'persona-maker.json'), {
|
|
67
|
+
id: 'persona-maker',
|
|
69
68
|
env: { POSTHOG_API_KEY: '$POSTHOG_API_KEY' }
|
|
70
69
|
});
|
|
71
70
|
const loaded = loadLocalPersonas({ cwd, homeDir });
|
|
72
71
|
assert.deepEqual(loaded.warnings, []);
|
|
73
|
-
const spec = loaded.byId.get('
|
|
72
|
+
const spec = loaded.byId.get('persona-maker');
|
|
74
73
|
assert.ok(spec);
|
|
75
|
-
assert.equal(loaded.sources.get('
|
|
76
|
-
// Library fields still flow through (
|
|
77
|
-
assert.
|
|
78
|
-
assert.equal(spec.
|
|
74
|
+
assert.equal(loaded.sources.get('persona-maker'), 'cwd');
|
|
75
|
+
// Library fields still flow through (tiers, description, inputs).
|
|
76
|
+
assert.equal(spec.tiers.best.harness, 'codex');
|
|
77
|
+
assert.equal(spec.inputs?.CREATE_MODE.default, 'local');
|
|
79
78
|
assert.equal(spec.env?.POSTHOG_API_KEY, '$POSTHOG_API_KEY');
|
|
80
79
|
});
|
|
81
80
|
});
|
|
@@ -84,7 +83,7 @@ test('cascade chain: cwd extends user extends library', () => {
|
|
|
84
83
|
// user defines a mid-layer override that adds a default env key.
|
|
85
84
|
writeJson(join(homeDir, 'ph-base.json'), {
|
|
86
85
|
id: 'ph-base',
|
|
87
|
-
extends: '
|
|
86
|
+
extends: 'persona-maker',
|
|
88
87
|
env: { DEFAULT_ORG: 'acme' }
|
|
89
88
|
});
|
|
90
89
|
// cwd extends the user persona (not the library directly).
|
|
@@ -100,8 +99,8 @@ test('cascade chain: cwd extends user extends library', () => {
|
|
|
100
99
|
// Both env keys flow through the chain.
|
|
101
100
|
assert.equal(prod.env?.DEFAULT_ORG, 'acme');
|
|
102
101
|
assert.equal(prod.env?.POSTHOG_API_KEY, '$PROD_KEY');
|
|
103
|
-
//
|
|
104
|
-
assert.
|
|
102
|
+
// Library inputs are preserved through the chain.
|
|
103
|
+
assert.equal(prod.inputs?.CREATE_MODE.default, 'local');
|
|
105
104
|
});
|
|
106
105
|
});
|
|
107
106
|
test('configured source directories cascade in configured order', () => {
|
|
@@ -110,7 +109,7 @@ test('configured source directories cascade in configured order', () => {
|
|
|
110
109
|
mkdirSync(extraDir, { recursive: true });
|
|
111
110
|
writeJson(join(homeDir, 'ph.json'), {
|
|
112
111
|
id: 'ph',
|
|
113
|
-
extends: '
|
|
112
|
+
extends: 'persona-maker',
|
|
114
113
|
env: { DEFAULT_ORG: 'acme', POSTHOG_API_KEY: 'user-key' }
|
|
115
114
|
});
|
|
116
115
|
writeJson(join(extraDir, 'ph.json'), {
|
|
@@ -142,7 +141,7 @@ test('cwd workforce config file is not scanned as a persona', () => {
|
|
|
142
141
|
});
|
|
143
142
|
writeJson(join(pwdDir, 'ph.json'), {
|
|
144
143
|
id: 'ph',
|
|
145
|
-
extends: '
|
|
144
|
+
extends: 'persona-maker'
|
|
146
145
|
});
|
|
147
146
|
const loaded = loadLocalPersonas({ cwd, homeDir });
|
|
148
147
|
assert.deepEqual(loaded.warnings, []);
|
|
@@ -153,7 +152,7 @@ test('per-tier override only replaces the named tier, leaving others untouched',
|
|
|
153
152
|
withLayers(({ cwd, homeDir }) => {
|
|
154
153
|
writeJson(join(homeDir, 'ph.json'), {
|
|
155
154
|
id: 'ph',
|
|
156
|
-
extends: '
|
|
155
|
+
extends: 'persona-maker',
|
|
157
156
|
tiers: {
|
|
158
157
|
best: { model: 'claude-sonnet-4-6' }
|
|
159
158
|
}
|
|
@@ -162,17 +161,17 @@ test('per-tier override only replaces the named tier, leaving others untouched',
|
|
|
162
161
|
const spec = loaded.byId.get('ph');
|
|
163
162
|
assert.equal(spec?.tiers.best.model, 'claude-sonnet-4-6');
|
|
164
163
|
// systemPrompt is inherited on the overridden tier too (partial per-tier merge).
|
|
165
|
-
assert.
|
|
164
|
+
assert.equal(spec?.tiers.best.systemPrompt, '$TASK_DESCRIPTION');
|
|
166
165
|
// Other tiers untouched.
|
|
167
|
-
assert.equal(spec?.tiers['best-value'].model, '
|
|
168
|
-
assert.equal(spec?.tiers.minimum.model, '
|
|
166
|
+
assert.equal(spec?.tiers['best-value'].model, 'opencode/gpt-5-nano');
|
|
167
|
+
assert.equal(spec?.tiers.minimum.model, 'opencode/minimax-m2.5-free');
|
|
169
168
|
});
|
|
170
169
|
});
|
|
171
170
|
test('top-level systemPrompt replaces prompt across all inherited tiers', () => {
|
|
172
171
|
withLayers(({ cwd, homeDir }) => {
|
|
173
172
|
writeJson(join(homeDir, 'ph.json'), {
|
|
174
173
|
id: 'ph',
|
|
175
|
-
extends: '
|
|
174
|
+
extends: 'persona-maker',
|
|
176
175
|
systemPrompt: 'You answer only yes or no.'
|
|
177
176
|
});
|
|
178
177
|
const loaded = loadLocalPersonas({ cwd, homeDir });
|
|
@@ -198,7 +197,7 @@ test('warns when an overlay combines extends with standalone intent', () => {
|
|
|
198
197
|
withLayers(({ cwd, homeDir }) => {
|
|
199
198
|
writeJson(join(homeDir, 'broken.json'), {
|
|
200
199
|
id: 'broken',
|
|
201
|
-
extends: '
|
|
200
|
+
extends: 'persona-maker',
|
|
202
201
|
intent: 'review'
|
|
203
202
|
});
|
|
204
203
|
const loaded = loadLocalPersonas({ cwd, homeDir });
|
|
@@ -209,8 +208,8 @@ test('warns when an overlay combines extends with standalone intent', () => {
|
|
|
209
208
|
});
|
|
210
209
|
test('warns on duplicate ids within a single layer', () => {
|
|
211
210
|
withLayers(({ cwd, homeDir }) => {
|
|
212
|
-
writeJson(join(homeDir, 'a.json'), { id: 'dup', extends: '
|
|
213
|
-
writeJson(join(homeDir, 'b.json'), { id: 'dup', extends: '
|
|
211
|
+
writeJson(join(homeDir, 'a.json'), { id: 'dup', extends: 'persona-maker' });
|
|
212
|
+
writeJson(join(homeDir, 'b.json'), { id: 'dup', extends: 'persona-maker' });
|
|
214
213
|
const loaded = loadLocalPersonas({ cwd, homeDir });
|
|
215
214
|
assert.equal(loaded.byId.size, 1);
|
|
216
215
|
assert.equal(loaded.warnings.length, 1);
|
|
@@ -219,9 +218,9 @@ test('warns on duplicate ids within a single layer', () => {
|
|
|
219
218
|
});
|
|
220
219
|
test('AGENT_WORKFORCE_CONFIG_DIR is trimmed before use (whitespace tolerated)', () => {
|
|
221
220
|
withLayers(({ cwd, homeDir }) => {
|
|
222
|
-
writeJson(join(homeDir, 'my-
|
|
223
|
-
id: 'my-
|
|
224
|
-
extends: '
|
|
221
|
+
writeJson(join(homeDir, 'my-persona.json'), {
|
|
222
|
+
id: 'my-persona',
|
|
223
|
+
extends: 'persona-maker'
|
|
225
224
|
});
|
|
226
225
|
const prev = process.env.AGENT_WORKFORCE_CONFIG_DIR;
|
|
227
226
|
process.env.AGENT_WORKFORCE_CONFIG_DIR = ` ${homeDir} `;
|
|
@@ -229,7 +228,7 @@ test('AGENT_WORKFORCE_CONFIG_DIR is trimmed before use (whitespace tolerated)',
|
|
|
229
228
|
// Don't pass homeDir — force the loader to fall back to the env var,
|
|
230
229
|
// which is the code path that used to return the untrimmed value.
|
|
231
230
|
const loaded = loadLocalPersonas({ cwd });
|
|
232
|
-
assert.ok(loaded.byId.has('my-
|
|
231
|
+
assert.ok(loaded.byId.has('my-persona'), 'persona should load despite whitespace in AGENT_WORKFORCE_CONFIG_DIR');
|
|
233
232
|
}
|
|
234
233
|
finally {
|
|
235
234
|
if (prev === undefined)
|
|
@@ -285,12 +284,11 @@ test('returns empty result when neither layer exists', () => {
|
|
|
285
284
|
});
|
|
286
285
|
test('permissions merge: allow/deny union dedup, mode overrides', () => {
|
|
287
286
|
withLayers(({ cwd, homeDir, pwdDir }) => {
|
|
288
|
-
//
|
|
289
|
-
//
|
|
290
|
-
// another allow and overrides the mode.
|
|
287
|
+
// User adds a Bash deny + sets default mode; cwd adds an allow and
|
|
288
|
+
// overrides the mode.
|
|
291
289
|
writeJson(join(homeDir, 'ph.json'), {
|
|
292
290
|
id: 'ph',
|
|
293
|
-
extends: '
|
|
291
|
+
extends: 'persona-maker',
|
|
294
292
|
permissions: {
|
|
295
293
|
deny: ['Bash(rm -rf *)'],
|
|
296
294
|
mode: 'default'
|
|
@@ -307,7 +305,7 @@ test('permissions merge: allow/deny union dedup, mode overrides', () => {
|
|
|
307
305
|
const loaded = loadLocalPersonas({ cwd, homeDir });
|
|
308
306
|
assert.deepEqual(loaded.warnings, []);
|
|
309
307
|
const spec = loaded.byId.get('ph');
|
|
310
|
-
assert.deepEqual(spec?.permissions?.allow?.slice().sort(), ['Bash(git *)'
|
|
308
|
+
assert.deepEqual(spec?.permissions?.allow?.slice().sort(), ['Bash(git *)']);
|
|
311
309
|
assert.deepEqual(spec?.permissions?.deny, ['Bash(rm -rf *)']);
|
|
312
310
|
assert.equal(spec?.permissions?.mode, 'acceptEdits');
|
|
313
311
|
});
|
|
@@ -316,12 +314,38 @@ test('permissions allow list dedupes across layers', () => {
|
|
|
316
314
|
withLayers(({ cwd, homeDir }) => {
|
|
317
315
|
writeJson(join(homeDir, 'ph.json'), {
|
|
318
316
|
id: 'ph',
|
|
319
|
-
extends: '
|
|
320
|
-
permissions: { allow: ['
|
|
317
|
+
extends: 'persona-maker',
|
|
318
|
+
permissions: { allow: ['Bash(git *)', 'Bash(git *)'] }
|
|
321
319
|
});
|
|
322
320
|
const loaded = loadLocalPersonas({ cwd, homeDir });
|
|
323
321
|
const spec = loaded.byId.get('ph');
|
|
324
|
-
assert.deepEqual(spec?.permissions?.allow, ['
|
|
322
|
+
assert.deepEqual(spec?.permissions?.allow, ['Bash(git *)']);
|
|
323
|
+
});
|
|
324
|
+
});
|
|
325
|
+
test('codex harness settings merge across local persona layers', () => {
|
|
326
|
+
withLayers(({ cwd, homeDir }) => {
|
|
327
|
+
writeJson(join(homeDir, 'planner.json'), {
|
|
328
|
+
id: 'planner',
|
|
329
|
+
extends: 'persona-maker',
|
|
330
|
+
tiers: {
|
|
331
|
+
best: {
|
|
332
|
+
harnessSettings: {
|
|
333
|
+
sandboxMode: 'workspace-write',
|
|
334
|
+
approvalPolicy: 'on-request',
|
|
335
|
+
workspaceWriteNetworkAccess: true,
|
|
336
|
+
webSearch: true
|
|
337
|
+
}
|
|
338
|
+
}
|
|
339
|
+
}
|
|
340
|
+
});
|
|
341
|
+
const loaded = loadLocalPersonas({ cwd, homeDir });
|
|
342
|
+
assert.deepEqual(loaded.warnings, []);
|
|
343
|
+
const settings = loaded.byId.get('planner')?.tiers.best.harnessSettings;
|
|
344
|
+
assert.equal(settings?.reasoning, 'high');
|
|
345
|
+
assert.equal(settings?.sandboxMode, 'workspace-write');
|
|
346
|
+
assert.equal(settings?.approvalPolicy, 'on-request');
|
|
347
|
+
assert.equal(settings?.workspaceWriteNetworkAccess, true);
|
|
348
|
+
assert.equal(settings?.webSearch, true);
|
|
325
349
|
});
|
|
326
350
|
});
|
|
327
351
|
test('inputs merge across local persona layers', () => {
|
|
@@ -355,7 +379,7 @@ test('mount patterns merge across local persona layers', () => {
|
|
|
355
379
|
withLayers(({ cwd, homeDir, pwdDir }) => {
|
|
356
380
|
writeJson(join(homeDir, 'site-agent.json'), {
|
|
357
381
|
id: 'site-agent',
|
|
358
|
-
extends: '
|
|
382
|
+
extends: 'persona-maker',
|
|
359
383
|
mount: {
|
|
360
384
|
ignoredPatterns: ['.env*'],
|
|
361
385
|
readonlyPatterns: ['*']
|
|
@@ -415,6 +439,220 @@ test('inputs are preserved on standalone local personas', () => {
|
|
|
415
439
|
assert.equal(spec?.inputs?.TARGET_DIR.default, '/tmp/reviews');
|
|
416
440
|
});
|
|
417
441
|
});
|
|
442
|
+
test('standalone local personas accept arbitrary intent names', () => {
|
|
443
|
+
withLayers(({ cwd, homeDir }) => {
|
|
444
|
+
writeJson(join(homeDir, 'nextjs-web-steward.json'), {
|
|
445
|
+
id: 'nextjs-web-steward',
|
|
446
|
+
intent: 'nextjs-web-steward',
|
|
447
|
+
tags: ['implementation'],
|
|
448
|
+
description: 'Stewards Next.js web surfaces.',
|
|
449
|
+
tiers: {
|
|
450
|
+
best: {
|
|
451
|
+
harness: 'codex',
|
|
452
|
+
model: 'openai-codex/gpt-5.3-codex',
|
|
453
|
+
systemPrompt: 'Implement Next.js UI work carefully.',
|
|
454
|
+
harnessSettings: { reasoning: 'high', timeoutSeconds: 30 }
|
|
455
|
+
},
|
|
456
|
+
'best-value': {
|
|
457
|
+
harness: 'opencode',
|
|
458
|
+
model: 'opencode/gpt-5-nano',
|
|
459
|
+
systemPrompt: 'Implement Next.js UI work carefully.',
|
|
460
|
+
harnessSettings: { reasoning: 'medium', timeoutSeconds: 30 }
|
|
461
|
+
},
|
|
462
|
+
minimum: {
|
|
463
|
+
harness: 'opencode',
|
|
464
|
+
model: 'opencode/minimax-m2.5-free',
|
|
465
|
+
systemPrompt: 'Implement Next.js UI work carefully.',
|
|
466
|
+
harnessSettings: { reasoning: 'low', timeoutSeconds: 30 }
|
|
467
|
+
}
|
|
468
|
+
}
|
|
469
|
+
});
|
|
470
|
+
const loaded = loadLocalPersonas({ cwd, homeDir });
|
|
471
|
+
assert.deepEqual(loaded.warnings, []);
|
|
472
|
+
const spec = loaded.byId.get('nextjs-web-steward');
|
|
473
|
+
assert.equal(spec?.intent, 'nextjs-web-steward');
|
|
474
|
+
});
|
|
475
|
+
});
|
|
476
|
+
test('standalone local personas can use inlined AGENTS content as prompt fallback', () => {
|
|
477
|
+
withLayers(({ cwd, homeDir }) => {
|
|
478
|
+
writeJson(join(homeDir, 'nextjs-web-steward.json'), {
|
|
479
|
+
id: 'nextjs-web-steward',
|
|
480
|
+
intent: 'nextjs-web-stewardship',
|
|
481
|
+
tags: ['implementation'],
|
|
482
|
+
description: 'Stewards Next.js web surfaces.',
|
|
483
|
+
agentsMd: 'AGENTS.md',
|
|
484
|
+
agentsMdContent: '# Next.js Web Steward\n\nOwn implementation work in web/.\n',
|
|
485
|
+
tiers: {
|
|
486
|
+
best: {
|
|
487
|
+
harness: 'codex',
|
|
488
|
+
model: 'openai-codex/gpt-5.3-codex',
|
|
489
|
+
systemPrompt: '',
|
|
490
|
+
harnessSettings: { reasoning: 'high', timeoutSeconds: 30 }
|
|
491
|
+
},
|
|
492
|
+
'best-value': {
|
|
493
|
+
harness: 'opencode',
|
|
494
|
+
model: 'opencode/gpt-5-nano',
|
|
495
|
+
systemPrompt: '',
|
|
496
|
+
harnessSettings: { reasoning: 'medium', timeoutSeconds: 30 }
|
|
497
|
+
},
|
|
498
|
+
minimum: {
|
|
499
|
+
harness: 'opencode',
|
|
500
|
+
model: 'opencode/minimax-m2.5-free',
|
|
501
|
+
systemPrompt: '',
|
|
502
|
+
harnessSettings: { reasoning: 'low', timeoutSeconds: 30 }
|
|
503
|
+
}
|
|
504
|
+
}
|
|
505
|
+
});
|
|
506
|
+
const loaded = loadLocalPersonas({ cwd, homeDir });
|
|
507
|
+
assert.deepEqual(loaded.warnings, []);
|
|
508
|
+
const spec = loaded.byId.get('nextjs-web-steward');
|
|
509
|
+
assert.match(spec?.tiers.best.systemPrompt ?? '', /Next\.js Web Steward/);
|
|
510
|
+
assert.match(spec?.agentsMdContent ?? '', /implementation work/);
|
|
511
|
+
assert.equal(spec?.agentsMd, undefined);
|
|
512
|
+
});
|
|
513
|
+
});
|
|
514
|
+
test('standalone local personas can use tier-level inlined AGENTS content as prompt fallback', () => {
|
|
515
|
+
withLayers(({ cwd, homeDir }) => {
|
|
516
|
+
writeJson(join(homeDir, 'nextjs-web-steward.json'), {
|
|
517
|
+
id: 'nextjs-web-steward',
|
|
518
|
+
intent: 'nextjs-web-stewardship',
|
|
519
|
+
tags: ['implementation'],
|
|
520
|
+
description: 'Stewards Next.js web surfaces.',
|
|
521
|
+
agentsMdContent: '# Default steward prompt\n',
|
|
522
|
+
tiers: {
|
|
523
|
+
best: {
|
|
524
|
+
harness: 'codex',
|
|
525
|
+
model: 'openai-codex/gpt-5.3-codex',
|
|
526
|
+
systemPrompt: '',
|
|
527
|
+
agentsMdContent: '# Best steward prompt\n',
|
|
528
|
+
harnessSettings: { reasoning: 'high', timeoutSeconds: 30 }
|
|
529
|
+
},
|
|
530
|
+
'best-value': {
|
|
531
|
+
harness: 'opencode',
|
|
532
|
+
model: 'opencode/gpt-5-nano',
|
|
533
|
+
systemPrompt: '',
|
|
534
|
+
harnessSettings: { reasoning: 'medium', timeoutSeconds: 30 }
|
|
535
|
+
},
|
|
536
|
+
minimum: {
|
|
537
|
+
harness: 'opencode',
|
|
538
|
+
model: 'opencode/minimax-m2.5-free',
|
|
539
|
+
systemPrompt: '',
|
|
540
|
+
harnessSettings: { reasoning: 'low', timeoutSeconds: 30 }
|
|
541
|
+
}
|
|
542
|
+
}
|
|
543
|
+
});
|
|
544
|
+
const loaded = loadLocalPersonas({ cwd, homeDir });
|
|
545
|
+
assert.deepEqual(loaded.warnings, []);
|
|
546
|
+
const spec = loaded.byId.get('nextjs-web-steward');
|
|
547
|
+
assert.match(spec?.tiers.best.systemPrompt ?? '', /Best steward prompt/);
|
|
548
|
+
assert.match(spec?.tiers.best.agentsMdContent ?? '', /Best steward prompt/);
|
|
549
|
+
assert.match(spec?.tiers.minimum.systemPrompt ?? '', /Default steward prompt/);
|
|
550
|
+
});
|
|
551
|
+
});
|
|
552
|
+
test('rejects whitespace-only inlined sidecar content', () => {
|
|
553
|
+
withLayers(({ cwd, homeDir }) => {
|
|
554
|
+
writeJson(join(homeDir, 'blank-top-level.json'), {
|
|
555
|
+
id: 'blank-top-level',
|
|
556
|
+
intent: 'blank-top-level',
|
|
557
|
+
tags: ['implementation'],
|
|
558
|
+
description: 'Invalid blank sidecar content.',
|
|
559
|
+
agentsMdContent: ' ',
|
|
560
|
+
tiers: {
|
|
561
|
+
best: {
|
|
562
|
+
harness: 'codex',
|
|
563
|
+
model: 'openai-codex/gpt-5.3-codex',
|
|
564
|
+
systemPrompt: 'Prompt.',
|
|
565
|
+
harnessSettings: { reasoning: 'high', timeoutSeconds: 30 }
|
|
566
|
+
},
|
|
567
|
+
'best-value': {
|
|
568
|
+
harness: 'opencode',
|
|
569
|
+
model: 'opencode/gpt-5-nano',
|
|
570
|
+
systemPrompt: 'Prompt.',
|
|
571
|
+
harnessSettings: { reasoning: 'medium', timeoutSeconds: 30 }
|
|
572
|
+
},
|
|
573
|
+
minimum: {
|
|
574
|
+
harness: 'opencode',
|
|
575
|
+
model: 'opencode/minimax-m2.5-free',
|
|
576
|
+
systemPrompt: 'Prompt.',
|
|
577
|
+
harnessSettings: { reasoning: 'low', timeoutSeconds: 30 }
|
|
578
|
+
}
|
|
579
|
+
}
|
|
580
|
+
});
|
|
581
|
+
writeJson(join(homeDir, 'blank-tier.json'), {
|
|
582
|
+
id: 'blank-tier',
|
|
583
|
+
intent: 'blank-tier',
|
|
584
|
+
tags: ['implementation'],
|
|
585
|
+
description: 'Invalid blank tier sidecar content.',
|
|
586
|
+
tiers: {
|
|
587
|
+
best: {
|
|
588
|
+
harness: 'codex',
|
|
589
|
+
model: 'openai-codex/gpt-5.3-codex',
|
|
590
|
+
systemPrompt: 'Prompt.',
|
|
591
|
+
agentsMdContent: ' ',
|
|
592
|
+
harnessSettings: { reasoning: 'high', timeoutSeconds: 30 }
|
|
593
|
+
},
|
|
594
|
+
'best-value': {
|
|
595
|
+
harness: 'opencode',
|
|
596
|
+
model: 'opencode/gpt-5-nano',
|
|
597
|
+
systemPrompt: 'Prompt.',
|
|
598
|
+
harnessSettings: { reasoning: 'medium', timeoutSeconds: 30 }
|
|
599
|
+
},
|
|
600
|
+
minimum: {
|
|
601
|
+
harness: 'opencode',
|
|
602
|
+
model: 'opencode/minimax-m2.5-free',
|
|
603
|
+
systemPrompt: 'Prompt.',
|
|
604
|
+
harnessSettings: { reasoning: 'low', timeoutSeconds: 30 }
|
|
605
|
+
}
|
|
606
|
+
}
|
|
607
|
+
});
|
|
608
|
+
const loaded = loadLocalPersonas({ cwd, homeDir });
|
|
609
|
+
assert.equal(loaded.byId.has('blank-top-level'), false);
|
|
610
|
+
assert.equal(loaded.byId.has('blank-tier'), false);
|
|
611
|
+
assert.match(loaded.warnings.join('\n'), /blank-top-level\.json.*agentsMdContent must be a non-empty string/);
|
|
612
|
+
assert.match(loaded.warnings.join('\n'), /blank-tier\.json.*agentsMdContent must be a non-empty string/);
|
|
613
|
+
});
|
|
614
|
+
});
|
|
615
|
+
test('extends can resolve a lower-layer standalone persona by intent', () => {
|
|
616
|
+
withLayers(({ cwd, homeDir, pwdDir }) => {
|
|
617
|
+
writeJson(join(homeDir, 'steward-base.json'), {
|
|
618
|
+
id: 'steward-base',
|
|
619
|
+
intent: 'nextjs-web-stewardship',
|
|
620
|
+
tags: ['implementation'],
|
|
621
|
+
description: 'Base steward persona.',
|
|
622
|
+
tiers: {
|
|
623
|
+
best: {
|
|
624
|
+
harness: 'codex',
|
|
625
|
+
model: 'openai-codex/gpt-5.3-codex',
|
|
626
|
+
systemPrompt: 'Base prompt.',
|
|
627
|
+
harnessSettings: { reasoning: 'high', timeoutSeconds: 30 }
|
|
628
|
+
},
|
|
629
|
+
'best-value': {
|
|
630
|
+
harness: 'opencode',
|
|
631
|
+
model: 'opencode/gpt-5-nano',
|
|
632
|
+
systemPrompt: 'Base prompt.',
|
|
633
|
+
harnessSettings: { reasoning: 'medium', timeoutSeconds: 30 }
|
|
634
|
+
},
|
|
635
|
+
minimum: {
|
|
636
|
+
harness: 'opencode',
|
|
637
|
+
model: 'opencode/minimax-m2.5-free',
|
|
638
|
+
systemPrompt: 'Base prompt.',
|
|
639
|
+
harnessSettings: { reasoning: 'low', timeoutSeconds: 30 }
|
|
640
|
+
}
|
|
641
|
+
}
|
|
642
|
+
});
|
|
643
|
+
writeJson(join(pwdDir, 'project-steward.json'), {
|
|
644
|
+
id: 'project-steward',
|
|
645
|
+
extends: 'nextjs-web-stewardship',
|
|
646
|
+
env: { PROJECT: 'web' }
|
|
647
|
+
});
|
|
648
|
+
const loaded = loadLocalPersonas({ cwd, homeDir });
|
|
649
|
+
assert.deepEqual(loaded.warnings, []);
|
|
650
|
+
const spec = loaded.byId.get('project-steward');
|
|
651
|
+
assert.equal(spec?.description, 'Base steward persona.');
|
|
652
|
+
assert.equal(spec?.intent, 'nextjs-web-stewardship');
|
|
653
|
+
assert.equal(spec?.env?.PROJECT, 'web');
|
|
654
|
+
});
|
|
655
|
+
});
|
|
418
656
|
test('surfaces parse errors as per-file warnings without throwing', () => {
|
|
419
657
|
withLayers(({ cwd, homeDir }) => {
|
|
420
658
|
writeFileSync(join(homeDir, 'bad.json'), '{ not valid json');
|
|
@@ -429,7 +667,7 @@ test('top-level claudeMd resolves to absolute path anchored to its layer dir', (
|
|
|
429
667
|
writeFileSync(join(homeDir, 'persona.md'), '# Persona-specific guidance\n');
|
|
430
668
|
writeJson(join(homeDir, 'docs-bot.json'), {
|
|
431
669
|
id: 'docs-bot',
|
|
432
|
-
extends: '
|
|
670
|
+
extends: 'persona-maker',
|
|
433
671
|
claudeMd: 'persona.md',
|
|
434
672
|
claudeMdMode: 'extend'
|
|
435
673
|
});
|
|
@@ -446,7 +684,7 @@ test('per-tier claudeMd overrides top-level path; mode resolves independently',
|
|
|
446
684
|
writeFileSync(join(homeDir, 'best.md'), '# best\n');
|
|
447
685
|
writeJson(join(homeDir, 'p.json'), {
|
|
448
686
|
id: 'p',
|
|
449
|
-
extends: '
|
|
687
|
+
extends: 'persona-maker',
|
|
450
688
|
claudeMd: 'top.md',
|
|
451
689
|
claudeMdMode: 'extend',
|
|
452
690
|
tiers: {
|
|
@@ -468,7 +706,7 @@ test('rejects claudeMd with .. segment', () => {
|
|
|
468
706
|
withLayers(({ cwd, homeDir }) => {
|
|
469
707
|
writeJson(join(homeDir, 'p.json'), {
|
|
470
708
|
id: 'p',
|
|
471
|
-
extends: '
|
|
709
|
+
extends: 'persona-maker',
|
|
472
710
|
claudeMd: '../escape.md'
|
|
473
711
|
});
|
|
474
712
|
const loaded = loadLocalPersonas({ cwd, homeDir });
|
|
@@ -483,7 +721,7 @@ test('rejects Windows-rooted sidecar paths (backslash, UNC, drive-letter)', () =
|
|
|
483
721
|
withLayers(({ cwd, homeDir }) => {
|
|
484
722
|
writeJson(join(homeDir, 'p.json'), {
|
|
485
723
|
id: 'p',
|
|
486
|
-
extends: '
|
|
724
|
+
extends: 'persona-maker',
|
|
487
725
|
claudeMd: bad
|
|
488
726
|
});
|
|
489
727
|
const loaded = loadLocalPersonas({ cwd, homeDir });
|
|
@@ -496,7 +734,7 @@ test('rejects non-md sidecar path', () => {
|
|
|
496
734
|
withLayers(({ cwd, homeDir }) => {
|
|
497
735
|
writeJson(join(homeDir, 'p.json'), {
|
|
498
736
|
id: 'p',
|
|
499
|
-
extends: '
|
|
737
|
+
extends: 'persona-maker',
|
|
500
738
|
claudeMd: 'persona.txt'
|
|
501
739
|
});
|
|
502
740
|
const loaded = loadLocalPersonas({ cwd, homeDir });
|
|
@@ -513,7 +751,7 @@ test('mode-only override: tier mode flips while inheriting top-level path', () =
|
|
|
513
751
|
writeFileSync(join(homeDir, 'top.md'), '# top\n');
|
|
514
752
|
writeJson(join(homeDir, 'sidecar-base.json'), {
|
|
515
753
|
id: 'sidecar-base',
|
|
516
|
-
extends: '
|
|
754
|
+
extends: 'persona-maker',
|
|
517
755
|
claudeMd: 'top.md',
|
|
518
756
|
claudeMdMode: 'overwrite'
|
|
519
757
|
});
|
|
@@ -539,7 +777,7 @@ test('missing sidecar file produces a warning, not a throw', () => {
|
|
|
539
777
|
withLayers(({ cwd, homeDir }) => {
|
|
540
778
|
writeJson(join(homeDir, 'p.json'), {
|
|
541
779
|
id: 'p',
|
|
542
|
-
extends: '
|
|
780
|
+
extends: 'persona-maker',
|
|
543
781
|
claudeMd: 'missing.md'
|
|
544
782
|
});
|
|
545
783
|
const loaded = loadLocalPersonas({ cwd, homeDir });
|