@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.
@@ -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-posthog.json'), {
28
- id: 'my-posthog',
29
- extends: 'posthog',
30
- env: { POSTHOG_API_KEY: '$POSTHOG_API_KEY', EXTRA: 'literal' }
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-posthog');
34
+ const spec = loaded.byId.get('my-persona');
35
35
  assert.ok(spec);
36
- assert.equal(loaded.sources.get('my-posthog'), 'user');
37
- assert.equal(spec.intent, 'posthog');
38
- assert.equal(spec.env?.POSTHOG_API_KEY, '$POSTHOG_API_KEY');
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: 'posthog',
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: 'posthog',
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 library/posthog directly via cwd's own `extends`.
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=posthog inherits from library posthog', () => {
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, 'posthog.json'), {
68
- id: 'posthog',
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('posthog');
72
+ const spec = loaded.byId.get('persona-maker');
74
73
  assert.ok(spec);
75
- assert.equal(loaded.sources.get('posthog'), 'cwd');
76
- // Library fields still flow through (mcpServers, tiers, description).
77
- assert.ok(spec.mcpServers?.posthog);
78
- assert.equal(spec.tiers.best.harness, 'claude');
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: 'posthog',
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
- // MCP from library is preserved.
104
- assert.ok(prod.mcpServers?.posthog);
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: 'posthog',
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: 'posthog'
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: 'posthog',
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.match(spec?.tiers.best.systemPrompt ?? '', /PostHog/);
164
+ assert.equal(spec?.tiers.best.systemPrompt, '$TASK_DESCRIPTION');
166
165
  // Other tiers untouched.
167
- assert.equal(spec?.tiers['best-value'].model, 'claude-sonnet-4-6');
168
- assert.equal(spec?.tiers.minimum.model, 'claude-haiku-4-5-20251001');
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: 'posthog',
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: 'posthog',
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: 'posthog' });
213
- writeJson(join(homeDir, 'b.json'), { id: 'dup', extends: 'posthog' });
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-posthog.json'), {
223
- id: 'my-posthog',
224
- extends: 'posthog'
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-posthog'), 'persona should load despite whitespace in AGENT_WORKFORCE_CONFIG_DIR');
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
- // Base posthog already has permissions.allow = ["mcp__posthog"] in the
289
- // library file. User adds a Bash deny + sets default mode; cwd adds
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: 'posthog',
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 *)', 'mcp__posthog'].sort());
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: 'posthog',
320
- permissions: { allow: ['mcp__posthog'] }
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, ['mcp__posthog']);
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: 'frontend-implementer',
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: 'documentation',
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: 'documentation',
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: 'documentation',
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: 'documentation',
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: 'documentation',
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: 'documentation',
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: 'documentation',
780
+ extends: 'persona-maker',
543
781
  claudeMd: 'missing.md'
544
782
  });
545
783
  const loaded = loadLocalPersonas({ cwd, homeDir });