@jaybeeuu/agent-cortex 2.5.1 → 2.7.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 +45 -0
- package/README.md +68 -6
- package/agents/ralph/agent.md +4 -2
- package/bin/agent-cortex.mjs +176 -67
- package/bin/installers/claude.mjs +33 -8
- package/bin/installers/pi.mjs +11 -1
- package/docs/skills/skill-anatomy.md +212 -0
- package/lib/cli.mjs +70 -1
- package/lib/harness-update.mjs +254 -0
- package/lib/harness-versions.mjs +190 -0
- package/package.json +2 -1
- package/scripts/lib/docs.mjs +67 -0
- package/skills/planning/create-task/REFERENCE.md +11 -3
- package/skills/planning/create-task/SKILL.md +1 -1
- package/skills/planning/create-task/pipeline.json +8 -0
- package/skills/planning/create-task/scripts/create-chores.mjs +25 -7
- package/skills/planning/create-task/templates/housekeeping.md +65 -0
- package/skills/productivity/write-a-skill/SKILL.md +2 -2
- package/skills/review/refactor-skill/SKILL.md +2 -2
- package/skills/workflow/ralph/REFERENCE.md +38 -7
- package/skills/workflow/ralph/SKILL.md +5 -4
- package/skills/workflow/ralph/scripts/prune-worktrees.mjs +346 -0
- package/skills/workflow/run-pipeline-stage/SKILL.md +8 -5
- package/skills/workflow/run-pipeline-stage/playbooks/housekeeping.md +58 -0
- package/skills/workflow/run-pipeline-stage/prompts/stage-runner.md +13 -1
- package/token-map.README.md +14 -4
- package/token-map.json +6 -1
|
@@ -0,0 +1,212 @@
|
|
|
1
|
+
# Skill Anatomy
|
|
2
|
+
|
|
3
|
+
This document is the **canonical skill anatomy** for `agent-cortex` skills.
|
|
4
|
+
Use it as the source of truth for `SKILL.md` structure, language, and review quality gates.
|
|
5
|
+
|
|
6
|
+
## Front-matter spec
|
|
7
|
+
|
|
8
|
+
Required fields:
|
|
9
|
+
- `name`: kebab-case skill identifier.
|
|
10
|
+
- `description`: max 1024 characters.
|
|
11
|
+
|
|
12
|
+
Frontmatter is machine-validated: `pnpm lint` runs `check:skills`, which fails on a
|
|
13
|
+
frontmatter block that would make pi silently drop the skill.
|
|
14
|
+
|
|
15
|
+
Optional fields:
|
|
16
|
+
- `argument-hint`: input prompt shown to users at invocation.
|
|
17
|
+
- `disable-model-invocation`: hides the skill from automatic model routing; explicit
|
|
18
|
+
`/skill-name` invocation still works. Set it when the danger is a fuzzy description
|
|
19
|
+
match, e.g. a skill whose steps are destructive. Preserved and honoured by pi and
|
|
20
|
+
Claude Code. See `docs/research/disable-model-invocation.md`.
|
|
21
|
+
|
|
22
|
+
## Description format rules
|
|
23
|
+
|
|
24
|
+
- Target ≤200 characters even though max is 1024.
|
|
25
|
+
- Write in third-person declarative voice.
|
|
26
|
+
- Sentence 1: what the skill does.
|
|
27
|
+
- Sentence 2: starts with `Use when ...`.
|
|
28
|
+
- Include 2-4 natural user trigger phrases (quoted) such as "diagnose this" or "something is broken".
|
|
29
|
+
- Prefer user vocabulary over internal jargon.
|
|
30
|
+
|
|
31
|
+
## Voice and tone rules
|
|
32
|
+
|
|
33
|
+
- Write the body in imperative second-person: "Run X", "Check Y", "Report Z".
|
|
34
|
+
- Use positive framing; reserve "do not" only for hard safety/correctness boundaries.
|
|
35
|
+
- Add one rationale sentence for non-obvious rules so behavior generalizes in novel cases.
|
|
36
|
+
- Avoid ALL-CAPS emphasis.
|
|
37
|
+
|
|
38
|
+
## Tool references
|
|
39
|
+
|
|
40
|
+
Reference agent tools by token, never by a hardcoded harness-specific name such as `bash`,
|
|
41
|
+
`task`, or `read_agent`. A tool token is the tool's canonical authoring name prefixed with
|
|
42
|
+
`TOOL:` and wrapped in doubled curly braces — two `{` before it and two `}` after it. The
|
|
43
|
+
wrapper is literal syntax: installers match it and substitute the per-harness name from
|
|
44
|
+
`token-map.json` at install time. Add any new tool to the map's `tools` section in the same
|
|
45
|
+
change; unknown tokens are hard errors. See `token-map.README.md` for the full authoring rules.
|
|
46
|
+
|
|
47
|
+
## Required sections
|
|
48
|
+
|
|
49
|
+
Every skill must include:
|
|
50
|
+
1. `## When to use`
|
|
51
|
+
2. `## Workflow` (numbered steps)
|
|
52
|
+
3. `## Verification checklist` (`- [ ]` items)
|
|
53
|
+
|
|
54
|
+
## Optional sections
|
|
55
|
+
|
|
56
|
+
For each optional section, define **what it is**, include an **example snippet**, and state **when to include** and **when to omit**.
|
|
57
|
+
|
|
58
|
+
| Section | What it is | Example snippet | When to include | When to omit |
|
|
59
|
+
|---|---|---|---|---|
|
|
60
|
+
| `When NOT to use` | Explicit out-of-scope invocation boundaries | `- When NOT to use: single-line typo fixes with no process risk.` | Skills with similar neighbors where invocation precision matters | Truly unique one-purpose utility skills |
|
|
61
|
+
| `Red Flags` | Detectable anti-patterns the agent should self-correct during execution | `- Red flag: skipping boundary validation to save time.` | Discipline-enforcement skills (security, TDD, review) | Simple utility wrappers |
|
|
62
|
+
| `Common Rationalizations` | Excuses models use to skip process with rebuttals | `| "This is small" | Small changes still need invariants checked. |` | Process-critical skills where shortcut pressure is common | Mechanical one-step skills |
|
|
63
|
+
| `Philosophy / rationale` | Why the method exists and what trade-offs it protects | `- We front-load validation because downstream rollback cost is higher.` | Opinionated skills expected to handle novel variants | Purely mechanical instructions |
|
|
64
|
+
| `Phase-gate checklists` | Mid-workflow gates that must pass before proceeding | `- [ ] Phase 1 complete: baseline tests captured.` | Multi-phase workflows (3+ distinct phases) | Linear single-phase tasks |
|
|
65
|
+
| `See Also` | Explicit handoff to another skill for a real sub-task: a relative link to its `SKILL.md` plus the reason to load it | `- [review-security](../../review/review-security/SKILL.md) — run before committing.` | Another skill owns a sub-domain better than restating it | Self-contained skills |
|
|
66
|
+
| `Examples` | Full worked sample of expected outcome/output | `- Input: ... Output: ...` | Any non-trivial output contract | Trivially self-evident skills |
|
|
67
|
+
| `Workflow diagrams` | Inline ASCII diagram showing step flow and decision points | Fenced block of `[Box]`, `|`, `-`, `>`, `v` arrows, e.g. `[Start] --> [Validate] --> [Run]` | Multi-step or branching workflows where a glanceable flow improves comprehension | Simple linear workflows already clear from numbered steps |
|
|
68
|
+
|
|
69
|
+
`## See Also` entries are one-line handoffs: a relative link to the related skill's `SKILL.md`, an
|
|
70
|
+
em-dash, and the reason a reader of this skill would load that one. A bare skill name leaves the
|
|
71
|
+
reader guessing whether the link matters; the reason is what turns a link list into routing.
|
|
72
|
+
|
|
73
|
+
Write the path against the authored tree (`../../<group>/<skill>/SKILL.md` from a skill directory).
|
|
74
|
+
The Claude Code installer flattens skills and rewrites these targets at copy time, so a hand-written
|
|
75
|
+
link that resolves here resolves in both install layouts. List only genuine handoffs — inline calls
|
|
76
|
+
inside `## Workflow` already cover skills used as a step, and `using-agent-skills` already maps the
|
|
77
|
+
whole corpus, so a duplicate entry adds noise.
|
|
78
|
+
|
|
79
|
+
## Workflow diagram style guide
|
|
80
|
+
|
|
81
|
+
When a skill includes a workflow diagram, follow these rules so it renders
|
|
82
|
+
reliably in any CLI:
|
|
83
|
+
|
|
84
|
+
- **ASCII only** — never Mermaid, images, or unicode box-drawing characters.
|
|
85
|
+
Keep to `|`, `-`, `>`, `+`, and `v`.
|
|
86
|
+
- **Fenced block** — wrap the diagram in a fenced code block so monospace is
|
|
87
|
+
preserved.
|
|
88
|
+
- **At most 80 characters per line** — narrow enough for terminal-width readers.
|
|
89
|
+
- **Short box labels** — imperative verbs or nouns (e.g. `[Validate scope]`),
|
|
90
|
+
never full sentences.
|
|
91
|
+
- **Read left-to-right, top-down** — prefer one branch level over dense joins.
|
|
92
|
+
- **Mirror the numbered workflow** — the diagram is a glanceable summary of the
|
|
93
|
+
`## Workflow` steps, never a source of truth that contradicts them.
|
|
94
|
+
|
|
95
|
+
## Section ordering
|
|
96
|
+
|
|
97
|
+
### When to use
|
|
98
|
+
|
|
99
|
+
State concrete invocation triggers using natural language phrases users actually type.
|
|
100
|
+
|
|
101
|
+
### When NOT to use
|
|
102
|
+
|
|
103
|
+
Place this section immediately after `When to use` whenever present.
|
|
104
|
+
|
|
105
|
+
### Workflow
|
|
106
|
+
|
|
107
|
+
Provide ordered, deterministic steps with observable outcomes.
|
|
108
|
+
|
|
109
|
+
### Verification checklist
|
|
110
|
+
|
|
111
|
+
Provide completion gates that are testable and stage-scoped.
|
|
112
|
+
|
|
113
|
+
## File layout and progressive disclosure
|
|
114
|
+
|
|
115
|
+
- Target ~150 lines for `SKILL.md`.
|
|
116
|
+
- Split long docs into purpose-named support files:
|
|
117
|
+
- `EXAMPLES.md` for worked examples.
|
|
118
|
+
- `FORMAT.md` for schemas/templates.
|
|
119
|
+
- `REFERENCE.md` for rare edge cases.
|
|
120
|
+
- Use slash-command references when another skill should be invoked.
|
|
121
|
+
|
|
122
|
+
## Acceptance criteria checklist
|
|
123
|
+
|
|
124
|
+
A skill passes anatomy review when:
|
|
125
|
+
- [ ] Front matter uses required fields and valid description format.
|
|
126
|
+
- [ ] Tool references use doubled-brace `TOOL:` tokens, not hardcoded tool names.
|
|
127
|
+
- [ ] Required sections exist and follow canonical ordering.
|
|
128
|
+
- [ ] Optional sections (if present) include what/when/example guidance.
|
|
129
|
+
- [ ] `See Also` entries link the target `SKILL.md` relatively and state why to load it.
|
|
130
|
+
- [ ] Workflow diagrams (if present) are ASCII-only and follow the workflow diagram style guide.
|
|
131
|
+
- [ ] Verification checklist contains concrete, machine-checkable items.
|
|
132
|
+
- [ ] Supporting docs are split by purpose when `SKILL.md` would exceed ~150 lines.
|
|
133
|
+
|
|
134
|
+
## Annotated template
|
|
135
|
+
|
|
136
|
+
```md
|
|
137
|
+
---
|
|
138
|
+
name: skill-name
|
|
139
|
+
description: This skill performs a specific capability. Use when a user asks for a matching workflow such as "triage this", "investigate this", or "prepare a handoff".
|
|
140
|
+
argument-hint: Optional prompt shown to users when extra input is needed.
|
|
141
|
+
disable-model-invocation: false
|
|
142
|
+
---
|
|
143
|
+
|
|
144
|
+
# Skill Name
|
|
145
|
+
|
|
146
|
+
## When to use
|
|
147
|
+
|
|
148
|
+
- Trigger phrases users actually say, e.g. "triage this failure".
|
|
149
|
+
- Include 2-4 concrete patterns.
|
|
150
|
+
|
|
151
|
+
## When NOT to use
|
|
152
|
+
|
|
153
|
+
- Out-of-scope cases that should route elsewhere.
|
|
154
|
+
|
|
155
|
+
## Workflow
|
|
156
|
+
|
|
157
|
+
1. Gather required context and validate scope boundaries.
|
|
158
|
+
2. Reject early with the reason when validation fails; otherwise continue.
|
|
159
|
+
3. Execute deterministic steps in order, recording key decisions.
|
|
160
|
+
4. Produce the required output artifact or terminal response.
|
|
161
|
+
|
|
162
|
+
Workflow at a glance (include only when the flow branches):
|
|
163
|
+
|
|
164
|
+
[Trigger]
|
|
165
|
+
|
|
|
166
|
+
v
|
|
167
|
+
[Gather context]
|
|
168
|
+
|
|
|
169
|
+
v
|
|
170
|
+
[Validate scope] --(invalid)--> [Reject early]
|
|
171
|
+
|
|
|
172
|
+
(valid)
|
|
173
|
+
v
|
|
174
|
+
[Run deterministic steps]
|
|
175
|
+
|
|
|
176
|
+
v
|
|
177
|
+
[Produce output artifact]
|
|
178
|
+
|
|
179
|
+
## Red Flags
|
|
180
|
+
|
|
181
|
+
- Red flag: skipping validation of unknown external input.
|
|
182
|
+
|
|
183
|
+
## Common Rationalizations
|
|
184
|
+
|
|
185
|
+
| Rationalization | Rebuttal |
|
|
186
|
+
|---|---|
|
|
187
|
+
| "This is too small for process" | Process protects against regressions regardless of size. |
|
|
188
|
+
|
|
189
|
+
## Philosophy / rationale
|
|
190
|
+
|
|
191
|
+
- We optimize for explicit boundaries because they reduce rework and improve reliability.
|
|
192
|
+
|
|
193
|
+
## Phase-gate checklist
|
|
194
|
+
|
|
195
|
+
- [ ] Phase 1 complete and baseline checks captured.
|
|
196
|
+
- [ ] Phase 2 complete and contract validation passed.
|
|
197
|
+
|
|
198
|
+
## See Also
|
|
199
|
+
|
|
200
|
+
- [review-security](../../review/review-security/SKILL.md) — run before committing any externally exposed change.
|
|
201
|
+
|
|
202
|
+
## Examples
|
|
203
|
+
|
|
204
|
+
Input: "Investigate this flaky suite"
|
|
205
|
+
Output: concise root-cause summary, failing tests list, and next action.
|
|
206
|
+
|
|
207
|
+
## Verification checklist
|
|
208
|
+
|
|
209
|
+
- [ ] Output matches requested format.
|
|
210
|
+
- [ ] Required tests/lints were run and results recorded.
|
|
211
|
+
- [ ] No unrelated files were changed.
|
|
212
|
+
```
|
package/lib/cli.mjs
CHANGED
|
@@ -4,8 +4,13 @@ import { basename } from "node:path";
|
|
|
4
4
|
* Result of parsing the CLI argv. Every branch returns `command`; the rest are
|
|
5
5
|
* populated only for the commands that accept them.
|
|
6
6
|
* @typedef {object} ParsedArgs
|
|
7
|
-
* @property {"help" | "version" | "install" | "migrate" | "unknown"} command
|
|
7
|
+
* @property {"help" | "version" | "install" | "update" | "migrate" | "unknown"} command
|
|
8
|
+
* @property {boolean} [report] `version` subcommand: report installed harnesses too
|
|
9
|
+
* @property {boolean} [json] `version --json`: print the report as JSON
|
|
8
10
|
* @property {string | null} [harness]
|
|
11
|
+
* @property {string[]} [harnesses] `update`: the harnesses named on the command line
|
|
12
|
+
* @property {boolean} [all] `update -a, --all`: every harness that is not at the CLI version
|
|
13
|
+
* @property {boolean} [force] `update -f, --force`: re-run every installer regardless of version
|
|
9
14
|
* @property {string} [name]
|
|
10
15
|
* @property {string} [optionError]
|
|
11
16
|
* @property {boolean} [dryRun]
|
|
@@ -69,6 +74,20 @@ export function parseArgs(argv, programName = DEFAULT_PROGRAM_NAME) {
|
|
|
69
74
|
|
|
70
75
|
const [command, ...rest] = argv;
|
|
71
76
|
|
|
77
|
+
if (command === "version") {
|
|
78
|
+
// The `version` subcommand reports the installed harness versions; the
|
|
79
|
+
// `--version` flag above stays a bare CLI version for scripts.
|
|
80
|
+
let json = false;
|
|
81
|
+
for (const arg of rest) {
|
|
82
|
+
if (arg === "--json") {
|
|
83
|
+
json = true;
|
|
84
|
+
continue;
|
|
85
|
+
}
|
|
86
|
+
return { command: "version", report: true, optionError: `Unknown option "${arg}". Run "${programName} --help" for usage.` };
|
|
87
|
+
}
|
|
88
|
+
return json ? { command: "version", report: true, json } : { command: "version", report: true };
|
|
89
|
+
}
|
|
90
|
+
|
|
72
91
|
if (command === "install") {
|
|
73
92
|
let harness = null;
|
|
74
93
|
let i = 0;
|
|
@@ -109,6 +128,35 @@ export function parseArgs(argv, programName = DEFAULT_PROGRAM_NAME) {
|
|
|
109
128
|
return { command: "install", harness, ...options };
|
|
110
129
|
}
|
|
111
130
|
|
|
131
|
+
if (command === "update") {
|
|
132
|
+
// `update <harness>...` refreshes exactly those harnesses; `-a` refreshes
|
|
133
|
+
// every harness that is not at the CLI version and `-f` forces a re-run.
|
|
134
|
+
// `-f` alone has no meaning, so it is rejected rather than ignored.
|
|
135
|
+
/** @type {string[]} */
|
|
136
|
+
const harnesses = [];
|
|
137
|
+
let all = false;
|
|
138
|
+
let force = false;
|
|
139
|
+
for (const arg of rest) {
|
|
140
|
+
if (arg === "--help" || arg === "-h") return { command: "help" };
|
|
141
|
+
if (arg === "-a" || arg === "--all") {
|
|
142
|
+
all = true;
|
|
143
|
+
continue;
|
|
144
|
+
}
|
|
145
|
+
if (arg === "-f" || arg === "--force") {
|
|
146
|
+
force = true;
|
|
147
|
+
continue;
|
|
148
|
+
}
|
|
149
|
+
if (arg.startsWith("-")) {
|
|
150
|
+
return { command: "update", optionError: `Unknown option "${arg}". Run "${programName} --help" for usage.` };
|
|
151
|
+
}
|
|
152
|
+
harnesses.push(arg);
|
|
153
|
+
}
|
|
154
|
+
if (force && !all) {
|
|
155
|
+
return { command: "update", optionError: `"--force" requires "--all". Run "${programName} --help" for usage.` };
|
|
156
|
+
}
|
|
157
|
+
return { command: "update", harnesses, all, force };
|
|
158
|
+
}
|
|
159
|
+
|
|
112
160
|
if (command === "migrate") {
|
|
113
161
|
// Dry run is the default, so `--apply` is the only state-changing flag;
|
|
114
162
|
// `--dry-run` stays accepted for scripts that pass it explicitly. `--dir`
|
|
@@ -223,6 +271,20 @@ Commands:
|
|
|
223
271
|
cannot register the staged package
|
|
224
272
|
(missing from PATH); default warns and
|
|
225
273
|
prints the manual commands
|
|
274
|
+
update [<harness>...] Refresh installed harnesses by re-running their
|
|
275
|
+
installers. With no argument the command lists every
|
|
276
|
+
harness with its installed version and the CLI version,
|
|
277
|
+
then asks which to refresh (a non-interactive shell
|
|
278
|
+
prints the same table and the flags below, then exits
|
|
279
|
+
non-zero without blocking).
|
|
280
|
+
Options:
|
|
281
|
+
<harness>... Refresh exactly these harnesses, no prompt
|
|
282
|
+
-a, --all Refresh every harness that is not at the
|
|
283
|
+
CLI version (not installed, stale, or
|
|
284
|
+
ahead); ignored when harnesses are named
|
|
285
|
+
-f, --force With --all: re-run every installer even
|
|
286
|
+
when the version already matches
|
|
287
|
+
--help, -h Show this help text
|
|
226
288
|
migrate Migrate the current beads workspace to the latest
|
|
227
289
|
agent-cortex migration version, applying versioned steps
|
|
228
290
|
in order (repo-agnostic — works in any beads workspace)
|
|
@@ -231,6 +293,13 @@ Commands:
|
|
|
231
293
|
--apply Apply every pending step, verifying each
|
|
232
294
|
--dir <path> Read idea files from <path> instead of
|
|
233
295
|
docs/ideas (step v2's source)
|
|
296
|
+
version Print the running CLI version, then one line per installed
|
|
297
|
+
harness: its name, the version its installer recorded, and
|
|
298
|
+
a marker comparing the two — current, stale, ahead (the
|
|
299
|
+
installed copy is newer), or unknown (no readable
|
|
300
|
+
fingerprint). Read-only: no install, no network, no writes.
|
|
301
|
+
Options:
|
|
302
|
+
--json Print the same data as JSON records
|
|
234
303
|
--help, -h Show this help text
|
|
235
304
|
--version, -V Print the package version`;
|
|
236
305
|
}
|
|
@@ -0,0 +1,254 @@
|
|
|
1
|
+
// The selection and reporting half of `ctx update`: which harnesses a refresh
|
|
2
|
+
// covers, how the table and the report read, and the interactive prompt.
|
|
3
|
+
//
|
|
4
|
+
// Delegation is not here. The caller injects `refresh(harness)`, which the CLI
|
|
5
|
+
// wires to the harness installers — so update-vs-install stays the installer's
|
|
6
|
+
// decision and this module never writes an artifact itself.
|
|
7
|
+
|
|
8
|
+
import { homedir } from "node:os";
|
|
9
|
+
import { collectHarnessStatus } from "./harness-versions.mjs";
|
|
10
|
+
|
|
11
|
+
/** @typedef {import("./harness-versions.mjs").HarnessStatus} HarnessStatus */
|
|
12
|
+
|
|
13
|
+
const NOT_INSTALLED = "not installed";
|
|
14
|
+
const UNKNOWN = "unknown";
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* One refreshed harness in the update report.
|
|
18
|
+
* @typedef {object} UpdateEntry
|
|
19
|
+
* @property {string} harness
|
|
20
|
+
* @property {string} previous The version before the refresh, or "not installed"
|
|
21
|
+
* @property {string} next The version the installer recorded afterwards
|
|
22
|
+
* @property {string[]} artifacts What the installer wrote
|
|
23
|
+
*/
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* The harnesses an update covers, and the ones it leaves alone.
|
|
27
|
+
* @typedef {object} UpdatePlan
|
|
28
|
+
* @property {HarnessStatus[]} targets
|
|
29
|
+
* @property {HarnessStatus[]} skipped
|
|
30
|
+
* @property {string[]} unknown Names that are not supported harnesses
|
|
31
|
+
*/
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Decide which harnesses an update refreshes. An explicit name is always a
|
|
35
|
+
* target (the user asked for that installer to re-run); `--all` targets every
|
|
36
|
+
* harness that is not at the CLI version; `--all --force` targets every harness.
|
|
37
|
+
* @param {object} options
|
|
38
|
+
* @param {HarnessStatus[]} options.statuses One entry per supported harness
|
|
39
|
+
* @param {string[]} [options.harnesses] Names from the command line or the prompt
|
|
40
|
+
* @param {boolean} [options.all] `ctx update -a`
|
|
41
|
+
* @param {boolean} [options.force] `ctx update -a -f`
|
|
42
|
+
* @returns {UpdatePlan}
|
|
43
|
+
*/
|
|
44
|
+
export function selectHarnesses({ statuses, harnesses = [], all = false, force = false }) {
|
|
45
|
+
/** @type {string[]} */
|
|
46
|
+
const unknown = [];
|
|
47
|
+
/** @type {HarnessStatus[]} */
|
|
48
|
+
let targets;
|
|
49
|
+
if (harnesses.length > 0) {
|
|
50
|
+
targets = [];
|
|
51
|
+
for (const name of harnesses) {
|
|
52
|
+
const status = statuses.find((entry) => entry.harness === name);
|
|
53
|
+
if (status === undefined) {
|
|
54
|
+
if (!unknown.includes(name)) unknown.push(name);
|
|
55
|
+
continue;
|
|
56
|
+
}
|
|
57
|
+
if (!targets.includes(status)) targets.push(status);
|
|
58
|
+
}
|
|
59
|
+
} else if (all && force) {
|
|
60
|
+
targets = [...statuses];
|
|
61
|
+
} else if (all) {
|
|
62
|
+
targets = statuses.filter((entry) => entry.staleness !== "current");
|
|
63
|
+
} else {
|
|
64
|
+
targets = [];
|
|
65
|
+
}
|
|
66
|
+
const skipped = statuses.filter((entry) => !targets.includes(entry));
|
|
67
|
+
return { targets, skipped, unknown };
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* Count a noun for the report: `1 file`, `4 files`.
|
|
72
|
+
* @param {number} amount
|
|
73
|
+
* @param {string} noun Singular form
|
|
74
|
+
* @returns {string}
|
|
75
|
+
*/
|
|
76
|
+
function count(amount, noun) {
|
|
77
|
+
return `${amount} ${noun}${amount === 1 ? "" : "s"}`;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* What a pi refresh wrote, for the update report.
|
|
82
|
+
* @param {import("../bin/installers/pi.mjs").InstallPiResult} result
|
|
83
|
+
* @returns {string[]}
|
|
84
|
+
*/
|
|
85
|
+
export function describePiArtifacts(result) {
|
|
86
|
+
return [
|
|
87
|
+
`agents (${count(result.agents.length, "file")})`,
|
|
88
|
+
`skills (${count(result.skills.skills, "skill")}, ${count(result.skills.md, "markdown file")})`,
|
|
89
|
+
`extensions (${count(result.extensions.files, "file")})`,
|
|
90
|
+
`bundled pi packages (${result.packages.bundled.length > 0 ? result.packages.bundled.join(", ") : "none"})`,
|
|
91
|
+
`package manifest (version ${result.manifest.manifest.version})`,
|
|
92
|
+
];
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
* What a claude refresh wrote, for the update report.
|
|
97
|
+
* @param {import("../bin/installers/claude.mjs").InstallClaudeResult} result
|
|
98
|
+
* @returns {string[]}
|
|
99
|
+
*/
|
|
100
|
+
export function describeClaudeArtifacts(result) {
|
|
101
|
+
return [
|
|
102
|
+
`agents (${count(result.agents.length, "file")})`,
|
|
103
|
+
`skills (${count(result.skills.names.length, "skill")}, ${count(result.skills.md, "markdown file")})`,
|
|
104
|
+
`hooks (${count(result.hookFiles.length, "file")})`,
|
|
105
|
+
`hand-authored extras (${count(result.handAuthored.length, "file")})`,
|
|
106
|
+
`plugin manifest (version ${result.pluginVersion})`,
|
|
107
|
+
`settings (${result.settings?.action ?? "not written"})`,
|
|
108
|
+
`instructions (${result.instructions?.action ?? "not written"})`,
|
|
109
|
+
];
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
/**
|
|
113
|
+
* Refresh every target through the injected installer and report the result.
|
|
114
|
+
* The new version is read back from the fingerprint the installer wrote, never
|
|
115
|
+
* assumed from the CLI version, so a failed materialisation shows as unknown.
|
|
116
|
+
* @param {object} options
|
|
117
|
+
* @param {string} options.cli The running CLI version
|
|
118
|
+
* @param {string} [options.home] Home directory holding the install roots
|
|
119
|
+
* @param {HarnessStatus[]} options.targets
|
|
120
|
+
* @param {(harness: string) => Promise<string[]>} options.refresh Runs one harness installer
|
|
121
|
+
* @returns {Promise<UpdateEntry[]>}
|
|
122
|
+
*/
|
|
123
|
+
export async function runUpdate({ cli, home = homedir(), targets, refresh }) {
|
|
124
|
+
/** @type {{ harness: string, previous: string, artifacts: string[] }[]} */
|
|
125
|
+
const refreshed = [];
|
|
126
|
+
for (const target of targets) {
|
|
127
|
+
const artifacts = await refresh(target.harness);
|
|
128
|
+
refreshed.push({ harness: target.harness, previous: target.installed ?? NOT_INSTALLED, artifacts });
|
|
129
|
+
}
|
|
130
|
+
if (refreshed.length === 0) return [];
|
|
131
|
+
const statuses = await collectHarnessStatus({ cli, home });
|
|
132
|
+
return refreshed.map((entry) => {
|
|
133
|
+
const status = statuses.find((candidate) => candidate.harness === entry.harness);
|
|
134
|
+
return { ...entry, next: status?.installed ?? UNKNOWN };
|
|
135
|
+
});
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
/**
|
|
139
|
+
* Render the harness table: one row per supported harness, so an absent
|
|
140
|
+
* install is visible instead of silently missing.
|
|
141
|
+
* @param {HarnessStatus[]} statuses
|
|
142
|
+
* @returns {string} Text without a trailing newline
|
|
143
|
+
*/
|
|
144
|
+
export function formatHarnessTable(statuses) {
|
|
145
|
+
/** @type {string[][]} */
|
|
146
|
+
const rows = [["harness", "installed", "cli", "status"]];
|
|
147
|
+
for (const status of statuses) {
|
|
148
|
+
rows.push([status.harness, status.installed ?? NOT_INSTALLED, status.cli, status.staleness]);
|
|
149
|
+
}
|
|
150
|
+
const widths = rows[0].map((_, column) => Math.max(...rows.map((row) => row[column].length)));
|
|
151
|
+
return rows
|
|
152
|
+
.map((row) => row.map((cell, column) => (column === widths.length - 1 ? cell : cell.padEnd(widths[column]))).join(" "))
|
|
153
|
+
.join("\n");
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
/**
|
|
157
|
+
* The instructions a non-interactive `ctx update` prints instead of prompting.
|
|
158
|
+
* @param {string} programName The resolved command name
|
|
159
|
+
* @returns {string} Text without a trailing newline
|
|
160
|
+
*/
|
|
161
|
+
export function formatUpdateFlags(programName) {
|
|
162
|
+
return [
|
|
163
|
+
"No harness selected, and this shell is not interactive. Pass a selection:",
|
|
164
|
+
` ${programName} update <harness>... refresh the named harnesses`,
|
|
165
|
+
` ${programName} update -a refresh every harness that is not at the CLI version`,
|
|
166
|
+
` ${programName} update -a -f re-run every harness installer regardless of version`,
|
|
167
|
+
].join("\n");
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
/**
|
|
171
|
+
* Render the outcome of an update. Only skipped harnesses that are already at
|
|
172
|
+
* the CLI version are listed; one left alone for another reason (a stale
|
|
173
|
+
* harness beside an explicit selection) is not "already at" anything.
|
|
174
|
+
* @param {object} options
|
|
175
|
+
* @param {string} options.cli The running CLI version
|
|
176
|
+
* @param {UpdateEntry[]} options.entries
|
|
177
|
+
* @param {HarnessStatus[]} [options.skipped]
|
|
178
|
+
* @returns {string} Text without a trailing newline
|
|
179
|
+
*/
|
|
180
|
+
export function formatUpdateReport({ cli, entries, skipped = [] }) {
|
|
181
|
+
const current = skipped.filter((entry) => entry.staleness === "current");
|
|
182
|
+
/** @type {string[]} */
|
|
183
|
+
const lines = [];
|
|
184
|
+
for (const entry of entries) {
|
|
185
|
+
lines.push(`${entry.harness} ${entry.previous} → ${entry.next}`);
|
|
186
|
+
for (const artifact of entry.artifacts) {
|
|
187
|
+
lines.push(` ✓ ${artifact}`);
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
if (entries.length === 0) {
|
|
191
|
+
lines.push(`Nothing to update — every harness is at ${cli}.`);
|
|
192
|
+
} else if (current.length > 0) {
|
|
193
|
+
lines.push(`Skipped (already at ${cli}): ${current.map((entry) => entry.harness).join(", ")}`);
|
|
194
|
+
}
|
|
195
|
+
return lines.join("\n");
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
/**
|
|
199
|
+
* Turn a prompt answer into harness names: row numbers, `a` for every row, or
|
|
200
|
+
* blank for none.
|
|
201
|
+
* @param {string} answer
|
|
202
|
+
* @param {HarnessStatus[]} statuses The rows the prompt listed, in order
|
|
203
|
+
* @returns {string[]}
|
|
204
|
+
*/
|
|
205
|
+
export function parseHarnessSelection(answer, statuses) {
|
|
206
|
+
const trimmed = answer.trim();
|
|
207
|
+
if (trimmed === "") return [];
|
|
208
|
+
if (/^(a|all)$/i.test(trimmed)) return statuses.map((status) => status.harness);
|
|
209
|
+
/** @type {string[]} */
|
|
210
|
+
const chosen = [];
|
|
211
|
+
for (const token of trimmed.split(/[,\s]+/)) {
|
|
212
|
+
const index = Number(token);
|
|
213
|
+
const status = Number.isInteger(index) ? statuses[index - 1] : undefined;
|
|
214
|
+
if (status === undefined) {
|
|
215
|
+
throw new Error(`Invalid selection "${token}". Enter row numbers 1-${statuses.length}, "a", or blank to cancel.`);
|
|
216
|
+
}
|
|
217
|
+
if (!chosen.includes(status.harness)) chosen.push(status.harness);
|
|
218
|
+
}
|
|
219
|
+
return chosen;
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
/**
|
|
223
|
+
* Ask on an interactive terminal which harnesses to refresh. The table is the
|
|
224
|
+
* list the user picks from; a closed input stream cancels the selection.
|
|
225
|
+
* @param {object} options
|
|
226
|
+
* @param {HarnessStatus[]} options.statuses
|
|
227
|
+
* @param {NodeJS.ReadableStream} [options.input]
|
|
228
|
+
* @param {NodeJS.WritableStream} [options.output]
|
|
229
|
+
* @returns {Promise<string[]>}
|
|
230
|
+
*/
|
|
231
|
+
export async function promptHarnessSelection({ statuses, input = process.stdin, output = process.stdout }) {
|
|
232
|
+
output.write(`${formatHarnessTable(statuses)}\n\n`);
|
|
233
|
+
const answer = await readLine(input, output, `Select harnesses (row numbers, "a" for all, blank to cancel): `);
|
|
234
|
+
return parseHarnessSelection(answer, statuses);
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
/**
|
|
238
|
+
* Read one line, prompting on `output` first. A stream that ends without a
|
|
239
|
+
* newline returns what it carried, so a closed stdin cancels rather than hangs.
|
|
240
|
+
* @param {NodeJS.ReadableStream} input
|
|
241
|
+
* @param {NodeJS.WritableStream} output
|
|
242
|
+
* @param {string} question
|
|
243
|
+
* @returns {Promise<string>}
|
|
244
|
+
*/
|
|
245
|
+
async function readLine(input, output, question) {
|
|
246
|
+
output.write(question);
|
|
247
|
+
let buffered = "";
|
|
248
|
+
for await (const chunk of input) {
|
|
249
|
+
buffered += String(chunk);
|
|
250
|
+
const newline = buffered.indexOf("\n");
|
|
251
|
+
if (newline !== -1) return buffered.slice(0, newline);
|
|
252
|
+
}
|
|
253
|
+
return buffered;
|
|
254
|
+
}
|