@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/CHANGELOG.md +8 -0
- package/README.md +74 -67
- package/dist/cli.d.ts +1 -0
- package/dist/cli.d.ts.map +1 -1
- package/dist/cli.js +179 -17
- package/dist/cli.js.map +1 -1
- package/dist/cli.test.js +91 -33
- package/dist/cli.test.js.map +1 -1
- package/dist/local-personas.d.ts.map +1 -1
- package/dist/local-personas.js +2 -2
- package/dist/local-personas.js.map +1 -1
- package/dist/local-personas.test.js +53 -55
- package/dist/local-personas.test.js.map +1 -1
- package/package.json +4 -4
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
|
|
121
|
-
agentworkforce agent
|
|
122
|
-
agentworkforce agent
|
|
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
|
|
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
|
|
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 =
|
|
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
|
|
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
|
|
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 =
|
|
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
|
|
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 = {
|
|
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 = {
|
|
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) => {
|