@execuro-sw-ecosystem/sw-ecosystem-agentic-harness 0.1.5 → 0.1.7

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/lib/guide.mjs CHANGED
@@ -21,16 +21,16 @@ registers the MCP servers and permission rules they depend on.
21
21
 
22
22
  1. \`${RUN} install\`
23
23
  The whole install in one command. It never prompts for consent — typing
24
- the verb is the consent — but it does still need to know **which** host(s)
25
- to write into, resolved in order: \`--host\`, if given; else the hosts a
26
- lock file already records, if this is a re-run; else, with a real
24
+ the verb is the consent — but it does still need to know **which** coding
25
+ agent(s) to write into, resolved in order: \`--agent\`, if given; else the
26
+ agents a lock file already records, if this is a re-run; else, with a real
27
27
  terminal attached, an interactive picker; else it exits 2 with the four
28
- \`${RUN} install --host <h>\` commands as \`help\` rather than guess or
28
+ \`${RUN} install --agent <a>\` commands as \`help\` rather than guess or
29
29
  hang — Codex's question tool does not block outside Plan mode, and CI has
30
- no terminal, so a skill invoking this must always pass \`--host\`.
30
+ no terminal, so a skill invoking this must always pass \`--agent\`.
31
31
 
32
32
  2. \`${RUN} status\`
33
- Read-only, optional. Reports which hosts are present, what is already
33
+ Read-only, optional. Reports which agents are present, what is already
34
34
  installed, and anything that has drifted since it was installed. Always safe.
35
35
 
36
36
  3. \`${RUN} plan\`
@@ -40,21 +40,26 @@ registers the MCP servers and permission rules they depend on.
40
40
  4. \`${RUN} apply --yes\`
41
41
  The scriptable name for the same write \`install\` performs — use it for
42
42
  re-runs and repairs. It refuses without \`--yes\`, and resolves which
43
- host(s) to touch the same way \`install\` does (see step 1).
43
+ agent(s) to touch the same way \`install\` does (see step 1).
44
44
 
45
45
  5. \`${RUN} uninstall --yes\`
46
46
  Removes what the lock file records and reverts the managed config blocks.
47
47
 
48
48
  ## Flags
49
49
 
50
- --host claude-code|codex|copilot|cursor select one host, repeatable —
51
- see the host-resolution order in
52
- step 1 above; a skill invoking
53
- \`install\`/\`apply\` must always
54
- pass it, since it has no terminal
50
+ --agent claude-code|codex|copilot|cursor select one coding agent,
51
+ repeatable — see the resolution
52
+ order in step 1 above; a skill
53
+ invoking \`install\`/\`apply\` must
54
+ always pass it, since it has no
55
+ terminal
55
56
  --scope project|user default: project
56
57
  --root <path> default: the current directory
57
58
  --no-extra-components skip the optional extra-component skills
59
+ --json print the result object instead of
60
+ the human summary — a skill or
61
+ agent always passes this
62
+ --verbose include the per-file detail
58
63
 
59
64
  ## Optional extra components
60
65
 
@@ -73,11 +78,14 @@ The exact command is also in a \`skipped\` action's \`remedy\`, and in
73
78
 
74
79
  ## What you can rely on
75
80
 
76
- - **stdout is exactly one JSON object.** Every successful result carries a
77
- \`next_step\` string: the literal command to run next. Errors carry a
78
- \`help\` array of runnable commands.
81
+ - **\`--json\` puts exactly one JSON object on stdout.** Every successful
82
+ result carries a \`next_step\` string: the literal command to run next.
83
+ Errors carry a \`help\` array of runnable commands. Without \`--json\`,
84
+ \`install\`, \`status\`, \`plan\` and \`uninstall\` print a short human
85
+ summary of that same object instead — same body, one view for a person and
86
+ one for a program. Anything parsing this output must pass \`--json\`.
79
87
  - **Exit codes:** 0 success, 2 usage error (your invocation was wrong,
80
- including \`install\`/\`apply\` run with no \`--host\`, no existing lock and
88
+ including \`install\`/\`apply\` run with no \`--agent\`, no existing lock and
81
89
  no terminal to ask on), 1 the work could not be done.
82
90
  - **Nothing is silently overwritten.** A file we did not install, or one you
83
91
  edited after we installed it, is reported as \`conflict\` or \`drift\` and
@@ -100,9 +108,9 @@ trailing commas is never rewritten at all; it becomes a manual step instead.
100
108
 
101
109
  ## After applying
102
110
 
103
- Every host reads its configuration at startup. The result's \`help\` array
104
- names the restart or reload each one needs — pass it on to the user rather
105
- than assuming the change is already live.
111
+ Every coding agent reads its configuration at startup. The result's \`help\`
112
+ array names the restart or reload each one needs — pass it on to the user
113
+ rather than assuming the change is already live.
106
114
 
107
115
  ## Keeping it current
108
116
 
@@ -122,7 +130,7 @@ export async function main() {
122
130
  return out.ok('guide', {
123
131
  version: PKG_VERSION,
124
132
  next_step: `${RUN} install`,
125
- help: [`${RUN} install`, `${RUN} status`, `${RUN} plan`, `${RUN} apply --yes`],
133
+ help: [`${RUN} install`, `${RUN} status --json`, `${RUN} plan --json`, `${RUN} apply --yes --json`],
126
134
  guide: GUIDE,
127
135
  });
128
136
  }
package/lib/hosts.mjs CHANGED
@@ -4,7 +4,7 @@
4
4
  // Detection is marker-directory existence only — no network, no process scan,
5
5
  // no `which`. It feeds `status` reporting only; it never selects which hosts
6
6
  // a write command touches. Which hosts get written to is decided by
7
- // commands.mjs's `resolveHosts()` chain (`--host`, then a recorded lock,
7
+ // commands.mjs's `resolveAgents()` chain (`--agent`, then a recorded lock,
8
8
  // then a TTY prompt, then a usage error) — detection has no vote there.
9
9
 
10
10
  import { homedir } from 'node:os';
@@ -17,7 +17,7 @@ import { renderTomlTable, tomlTableExists, findFence } from './merge.mjs';
17
17
  import { readTextMaybe } from './fsx.mjs';
18
18
  import { EXTRA_COMPONENTS } from './extra-components.mjs';
19
19
 
20
- export const HOSTS = ['claude-code', 'codex', 'copilot', 'cursor'];
20
+ export const AGENTS = ['claude-code', 'codex', 'copilot', 'cursor'];
21
21
 
22
22
  /** Where each host reads its skills from — the base every skill-copy and
23
23
  * extra-component skill-copy action is rooted at. */
@@ -45,7 +45,7 @@ export function hostRoot(scope) {
45
45
  * but it matches the documented pattern and keeps a slow stat from serialising.
46
46
  */
47
47
  export async function detectAll(ctx) {
48
- return Promise.all(HOSTS.map((id) => Promise.resolve(detect(id, ctx))));
48
+ return Promise.all(AGENTS.map((id) => Promise.resolve(detect(id, ctx))));
49
49
  }
50
50
 
51
51
  export function detect(id, ctx) {
@@ -67,10 +67,10 @@ export function detect(id, ctx) {
67
67
 
68
68
  // --------------------------------------------------------------- plans
69
69
 
70
- /** Build the full action list for the selected hosts. */
70
+ /** Build the full action list for the selected agents. */
71
71
  export function buildPlan(ctx) {
72
72
  const actions = [];
73
- for (const id of ctx.hosts) actions.push(...planFor(id, ctx));
73
+ for (const id of ctx.agents) actions.push(...planFor(id, ctx));
74
74
  // Shared targets are emitted per host; the id dedupe collapses them.
75
75
  return actions;
76
76
  }
@@ -249,10 +249,10 @@ function planCursor(ctx) {
249
249
  const out = [];
250
250
  // Cursor reads `.claude/skills/` and `.claude/agents/` directly. If Claude
251
251
  // Code is also being installed, or already is, there is nothing to duplicate.
252
- // `ctx.hosts` is narrowed to one host while previewing, so the question
252
+ // `ctx.agents` is narrowed to one agent while previewing, so the question
253
253
  // "is Claude Code also being installed?" must be asked of the user's actual
254
- // selection, which `selectedHosts` carries unchanged.
255
- const selected = ctx.selectedHosts ?? ctx.hosts;
254
+ // selection, which `selectedAgents` carries unchanged.
255
+ const selected = ctx.selectedAgents ?? ctx.agents;
256
256
  const claudeTrees = selected.includes('claude-code')
257
257
  || (isDir(abs(ctx.root, '.claude/skills')) && isDir(abs(ctx.root, '.claude/agents')));
258
258
  if (!claudeTrees) {
package/lib/lock.mjs CHANGED
@@ -28,7 +28,7 @@ export function empty(scope) {
28
28
  installed_at: null,
29
29
  updated_at: null,
30
30
  platform: process.platform,
31
- hosts: {},
31
+ agents: {},
32
32
  files: {},
33
33
  dirs: [],
34
34
  json_pointers: [],
@@ -57,7 +57,12 @@ export function read(root, scope) {
57
57
  `version of ${PKG_NAME} (${PKG_VERSION}) does not understand.`,
58
58
  );
59
59
  }
60
- return { ...empty(scope), ...lock };
60
+ // Locks written before the `--host` -> `--agent` rename record the
61
+ // selection under `hosts`. Read either, write `agents` — missing this
62
+ // would make every existing install look like it had never chosen an
63
+ // agent, and send it straight to the exit-2 "no --agent given" path.
64
+ const { hosts, ...rest } = lock;
65
+ return { ...empty(scope), ...rest, agents: lock.agents ?? hosts ?? {} };
61
66
  }
62
67
 
63
68
  export function write(root, lock) {
@@ -68,7 +73,7 @@ export function write(root, lock) {
68
73
  }
69
74
 
70
75
  /** Fold a batch of executed entries into the lock. */
71
- export function record(lock, entries, { host, version = PKG_VERSION } = {}) {
76
+ export function record(lock, entries, { agent, version = PKG_VERSION } = {}) {
72
77
  const next = {
73
78
  ...lock,
74
79
  files: { ...lock.files },
@@ -77,7 +82,7 @@ export function record(lock, entries, { host, version = PKG_VERSION } = {}) {
77
82
  rules: [...lock.rules],
78
83
  fences: [...lock.fences],
79
84
  manual: [...lock.manual],
80
- hosts: { ...lock.hosts },
85
+ agents: { ...lock.agents },
81
86
  };
82
87
  for (const entry of entries) {
83
88
  if (!entry) continue;
@@ -121,7 +126,7 @@ export function record(lock, entries, { host, version = PKG_VERSION } = {}) {
121
126
  break;
122
127
  }
123
128
  }
124
- if (host) next.hosts[host] = { installed_at: new Date().toISOString(), version };
129
+ if (agent) next.agents[agent] = { installed_at: new Date().toISOString(), version };
125
130
  return next;
126
131
  }
127
132
 
package/lib/out.mjs CHANGED
@@ -1,14 +1,19 @@
1
1
  // Output contract shared by every sw-ecosystem-agentic-harness subcommand.
2
2
  //
3
- // stdout carries exactly one JSON object and nothing else. Its keys are
3
+ // There is one result object per run. `--json` prints it verbatim: keys
4
4
  // ordered small-first with the big payload LAST, so an agent that reads only
5
5
  // the head of the output already knows whether it worked and what to run next.
6
+ // Without `--json` the same object is rendered for a human by lib/render.mjs —
7
+ // the object is the model, the renderer is only a view, and neither can drift
8
+ // from the other because there is only ever one body.
6
9
  //
7
- // stderr carries progress and warnings that are not part of the result.
10
+ // Human text goes to stdout: it is the result, not progress. stderr carries
11
+ // progress, warnings and the interactive picker — never part of the result.
8
12
  //
9
- // The contract: structured JSON on stdout, a mandatory `next_step` on every
10
- // success, `exit 2` on a usage error carrying a `help` array of runnable
11
- // commands.
13
+ // The contract: a mandatory `next_step` on every success, `exit 2` on a usage
14
+ // error carrying a `help` array of runnable commands.
15
+
16
+ import { render } from './render.mjs';
12
17
 
13
18
  export const EXIT_OK = 0;
14
19
  export const EXIT_FAIL = 1;
@@ -19,8 +24,24 @@ export function json(value) {
19
24
  return `${JSON.stringify(value, null, 2)}\n`;
20
25
  }
21
26
 
27
+ // Read from argv rather than threaded through every call: `bin/cli.mjs`
28
+ // dispatches some usage errors before any flag parsing has happened, and those
29
+ // must honour `--json` too. `setFormat`/`setVerbose` exist so a unit test can
30
+ // drive this without re-spawning the process.
31
+ let format = process.argv.includes('--json') ? 'json' : 'human';
32
+ let verbose = process.argv.includes('--verbose');
33
+
34
+ export function setFormat(value) { format = value === 'json' ? 'json' : 'human'; }
35
+ export function setVerbose(value) { verbose = value === true; }
36
+ export function isJson() { return format === 'json'; }
37
+
22
38
  function emit(body, code) {
23
- process.stdout.write(json(body));
39
+ // `guide` is already prose inside its own body; rendering it would only
40
+ // hide it. Everything else gets the human view unless `--json` asked.
41
+ const asJson = format === 'json' || body.command === 'guide';
42
+ process.stdout.write(asJson
43
+ ? json(body)
44
+ : render(body, { verbose, color: process.stdout.isTTY === true }));
24
45
  process.exitCode = code;
25
46
  return code;
26
47
  }
package/lib/prompt.mjs CHANGED
@@ -1,42 +1,37 @@
1
- // The interactive host picker. Used only when `apply`/`install` reach step 3
2
- // of the host-resolution chain: no `--host`, no lock to reuse, but a real
1
+ // The interactive agent picker. Used only when `apply`/`install` reach step 3
2
+ // of the agent-resolution chain: no `--agent`, no lock to reuse, but a real
3
3
  // terminal to ask on.
4
4
  //
5
5
  // node:readline only — the package has zero dependencies and that must not
6
- // change. Every prompt and echo goes to STDERR, never stdout: stdout carries
7
- // exactly one JSON object per run and every skill parses it, so a menu on
8
- // stdout would corrupt that contract. This module never inspects
9
- // `process.stdin.isTTY` itself — the caller decides whether to invoke it.
6
+ // change. Every prompt and echo goes to STDERR, never stdout: the picker is an
7
+ // interaction, not the result, and stdout carries the result alone — under
8
+ // `--json` a menu there would corrupt the object every skill parses.
9
+ // This module never inspects `process.stdin.isTTY` itself — the caller decides
10
+ // whether to invoke it.
10
11
 
11
12
  import { createInterface } from 'node:readline';
12
-
13
- const LABELS = {
14
- 'claude-code': 'Claude Code',
15
- codex: 'Codex',
16
- copilot: 'GitHub Copilot',
17
- cursor: 'Cursor',
18
- };
13
+ import { LABELS } from './render.mjs';
19
14
 
20
15
  /**
21
16
  * Pure parsing, no I/O — so it can be unit-tested without a TTY.
22
17
  *
23
- * Accepts comma-separated 1-based indexes into `hosts` (e.g. "1,3"), the
24
- * word "all", or an empty string — both of the latter two mean every host,
18
+ * Accepts comma-separated 1-based indexes into `agents` (e.g. "1,3"), the
19
+ * word "all", or an empty string — both of the latter two mean every agent,
25
20
  * made explicit rather than left as a silent default. Throws a short,
26
21
  * human-readable `Error` on anything else (out of range, not a number,
27
22
  * nothing at all).
28
23
  */
29
- export function parseSelection(input, hosts) {
24
+ export function parseSelection(input, agents) {
30
25
  const trimmed = (input ?? '').trim();
31
- if (trimmed === '' || trimmed.toLowerCase() === 'all') return [...hosts];
26
+ if (trimmed === '' || trimmed.toLowerCase() === 'all') return [...agents];
32
27
  const parts = trimmed.split(',').map((p) => p.trim()).filter(Boolean);
33
28
  if (!parts.length) throw new Error('no selection given');
34
29
  const chosen = [];
35
30
  for (const part of parts) {
36
31
  if (!/^\d+$/.test(part)) throw new Error(`not a number: "${part}"`);
37
32
  const n = Number(part);
38
- if (n < 1 || n > hosts.length) throw new Error(`out of range 1-${hosts.length}: ${part}`);
39
- const id = hosts[n - 1];
33
+ if (n < 1 || n > agents.length) throw new Error(`out of range 1-${agents.length}: ${part}`);
34
+ const id = agents[n - 1];
40
35
  if (!chosen.includes(id)) chosen.push(id);
41
36
  }
42
37
  return chosen;
@@ -47,20 +42,20 @@ function question(rl, text) {
47
42
  }
48
43
 
49
44
  /**
50
- * Ask which host(s) to install into. Re-asks once on unparseable input, then
45
+ * Ask which agent(s) to install into. Re-asks once on unparseable input, then
51
46
  * gives up and returns `null` — the caller turns that into the same exit-2
52
47
  * usage error the no-TTY path uses, so the output contract stays one shape.
53
48
  */
54
- export async function askHosts(hosts) {
49
+ export async function askAgents(agents) {
55
50
  const rl = createInterface({ input: process.stdin, output: process.stderr, terminal: true });
56
- const menu = hosts.map((h, i) => ` ${i + 1}. ${LABELS[h] ?? h}`).join('\n');
51
+ const menu = agents.map((a, i) => ` ${i + 1}. ${LABELS[a] ?? a}`).join('\n');
57
52
  const prompt = `Which coding agent(s) should this install into?\n${menu}\n`
58
53
  + 'Enter comma-separated numbers (e.g. "1,3"), "all", or press Enter for all: ';
59
54
  try {
60
55
  for (let attempt = 0; attempt < 2; attempt += 1) {
61
56
  const answer = await question(rl, attempt === 0 ? prompt : 'Not understood — try again: ');
62
57
  try {
63
- return parseSelection(answer, hosts);
58
+ return parseSelection(answer, agents);
64
59
  } catch {
65
60
  // Re-ask once; the loop's second pass falls through to null below.
66
61
  }