orchestrator-workflow 0.1.0 → 0.3.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/INSTALL-AGENT.md +66 -10
- package/README.md +70 -8
- package/assets/agents-md-section.md +22 -7
- package/assets/skill/SKILL.md +10 -0
- package/dist/cli.js +76 -17
- package/dist/index.d.ts +2 -0
- package/dist/index.js +1 -0
- package/dist/init.d.ts +7 -0
- package/dist/init.js +17 -2
- package/dist/uninstall.d.ts +14 -0
- package/dist/uninstall.js +150 -0
- package/dist/writers.d.ts +5 -0
- package/dist/writers.js +7 -2
- package/package.json +1 -1
package/INSTALL-AGENT.md
CHANGED
|
@@ -1,16 +1,71 @@
|
|
|
1
1
|
# Installing via a coding agent
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
should be done by a coding agent instead (Claude Code, Codex, opencode), the
|
|
5
|
-
agent takes over that interactivity: it asks the operator the same questions
|
|
6
|
-
in chat, then runs the non-interactive CLI, or scaffolds manually where npx
|
|
7
|
-
is unavailable.
|
|
3
|
+
Give your agent this single line:
|
|
8
4
|
|
|
9
|
-
|
|
5
|
+
```text
|
|
6
|
+
Follow the install instructions at https://raw.githubusercontent.com/LanNguyenSi/agent-dx/master/packages/orchestrator-workflow/INSTALL-AGENT.md
|
|
7
|
+
```
|
|
10
8
|
|
|
11
|
-
|
|
9
|
+
The agent fetches this file and executes the numbered instructions in the
|
|
10
|
+
"Instructions for the agent" section at the bottom. The two sections in
|
|
11
|
+
between explain, for you, what those instructions make the agent do and
|
|
12
|
+
which files it may touch, so you can audit the prompt before delegating.
|
|
12
13
|
|
|
13
|
-
|
|
14
|
+
The audit applies to the revision you read: the link above tracks `master`,
|
|
15
|
+
which is mutable. For a stable audit, pin the URL to a commit SHA instead
|
|
16
|
+
(replace `master` with the SHA).
|
|
17
|
+
|
|
18
|
+
## What the linked instructions do
|
|
19
|
+
|
|
20
|
+
1. **Locate existing harness configs** in the repo root and report them to
|
|
21
|
+
you (Claude Code, opencode, Codex marker files; full list in step 1
|
|
22
|
+
below).
|
|
23
|
+
2. **Ask you, not guess**: which harnesses should get adapters, and which
|
|
24
|
+
model each subagent role (task-slicer, implementer, reviewer) should use.
|
|
25
|
+
Suggested defaults: task-slicer `sonnet`, implementer `sonnet`, reviewer
|
|
26
|
+
`opus`.
|
|
27
|
+
3. **Run the non-interactive installer** with your answers:
|
|
28
|
+
`npx orchestrator-workflow init --yes --harness ... --models ...`.
|
|
29
|
+
If the installer reports conflicts with locally edited files, the agent
|
|
30
|
+
shows them to you and asks before any `--force` re-run.
|
|
31
|
+
4. **Manual fallback only when npx or the registry is unavailable**: create
|
|
32
|
+
the same files by hand from this repository's `assets/` directory,
|
|
33
|
+
following the byte-precise rules in step 4 below.
|
|
34
|
+
5. **Report back**: which harnesses were installed, which model each role
|
|
35
|
+
uses, and any conflicts left in place.
|
|
36
|
+
|
|
37
|
+
### Write surface
|
|
38
|
+
|
|
39
|
+
The install creates or touches only these paths:
|
|
40
|
+
|
|
41
|
+
- `.ai/workflow/templates/00-goal.md` through `06-handoff.md`,
|
|
42
|
+
`.ai/workflow/manifest.json`, `.ai/runs/.gitkeep` (new files)
|
|
43
|
+
- `AGENTS.md`: the marker-fenced workflow section is appended (file created
|
|
44
|
+
when missing); content outside the
|
|
45
|
+
`<!-- orchestrator-workflow:begin -->` / `<!-- orchestrator-workflow:end -->`
|
|
46
|
+
markers is never modified
|
|
47
|
+
- `CLAUDE.md`: one `@AGENTS.md` import line appended when missing; the file
|
|
48
|
+
is created with a short heading when absent (Claude Code harness only)
|
|
49
|
+
- `.claude/skills/orchestrator-workflow/SKILL.md` and
|
|
50
|
+
`.claude/agents/{task-slicer,implementer,reviewer}.md` (Claude Code)
|
|
51
|
+
- `.agents/skills/orchestrator-workflow/SKILL.md` (Codex)
|
|
52
|
+
- `.opencode/agents/{task-slicer,implementer,reviewer}.md` (opencode)
|
|
53
|
+
|
|
54
|
+
Nothing else in the repository is modified. Locally edited files are
|
|
55
|
+
reported as conflicts and left alone, never overwritten silently; the
|
|
56
|
+
exceptions are the kit-owned surfaces: `.ai/workflow/manifest.json` (the
|
|
57
|
+
kit's state file, rewritten whenever the applied state changes) and the
|
|
58
|
+
fenced AGENTS.md section itself, which is restored on re-run. The install
|
|
59
|
+
is fully reversible: `npx orchestrator-workflow uninstall` removes exactly
|
|
60
|
+
this write surface again, keeping run history under `.ai/runs/` and any
|
|
61
|
+
locally edited files. Network access is limited to the npm registry (for
|
|
62
|
+
`npx`) and, in the manual fallback, this repository on github.com /
|
|
63
|
+
raw.githubusercontent.com.
|
|
64
|
+
|
|
65
|
+
## Instructions for the agent
|
|
66
|
+
|
|
67
|
+
You are a coding agent that was pointed at this file. Execute the following
|
|
68
|
+
steps in the repository you were asked to install into.
|
|
14
69
|
|
|
15
70
|
1. Detect existing harness configs in the repo root: `.claude/` or
|
|
16
71
|
`CLAUDE.md` (Claude Code), `.opencode/`, `opencode.json` or
|
|
@@ -42,7 +97,8 @@ Install the orchestrator-workflow kit into this repository.
|
|
|
42
97
|
`assets/templates/`, unchanged.
|
|
43
98
|
- `.ai/runs/.gitkeep`, empty.
|
|
44
99
|
- Append the content of `assets/agents-md-section.md` to `AGENTS.md`
|
|
45
|
-
(create the file when missing
|
|
100
|
+
(create the file when missing; the installer starts a fresh file with a
|
|
101
|
+
`# Agent instructions` heading). Never change anything outside the
|
|
46
102
|
`<!-- orchestrator-workflow:begin -->` / `<!-- orchestrator-workflow:end -->`
|
|
47
103
|
markers.
|
|
48
104
|
- Claude Code: `.claude/skills/orchestrator-workflow/SKILL.md` from
|
|
@@ -59,7 +115,7 @@ Install the orchestrator-workflow kit into this repository.
|
|
|
59
115
|
`anthropic/claude-sonnet-4-6`, `anthropic/claude-opus-4-8`,
|
|
60
116
|
`anthropic/claude-haiku-4-5`.
|
|
61
117
|
- `.ai/workflow/manifest.json`, exactly this shape (harnesses MUST be an
|
|
62
|
-
array, models keyed by role):
|
|
118
|
+
array, models keyed by role, version = the kit version you installed):
|
|
63
119
|
|
|
64
120
|
```json
|
|
65
121
|
{
|
package/README.md
CHANGED
|
@@ -10,16 +10,56 @@ plan, task validation, acceptance, and the operator handoff. Implementation
|
|
|
10
10
|
and review are delegated to narrow subagents that return structured YAML
|
|
11
11
|
evidence. Every unit of work leaves an auditable run directory behind.
|
|
12
12
|
|
|
13
|
+
## Why this shape
|
|
14
|
+
|
|
15
|
+
```text
|
|
16
|
+
Operator
|
|
17
|
+
goal | ^ handoff: what changed, how verified,
|
|
18
|
+
v | what remains open
|
|
19
|
+
Orchestrator . . . . . . . .ai/runs/<date>-<slug>/
|
|
20
|
+
session model 00-goal 04-implementation-summary
|
|
21
|
+
plans, validates slices, 01-plan 05-review-findings
|
|
22
|
+
decides acceptance 02-tasks 06-handoff
|
|
23
|
+
| 03-decisions
|
|
24
|
+
narrow | ^ structured (state lives in files,
|
|
25
|
+
contracts v | YAML evidence not in chat history)
|
|
26
|
+
+--------------+--------------+
|
|
27
|
+
| | |
|
|
28
|
+
task-slicer implementer reviewer
|
|
29
|
+
sonnet sonnet opus
|
|
30
|
+
small, one narrow skeptical, severity-rated
|
|
31
|
+
testable task, plus findings, no rewrites
|
|
32
|
+
slices tests
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
Two effects fall out of this shape:
|
|
36
|
+
|
|
37
|
+
- **Token efficiency.** The orchestrator's context stays small: subagents
|
|
38
|
+
receive narrow task contracts instead of the whole conversation, return
|
|
39
|
+
structured YAML evidence instead of transcripts, and durable state lives
|
|
40
|
+
in run files that survive context compaction. The cheap models do the
|
|
41
|
+
volume work; the strongest model is spent only on orchestration decisions
|
|
42
|
+
and the skeptical review. The ceremony scales to the task: a trivial change
|
|
43
|
+
is done directly, the full flow is for non-trivial work.
|
|
44
|
+
- **Quality through structure.** Writing and reviewing are separated by
|
|
45
|
+
role and model, task slices are validated before any implementation
|
|
46
|
+
starts, acceptance is decided on evidence (tests executed, findings
|
|
47
|
+
addressed), and every run leaves an auditable trail in `.ai/runs/`.
|
|
48
|
+
|
|
13
49
|
## Install
|
|
14
50
|
|
|
15
51
|
```bash
|
|
16
52
|
npx orchestrator-workflow init
|
|
17
53
|
```
|
|
18
54
|
|
|
19
|
-
Run it at the root of the target repository
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
55
|
+
Run it at the root of the target repository: **without a directory argument,
|
|
56
|
+
files are created in the current working directory.** The CLI prints the
|
|
57
|
+
resolved target (`Installing into ...`) before it writes anything and warns
|
|
58
|
+
when the target is not a git repository root; pass `init <dir>` to install
|
|
59
|
+
into a different directory. The installer is interactive by default: it
|
|
60
|
+
locates existing harness configs (`.claude/`, `CLAUDE.md`, `.opencode/`,
|
|
61
|
+
`opencode.json`, `.agents/`, `.codex/`), preselects what it found, and asks
|
|
62
|
+
which model each subagent role should use.
|
|
23
63
|
|
|
24
64
|
Non-interactive:
|
|
25
65
|
|
|
@@ -28,10 +68,18 @@ npx orchestrator-workflow init --yes
|
|
|
28
68
|
npx orchestrator-workflow init --harness claude,codex,opencode --models "implementer=sonnet,reviewer=opus" --yes
|
|
29
69
|
```
|
|
30
70
|
|
|
31
|
-
To let a coding agent do the install,
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
71
|
+
To let a coding agent do the install, give it this single line:
|
|
72
|
+
|
|
73
|
+
```text
|
|
74
|
+
Follow the install instructions at https://raw.githubusercontent.com/LanNguyenSi/agent-dx/master/packages/orchestrator-workflow/INSTALL-AGENT.md
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
The agent then asks you the harness and model questions in chat and runs the
|
|
78
|
+
non-interactive CLI (manual scaffolding where npx is unavailable).
|
|
79
|
+
[INSTALL-AGENT.md](INSTALL-AGENT.md) documents, step by step, what the
|
|
80
|
+
linked instructions make the agent do and which files it may touch, so the
|
|
81
|
+
prompt can be audited before delegating. The link tracks `master`; pin it
|
|
82
|
+
to a commit SHA for a stable audit.
|
|
35
83
|
|
|
36
84
|
## What gets installed
|
|
37
85
|
|
|
@@ -85,6 +133,20 @@ in `.ai/workflow/manifest.json` and reused as the default on later re-runs.
|
|
|
85
133
|
version, harnesses, models, and file hashes, and is rewritten whenever that
|
|
86
134
|
state changes; do not edit it by hand.
|
|
87
135
|
|
|
136
|
+
## Uninstall
|
|
137
|
+
|
|
138
|
+
```bash
|
|
139
|
+
npx orchestrator-workflow uninstall
|
|
140
|
+
```
|
|
141
|
+
|
|
142
|
+
Removes exactly what `init` created, driven by the manifest's file hashes:
|
|
143
|
+
unedited kit files are deleted, locally edited ones are kept and reported
|
|
144
|
+
(`--force` removes those too). The AGENTS.md section and the CLAUDE.md
|
|
145
|
+
import line are taken out; either file is deleted only when nothing but
|
|
146
|
+
init's own boilerplate remains. Kit directories are pruned only when empty,
|
|
147
|
+
and run history under `.ai/runs/` is always kept. Interactive runs ask for
|
|
148
|
+
confirmation; non-interactive runs require `--yes`.
|
|
149
|
+
|
|
88
150
|
## Relation to agentic-coding-playbook
|
|
89
151
|
|
|
90
152
|
This kit ships the orchestration layer: who coordinates whom, where state
|
|
@@ -5,10 +5,9 @@ This repository uses an orchestrator-led agent workflow, installed and updated b
|
|
|
5
5
|
[orchestrator-workflow](https://github.com/LanNguyenSi/agent-dx/tree/master/packages/orchestrator-workflow).
|
|
6
6
|
|
|
7
7
|
The primary agent acts as the orchestrator. It owns the goal, planning, task
|
|
8
|
-
validation, delegation, final acceptance, and the operator handoff.
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
skill.
|
|
8
|
+
validation, delegation, final acceptance, and the operator handoff. Non-trivial
|
|
9
|
+
implementation and review are delegated to narrow subagents. The full procedure
|
|
10
|
+
and the subagent I/O contracts live in the `orchestrator-workflow` skill.
|
|
12
11
|
|
|
13
12
|
### Core rules
|
|
14
13
|
|
|
@@ -16,12 +15,28 @@ skill.
|
|
|
16
15
|
further subagents.
|
|
17
16
|
- The orchestrator plans features itself. It may delegate task slicing, but it
|
|
18
17
|
validates the sliced tasks before implementation starts.
|
|
19
|
-
-
|
|
20
|
-
|
|
21
|
-
|
|
18
|
+
- Non-trivial implementation goes to narrow implementer subagents, one task
|
|
19
|
+
per subagent.
|
|
20
|
+
- Non-trivial review goes to a separate reviewer subagent (see Scaling
|
|
21
|
+
delegation). Review itself is never skipped, not even for docs or batch
|
|
22
|
+
changes.
|
|
22
23
|
- Final acceptance and the final answer to the operator stay with the
|
|
23
24
|
orchestrator.
|
|
24
25
|
|
|
26
|
+
### Scaling delegation
|
|
27
|
+
|
|
28
|
+
The orchestrator matches the ceremony to the task; the full flow is a
|
|
29
|
+
default, not a ritual.
|
|
30
|
+
|
|
31
|
+
- A trivial change (a typo, a one-line fix, a rename) may be implemented by
|
|
32
|
+
the orchestrator directly, without slicing or an implementer subagent.
|
|
33
|
+
- Slicing and implementer subagents are for non-trivial work: multiple files,
|
|
34
|
+
real logic, or anything that benefits from decomposition or a fresh context.
|
|
35
|
+
- Review judgment applies to every change. For a trivial change the
|
|
36
|
+
orchestrator may review it itself; reserve the reviewer subagent for
|
|
37
|
+
changes whose risk or size warrants an independent skeptical pass. Either
|
|
38
|
+
way, review is never skipped.
|
|
39
|
+
|
|
25
40
|
### Context discipline
|
|
26
41
|
|
|
27
42
|
- Prefer task-local context over repository-wide context.
|
package/assets/skill/SKILL.md
CHANGED
|
@@ -15,6 +15,12 @@ tasks to specialized subagents. The goal is to improve quality, reduce
|
|
|
15
15
|
context-window pressure, and keep the operator informed through structured
|
|
16
16
|
handoffs.
|
|
17
17
|
|
|
18
|
+
Scale the ceremony to the task. The workflow below is the default for
|
|
19
|
+
non-trivial work; a trivial change (a typo, a one-line fix) may be done
|
|
20
|
+
directly by the orchestrator and reviewed by it, without slicing or spawning
|
|
21
|
+
subagents. Review judgment still applies to every change; only the size of
|
|
22
|
+
the apparatus changes.
|
|
23
|
+
|
|
18
24
|
## Roles
|
|
19
25
|
|
|
20
26
|
- **Operator**: the human requester. Provides goal and constraints, approves or
|
|
@@ -58,6 +64,10 @@ older directories are the auditable history. Do not edit past runs.
|
|
|
58
64
|
|
|
59
65
|
## Workflow
|
|
60
66
|
|
|
67
|
+
For a non-trivial change, run the full flow below. For a trivial change, do
|
|
68
|
+
the work directly, review it, and still leave a short handoff; skip the run
|
|
69
|
+
directory and the subagents.
|
|
70
|
+
|
|
61
71
|
1. **Understand the goal.** Create the run directory and fill `00-goal.md`:
|
|
62
72
|
operator request, goal, non-goals, constraints, assumptions, open questions.
|
|
63
73
|
If the task can proceed on reasonable assumptions, proceed without blocking.
|
package/dist/cli.js
CHANGED
|
@@ -1,15 +1,32 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import { existsSync, statSync } from "node:fs";
|
|
3
|
-
import { resolve } from "node:path";
|
|
3
|
+
import { join, resolve } from "node:path";
|
|
4
4
|
import { Command } from "commander";
|
|
5
5
|
import inquirer from "inquirer";
|
|
6
6
|
import { PACKAGE_VERSION } from "./assets.js";
|
|
7
7
|
import { HARNESSES, detectHarnesses, parseHarnessList } from "./detect.js";
|
|
8
8
|
import { DEFAULT_MODELS, MODEL_ALIASES, ROLES, assertValidModelId, parseModelsSpec, } from "./models.js";
|
|
9
9
|
import { readInstalledManifest, runInit } from "./init.js";
|
|
10
|
+
import { runUninstall } from "./uninstall.js";
|
|
10
11
|
function isInteractive() {
|
|
11
12
|
return Boolean(process.stdin.isTTY && process.stdout.isTTY);
|
|
12
13
|
}
|
|
14
|
+
function showPaths(label, paths) {
|
|
15
|
+
if (paths.length === 0)
|
|
16
|
+
return;
|
|
17
|
+
console.log(`${label}:`);
|
|
18
|
+
for (const path of paths)
|
|
19
|
+
console.log(` ${path}`);
|
|
20
|
+
}
|
|
21
|
+
function requireDirectory(dir) {
|
|
22
|
+
const targetDir = resolve(dir);
|
|
23
|
+
if (!existsSync(targetDir) || !statSync(targetDir).isDirectory()) {
|
|
24
|
+
console.error(`Target is not a directory: ${targetDir}`);
|
|
25
|
+
process.exitCode = 1;
|
|
26
|
+
return undefined;
|
|
27
|
+
}
|
|
28
|
+
return targetDir;
|
|
29
|
+
}
|
|
13
30
|
async function promptHarnesses(detected, installed) {
|
|
14
31
|
const known = [...new Set([...detected, ...installed])];
|
|
15
32
|
const preselected = known.length > 0 ? known : ["claude"];
|
|
@@ -85,13 +102,16 @@ program
|
|
|
85
102
|
.option("--harness <list>", `comma-separated harnesses (${HARNESSES.join(", ")}); default: detected`)
|
|
86
103
|
.option("--models <spec>", 'per-role model overrides, e.g. "implementer=sonnet,reviewer=opus"')
|
|
87
104
|
.action(async (dir, opts) => {
|
|
88
|
-
const targetDir =
|
|
89
|
-
if (!
|
|
90
|
-
console.error(`Target is not a directory: ${targetDir}`);
|
|
91
|
-
process.exitCode = 1;
|
|
105
|
+
const targetDir = requireDirectory(dir);
|
|
106
|
+
if (!targetDir)
|
|
92
107
|
return;
|
|
93
|
-
}
|
|
94
108
|
const interactive = !opts.yes && isInteractive();
|
|
109
|
+
// Say where files will land BEFORE anything is written; an accidental
|
|
110
|
+
// cwd (e.g. $HOME) is the most likely operator mistake.
|
|
111
|
+
console.log(`Installing into ${targetDir}`);
|
|
112
|
+
if (!existsSync(join(targetDir, ".git"))) {
|
|
113
|
+
console.log("Note: the target is not a git repository root. Pass a directory argument (init <dir>) if this is not the repo you meant.");
|
|
114
|
+
}
|
|
95
115
|
const detected = detectHarnesses(targetDir);
|
|
96
116
|
console.log(detected.length > 0
|
|
97
117
|
? `Detected harness configs: ${detected.join(", ")}`
|
|
@@ -133,19 +153,58 @@ program
|
|
|
133
153
|
models,
|
|
134
154
|
force: opts.force,
|
|
135
155
|
});
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
for (const path of paths)
|
|
141
|
-
console.log(` ${path}`);
|
|
142
|
-
};
|
|
143
|
-
show("Created", report.written);
|
|
144
|
-
show("Updated", report.updated);
|
|
145
|
-
show("Unchanged", report.skipped);
|
|
146
|
-
show("Conflicts (local edits kept, re-run with --force to overwrite)", report.conflicted);
|
|
156
|
+
showPaths("Created", report.written);
|
|
157
|
+
showPaths("Updated", report.updated);
|
|
158
|
+
showPaths("Unchanged", report.skipped);
|
|
159
|
+
showPaths("Conflicts (local edits kept, re-run with --force to overwrite)", report.conflicted);
|
|
147
160
|
console.log(`\norchestrator-workflow v${PACKAGE_VERSION} installed for: ${harnesses.join(", ")}`);
|
|
148
161
|
});
|
|
162
|
+
program
|
|
163
|
+
.command("uninstall")
|
|
164
|
+
.description("Remove everything init installed from a target repository; run history under .ai/runs/ is kept")
|
|
165
|
+
.argument("[dir]", "target repository directory", ".")
|
|
166
|
+
.option("-y, --yes", "do not ask for confirmation")
|
|
167
|
+
.option("-f, --force", "also remove kit files that have local edits")
|
|
168
|
+
.action(async (dir, opts) => {
|
|
169
|
+
const targetDir = requireDirectory(dir);
|
|
170
|
+
if (!targetDir)
|
|
171
|
+
return;
|
|
172
|
+
console.log(`Uninstalling from ${targetDir}`);
|
|
173
|
+
if (!opts.yes) {
|
|
174
|
+
if (!isInteractive()) {
|
|
175
|
+
console.error("Refusing to uninstall without confirmation in a non-interactive session; pass --yes.");
|
|
176
|
+
process.exitCode = 1;
|
|
177
|
+
return;
|
|
178
|
+
}
|
|
179
|
+
const { confirmed } = await inquirer.prompt([
|
|
180
|
+
{
|
|
181
|
+
type: "confirm",
|
|
182
|
+
name: "confirmed",
|
|
183
|
+
message: `Remove the orchestrator-workflow kit from ${targetDir}?`,
|
|
184
|
+
default: false,
|
|
185
|
+
},
|
|
186
|
+
]);
|
|
187
|
+
if (!confirmed) {
|
|
188
|
+
console.log("Aborted.");
|
|
189
|
+
return;
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
let report;
|
|
193
|
+
try {
|
|
194
|
+
report = runUninstall({ targetDir, force: opts.force });
|
|
195
|
+
}
|
|
196
|
+
catch (error) {
|
|
197
|
+
console.error(error instanceof Error ? error.message : error);
|
|
198
|
+
process.exitCode = 1;
|
|
199
|
+
return;
|
|
200
|
+
}
|
|
201
|
+
showPaths("Removed", report.removed);
|
|
202
|
+
showPaths("Kept (local edits or damaged fence)", report.kept);
|
|
203
|
+
showPaths("Already absent", report.missing);
|
|
204
|
+
for (const note of report.notes)
|
|
205
|
+
console.log(note);
|
|
206
|
+
console.log(`\norchestrator-workflow uninstalled from ${targetDir}`);
|
|
207
|
+
});
|
|
149
208
|
program.parseAsync(process.argv).catch((error) => {
|
|
150
209
|
console.error(error instanceof Error ? error.message : error);
|
|
151
210
|
process.exitCode = 1;
|
package/dist/index.d.ts
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
export { runInit } from "./init.js";
|
|
2
2
|
export type { InitOptions } from "./init.js";
|
|
3
|
+
export { runUninstall } from "./uninstall.js";
|
|
4
|
+
export type { UninstallReport } from "./uninstall.js";
|
|
3
5
|
export { detectHarnesses, parseHarnessList, HARNESSES } from "./detect.js";
|
|
4
6
|
export type { Harness } from "./detect.js";
|
|
5
7
|
export { DEFAULT_MODELS, MODEL_ALIASES, ROLES, claudeModelValue, opencodeModelValue, parseModelsSpec, } from "./models.js";
|
package/dist/index.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
export { runInit } from "./init.js";
|
|
2
|
+
export { runUninstall } from "./uninstall.js";
|
|
2
3
|
export { detectHarnesses, parseHarnessList, HARNESSES } from "./detect.js";
|
|
3
4
|
export { DEFAULT_MODELS, MODEL_ALIASES, ROLES, claudeModelValue, opencodeModelValue, parseModelsSpec, } from "./models.js";
|
|
4
5
|
export { PACKAGE_VERSION } from "./assets.js";
|
package/dist/init.d.ts
CHANGED
|
@@ -19,6 +19,13 @@ export interface Manifest {
|
|
|
19
19
|
files: Record<string, string>;
|
|
20
20
|
installedAt: string;
|
|
21
21
|
}
|
|
22
|
+
/**
|
|
23
|
+
* A manifest can be hand-written or tampered with, and uninstall deletes by
|
|
24
|
+
* these paths. Only relative paths that stay inside the target are accepted;
|
|
25
|
+
* an absolute path or one that normalizes to a `..` escape is rejected so it
|
|
26
|
+
* can never reach an unlink.
|
|
27
|
+
*/
|
|
28
|
+
export declare function isContainedRelativePath(relativePath: string): boolean;
|
|
22
29
|
/**
|
|
23
30
|
* Reads the manifest of a previous install, if any. Manifests can be written
|
|
24
31
|
* by hand (manual agent installs) or damaged, so every field is sanitized;
|
package/dist/init.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { createHash } from "node:crypto";
|
|
2
2
|
import { existsSync, readFileSync, statSync } from "node:fs";
|
|
3
|
-
import { join } from "node:path";
|
|
3
|
+
import { isAbsolute, join, normalize, sep } from "node:path";
|
|
4
4
|
import { PACKAGE_VERSION, listTemplateNames, readAgentAsset, readAsset, } from "./assets.js";
|
|
5
5
|
import { HARNESSES } from "./detect.js";
|
|
6
6
|
import { ROLES, assertValidModelId, claudeModelValue, opencodeModelValue, } from "./models.js";
|
|
@@ -10,6 +10,18 @@ const MANIFEST_PATH = join(".ai", "workflow", "manifest.json");
|
|
|
10
10
|
function sha256(content) {
|
|
11
11
|
return createHash("sha256").update(content, "utf8").digest("hex");
|
|
12
12
|
}
|
|
13
|
+
/**
|
|
14
|
+
* A manifest can be hand-written or tampered with, and uninstall deletes by
|
|
15
|
+
* these paths. Only relative paths that stay inside the target are accepted;
|
|
16
|
+
* an absolute path or one that normalizes to a `..` escape is rejected so it
|
|
17
|
+
* can never reach an unlink.
|
|
18
|
+
*/
|
|
19
|
+
export function isContainedRelativePath(relativePath) {
|
|
20
|
+
if (relativePath === "" || isAbsolute(relativePath))
|
|
21
|
+
return false;
|
|
22
|
+
const normalized = normalize(relativePath);
|
|
23
|
+
return normalized !== ".." && !normalized.startsWith(`..${sep}`);
|
|
24
|
+
}
|
|
13
25
|
/**
|
|
14
26
|
* Reads the manifest of a previous install, if any. Manifests can be written
|
|
15
27
|
* by hand (manual agent installs) or damaged, so every field is sanitized;
|
|
@@ -51,8 +63,11 @@ export function readInstalledManifest(targetDir) {
|
|
|
51
63
|
const files = {};
|
|
52
64
|
if (typeof candidate.files === "object" && candidate.files !== null) {
|
|
53
65
|
for (const [key, value] of Object.entries(candidate.files)) {
|
|
54
|
-
|
|
66
|
+
// Drop absolute or directory-escaping keys: uninstall deletes by these
|
|
67
|
+
// paths, so a tampered key must never enter the record.
|
|
68
|
+
if (typeof value === "string" && isContainedRelativePath(key)) {
|
|
55
69
|
files[key] = value;
|
|
70
|
+
}
|
|
56
71
|
}
|
|
57
72
|
}
|
|
58
73
|
return {
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
export interface UninstallReport {
|
|
2
|
+
/** Kit files removed (hash matched the install record, or --force). */
|
|
3
|
+
removed: string[];
|
|
4
|
+
/** Files left in place: locally edited kit files, damaged fences. */
|
|
5
|
+
kept: string[];
|
|
6
|
+
/** Ledger entries whose file was already gone. */
|
|
7
|
+
missing: string[];
|
|
8
|
+
/** Human-readable notes (run history kept, fence state, ...). */
|
|
9
|
+
notes: string[];
|
|
10
|
+
}
|
|
11
|
+
export declare function runUninstall(options: {
|
|
12
|
+
targetDir: string;
|
|
13
|
+
force?: boolean;
|
|
14
|
+
}): UninstallReport;
|
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
import { createHash } from "node:crypto";
|
|
2
|
+
import { existsSync, readFileSync, rmdirSync, statSync, unlinkSync, writeFileSync, } from "node:fs";
|
|
3
|
+
import { join } from "node:path";
|
|
4
|
+
import { isContainedRelativePath, readInstalledManifest } from "./init.js";
|
|
5
|
+
import { AGENTS_MD_HEADING, CLAUDE_IMPORT_LINE, CLAUDE_MD_BOILERPLATE, SECTION_BEGIN, SECTION_END, } from "./writers.js";
|
|
6
|
+
function sha256(content) {
|
|
7
|
+
return createHash("sha256").update(content, "utf8").digest("hex");
|
|
8
|
+
}
|
|
9
|
+
function removeAgentsSection(report, path) {
|
|
10
|
+
if (!existsSync(path))
|
|
11
|
+
return;
|
|
12
|
+
const existing = readFileSync(path, "utf8");
|
|
13
|
+
const lines = existing.split("\n");
|
|
14
|
+
const beginLines = [];
|
|
15
|
+
const endLines = [];
|
|
16
|
+
lines.forEach((line, index) => {
|
|
17
|
+
if (line.trim() === SECTION_BEGIN)
|
|
18
|
+
beginLines.push(index);
|
|
19
|
+
if (line.trim() === SECTION_END)
|
|
20
|
+
endLines.push(index);
|
|
21
|
+
});
|
|
22
|
+
if (beginLines.length === 0 && endLines.length === 0)
|
|
23
|
+
return;
|
|
24
|
+
if (beginLines.length !== 1 ||
|
|
25
|
+
endLines.length !== 1 ||
|
|
26
|
+
endLines[0] < beginLines[0]) {
|
|
27
|
+
report.kept.push(path);
|
|
28
|
+
report.notes.push(`${path}: marker fence is broken or duplicated; section left in place.`);
|
|
29
|
+
return;
|
|
30
|
+
}
|
|
31
|
+
const remaining = [
|
|
32
|
+
...lines.slice(0, beginLines[0]),
|
|
33
|
+
...lines.slice(endLines[0] + 1),
|
|
34
|
+
]
|
|
35
|
+
.join("\n")
|
|
36
|
+
.trim();
|
|
37
|
+
if (remaining === "" || remaining === AGENTS_MD_HEADING) {
|
|
38
|
+
// Nothing but what init itself created remains; remove the file.
|
|
39
|
+
unlinkSync(path);
|
|
40
|
+
report.removed.push(path);
|
|
41
|
+
return;
|
|
42
|
+
}
|
|
43
|
+
writeFileSync(path, `${remaining}\n`, "utf8");
|
|
44
|
+
report.removed.push(`${path} (workflow section)`);
|
|
45
|
+
}
|
|
46
|
+
function removeClaudeImport(report, path) {
|
|
47
|
+
if (!existsSync(path))
|
|
48
|
+
return;
|
|
49
|
+
const existing = readFileSync(path, "utf8");
|
|
50
|
+
if (existing === CLAUDE_MD_BOILERPLATE) {
|
|
51
|
+
unlinkSync(path);
|
|
52
|
+
report.removed.push(path);
|
|
53
|
+
return;
|
|
54
|
+
}
|
|
55
|
+
const lines = existing.split("\n");
|
|
56
|
+
const without = lines.filter((line) => line.trim() !== CLAUDE_IMPORT_LINE);
|
|
57
|
+
if (without.length === lines.length)
|
|
58
|
+
return;
|
|
59
|
+
const remaining = without.join("\n").trimEnd();
|
|
60
|
+
if (remaining === "") {
|
|
61
|
+
unlinkSync(path);
|
|
62
|
+
report.removed.push(path);
|
|
63
|
+
return;
|
|
64
|
+
}
|
|
65
|
+
writeFileSync(path, `${remaining}\n`, "utf8");
|
|
66
|
+
report.removed.push(`${path} (@AGENTS.md import line)`);
|
|
67
|
+
}
|
|
68
|
+
/**
|
|
69
|
+
* Directories init may have created, deepest first. Each is removed only
|
|
70
|
+
* when empty, so directories shared with user content always survive.
|
|
71
|
+
*/
|
|
72
|
+
const PRUNE_CANDIDATES = [
|
|
73
|
+
join(".ai", "workflow", "templates"),
|
|
74
|
+
join(".ai", "workflow"),
|
|
75
|
+
join(".ai", "runs"),
|
|
76
|
+
".ai",
|
|
77
|
+
join(".claude", "skills", "orchestrator-workflow"),
|
|
78
|
+
join(".claude", "skills"),
|
|
79
|
+
join(".claude", "agents"),
|
|
80
|
+
".claude",
|
|
81
|
+
join(".agents", "skills", "orchestrator-workflow"),
|
|
82
|
+
join(".agents", "skills"),
|
|
83
|
+
".agents",
|
|
84
|
+
join(".opencode", "agents"),
|
|
85
|
+
".opencode",
|
|
86
|
+
];
|
|
87
|
+
export function runUninstall(options) {
|
|
88
|
+
const { targetDir } = options;
|
|
89
|
+
const force = options.force ?? false;
|
|
90
|
+
const manifest = readInstalledManifest(targetDir);
|
|
91
|
+
if (!manifest) {
|
|
92
|
+
throw new Error(`No orchestrator-workflow install found in ${targetDir} (missing or unreadable .ai/workflow/manifest.json)`);
|
|
93
|
+
}
|
|
94
|
+
const report = {
|
|
95
|
+
removed: [],
|
|
96
|
+
kept: [],
|
|
97
|
+
missing: [],
|
|
98
|
+
notes: [],
|
|
99
|
+
};
|
|
100
|
+
for (const [relativePath, recordedHash] of Object.entries(manifest.files)) {
|
|
101
|
+
// Defense in depth: readInstalledManifest already drops escaping keys,
|
|
102
|
+
// but never unlink a path that is absolute, escapes the target, or is a
|
|
103
|
+
// directory rather than a kit file.
|
|
104
|
+
if (!isContainedRelativePath(relativePath)) {
|
|
105
|
+
report.kept.push(relativePath);
|
|
106
|
+
report.notes.push(`${relativePath}: manifest path is outside the target; ignored.`);
|
|
107
|
+
continue;
|
|
108
|
+
}
|
|
109
|
+
const path = join(targetDir, relativePath);
|
|
110
|
+
if (!existsSync(path)) {
|
|
111
|
+
report.missing.push(path);
|
|
112
|
+
continue;
|
|
113
|
+
}
|
|
114
|
+
if (!statSync(path).isFile()) {
|
|
115
|
+
report.kept.push(path);
|
|
116
|
+
report.notes.push(`${path}: not a regular file; ignored.`);
|
|
117
|
+
continue;
|
|
118
|
+
}
|
|
119
|
+
const content = readFileSync(path, "utf8");
|
|
120
|
+
if (force || sha256(content) === recordedHash) {
|
|
121
|
+
unlinkSync(path);
|
|
122
|
+
report.removed.push(path);
|
|
123
|
+
}
|
|
124
|
+
else {
|
|
125
|
+
report.kept.push(path);
|
|
126
|
+
report.notes.push(`${path}: locally edited since install; re-run with --force to remove.`);
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
removeAgentsSection(report, join(targetDir, "AGENTS.md"));
|
|
130
|
+
removeClaudeImport(report, join(targetDir, "CLAUDE.md"));
|
|
131
|
+
const manifestPath = join(targetDir, ".ai", "workflow", "manifest.json");
|
|
132
|
+
if (existsSync(manifestPath)) {
|
|
133
|
+
unlinkSync(manifestPath);
|
|
134
|
+
report.removed.push(manifestPath);
|
|
135
|
+
}
|
|
136
|
+
for (const candidate of PRUNE_CANDIDATES) {
|
|
137
|
+
const path = join(targetDir, candidate);
|
|
138
|
+
try {
|
|
139
|
+
rmdirSync(path);
|
|
140
|
+
}
|
|
141
|
+
catch {
|
|
142
|
+
// Not empty or not present; either way it stays.
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
const runsDir = join(targetDir, ".ai", "runs");
|
|
146
|
+
if (existsSync(runsDir)) {
|
|
147
|
+
report.notes.push(`${runsDir}: run history kept; remove manually if no longer needed.`);
|
|
148
|
+
}
|
|
149
|
+
return report;
|
|
150
|
+
}
|
package/dist/writers.d.ts
CHANGED
|
@@ -18,6 +18,8 @@ export declare function installFile(report: Report, path: string, content: strin
|
|
|
18
18
|
}): void;
|
|
19
19
|
export declare const SECTION_BEGIN = "<!-- orchestrator-workflow:begin -->";
|
|
20
20
|
export declare const SECTION_END = "<!-- orchestrator-workflow:end -->";
|
|
21
|
+
/** Heading used when init creates AGENTS.md itself. */
|
|
22
|
+
export declare const AGENTS_MD_HEADING = "# Agent instructions";
|
|
21
23
|
/**
|
|
22
24
|
* Creates AGENTS.md or replaces exactly the marker-fenced workflow section in
|
|
23
25
|
* it. Content outside the markers is never touched. Markers only count when
|
|
@@ -26,6 +28,9 @@ export declare const SECTION_END = "<!-- orchestrator-workflow:end -->";
|
|
|
26
28
|
*/
|
|
27
29
|
export declare function upsertMarkerSection(report: Report, path: string, section: string): void;
|
|
28
30
|
export declare const CLAUDE_IMPORT_LINE = "@AGENTS.md";
|
|
31
|
+
/** The exact CLAUDE.md that init creates from scratch; uninstall relies on
|
|
32
|
+
* recognizing this byte-identically before removing the whole file. */
|
|
33
|
+
export declare const CLAUDE_MD_BOILERPLATE = "# CLAUDE.md\n\nProject agent instructions live in AGENTS.md.\n\n@AGENTS.md\n";
|
|
29
34
|
/**
|
|
30
35
|
* Claude Code reads CLAUDE.md, not AGENTS.md. Ensure CLAUDE.md exists and
|
|
31
36
|
* imports AGENTS.md so the policy section is loaded there too.
|
package/dist/writers.js
CHANGED
|
@@ -31,6 +31,8 @@ export function installFile(report, path, content, options) {
|
|
|
31
31
|
}
|
|
32
32
|
export const SECTION_BEGIN = "<!-- orchestrator-workflow:begin -->";
|
|
33
33
|
export const SECTION_END = "<!-- orchestrator-workflow:end -->";
|
|
34
|
+
/** Heading used when init creates AGENTS.md itself. */
|
|
35
|
+
export const AGENTS_MD_HEADING = "# Agent instructions";
|
|
34
36
|
/**
|
|
35
37
|
* Creates AGENTS.md or replaces exactly the marker-fenced workflow section in
|
|
36
38
|
* it. Content outside the markers is never touched. Markers only count when
|
|
@@ -40,7 +42,7 @@ export const SECTION_END = "<!-- orchestrator-workflow:end -->";
|
|
|
40
42
|
export function upsertMarkerSection(report, path, section) {
|
|
41
43
|
const block = section.trimEnd();
|
|
42
44
|
if (!existsSync(path)) {
|
|
43
|
-
write(path,
|
|
45
|
+
write(path, `${AGENTS_MD_HEADING}\n\n${block}\n`);
|
|
44
46
|
report.written.push(path);
|
|
45
47
|
return;
|
|
46
48
|
}
|
|
@@ -80,13 +82,16 @@ export function upsertMarkerSection(report, path, section) {
|
|
|
80
82
|
report.updated.push(path);
|
|
81
83
|
}
|
|
82
84
|
export const CLAUDE_IMPORT_LINE = "@AGENTS.md";
|
|
85
|
+
/** The exact CLAUDE.md that init creates from scratch; uninstall relies on
|
|
86
|
+
* recognizing this byte-identically before removing the whole file. */
|
|
87
|
+
export const CLAUDE_MD_BOILERPLATE = `# CLAUDE.md\n\nProject agent instructions live in AGENTS.md.\n\n${CLAUDE_IMPORT_LINE}\n`;
|
|
83
88
|
/**
|
|
84
89
|
* Claude Code reads CLAUDE.md, not AGENTS.md. Ensure CLAUDE.md exists and
|
|
85
90
|
* imports AGENTS.md so the policy section is loaded there too.
|
|
86
91
|
*/
|
|
87
92
|
export function ensureClaudeImport(report, path) {
|
|
88
93
|
if (!existsSync(path)) {
|
|
89
|
-
write(path,
|
|
94
|
+
write(path, CLAUDE_MD_BOILERPLATE);
|
|
90
95
|
report.written.push(path);
|
|
91
96
|
return;
|
|
92
97
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "orchestrator-workflow",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.3.0",
|
|
4
4
|
"description": "Installer for an orchestrator-led agent workflow: .ai/ run state, an AGENTS.md policy section, and per-harness subagent definitions for Claude Code, OpenAI Codex, and opencode",
|
|
5
5
|
"main": "dist/index.js",
|
|
6
6
|
"type": "module",
|