@agentworkforce/cli 0.15.0 → 0.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.
package/dist/cli.js CHANGED
@@ -1,11 +1,11 @@
1
1
  #!/usr/bin/env node
2
2
  import { spawn, spawnSync } from 'node:child_process';
3
3
  import { randomBytes } from 'node:crypto';
4
- import { appendFileSync, existsSync, mkdirSync, readFileSync, readSync, rmSync, statSync, writeFileSync } from 'node:fs';
5
- import { constants, homedir } from 'node:os';
4
+ import { appendFileSync, existsSync, mkdirSync, mkdtempSync, readFileSync, readSync, rmSync, statSync, writeFileSync } from 'node:fs';
5
+ import { constants, homedir, tmpdir } from 'node:os';
6
6
  import { dirname, isAbsolute, join, resolve as resolvePath } from 'node:path';
7
7
  import { pathToFileURL } from 'node:url';
8
- import { HARNESS_VALUES, PERSONA_TAGS, PERSONA_TIERS, personaCatalog, resolveSidecar, routingProfiles, useSelection } from '@agentworkforce/workload-router';
8
+ import { HARNESS_VALUES, materializeSkills, PERSONA_TAGS, PERSONA_TIERS, listBuiltInPersonas, personaCatalog, resolveSidecar, routingProfiles, useSelection } from '@agentworkforce/workload-router';
9
9
  import { buildInteractiveSpec, detectHarnesses, formatDropWarnings, MissingPersonaInputError, renderPersonaInputs, resolvePersonaInputs, resolveMcpServersLenient, resolveStringMapLenient } from '@agentworkforce/harness-kit';
10
10
  import { launchOnMount, readAgentDotfiles } from '@relayfile/local-mount';
11
11
  import ora from 'ora';
@@ -57,6 +57,26 @@ Commands:
57
57
  Disable launch metadata recording.
58
58
  Also disabled by
59
59
  AGENTWORKFORCE_LAUNCH_METADATA=0.
