orchestrator-workflow 0.1.0 → 0.2.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 CHANGED
@@ -1,16 +1,71 @@
1
1
  # Installing via a coding agent
2
2
 
3
- The interactive installer asks a human questions on a TTY. When the install
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
- Paste the prompt below to your agent, as is.
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
- Install the orchestrator-workflow kit into this repository.
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). Never change anything outside the
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,55 @@ 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.
43
+ - **Quality through structure.** Writing and reviewing are separated by
44
+ role and model, task slices are validated before any implementation
45
+ starts, acceptance is decided on evidence (tests executed, findings
46
+ addressed), and every run leaves an auditable trail in `.ai/runs/`.
47
+
13
48
  ## Install
14
49
 
15
50
  ```bash
16
51
  npx orchestrator-workflow init
17
52
  ```
18
53
 
19
- Run it at the root of the target repository. The installer is interactive by
20
- default: it locates existing harness configs (`.claude/`, `CLAUDE.md`,
21
- `.opencode/`, `opencode.json`, `.agents/`, `.codex/`), preselects what it
22
- found, and asks which model each subagent role should use.
54
+ Run it at the root of the target repository: **without a directory argument,
55
+ files are created in the current working directory.** The CLI prints the
56
+ resolved target (`Installing into ...`) before it writes anything and warns
57
+ when the target is not a git repository root; pass `init <dir>` to install
58
+ into a different directory. The installer is interactive by default: it
59
+ locates existing harness configs (`.claude/`, `CLAUDE.md`, `.opencode/`,
60
+ `opencode.json`, `.agents/`, `.codex/`), preselects what it found, and asks
61
+ which model each subagent role should use.
23
62
 
24
63
  Non-interactive:
25
64
 
@@ -28,10 +67,18 @@ npx orchestrator-workflow init --yes
28
67
  npx orchestrator-workflow init --harness claude,codex,opencode --models "implementer=sonnet,reviewer=opus" --yes
29
68
  ```
30
69
 
31
- To let a coding agent do the install, paste the prompt from
32
- [INSTALL-AGENT.md](INSTALL-AGENT.md): the agent asks the operator the
33
- harness and model questions in chat, then runs the non-interactive CLI, or
34
- scaffolds manually where npx is unavailable.
70
+ To let a coding agent do the install, give it this single line:
71
+
72
+ ```text
73
+ Follow the install instructions at https://raw.githubusercontent.com/LanNguyenSi/agent-dx/master/packages/orchestrator-workflow/INSTALL-AGENT.md
74
+ ```
75
+
76
+ The agent then asks you the harness and model questions in chat and runs the
77
+ non-interactive CLI (manual scaffolding where npx is unavailable).
78
+ [INSTALL-AGENT.md](INSTALL-AGENT.md) documents, step by step, what the
79
+ linked instructions make the agent do and which files it may touch, so the
80
+ prompt can be audited before delegating. The link tracks `master`; pin it
81
+ to a commit SHA for a stable audit.
35
82
 
36
83
  ## What gets installed
37
84
 
@@ -85,6 +132,20 @@ in `.ai/workflow/manifest.json` and reused as the default on later re-runs.
85
132
  version, harnesses, models, and file hashes, and is rewritten whenever that
86
133
  state changes; do not edit it by hand.
87
134
 
135
+ ## Uninstall
136
+
137
+ ```bash
138
+ npx orchestrator-workflow uninstall
139
+ ```
140
+
141
+ Removes exactly what `init` created, driven by the manifest's file hashes:
142
+ unedited kit files are deleted, locally edited ones are kept and reported
143
+ (`--force` removes those too). The AGENTS.md section and the CLAUDE.md
144
+ import line are taken out; either file is deleted only when nothing but
145
+ init's own boilerplate remains. Kit directories are pruned only when empty,
146
+ and run history under `.ai/runs/` is always kept. Interactive runs ask for
147
+ confirmation; non-interactive runs require `--yes`.
148
+
88
149
  ## Relation to agentic-coding-playbook
89
150
 
90
151
  This kit ships the orchestration layer: who coordinates whom, where state
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 = resolve(dir);
89
- if (!existsSync(targetDir) || !statSync(targetDir).isDirectory()) {
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
- const show = (label, paths) => {
137
- if (paths.length === 0)
138
- return;
139
- console.log(`${label}:`);
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
- if (typeof value === "string")
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, `# Agent instructions\n\n${block}\n`);
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, `# CLAUDE.md\n\nProject agent instructions live in AGENTS.md.\n\n${CLAUDE_IMPORT_LINE}\n`);
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.1.0",
3
+ "version": "0.2.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",