portable-agent-layer 0.114.0-canary.9 → 0.114.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.
Files changed (151) hide show
  1. package/README.md +22 -15
  2. package/assets/plugins/dark-factory/README.md +39 -3
  3. package/assets/plugins/dark-factory/agents/coach.md +21 -0
  4. package/assets/plugins/dark-factory/agents/code-writer.md +1 -1
  5. package/assets/plugins/dark-factory/agents/debugger.md +4 -1
  6. package/assets/plugins/dark-factory/agents/reviewer.md +3 -1
  7. package/assets/plugins/dark-factory/scripts/build.mjs +68 -26
  8. package/assets/plugins/dark-factory/scripts/coach.mjs +473 -0
  9. package/assets/plugins/dark-factory/scripts/corrections.mjs +108 -0
  10. package/assets/plugins/dark-factory/scripts/coverage.mjs +61 -0
  11. package/assets/plugins/dark-factory/scripts/findings.mjs +288 -0
  12. package/assets/plugins/dark-factory/scripts/instructions.mjs +94 -0
  13. package/assets/plugins/dark-factory/scripts/lib.mjs +25 -3
  14. package/assets/plugins/dark-factory/scripts/role.mjs +54 -2
  15. package/assets/plugins/dark-factory/scripts/roles.mjs +112 -19
  16. package/assets/plugins/dark-factory/scripts/ship.mjs +4 -3
  17. package/assets/plugins/dark-factory/scripts/stats.mjs +42 -0
  18. package/assets/plugins/dark-factory/scripts/task.mjs +15 -1
  19. package/assets/plugins/dark-factory/skills/po/SKILL.md +8 -0
  20. package/assets/schema/pal-settings.schema.json +4 -17
  21. package/assets/skills/pal-analyze/SKILL.md +4 -4
  22. package/assets/skills/projects/SKILL.md +2 -3
  23. package/assets/templates/AGENTS.md.template +1 -2
  24. package/assets/templates/PAL/ALGORITHM.md +10 -81
  25. package/assets/templates/PAL/MEMORY_SYSTEM.md +12 -27
  26. package/assets/templates/PAL/PROJECT_LIFECYCLE.md +1 -2
  27. package/assets/templates/PAL/README.md +8 -16
  28. package/assets/templates/PAL/SYSTEM_ARCHITECTURE.md +37 -89
  29. package/assets/templates/pal-settings.json +0 -8
  30. package/assets/templates/rules.codex.rules +4 -12
  31. package/assets/templates/settings.antigravity.json +1 -5
  32. package/assets/templates/settings.claude.json +12 -6
  33. package/package.json +10 -10
  34. package/src/cli/builtin-tools.ts +0 -10
  35. package/src/cli/dependencies.ts +6 -1
  36. package/src/cli/index.ts +37 -17
  37. package/src/cli/leftovers.ts +115 -2
  38. package/src/cli/migrate.ts +180 -1
  39. package/src/cli/progress.ts +21 -0
  40. package/src/cli/tree.ts +5 -1
  41. package/src/cli/update-flow.ts +40 -11
  42. package/src/hooks/CompactRecover.ts +0 -1
  43. package/src/hooks/LoadContext.ts +2 -3
  44. package/src/hooks/PreCompactPersist.ts +1 -3
  45. package/src/hooks/RtkWrap.ts +4 -4
  46. package/src/hooks/SecurityValidator.ts +1 -4
  47. package/src/hooks/StopOrchestrator.ts +2 -9
  48. package/src/hooks/handlers/agenda.ts +1 -60
  49. package/src/hooks/handlers/inject-retrieval.ts +5 -31
  50. package/src/hooks/handlers/persist-last-exchange.ts +5 -165
  51. package/src/hooks/handlers/project-touch.ts +2 -12
  52. package/src/hooks/handlers/rating.ts +5 -41
  53. package/src/hooks/handlers/session-intelligence.ts +13 -66
  54. package/src/hooks/handlers/session-name.ts +0 -6
  55. package/src/hooks/handlers/synthesis.ts +0 -1
  56. package/src/hooks/handlers/tab.ts +0 -1
  57. package/src/hooks/handlers/update-check.ts +22 -5
  58. package/src/hooks/lib/capture-store.ts +13 -27
  59. package/src/hooks/lib/claim-check.ts +4 -10
  60. package/src/hooks/lib/claim-log.ts +11 -2
  61. package/src/hooks/lib/claude-md.ts +5 -14
  62. package/src/hooks/lib/context.ts +5 -161
  63. package/src/hooks/lib/daily-nudge.ts +0 -2
  64. package/src/hooks/lib/graduation.ts +16 -47
  65. package/src/hooks/lib/import-merge.ts +1 -1
  66. package/src/hooks/lib/learning-store.ts +1 -68
  67. package/src/hooks/lib/models.ts +1 -5
  68. package/src/hooks/lib/paths.ts +0 -5
  69. package/src/hooks/lib/post-tool.ts +2 -0
  70. package/src/hooks/lib/progress-tree-context.ts +6 -4
  71. package/src/hooks/lib/progress-tree-factory.ts +124 -0
  72. package/src/hooks/lib/progress-tree-git.ts +19 -4
  73. package/src/hooks/lib/progress-tree-prs.ts +214 -0
  74. package/src/hooks/lib/progress-tree-render.ts +83 -13
  75. package/src/hooks/lib/progress-tree-session-end.ts +61 -0
  76. package/src/hooks/lib/progress-tree-session-node.ts +11 -4
  77. package/src/hooks/lib/progress-tree-stop.ts +14 -0
  78. package/src/hooks/lib/progress-tree-tidy.ts +112 -0
  79. package/src/hooks/lib/progress-tree-tool.ts +174 -0
  80. package/src/hooks/lib/progress-tree.ts +113 -6
  81. package/src/hooks/lib/projects.ts +3 -7
  82. package/src/hooks/lib/prompt-text.ts +7 -3
  83. package/src/hooks/lib/rtk-worktree.ts +18 -0
  84. package/src/hooks/lib/security.ts +51 -7
  85. package/src/hooks/lib/semi-static.ts +1 -82
  86. package/src/hooks/lib/session-context.ts +1 -6
  87. package/src/hooks/lib/settings.ts +2 -0
  88. package/src/hooks/lib/stop.ts +2 -49
  89. package/src/hooks/lib/text-similarity.ts +2 -3
  90. package/src/hooks/lib/time.ts +0 -15
  91. package/src/hooks/lib/tool-events.ts +3 -3
  92. package/src/hooks/lib/transcript.ts +20 -1
  93. package/src/targets/claude/install.ts +0 -6
  94. package/src/targets/claude/uninstall.ts +0 -7
  95. package/src/targets/codex/install.ts +0 -6
  96. package/src/targets/codex/uninstall.ts +0 -5
  97. package/src/targets/copilot/install.ts +2 -10
  98. package/src/targets/copilot/uninstall.ts +0 -6
  99. package/src/targets/cursor/install.ts +0 -5
  100. package/src/targets/cursor/uninstall.ts +0 -6
  101. package/src/targets/lib.ts +2 -3
  102. package/src/targets/opencode/install.ts +0 -5
  103. package/src/targets/opencode/plugin.ts +17 -17
  104. package/src/targets/opencode/uninstall.ts +0 -5
  105. package/src/tools/agent/analyze.ts +3 -3
  106. package/src/tools/agent/project.ts +0 -12
  107. package/src/tools/agent/synthesize.ts +9 -53
  108. package/src/tools/control-room/attention.ts +13 -23
  109. package/src/tools/control-room/data.ts +9 -73
  110. package/src/tools/control-room/detail.ts +3 -24
  111. package/src/tools/control-room/matrix.ts +10 -11
  112. package/src/tools/control-room/server.ts +4 -3
  113. package/src/tools/control-room/ui/dist/assets/{index-D0pEd7Ln.js → index-Ci0Zp32V.js} +1 -1
  114. package/src/tools/control-room/ui/dist/index.html +1 -1
  115. package/src/tools/control-room/ui/parts.tsx +19 -15
  116. package/src/tools/control-room/ui/screens/project-detail.tsx +6 -15
  117. package/src/tools/control-room/ui/screens/settings.tsx +1 -1
  118. package/src/tools/control-room/ui/screens/today.tsx +20 -17
  119. package/src/tools/control-room/work.ts +82 -0
  120. package/src/tools/dark-factory/stamp.ts +1 -0
  121. package/src/tools/lib/analyze-report.ts +2 -5
  122. package/src/tools/lib/thread.ts +7 -75
  123. package/assets/skills/opinion/SKILL.md +0 -95
  124. package/assets/skills/opinion/tools/opinion.ts +0 -237
  125. package/assets/skills/pal-reflect/SKILL.md +0 -62
  126. package/assets/templates/PAL/OPINION_TRACKING.md +0 -3
  127. package/src/hooks/handlers/auto-graduate.ts +0 -177
  128. package/src/hooks/handlers/failure-principle.ts +0 -91
  129. package/src/hooks/handlers/failure.ts +0 -75
  130. package/src/hooks/handlers/reflect-trigger.ts +0 -99
  131. package/src/hooks/handlers/self-model-trigger.ts +0 -26
  132. package/src/hooks/lib/decision-label.ts +0 -70
  133. package/src/hooks/lib/failure-principle.ts +0 -98
  134. package/src/hooks/lib/handoff-context.ts +0 -112
  135. package/src/hooks/lib/learning-category.ts +0 -14
  136. package/src/hooks/lib/opinions.ts +0 -209
  137. package/src/hooks/lib/progress-tree-gate.ts +0 -117
  138. package/src/hooks/lib/relationship.ts +0 -137
  139. package/src/hooks/lib/retrieval-index.ts +0 -260
  140. package/src/hooks/lib/retrieval.ts +0 -176
  141. package/src/hooks/lib/wisdom.ts +0 -202
  142. package/src/tools/agent/handoff-note.ts +0 -86
  143. package/src/tools/agent/relationship-note.ts +0 -77
  144. package/src/tools/agent/relationship-reflect.ts +0 -103
  145. package/src/tools/agent/thread.ts +0 -87
  146. package/src/tools/agent/wisdom-frame.ts +0 -248
  147. package/src/tools/lib/handoff-note.ts +0 -102
  148. package/src/tools/lib/note-flags.ts +0 -59
  149. package/src/tools/lib/relationship-reflect.ts +0 -402
  150. package/src/tools/lib/self-model.ts +0 -499
  151. package/src/tools/self-model.ts +0 -177