60
+ --dry-run Validate the persona without
61
+ spawning the harness or burning
62
+ tier-model tokens. Three checks:
63
+ (1) sidecar — claudeMd / agentsMd
64
+ filename refs are readable;
65
+ (2) harness spec — permissions /
66
+ mcpServers / harness-settings
67
+ shape is accepted by the harness
68
+ translator; (3) skills — each
69
+ \`skills[].source\` is run through
70
+ its real installer (npx skills
71
+ add / npx prpm install) inside a
72
+ fresh temp dir, with per-skill
73
+ pass/fail reporting. Use this in
74
+ the persona-author loop to catch
75
+ hallucinated skill names and
76
+ malformed config before a persona
77
+ ships. Temp dir is removed on
78
+ success, kept on failure for
79
+ inspection.
60
80
  list [flags] List available personas from the cascade (cwd →
61
81
  configured persona dirs → library). By default shows
62
82
  one row per persona at the recommended tier for its
@@ -117,11 +137,11 @@ configured persona dir is ~/.agentworkforce/workforce/personas.
117
137
  Examples:
118
138
  agentworkforce create
119
139
  agentworkforce create --save-in-directory=user
120
- agentworkforce agent npm-provenance-publisher@best
121
- agentworkforce agent my-posthog@best
122
- agentworkforce agent review@best-value
140
+ agentworkforce install @agentworkforce/personas-core --persona code-reviewer
141
+ agentworkforce agent code-reviewer@best-value
142
+ agentworkforce agent my-reviewer@best
123
143
  agentworkforce list
124
- agentworkforce show posthog
144
+ agentworkforce show code-reviewer
125
145
  agentworkforce install @agentrelay/personas --persona relay-orchestrator
126
146
  agentworkforce install ./local-personas --overwrite
127
147
  agentworkforce sources list
@@ -159,7 +179,7 @@ function collectKnownPersonas() {
159
179
  description: spec.description
160
180
  });
161
181
  }
162
- for (const spec of Object.values(personaCatalog)) {
182
+ for (const spec of listBuiltInPersonas()) {
163
183
  if (byName.has(spec.id))
164
184
  continue;
165
185
  byName.set(spec.id, {
@@ -196,11 +216,14 @@ function resolveSpec(key) {
196
216
  const catalogAsIntent = personaCatalog[key];
197
217
  if (catalogAsIntent)
198
218
  return catalogAsIntent;
199
- const byId = Object.values(personaCatalog).find((p) => p.id === key);
219
+ const byId = listBuiltInPersonas().find((p) => p.id === key);
200
220
  if (byId)
201
221
  return byId;
222
+ const packHint = 'Optional first-party personas are installed from packs, for example:\n' +
223
+ ' agentworkforce install @agentworkforce/personas-core\n' +
224
+ ' agentworkforce install @agentrelay/personas';
202
225
  return {
203
- error: `Unknown persona "${key}". Known personas:\n${formatNameDescriptionTable(collectKnownPersonas())}`
226
+ error: `Unknown persona "${key}". Known personas:\n${formatNameDescriptionTable(collectKnownPersonas())}\n\n${packHint}`
204
227
  };
205
228
  }
206
229
  function parseSelector(sel) {
@@ -674,6 +697,128 @@ export function decideCleanMode(harness, installInRepo = false) {
674
697
  }
675
698
  return { useClean: false };
676
699
  }
700
+ /**
701
+ * Persona authoring dry-run. Used by persona authors to verify a persona
702
+ * actually launches before it ships, without spawning the harness or
703
+ * running the agent's tier model. Three checks, in order:
704
+ *
705
+ * 1. Sidecar resolution — `claudeMd` / `agentsMd` filename references
706
+ * that point at unreadable files would brick the launch silently
707
+ * today (lenient warning); dry-run promotes them to failures.
708
+ * 2. Interactive spec build — runs `buildInteractiveSpec` on the
709
+ * resolved selection. Catches malformed `permissions` patterns,
710
+ * `mcpServers` shape errors, and missing required harness fields.
711
+ * Pure / no side effects.
712
+ * 3. Skill install — runs each `skills[].source` through its real
713
+ * installer (`npx skills add` / `npx prpm install`) inside a fresh
714
+ * temp dir and reports per-skill pass/fail.
715
+ *
716
+ * Temp dir is deleted on success so dry-run leaves no trace; on a skill
717
+ * failure it is left in place and its path printed so the author can
718
+ * inspect the installer's output. Checks 1 and 2 run before any temp
719
+ * dir is created so an early failure doesn't litter `/tmp`.
720
+ */
721
+ function runDryRun(selection) {
722
+ const inputResolution = resolvePersonaInputs(selection.inputs, selection.inputValues, process.env);
723
+ const renderedSystemPrompt = renderPersonaInputs(selection.runtime.systemPrompt, inputResolution.values);
724
+ const renderedClaudeContent = selection.claudeMdContent !== undefined
725
+ ? renderPersonaInputs(selection.claudeMdContent, inputResolution.values)
726
+ : undefined;
727
+ const renderedAgentsContent = selection.agentsMdContent !== undefined
728
+ ? renderPersonaInputs(selection.agentsMdContent, inputResolution.values)
729
+ : undefined;
730
+ const effectiveSelection = {
731
+ ...selection,
732
+ runtime: { ...selection.runtime, systemPrompt: renderedSystemPrompt },
733
+ ...(renderedClaudeContent !== undefined ? { claudeMdContent: renderedClaudeContent } : {}),
734
+ ...(renderedAgentsContent !== undefined ? { agentsMdContent: renderedAgentsContent } : {})
735
+ };
736
+ const { runtime, personaId, tier } = effectiveSelection;
737
+ process.stderr.write(`→ ${personaId} [${tier}] via ${runtime.harness} (${runtime.model}) [DRY-RUN]\n`);
738
+ // Check 1: sidecar resolution. A loadSidecarForSelection warning means
739
+ // the persona points `claudeMd` / `agentsMd` at a file we couldn't
740
+ // read; the launch path degrades to a warning today, which silently
741
+ // drops the persona's operating spec. In dry-run that's a failure.
742
+ const sidecarLookup = loadSidecarForSelection(effectiveSelection);
743
+ if (sidecarLookup.warning) {
744
+ process.stderr.write(`✗ sidecar: ${sidecarLookup.warning}\n`);
745
+ return 1;
746
+ }
747
+ process.stderr.write(`✓ sidecar: ${sidecarLookup.sidecar ? sidecarLookup.sidecar.mountFile : '(none)'}\n`);
748
+ // Check 2: harness-kit translation. buildInteractiveSpec validates
749
+ // permissions shape, mcpServers shape, and required runtime fields.
750
+ // We resolve env + mcp leniently (same as the live launch path) so
751
+ // the spec call sees the same inputs it would at runtime.
752
+ const callerEnv = { ...process.env, ...inputResolution.values };
753
+ const envResolution = resolveStringMapLenient(effectiveSelection.env, callerEnv, 'env');
754
+ const mcpResolution = resolveMcpServersLenient(effectiveSelection.mcpServers, callerEnv);
755
+ emitDropWarnings(formatDropWarnings(envResolution.dropped, mcpResolution.dropped, mcpResolution.droppedServers));
756
+ let spec;
757
+ try {
758
+ spec = buildInteractiveSpec({
759
+ harness: runtime.harness,
760
+ personaId,
761
+ model: runtime.model,
762
+ systemPrompt: runtime.systemPrompt,
763
+ harnessSettings: runtime.harnessSettings,
764
+ mcpServers: mcpResolution.servers,
765
+ permissions: effectiveSelection.permissions
766
+ });
767
+ }
768
+ catch (err) {
769
+ const msg = err instanceof Error ? err.message : String(err);
770
+ process.stderr.write(`✗ harness spec: ${msg}\n`);
771
+ return 1;
772
+ }
773
+ for (const w of spec.warnings)
774
+ process.stderr.write(`warning: ${w}\n`);
775
+ process.stderr.write(`✓ harness spec: ${spec.bin} (${spec.args.length} args)\n`);
776
+ // Check 3: skill installs.
777
+ const plan = materializeSkills(effectiveSelection.skills, runtime.harness);
778
+ if (plan.installs.length === 0) {
779
+ process.stderr.write('✓ skills: (none declared)\n');
780
+ process.stderr.write('✓ dry-run ok\n');
781
+ return 0;
782
+ }
783
+ const tempDir = mkdtempSync(join(tmpdir(), `agentworkforce-dryrun-${personaId}-`));
784
+ process.stderr.write(`• temp dir: ${tempDir}\n`);
785
+ process.stderr.write(`• installing ${plan.installs.length} skill(s)\n`);
786
+ const failures = [];
787
+ for (const inst of plan.installs) {
788
+ const [bin, ...args] = inst.installCommand;
789
+ if (!bin) {
790
+ process.stderr.write(` skipped ${inst.skillId}: empty install command\n`);
791
+ continue;
792
+ }
793
+ process.stderr.write(`• ${inst.skillId} (${inst.sourceKind}) → ${inst.packageRef}\n`);
794
+ const res = spawnSync(bin, args, { stdio: 'inherit', shell: false, cwd: tempDir });
795
+ const code = subprocessExitCode(res);
796
+ if (code === 0) {
797
+ process.stderr.write(` ✓ ${inst.skillId}\n`);
798
+ }
799
+ else {
800
+ process.stderr.write(` ✗ ${inst.skillId} (exit ${code})\n`);
801
+ failures.push({ skillId: inst.skillId, exitCode: code });
802
+ }
803
+ }
804
+ if (failures.length === 0) {
805
+ try {
806
+ rmSync(tempDir, { recursive: true, force: true });
807
+ }
808
+ catch (err) {
809
+ const msg = err instanceof Error ? err.message : String(err);
810
+ process.stderr.write(`warning: failed to clean up ${tempDir}: ${msg}\n`);
811
+ }
812
+ process.stderr.write(`✓ dry-run ok: ${plan.installs.length} skill(s) installed cleanly\n`);
813
+ return 0;
814
+ }
815
+ process.stderr.write(`✗ dry-run failed: ${failures.length} of ${plan.installs.length} skill(s) failed\n`);
816
+ process.stderr.write(` inspect ${tempDir} for installer output\n`);
817
+ for (const f of failures) {
818
+ process.stderr.write(` - ${f.skillId} (exit ${f.exitCode})\n`);
819
+ }
820
+ return failures[0]?.exitCode || 1;
821
+ }
677
822
  async function runInteractive(selection, options) {
678
823
  const inputResolution = resolvePersonaInputs(selection.inputs, selection.inputValues, process.env);
679
824
  const renderedSystemPrompt = renderPersonaInputs(selection.runtime.systemPrompt, inputResolution.values);
@@ -748,7 +893,7 @@ async function runInteractive(selection, options) {
748
893
  const resolvedMcp = mcpResolution.servers;
749
894
  // In session mode the install command is never `:` — it at minimum runs
750
895
  // the plugin scaffold (mkdir + manifest + symlink) so `--plugin-dir` has a
751
- // valid target even for skill-less personas like posthog. Gate on the
896
+ // valid target even for skill-less local personas. Gate on the
752
897
  // command string rather than `installs.length` so we don't skip that.
753
898
  const skillIds = install.plan.installs.map((i) => i.skillId).join(', ');
754
899
  const installLabel = install.plan.installs.length === 0
@@ -1374,7 +1519,7 @@ function collectPersonaRows() {
1374
1519
  pushSpec(spec, local.sources.get(id) ?? 'library');
1375
1520
  seen.add(id);
1376
1521
  }
1377
- for (const spec of Object.values(personaCatalog)) {
1522
+ for (const spec of listBuiltInPersonas()) {
1378
1523
  if (seen.has(spec.id))
1379
1524
  continue;
1380
1525
  pushSpec(spec, 'library');
@@ -1588,7 +1733,7 @@ function resolveShowTarget(selector, all) {
1588
1733
  spec = byIntent;
1589
1734
  }
1590
1735
  else {
1591
- const byId = Object.values(personaCatalog).find((p) => p.id === key);
1736
+ const byId = listBuiltInPersonas().find((p) => p.id === key);
1592
1737
  if (byId)
1593
1738
  spec = byId;
1594
1739
  }
@@ -1843,6 +1988,10 @@ async function runAgentSelector(selector, flags, inputValues) {
1843
1988
  ...buildSelection(target.spec, target.tier, target.kind),
1844
1989
  ...(inputValues ? { inputValues } : {})
1845
1990
  };
1991
+ if (flags.dryRun) {
1992
+ const code = runDryRun(selection);
1993
+ process.exit(code);
1994
+ }
1846
1995
  const code = await runInteractive(selection, {
1847
1996
  installInRepo: flags.installInRepo,
1848
1997
  noLaunchMetadata: flags.noLaunchMetadata,
@@ -1858,7 +2007,7 @@ async function runAgentSelector(selector, flags, inputValues) {
1858
2007
  */
1859
2008
  export function buildPickCandidates() {
1860
2009
  const byId = new Map();
1861
- for (const spec of Object.values(personaCatalog)) {
2010
+ for (const spec of listBuiltInPersonas()) {
1862
2011
  byId.set(spec.id, {
1863
2012
  id: spec.id,
1864
2013
  intent: spec.intent,
@@ -1969,7 +2118,7 @@ async function runPick(args) {
1969
2118
  ...buildCreateInputValues(target),
1970
2119
  TASK_DESCRIPTION: task
1971
2120
  };
1972
- await runAgentSelector(CREATE_SELECTOR, { installInRepo: false, noLaunchMetadata: false }, inputValues);
2121
+ await runAgentSelector(CREATE_SELECTOR, { installInRepo: false, noLaunchMetadata: false, dryRun: false }, inputValues);
1973
2122
  // runAgentSelector terminates via process.exit; this satisfies TS's
1974
2123
  // reachable-end-point check for the `Promise<never>` return type.
1975
2124
  process.exit(0);
@@ -2030,7 +2179,11 @@ export async function main() {
2030
2179
  await runAgentSelector(selector, flags);
2031
2180
  }
2032
2181
  export function parseAgentArgs(args) {
2033
- const flags = { installInRepo: false, noLaunchMetadata: false };
2182
+ const flags = {
2183
+ installInRepo: false,
2184
+ noLaunchMetadata: false,
2185
+ dryRun: false
2186
+ };
2034
2187
  const positional = [];
2035
2188
  let seenDoubleDash = false;
2036
2189
  for (const arg of args) {
@@ -2050,6 +2203,10 @@ export function parseAgentArgs(args) {
2050
2203
  flags.noLaunchMetadata = true;
2051
2204
  continue;
2052
2205
  }
2206
+ if (arg === '--dry-run') {
2207
+ flags.dryRun = true;
2208
+ continue;
2209
+ }
2053
2210
  if (arg === '-h' || arg === '--help') {
2054
2211
  process.stdout.write(USAGE);
2055
2212
  process.exit(0);
@@ -2059,7 +2216,12 @@ export function parseAgentArgs(args) {
2059
2216
  return { flags, positional };
2060
2217
  }
2061
2218
  export function parseCreateArgs(args) {
2062
- const flags = { installInRepo: false, noLaunchMetadata: false, saveDefault: false };
2219
+ const flags = {
2220
+ installInRepo: false,
2221
+ noLaunchMetadata: false,
2222
+ dryRun: false,
2223
+ saveDefault: false
2224
+ };
2063
2225
  let seenDoubleDash = false;
2064
2226
  const positional = [];
2065
2227
  const valueOf = (i, flag) => {