@execuro-sw-ecosystem/sw-ecosystem-agentic-harness 0.1.6 → 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/CHANGELOG.md +35 -0
- package/CONTRIBUTING.md +13 -4
- package/README.md +38 -23
- package/bin/cli.mjs +17 -13
- package/lib/commands.mjs +71 -67
- package/lib/guide.mjs +28 -20
- package/lib/hosts.mjs +8 -8
- package/lib/lock.mjs +10 -5
- package/lib/out.mjs +27 -6
- package/lib/prompt.mjs +18 -23
- package/lib/render.mjs +364 -0
- package/package.json +1 -1
- package/plugin.json +1 -1
- package/skills/sw-setup/SKILL.md +14 -12
- package/skills/sw-setup/reference/rows.md +20 -14
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**
|
|
25
|
-
to write into, resolved in order: \`--
|
|
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 --
|
|
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 \`--
|
|
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
|
|
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
|
-
|
|
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
|
-
--
|
|
51
|
-
see the
|
|
52
|
-
step 1 above; a skill
|
|
53
|
-
\`install\`/\`apply\` must
|
|
54
|
-
pass it, since it has no
|
|
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
|
-
-
|
|
77
|
-
\`next_step\` string: the literal command to run next.
|
|
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 \`--
|
|
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
|
|
104
|
-
names the restart or reload each one needs — pass it on to the user
|
|
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 `
|
|
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
|
|
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(
|
|
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
|
|
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.
|
|
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.
|
|
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 `
|
|
255
|
-
const selected = ctx.
|
|
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
|
-
|
|
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
|
-
|
|
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, {
|
|
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
|
-
|
|
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 (
|
|
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
|
-
//
|
|
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
|
-
//
|
|
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:
|
|
10
|
-
//
|
|
11
|
-
|
|
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
|
-
|
|
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
|
|
2
|
-
// of the
|
|
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:
|
|
7
|
-
//
|
|
8
|
-
//
|
|
9
|
-
// `process.stdin.isTTY` itself — the caller decides
|
|
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 `
|
|
24
|
-
* word "all", or an empty string — both of the latter two mean every
|
|
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,
|
|
24
|
+
export function parseSelection(input, agents) {
|
|
30
25
|
const trimmed = (input ?? '').trim();
|
|
31
|
-
if (trimmed === '' || trimmed.toLowerCase() === 'all') return [...
|
|
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 >
|
|
39
|
-
const id =
|
|
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
|
|
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
|
|
49
|
+
export async function askAgents(agents) {
|
|
55
50
|
const rl = createInterface({ input: process.stdin, output: process.stderr, terminal: true });
|
|
56
|
-
const menu =
|
|
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,
|
|
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
|
}
|