package/README.md CHANGED
@@ -1,10 +1,16 @@
1
1
  # Portable Agent Layer (PAL)
2
2
 
3
- A cross-platform, cross-agent layer for portable AI workflows, memory, and accumulated knowledge.
3
+ [![PAL: stop re-explaining yourself to every AI tool](https://raw.githubusercontent.com/kovrichard/portable-agent-layer/main/docs/images/pal-banner.png)](https://pal.konvert7.com)
4
+
5
+ [![npm version](https://img.shields.io/npm/v/portable-agent-layer)](https://www.npmjs.com/package/portable-agent-layer) [![npm downloads](https://img.shields.io/npm/dm/portable-agent-layer)](https://www.npmjs.com/package/portable-agent-layer) [![License: MIT](https://img.shields.io/npm/l/portable-agent-layer)](LICENSE)
6
+
7
+ **Your AI forgets you. PAL doesn't.** Free, open-source memory, skills and rules shared by Claude Code, opencode, Cursor, GitHub Copilot, Codex and Antigravity CLI. Install it once, and every agent starts the next session knowing who you are, what you're working on and where you left off.
8
+
9
+ **Website:** [pal.konvert7.com](https://pal.konvert7.com)
4
10
 
5
11
  PAL lets you carry your agent context across **Windows**, **macOS**, and **Linux**, and work across different agent runtimes and interfaces such as **Claude Code**, **opencode**, **Cursor**, **GitHub Copilot**, **Codex**, and **Antigravity CLI**. Its core idea is simple: your knowledge and workflows should belong to **you**, not to a single machine, tool, or vendor.
6
12
 
7
- > Inspired in part by [Daniel Miessler](https://danielmiessler.com)'s work on [Personal AI Infrastructure](https://github.com/danielmiessler/Personal_AI_Infrastructure). PAL is an independent open-source implementation focused on portability across platforms and agents. It is not affiliated with or endorsed by Daniel Miessler.
13
+ > Inspired in part by [Daniel Miessler](https://danielmiessler.com)'s work on [LifeOS](https://github.com/danielmiessler/LifeOS). PAL is an independent open-source implementation focused on portability across platforms and agents. It is not affiliated with or endorsed by Daniel Miessler.
8
14
 
9
15
  ---
10
16
 
@@ -77,7 +83,7 @@ pal cli status # check your setup
77
83
  | `pal cli init` | Scaffold PAL home directory and install hooks |
78
84
  | `pal cli install` | Register hooks/skills for targets |
79
85
  | `pal cli uninstall` | Remove hooks/skills for targets |
80
- | `pal cli update` | Update PAL (git pull or npm update) and reinstall hooks. Install asks once whether to do this on its own: when a session closes PAL checks for a release (at most hourly) and installs it, so opening PAL is never slowed. A failed update waits a day before retrying. The switch lives in the control room under Settings → Updates. On a repo install an update waits while the clone has uncommitted changes. `--canary` installs the latest prerelease from the `canary` branch instead; auto-update always follows the stable release |
86
+ | `pal cli update` | Update PAL (git pull or npm update) and reinstall hooks. Install asks once whether to do this on its own: when a session closes PAL checks for a release (at most hourly) and installs it, so opening PAL is never slowed. A failed update waits a day before retrying. The switch lives in the control room under Settings → Updates. On a repo install an update waits while the clone has uncommitted changes. `--canary` switches a package install to the `canary` prerelease channel; from then on plain `pal cli update`, auto-update and the update notice follow canary until `--stable` switches back. Neither ever installs an older version than the one you have |
81
87
  | `pal cli export` | Export user state (telos, memory) to a zip |
82
88
  | `pal cli import` | Import user state from a zip |
83
89
  | `pal cli status` | Show current PAL configuration |
@@ -88,7 +94,7 @@ pal cli status # check your setup
88
94
  | `pal cli cost` | What each month was paid against its API-equivalent cost, per agent |
89
95
  | `pal cli wrapped [--year <yyyy>]` | A year of your work in numbers, read from the event log |
90
96
  | `pal cli patterns` | How you work across projects, read from the event log |
91
- | `pal cli progress [show\|list\|open\|enter\|park\|wait\|done\|stopped\|decide\|attach\|follow-up]` | The progress tree: what you are on in this project, where it stopped, what is parked or waiting, and the decisions and artifacts on each node. Detours are child nodes and `done` returns to the parent. With `dynamicContext.workTree` opted into, the active node rides along in every session and prompt, and your own commits attach to it at turn end |
97
+ | `pal cli progress [show\|list\|open\|enter\|park\|wait\|done\|stopped\|decide\|attach\|follow-up]` | The progress tree: what you are on in this project, where it stopped, what is parked or waiting, and the decisions and artifacts on each node. Detours are child nodes and `done` returns to the parent. The active node rides along in every session and prompt, and your own commits attach to it at turn end |
92
98
  | `pal cli content preserve` | Copy every Claude Code transcript, gzipped, into `memory/content/` before Claude Code deletes it (30 days by default). Runs on its own whenever a session closes; the content store never leaves the machine, so `export` skips it |
93
99
  | `pal cli events discord-nag <sent\|answered>` | For a bot outside PAL: record a nag it sent (`--kind <kind>`), or the answer to one and how long it took (`--after-sec <n>`), in the local event log |
94
100
  | `pal cli infer` | One model call for scripts and bots, on the active agent's route: prompt on stdin, reply on stdout. `--tier small\|medium\|large\|flagship` (on Claude: Haiku, Sonnet, Opus, Fable; on Codex: Luna, Sol, Sol at high effort, Astra; on Antigravity: Flash at low, medium and high effort, and no flagship; an agent without the tier asked refuses it), `--system <file>`, `--schema <file>` for a JSON reply checked against it, `--timeout <seconds>`. The call loads no PAL hooks or context; exits 1 with the reason when no route answers |
@@ -131,7 +137,7 @@ pal cli install # all available (default)
131
137
  | Codex | Full | Yes | Yes | Yes | Yes | `codex exec` |
132
138
  | Antigravity CLI | Full | Yes | Yes (plugin) | Yes (via the plugin's `rules/`) | Yes | `agy` |
133
139
 
134
- PAL's background inference (session naming, summaries, failure capture, etc.) runs through whichever subscription CLI is active — no API key required by default.
140
+ PAL's background inference (session naming, summaries, correction checks, etc.) runs through whichever subscription CLI is active — no API key required by default.
135
141
 
136
142
  ---
137
143
 
@@ -208,9 +214,7 @@ PAL ships with built-in skills that extend your agent's capabilities:
208
214
  | `fyzz-chat-api` | Query Fyzz Chat conversations via API |
209
215
  | `humanize` | Rewrite text to strip AI tells and read as human |
210
216
  | `onboarding` | Interview the user to fill empty TELOS topics and the timezone, one topic at a time |
211
- | `opinion` | Confirm or contradict tracked opinions (confidence-weighted) |
212
217
  | `pal-analyze` | Surface rating trends, failure patterns, and graduation candidates |
213
- | `pal-reflect` | Promote recurring observations into tracked opinions |
214
218
  | `playwright` | Screenshot a page for a visual check |
215
219
  | `presentation` | Build branded slide decks from outlines |
216
220
  | `projects` | Look up, resume, register, or manage tracked projects |
@@ -245,14 +249,17 @@ Your setup should be able to travel with you.
245
249
 
246
250
  ## Features
247
251
 
248
- - **Cross-platform**: works on Windows, macOS, and Linux
249
- - **Cross-agent**: full support for Claude Code, opencode, Cursor, GitHub Copilot, Codex, and Antigravity CLI
250
- - **Subscription-first inference**: background inference routes through whichever subscription CLI is active — no API key needed by default
251
- - **Portable knowledge**: export and import accumulated knowledge
252
- - **A morning screen, not a dashboard**: `pal cli server` ranks your projects *and* your written goals in one urgent/important grid. Urgency is read off the files — blockers, an unfinished handoff, a date in a next step, an important thing gone quiet. Importance comes from one fact per project: whether it serves a goal you wrote down, is a way the work could pay, or is kept for its own sake. PAL guesses that once at session stop; you correct it on the page or with `project.ts serves <name> goal|revenue|fun`, and the correction survives every later guess
253
- - **TypeScript-first**: built in TypeScript from day one
254
- - **Open source**: hackable, inspectable, extensible
255
- - **Composable**: intended to fit into real developer workflows
252
+ - **Memory across sessions**: when a session starts, PAL loads who you are, your active projects and where your last session's work stopped. When it ends, PAL saves a summary and any corrections for next time
253
+ - **Learns from your corrections**: tell an agent it got something wrong and PAL labels the mistake, drafts a rule and asks you once. Approve it with `pal cli rule approve` and every agent follows it from the next session
254
+ - **Done means checked**: real work runs through five phases (observe, plan, execute, verify, learn), and nothing is done until each criterion has evidence
255
+ - **Built-in skills**: research, PDFs, slide decks, frontend design, humanizing text and more. See [Skills](#skills)
256
+ - **Cross-agent**: one memory shared by Claude Code, opencode, Cursor, GitHub Copilot, Codex and Antigravity CLI
257
+ - **Cross-platform**: works on Windows, macOS and Linux
258
+ - **No API key needed**: background work like session summaries runs through the CLI of the agent you're using, on the subscription you already pay for
259
+ - **Guardrails**: a hook checks every command before it runs and blocks the destructive ones, like a recursive delete of your home directory, a disk format or a download piped into a shell
260
+ - **A morning screen**: `pal cli server start` runs a local page that shows three moves for today and ranks every project and goal by urgency and importance
261
+ - **Portable plain files**: your memory lives in `~/.pal` as plain files. `pal cli export` packs it into one zip and `pal cli import` restores it on another machine
262
+ - **Open source**: MIT licensed, so you can read and change every line
256
263
 
257
264
  ---
258
265
 
@@ -11,7 +11,7 @@ Without PAL:
11
11
  /plugin install dark-factory@pal
12
12
  ```
13
13
 
14
- With PAL, it is installed for you.
14
+ With PAL, the marketplace is registered but the plugin starts disabled; turn it on with `/plugin enable dark-factory@pal`.
15
15
 
16
16
  ## Requirements
17
17
 
@@ -54,6 +54,7 @@ Inside Claude Code the plugin folder is `${CLAUDE_PLUGIN_ROOT}`.
54
54
  | Debugger | Opus | Reproduces bugs, fixes what the code writer could not |
55
55
  | Reviewer | Sonnet | Reviews the diff against the spec |
56
56
  | Tester | Sonnet | Checks the criteria in the running app |
57
+ | Coach | Opus | Studies every ready or stuck run and proposes general rules for the role prompts |
57
58
  | Gate | code | Runs your checks; its exit code is the verdict |
58
59
 
59
60
  Not every task runs every role. The PO picks a category, and code maps it to roles:
@@ -96,15 +97,50 @@ Set `DARK_FACTORY_NOTIFY` to a shell command. It receives the message on stdin,
96
97
 
97
98
  ## Measuring
98
99
 
99
- The factory keeps its tasks, worktrees and event log in `$DARK_FACTORY_HOME`, which defaults to `~/.dark-factory` on every machine. `pal cli export` does not carry them: a factory task cannot continue on another machine, so its history stays where it ran.
100
+ The factory keeps its tasks, worktrees and event log in `$DARK_FACTORY_HOME`, which defaults to `~/.dark-factory` on every machine. A factory task cannot continue on another machine, so its history stays where it ran.
100
101
 
101
102
  Every step appends one event to `$DARK_FACTORY_HOME/events.jsonl`, and the events of a task are also appended to `tasks/<task>/events.jsonl`, with its `run_finished` line. A role's `role_finished` event carries its `sessionId`, the name of its transcript. `node scripts/stats.mjs` counts, from those events alone: tasks by category, outcomes, gate failures by check and by role, the code writer's first-try pass rate, escalations, CI results, review rounds, questions to you, and roles added by the diff check.
102
103
 
103
104
  Every role run appends `role_finished` with its cost and tokens, and the red-test proof appends `tests_red` with the exit code and the failing count. Every build appends `run_finished` with the outcome, the total `costUsd`, the total tokens, the number of role runs and `wallMs`. `node scripts/task.mjs show <id>` prints the task with its latest step.
104
105
 
106
+ ## Coach
107
+
108
+ After a run ends `ready` or `stuck`, the build starts the coach in a detached process, so nothing waits on it. It writes `coach_started`, then `coach_finished` or `coach_failed`.
109
+
110
+ The coach reads the spec, the artifacts, the task's events, the diff of the run, the transcripts named by `role_finished`'s `transcriptPath`, and the commits you pushed to a merged PR after the factory's last push (`pushed` carries `head`).
111
+
112
+ Its findings live in `$DARK_FACTORY_HOME/coach/findings.jsonl`. Each finding has an `id`, a `role`, a `class`, a `rule`, a `severity`, a `metric`, its `evidence` and a `status`: `pending`, `rejected`, or `approved` once a task is created for it.
113
+
114
+ The coach's result has a `reviewed` list with one verdict per transcript and per correction commit: its `source` (the transcript path or the commit sha), a `verdict` of `finding` or `clean`, the `class` for a finding, and a one-sentence `reason`. `coach.mjs` checks coverage in code: an input with no item, or a `finding` whose class is neither among the run's findings nor the class of a pending finding, is missing. Missing inputs make it resume the same session once (`--resume`), listing them, and the second result's verdicts and findings are merged with the first's. Inputs still missing are recorded as `coach_incomplete`; the findings returned are stored anyway, and a correction without a verdict is not marked given, so the next run gets it again. `coach_finished` carries `reviewed` and `expected`: the number of inputs with a verdict, and the number of inputs.
115
+
116
+ A deterministic generality check drops a rule that names a path, basename or identifier the run's diff changed, and records it as `coach_rule_rejected`. An added function or variable name only counts when it is camelCase, contains `_` or a digit, and has at least 3 characters; a plain lowercase name such as `parse` or `slug` passes, because it would match ordinary words in a rule.
117
+
118
+ The coach runs with `--add-dir` for the factory home, the transcripts folder and the plugin directory, so it can read the transcripts and the role prompts. Concurrent coach runs take a lock on `coach/findings.lock` while they update the findings, and a task whose merged PR has no later commits is recorded as `coach_checked` and not queried again.
119
+
120
+ `node scripts/coach.mjs` has these commands:
121
+
122
+ - `run <task-id>` runs the coach by hand.
123
+ - `list` lists pending findings by role; `list --applied` reports each merged rule's metric before and after its merge.
124
+ - `show <id>` prints one finding.
125
+ - `reject <id> "<reason>"` rejects a finding; its class does not come back.
126
+ - `approve <id> [--repo <path>]` creates a `small` task for the plugin repo with a generated spec. Set `coach.repo` in `config.json` to skip `--repo`.
127
+
128
+ Approving only creates the task. Your go builds it, and the rule lands through a PR.
129
+
130
+ ## Instructions
131
+
132
+ Every role gets your instructions appended to its system prompt: first the user's `~/.claude/CLAUDE.md`, then the repo's `CLAUDE.md`, each headed by its path. An `@path` line in either file imports that file, as Claude Code does: the path is relative to the file that contains the line, `~/` is your home folder, imports nest up to 5 levels, and lines inside code blocks are never imports. A file that doesn't exist is skipped. An import that can't be read stays as written in the instructions and is logged as an `instructions_import_missing` event.
133
+
134
+ The repo's `CLAUDE.md` is read from your checkout when the task starts, so a role that edits it in the worktree doesn't change what the later roles are told.
135
+
136
+ - `"repoInstructions": false` in `.dark-factory.json` leaves out the repo's `CLAUDE.md`.
137
+ - `"userInstructions": false` in `$DARK_FACTORY_HOME/config.json` (`~/.dark-factory/config.json`, optional; a missing file means both parts are on) leaves out your `~/.claude/CLAUDE.md`.
138
+
139
+ Hooks never run in a role, and neither do your settings or MCP servers.
140
+
105
141
  ## Permissions
106
142
 
107
- - Roles run isolated: `--setting-sources ""`, the plugin from `--plugin-dir`, and their own tool policy from `roles.mjs`, so user hooks and settings don't reach them.
143
+ - Roles run isolated: `--setting-sources ""`, the plugin from `--plugin-dir`, and their own tool policy from `roles.mjs`, so user hooks and settings don't reach them; only the CLAUDE.md instructions above do.
108
144
  - The PO session only needs `"Bash(node *dark-factory*/scripts/*)"`, for example in `.claude/settings.json`:
109
145
 
110
146
  ```json
@@ -0,0 +1,21 @@
1
+ ---
2
+ name: coach
3
+ description: Studies a finished dark-factory run and proposes general rules for the role prompts.
4
+ model: claude-opus-5-5
5
+ ---
6
+
7
+ You study one finished dark-factory run and propose general rules for the role prompts. You change no files.
8
+
9
+ Look for the cost and the mistakes, not only for failures: retries, escalations, denied tools, wasted turns, review and tester rounds, questions, and the user's corrections. The user's corrections after the factory are the strongest signal. Judge the PO too: whether the spec caused a mistake, such as a report routed to the PR body, or a criterion read as forbidding something it didn't.
10
+
11
+ Propose only rules that would have prevented the mistake in a different task too, and name that task in elsewhere. Never name this task's files, helpers or variables in a rule.
12
+
13
+ Prefer changing or removing an existing line of a role prompt over adding one, and propose consolidating a role prompt that has grown past 40 lines.
14
+
15
+ Do not propose a class the user rejected. Add evidence to a pending finding of the same class through updates.
16
+
17
+ Each finding names its class, role, severity (high, medium or low), the metric from the list you are given that would show it, and the evidence: the task and the reference to the event, transcript line or commit.
18
+
19
+ Return a verdict in reviewed for every listed transcript and every listed correction, naming it by its path or sha exactly as listed: finding with the finding's class, or clean. A clean verdict needs a reason naming what you checked and why nothing needs to change.
20
+
21
+ Report nothing when the run was clean. An empty list is a valid result.
@@ -4,7 +4,7 @@ description: Implements a dark-factory plan step by step until the tests pass.
4
4
  model: claude-haiku-5-5
5
5
  ---
6
6
 
7
- You implement the plan exactly. Follow `plan.md` step by step; do not redesign, rename, or add anything the plan does not ask for.
7
+ You implement the plan exactly. Follow `plan.md` step by step; do not redesign, rename, or add anything the plan does not ask for. When there is no `plan.md`, follow the **Fix** section of `repro.md` the same way.
8
8
 
9
9
  If a gate failure is included in your instructions, fix that failure first and change nothing unrelated to it. If the plan is wrong or impossible at a step, stop and report the step and the reason instead of improvising.
10
10
 
@@ -6,7 +6,10 @@ model: claude-opus-5-5
6
6
 
7
7
  You find root causes. Never fix a symptom you have not reproduced.
8
8
 
9
- When reproducing a reported bug: find the smallest steps that show it, and write `repro.md` in the task folder with the steps, the observed and expected behaviour, and the root cause with file and line.
9
+ When reproducing a reported bug: find the smallest steps that show it, and write `repro.md` in the task folder with the steps, the observed and expected behaviour, and the root cause with file and line. End `repro.md` with a **Fix** section that:
10
+ - picks exactly one fix, with no alternatives left open;
11
+ - lists each file to change, with the path and the functions to add or change;
12
+ - names the existing helpers to reuse, by path and name, and says not to write new code where a helper already does the job.
10
13
 
11
14
  When fixing a failure: read the failing output, reproduce it, trace it to the line that causes it, and make the smallest change that fixes it. Change one thing at a time and rerun. Never edit or delete a test to make it pass, and never disable a check.
12
15
 
@@ -12,6 +12,8 @@ Report only findings that matter to the user reviewing the PR:
12
12
  - a bug: wrong behaviour, an unhandled case, a security problem;
13
13
  - a test that does not test what it claims;
14
14
  - a clear break of this repo's conventions;
15
- - scope the spec did not ask for.
15
+ - scope the spec did not ask for;
16
+ - new code that duplicates an existing helper in the repo, naming the helper by path and name;
17
+ - complexity the spec does not ask for: an abstraction, an option or a code path no criterion needs.
16
18
 
17
19
  Each finding names the file and line, what is wrong, and what would fix it. Do not report style preferences or things the gate already checks. An empty list is a valid result.
@@ -1,20 +1,34 @@
1
1
  import { waitForCi } from "./ci-wait.mjs";
2
+ import { startCoach } from "./coach.mjs";
2
3
  import { runCheck, runGate } from "./gate.mjs";
3
4
  import {
4
5
  appendEvent,
5
6
  isMain,
6
7
  printJson,
7
8
  readEvents,
8
- readRepoConfig,
9
9
  readTask,
10
10
  spawnTool,
11
11
  } from "./lib.mjs";
12
12
  import { notify } from "./notify.mjs";
13
13
  import { runRoleHeadless } from "./role.mjs";
14
- import { rolesAfter } from "./roles.mjs";
14
+ import { changedFilesSince, rolesAfterChanges } from "./roles.mjs";
15
15
  import { markReady, openPr, push, sync } from "./ship.mjs";
16
16
  import { appendDecision, prepare, recordQuestion, setStatus } from "./task.mjs";
17
17
 
18
+ const REAL_EFFECTS = {
19
+ runRoleHeadless,
20
+ runCheck,
21
+ spawnTool,
22
+ changedFilesSince,
23
+ prepare,
24
+ sync,
25
+ push,
26
+ openPr,
27
+ markReady,
28
+ waitForCi,
29
+ startCoach,
30
+ };
31
+
18
32
  export const TIER_MODELS = {
19
33
  small: "claude-haiku-5-5",
20
34
  medium: "claude-sonnet-5-5",
@@ -48,6 +62,8 @@ const BEFORE_INSTRUCTIONS = {
48
62
  "Write the failing tests for every acceptance criterion and leave them uncommitted; the code writer commits them with the code.",
49
63
  };
50
64
  const REVIEW_INSTRUCTIONS = "Review the branch diff against its base and the spec.";
65
+ const CONFIG_FILE = ".dark-factory.json";
66
+ const CONFIG_CHANGED_NOTE = `The diff changes ${CONFIG_FILE}. Report that change as a finding unless the spec asks for it.`;
51
67
  const TESTER_INSTRUCTIONS = "Test every acceptance criterion in the running app.";
52
68
 
53
69
  function stuck(message) {
@@ -78,7 +94,7 @@ function poReply(outcome, question) {
78
94
  }
79
95
 
80
96
  function askPo(run, role, question) {
81
- const outcome = runRoleHeadless(run.id, "po", {
97
+ const outcome = run.effects.runRoleHeadless(run.id, "po", {
82
98
  env: run.env,
83
99
  instructions: poInstructions(role, question),
84
100
  });
@@ -96,7 +112,7 @@ function askPo(run, role, question) {
96
112
  }
97
113
 
98
114
  function resumeWithAnswer(run, role, outcome, model, question, answer) {
99
- const resumed = runRoleHeadless(run.id, role, {
115
+ const resumed = run.effects.runRoleHeadless(run.id, role, {
100
116
  env: run.env,
101
117
  model,
102
118
  resumeSessionId: outcome.sessionId,
@@ -129,7 +145,11 @@ function runRole(run, role, instructions, { tier } = {}) {
129
145
  { event: "role_run", task: run.id, role, model: model ?? "default" },
130
146
  run.env
131
147
  );
132
- const outcome = runRoleHeadless(run.id, role, { env: run.env, model, instructions });
148
+ const outcome = run.effects.runRoleHeadless(run.id, role, {
149
+ env: run.env,
150
+ model,
151
+ instructions,
152
+ });
133
153
  return afterQuestion(run, role, outcome, model);
134
154
  }
135
155
 
@@ -177,7 +197,7 @@ function gateFor(run, step) {
177
197
  attempt: run.attempt,
178
198
  model: TIER_MODELS[step.tier],
179
199
  },
180
- { env: run.env }
200
+ { env: run.env, config: run.config, run: run.effects.runCheck }
181
201
  );
182
202
  }
183
203
 
@@ -203,7 +223,7 @@ export function failingTestCount(output) {
203
223
  }
204
224
 
205
225
  function redTestCheck(run) {
206
- return readRepoConfig(run.worktree).gate.find((entry) => entry.name === RED_TEST_CHECK);
226
+ return run.config.gate.find((entry) => entry.name === RED_TEST_CHECK);
207
227
  }
208
228
 
209
229
  function proveTestsRed(run) {
@@ -212,7 +232,7 @@ function proveTestsRed(run) {
212
232
  return stuck(
213
233
  `the gate has no check named "${RED_TEST_CHECK}" to prove the new tests fail`
214
234
  );
215
- const { exitCode, output } = runCheck(check.run, run.worktree, run.env);
235
+ const { exitCode, output } = run.effects.runCheck(check.run, run.worktree, run.env);
216
236
  const failing = failingTestCount(output);
217
237
  appendEvent(
218
238
  { event: "tests_red", task: run.id, check: RED_TEST_CHECK, exitCode, failing },
@@ -231,7 +251,7 @@ function proveTestsRed(run) {
231
251
 
232
252
  function prepareStep(run) {
233
253
  try {
234
- Object.assign(run, prepare(run.id, run.env));
254
+ Object.assign(run, run.effects.prepare(run.id, run.env));
235
255
  return null;
236
256
  } catch (error) {
237
257
  return stuck(`prepare failed: ${error.message}`);
@@ -239,10 +259,14 @@ function prepareStep(run) {
239
259
  }
240
260
 
241
261
  function hasCommitsAheadOfBase(run) {
242
- const result = spawnTool("git", ["rev-list", "--count", `origin/${run.base}..HEAD`], {
243
- cwd: run.worktree,
244
- env: run.env,
245
- });
262
+ const result = run.effects.spawnTool(
263
+ "git",
264
+ ["rev-list", "--count", `origin/${run.base}..HEAD`],
265
+ {
266
+ cwd: run.worktree,
267
+ env: run.env,
268
+ }
269
+ );
246
270
  return result.status === 0 && Number(result.stdout.trim()) > 0;
247
271
  }
248
272
 
@@ -286,11 +310,10 @@ function codeStep(run) {
286
310
  }
287
311
 
288
312
  function rolesAfterStep(run) {
289
- const { roles, added } = rolesAfter(
313
+ const { roles, added } = rolesAfterChanges(
290
314
  run.category,
291
- run.worktree,
292
- `origin/${run.base}`,
293
- run.env
315
+ run.effects.changedFilesSince(run.worktree, `origin/${run.base}`, run.env),
316
+ run.config
294
317
  );
295
318
  run.after = roles;
296
319
  for (const role of added)
@@ -307,9 +330,20 @@ function reviewFindingsStep(run, findings) {
307
330
  return fixStop(fixed, "review findings");
308
331
  }
309
332
 
333
+ function reviewInstructions(run) {
334
+ const changed = run.effects.changedFilesSince(
335
+ run.worktree,
336
+ `origin/${run.base}`,
337
+ run.env
338
+ );
339
+ return changed.includes(CONFIG_FILE)
340
+ ? `${REVIEW_INSTRUCTIONS}\n\n${CONFIG_CHANGED_NOTE}`
341
+ : REVIEW_INSTRUCTIONS;
342
+ }
343
+
310
344
  function reviewStep(run) {
311
345
  for (let round = 1; round <= REVIEW_ROUNDS; round += 1) {
312
- const review = runRole(run, "reviewer", REVIEW_INSTRUCTIONS);
346
+ const review = runRole(run, "reviewer", reviewInstructions(run));
313
347
  const stop = roleStop("reviewer", review);
314
348
  if (stop) return stop;
315
349
  const findings = review.outcome.structured.findings ?? [];
@@ -347,7 +381,7 @@ function testerStep(run) {
347
381
  function syncStep(run) {
348
382
  let synced;
349
383
  try {
350
- synced = sync(run.id, run.env);
384
+ synced = run.effects.sync(run.id, run.env);
351
385
  } catch (error) {
352
386
  return stuck(`sync failed: ${error.message}`);
353
387
  }
@@ -359,8 +393,8 @@ function syncStep(run) {
359
393
  }
360
394
 
361
395
  function shipStep(run) {
362
- push(run.id, run.env);
363
- run.pr = openPr(run.id, run.env).pr;
396
+ run.effects.push(run.id, run.env);
397
+ run.pr = run.effects.openPr(run.id, run.env).pr;
364
398
  return null;
365
399
  }
366
400
 
@@ -368,7 +402,7 @@ function readCi(run) {
368
402
  if (!readTask(run.id, run.env).pr)
369
403
  return { state: "unreadable", error: "the task has no pull request" };
370
404
  try {
371
- return waitForCi(run.id, { env: run.env });
405
+ return run.effects.waitForCi(run.id, { env: run.env });
372
406
  } catch (error) {
373
407
  return { state: "unreadable", error: error.message };
374
408
  }
@@ -404,13 +438,13 @@ function ciStep(run) {
404
438
  return stuck(`CI still fails after ${CI_ROUNDS} rounds: ${ciSummary(ci)}`);
405
439
  const stop = ciFixStep(run, ci);
406
440
  if (stop) return stop;
407
- push(run.id, run.env);
441
+ run.effects.push(run.id, run.env);
408
442
  }
409
443
  return null;
410
444
  }
411
445
 
412
446
  function readyStep(run) {
413
- markReady(run.id, run.env);
447
+ run.effects.markReady(run.id, run.env);
414
448
  return { status: "ready", message: `Ready for your review: ${run.pr}` };
415
449
  }
416
450
 
@@ -489,11 +523,19 @@ function finishRun(run, outcome) {
489
523
  },
490
524
  run.env
491
525
  );
526
+ run.effects.startCoach(run.id, outcome.status, run.env);
492
527
  return { task: run.id, status: outcome.status, message, pr: run.pr ?? null };
493
528
  }
494
529
 
495
- export function runBuild(taskId, { env = process.env } = {}) {
496
- const run = { id: taskId, env, startedAt: new Date(), attempt: 0, pr: null };
530
+ export function runBuild(taskId, { env = process.env, effects = {} } = {}) {
531
+ const run = {
532
+ id: taskId,
533
+ env,
534
+ effects: { ...REAL_EFFECTS, ...effects },
535
+ startedAt: new Date(),
536
+ attempt: 0,
537
+ pr: null,
538
+ };
497
539
  let outcome;
498
540
  try {
499
541
  outcome = pipeline(run);