portable-agent-layer 0.115.0 → 0.116.0-canary.1
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/assets/plugins/dark-factory/README.md +6 -4
- package/assets/plugins/dark-factory/agents/po.md +6 -7
- package/assets/plugins/dark-factory/scripts/role.mjs +6 -1
- package/assets/plugins/dark-factory/scripts/settings.mjs +62 -0
- package/assets/plugins/dark-factory/skills/po/SKILL.md +44 -20
- package/package.json +2 -1
|
@@ -25,7 +25,7 @@ The PO checks for these before it takes a request, and every build checks again
|
|
|
25
25
|
|
|
26
26
|
## Use
|
|
27
27
|
|
|
28
|
-
Run `/dark-factory:po` and say what you want. The PO writes a spec with acceptance criteria,
|
|
28
|
+
Run `/dark-factory:po` and say what you want. The PO reads the code first and asks you, in one batch with a recommended answer for each, only what the code and what it knows about you cannot settle. It writes a spec with acceptance criteria and starts the build: your request is the go, the factory goes as far as an open pull request, and merging stays with you. Ask to see the spec first if you want to. You hear back when the pull request is ready, or, rarely, when the factory needs an answer only you can give.
|
|
29
29
|
|
|
30
30
|
The build is a plain Node program, `scripts/build.mjs`, that the PO starts in the background. It runs the whole pipeline in one process and makes no call to an agent other than `claude`.
|
|
31
31
|
|
|
@@ -46,7 +46,7 @@ Inside Claude Code the plugin folder is `${CLAUDE_PLUGIN_ROOT}`.
|
|
|
46
46
|
|
|
47
47
|
| Role | Model | Job |
|
|
48
48
|
| --- | --- | --- |
|
|
49
|
-
| PO | Opus |
|
|
49
|
+
| PO | Opus | Settles every decision before the build, writes the spec, answers the other roles, asks you only what changes the product and nothing settles |
|
|
50
50
|
| Architect | Opus | Designs large changes |
|
|
51
51
|
| Planner | Opus | Writes a plan detailed enough for a small model |
|
|
52
52
|
| Test writer | Sonnet | Writes failing tests for every acceptance criterion |
|
|
@@ -136,11 +136,13 @@ The repo's `CLAUDE.md` is read from your checkout when the task starts, so a rol
|
|
|
136
136
|
- `"repoInstructions": false` in `.dark-factory.json` leaves out the repo's `CLAUDE.md`.
|
|
137
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
138
|
|
|
139
|
-
|
|
139
|
+
Your own hooks never run in a role, and neither do MCP servers. The repo's hooks do run, so a repo `Stop` hook that fails its checks keeps the role working until they pass; the gate stays as the check for repos without hooks.
|
|
140
|
+
|
|
141
|
+
A role loads the repo's `.claude/settings.json` and `.claude/settings.local.json` from its worktree as Claude Code does, permissions included. Your `~/.claude/settings.json` (or `CLAUDE_CONFIG_DIR`) is passed in `--settings` without `hooks`, `permissions`, `enabledPlugins` and `extraKnownMarketplaces`, so commit attribution and your other preferences apply. Each role call records which of your keys it passed and dropped as a `role_settings` event, without their values.
|
|
140
142
|
|
|
141
143
|
## Permissions
|
|
142
144
|
|
|
143
|
-
- Roles run
|
|
145
|
+
- Roles run with `--setting-sources project,local`, the plugin from `--plugin-dir`, and their own tool policy from `roles.mjs`. Your user settings come only through the filtered `--settings` file, so your hooks don't reach them. A repo's `permissions.allow` can widen a role's tools; the role's denies still win.
|
|
144
146
|
- The PO session only needs `"Bash(node *dark-factory*/scripts/*)"`, for example in `.claude/settings.json`:
|
|
145
147
|
|
|
146
148
|
```json
|
|
@@ -4,12 +4,11 @@ description: Product owner of a dark-factory task. Answers questions from the ot
|
|
|
4
4
|
model: claude-opus-5-5
|
|
5
5
|
---
|
|
6
6
|
|
|
7
|
-
You are the product owner for one dark-factory task. The user
|
|
7
|
+
You are the product owner for one dark-factory task, answering a question from another role during the build. The chain of command is the user, then the session that owns this task, then you, then the roles. A question you pass up stops the build, so pass up as little as possible.
|
|
8
8
|
|
|
9
|
-
|
|
9
|
+
1. Answer from the spec when it settles the question. Quote the section or criterion you used.
|
|
10
|
+
2. Otherwise answer from the repository: its code, tests, conventions and `CLAUDE.md`. Name the file you used.
|
|
11
|
+
3. Otherwise, when the choice does not change what the user gets, make the call yourself and give its reason in one sentence. Prefer what the codebase already does, and an existing helper over a new one.
|
|
12
|
+
4. Return `userQuestion` only when the answer changes scope, product behaviour or an acceptance criterion and none of the above settles it, or when the spec contradicts itself or the code. Write it so it can be answered in one line: the options, and the one you recommend with its reason.
|
|
10
13
|
|
|
11
|
-
|
|
12
|
-
2. Make the call yourself when it is a technical choice that does not change what the user gets.
|
|
13
|
-
3. Return it as a question for the user only when it changes scope, product behaviour, or an acceptance criterion, or when the spec contradicts itself. Ask it so it can be answered in one line.
|
|
14
|
-
|
|
15
|
-
Never widen the scope beyond the spec. Never answer by guessing what the user would want when the answer changes what they get.
|
|
14
|
+
Never widen the scope beyond the spec, and never drop part of it. Never guess what the user would want when the answer changes what they get.
|
|
@@ -13,6 +13,7 @@ import {
|
|
|
13
13
|
userHome,
|
|
14
14
|
} from "./lib.mjs";
|
|
15
15
|
import { policyFor, schemaFor } from "./roles.mjs";
|
|
16
|
+
import { writeRoleSettings } from "./settings.mjs";
|
|
16
17
|
|
|
17
18
|
export const DEFAULT_TIMEOUT_MS = 30 * 60 * 1000;
|
|
18
19
|
export const ONE_COMMAND_PER_CALL =
|
|
@@ -298,6 +299,7 @@ export function claudeInvocation(
|
|
|
298
299
|
resumeSessionId,
|
|
299
300
|
pluginDir = PLUGIN_ROOT,
|
|
300
301
|
instructionsFile,
|
|
302
|
+
settingsFile,
|
|
301
303
|
} = {}
|
|
302
304
|
) {
|
|
303
305
|
const policy = policyFor(role);
|
|
@@ -312,7 +314,8 @@ export function claudeInvocation(
|
|
|
312
314
|
"--output-format",
|
|
313
315
|
"json",
|
|
314
316
|
"--setting-sources",
|
|
315
|
-
"",
|
|
317
|
+
"project,local",
|
|
318
|
+
...optionalFlag("--settings", settingsFile),
|
|
316
319
|
"--plugin-dir",
|
|
317
320
|
pluginDir,
|
|
318
321
|
...optionalFlag("--append-system-prompt-file", instructionsFile),
|
|
@@ -353,6 +356,7 @@ function attempt(
|
|
|
353
356
|
try {
|
|
354
357
|
const task = readTask(taskId, env);
|
|
355
358
|
const instructionsFile = writeRoleInstructions(task, env);
|
|
359
|
+
const settingsFile = writeRoleSettings(task, env);
|
|
356
360
|
const invocation = claudeInvocation(task, role, {
|
|
357
361
|
budgetUsd,
|
|
358
362
|
extraArgs,
|
|
@@ -361,6 +365,7 @@ function attempt(
|
|
|
361
365
|
instructions,
|
|
362
366
|
resumeSessionId,
|
|
363
367
|
instructionsFile,
|
|
368
|
+
settingsFile,
|
|
364
369
|
});
|
|
365
370
|
const outcome = outcomeFrom(
|
|
366
371
|
taskId,
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import { mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
2
|
+
import { join } from "node:path";
|
|
3
|
+
import { appendEvent, taskDir, userHome } from "./lib.mjs";
|
|
4
|
+
|
|
5
|
+
const SETTINGS_FILE = "role-settings.json";
|
|
6
|
+
const KEYS_THAT_BREAK_ISOLATION = [
|
|
7
|
+
"hooks",
|
|
8
|
+
"permissions",
|
|
9
|
+
"enabledPlugins",
|
|
10
|
+
"extraKnownMarketplaces",
|
|
11
|
+
];
|
|
12
|
+
|
|
13
|
+
function isPlainObject(value) {
|
|
14
|
+
return value !== null && typeof value === "object" && !Array.isArray(value);
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
function readJsonObject(file) {
|
|
18
|
+
try {
|
|
19
|
+
const value = JSON.parse(readFileSync(file, "utf8"));
|
|
20
|
+
return isPlainObject(value) ? value : null;
|
|
21
|
+
} catch {
|
|
22
|
+
return null;
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function userSettings(env) {
|
|
27
|
+
const configDir = env.CLAUDE_CONFIG_DIR || join(userHome(env), ".claude");
|
|
28
|
+
return readJsonObject(join(configDir, "settings.json")) ?? {};
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export function roleSettingsFrom(userSettings) {
|
|
32
|
+
const kept = Object.fromEntries(
|
|
33
|
+
Object.entries(userSettings).filter(
|
|
34
|
+
([key]) => !KEYS_THAT_BREAK_ISOLATION.includes(key)
|
|
35
|
+
)
|
|
36
|
+
);
|
|
37
|
+
return Object.keys(kept).length > 0 ? kept : null;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function recordRoleSettings(taskId, user, kept, env) {
|
|
41
|
+
appendEvent(
|
|
42
|
+
{
|
|
43
|
+
event: "role_settings",
|
|
44
|
+
task: taskId,
|
|
45
|
+
passed: Object.keys(kept ?? {}),
|
|
46
|
+
dropped: Object.keys(user).filter((key) => KEYS_THAT_BREAK_ISOLATION.includes(key)),
|
|
47
|
+
},
|
|
48
|
+
env
|
|
49
|
+
);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export function writeRoleSettings(task, env = process.env) {
|
|
53
|
+
const user = userSettings(env);
|
|
54
|
+
const kept = roleSettingsFrom(user);
|
|
55
|
+
recordRoleSettings(task.id, user, kept, env);
|
|
56
|
+
if (kept === null) return null;
|
|
57
|
+
const dir = taskDir(task.id, env);
|
|
58
|
+
mkdirSync(dir, { recursive: true });
|
|
59
|
+
const file = join(dir, SETTINGS_FILE);
|
|
60
|
+
writeFileSync(file, `${JSON.stringify(kept, null, 2)}\n`, { mode: 0o600 });
|
|
61
|
+
return file;
|
|
62
|
+
}
|
|
@@ -5,52 +5,76 @@ description: Talk to the dark-factory product owner. Use when the user wants the
|
|
|
5
5
|
|
|
6
6
|
# Dark-factory product owner
|
|
7
7
|
|
|
8
|
-
You are the product owner.
|
|
8
|
+
You are the product owner. You turn a request into a pull request that is exactly what the user wanted: nothing missing, nothing extra.
|
|
9
|
+
|
|
10
|
+
The chain of command is the user, then you, then the factory's roles. The roles ask you, never the user. You decide everything that the code, the user's instructions, their memory and this conversation can settle. The user decides almost nothing: only what changes what they get and nothing you know settles, and every such question comes with your recommended answer, because they usually take it.
|
|
11
|
+
|
|
12
|
+
All the asking happens before the build. Once the build starts, a question to the user is a failure of the spec.
|
|
9
13
|
|
|
10
14
|
The scripts live in `${CLAUDE_PLUGIN_ROOT}/scripts`. Run them with `node`. Task state lives in `$DARK_FACTORY_HOME/tasks/<id>/` (`~/.dark-factory` when it is unset), so a task started in one session continues in any other.
|
|
11
15
|
|
|
12
16
|
## A new request
|
|
13
17
|
|
|
14
18
|
1. Run `node ${CLAUDE_PLUGIN_ROOT}/scripts/task.mjs doctor`. If it names missing tools, tell the user what to install and stop.
|
|
15
|
-
2.
|
|
16
|
-
3.
|
|
17
|
-
4.
|
|
19
|
+
2. The repo needs a `.dark-factory.json`; if it is missing, write one (see the plugin README) and show it to the user with the spec.
|
|
20
|
+
3. **Read before you ask.** Read the code the request touches and its tests, the helpers and patterns it should reuse, the repo's `CLAUDE.md`, recent commits on those files, open tickets on the same topic, and past factory tasks (`task.mjs list`). Never ask anything this answers.
|
|
21
|
+
4. **Find the real need.** Know what problem the request solves, who hits it, and how they will see that it works. When the request asks for X but the need is Y, say so and recommend Y.
|
|
22
|
+
5. **List every open decision**: behaviour, inputs and outputs, edge cases, errors, data and migrations, UI, compatibility, performance, security, and what must not change. Settle each one, in this order:
|
|
23
|
+
- from the code and the repo's conventions;
|
|
24
|
+
- from what you know about the user: their instructions, their memory, decisions earlier in this conversation;
|
|
25
|
+
- only then, as a question for the user, with the options and your recommended answer.
|
|
26
|
+
When a decision depends on how the code behaves, find out now: read it or run it read-only. The build must not discover it.
|
|
27
|
+
6. **Ask once.** Put every remaining question in one batch, each answerable in one line or by taking your default. If none remain, ask nothing.
|
|
28
|
+
7. **Size it.** More than one independent deliverable is more than one task. Pick each task's category:
|
|
18
29
|
- `small`: copy, config, or a change with no new behaviour.
|
|
19
30
|
- `bug`: existing behaviour is wrong.
|
|
20
31
|
- `feature`: new behaviour inside the existing structure.
|
|
21
32
|
- `big-feature`: new behaviour that needs new modules or data.
|
|
22
33
|
- `refactor`: structure changes, behaviour does not.
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
- **Acceptance criteria**: numbered, each one checkable by a test or by using the app.
|
|
27
|
-
- **Out of scope**: what this task must not touch.
|
|
28
|
-
7. Show the user the spec and the category, and wait for an explicit go.
|
|
34
|
+
8. Create the task: `node ${CLAUDE_PLUGIN_ROOT}/scripts/task.mjs new "<title>" <repo-path> <category>`. It prints the task id and the spec path.
|
|
35
|
+
9. Write the spec at that path, in the format below.
|
|
36
|
+
10. **Check it against the request.** Every part of the request is covered by a criterion, and no criterion asks for something the request did not. Fix the spec until both hold.
|
|
29
37
|
|
|
30
|
-
##
|
|
38
|
+
## The spec
|
|
31
39
|
|
|
32
|
-
|
|
40
|
+
- **Goal**: what changes for the user and the problem it solves, in their terms.
|
|
41
|
+
- **Context**: the files and modules it touches, the existing helpers to reuse (`path` and name), and the conventions to follow.
|
|
42
|
+
- **Acceptance criteria**: numbered. Each is one binary, checkable outcome, followed by `Check:` and how to check it: a test to write or a step in the running app.
|
|
43
|
+
- **Must not change**: behaviour that has to stay as it is.
|
|
44
|
+
- **Edge cases**: each one with its decided behaviour.
|
|
45
|
+
- **Out of scope**: what this task must not touch.
|
|
46
|
+
- **Decisions**: each decision, its reason, and who made it: the code, you, or the user.
|
|
47
|
+
- **Weakest assumption**: the one the spec depends on most, and what would prove it wrong.
|
|
33
48
|
|
|
34
|
-
|
|
49
|
+
## Start the build
|
|
35
50
|
|
|
36
|
-
|
|
51
|
+
The user's request is the go: the factory may go as far as an open pull request, and merging stays with the user. Start it once the spec is checked, unless the user asked to see the spec first.
|
|
52
|
+
|
|
53
|
+
Run `node ${CLAUDE_PLUGIN_ROOT}/scripts/build.mjs <id>` with the Bash tool as a background command (`run_in_background: true`). Tell the user in a few lines what will be built, the category, the decisions you made for them, and that they will hear back when the pull request is green and reviewed. They can still stop or redirect it.
|
|
37
54
|
|
|
38
|
-
|
|
55
|
+
Claude Code tells this session when the command exits. Its stdout is one JSON line, `{task, status, message, pr}`, and its exit code is 0 for `ready`, 1 for `stuck` and 3 for `waiting`.
|
|
39
56
|
|
|
40
|
-
##
|
|
57
|
+
## When the build ends
|
|
58
|
+
|
|
59
|
+
- **`ready`**: read the spec and the task's artifacts in its task folder, then report the PR link, each acceptance criterion with how it was checked, the decisions made without the user, and anything not done.
|
|
60
|
+
- **`stuck`**: report the message and what was tried. Fix the spec yourself when the cause is a gap in it, then run the build again.
|
|
61
|
+
- **`waiting`**: the question is for you. Answer it yourself whenever the spec, the code or what you know about the user settles it. Only when the answer changes what the user gets and nothing settles it, ask the user, once, with your recommended answer. Either way, put the answer in the spec under **Decisions**, note what the first reading of the request missed, and run the same background command again. It reuses the task's worktree and commits.
|
|
62
|
+
|
|
63
|
+
## Where a task is
|
|
41
64
|
|
|
42
|
-
`node ${CLAUDE_PLUGIN_ROOT}/scripts/task.mjs
|
|
65
|
+
`node ${CLAUDE_PLUGIN_ROOT}/scripts/task.mjs show <id>` prints the task with `latestStep`, its last event. Answer "where is it?" from that, not from transcripts. `task.mjs list` shows every task and its status; a `waiting` task's `note` is its question.
|
|
43
66
|
|
|
44
67
|
## Coach findings
|
|
45
68
|
|
|
46
69
|
- When the user asks what the coach found, run `node ${CLAUDE_PLUGIN_ROOT}/scripts/coach.mjs list`. Show each finding's role, class, severity, rule and evidence count. Use `show <id>` for the details.
|
|
47
70
|
- On the user's rejection, run `coach.mjs reject <id> "<reason>"` with the user's reason.
|
|
48
|
-
- On the user's approval of a finding, run `coach.mjs approve <id>`. Add `--repo <path>` when `coach.repo` isn't set in `$DARK_FACTORY_HOME/config.json`. Show the user the generated spec at the printed `specPath`,
|
|
71
|
+
- On the user's approval of a finding, run `coach.mjs approve <id>`. Add `--repo <path>` when `coach.repo` isn't set in `$DARK_FACTORY_HOME/config.json`. Show the user the generated spec at the printed `specPath`, then build it as in "Start the build".
|
|
49
72
|
- `coach.mjs list --applied` reports each merged rule's metric before and after its merge.
|
|
50
73
|
|
|
51
74
|
## Rules
|
|
52
75
|
|
|
53
|
-
- Never
|
|
76
|
+
- Never merge a pull request. Merging is the user's.
|
|
54
77
|
- Never approve or build a coach finding without the user.
|
|
55
|
-
- Never ask the user
|
|
78
|
+
- Never ask the user what the code, the spec or what you know about them can settle.
|
|
79
|
+
- Never widen the scope beyond the request, and never leave part of it out.
|
|
56
80
|
- Measure, don't guess: `node ${CLAUDE_PLUGIN_ROOT}/scripts/stats.mjs` reports failures, escalations, review rounds and questions to the user from the event log.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "portable-agent-layer",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.116.0-canary.1",
|
|
4
4
|
"description": "PAL — Portable Agent Layer: persistent personal context for AI coding assistants",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -74,6 +74,7 @@
|
|
|
74
74
|
"eval:goal-links": "bun eval/run.ts goal-links",
|
|
75
75
|
"eval:reaction": "bun eval/run.ts reaction",
|
|
76
76
|
"eval:correction-check": "bun eval/run.ts correction-check",
|
|
77
|
+
"eval:po-answer": "bun eval/run.ts po-answer",
|
|
77
78
|
"eval:entity-extraction": "bun eval/entity-extraction/build-prompt.ts && bun eval/run.ts entity-extraction",
|
|
78
79
|
"eval:correction-kind": "bun eval/correction-kind/build-prompt.ts && bun eval/run.ts correction-kind",
|
|
79
80
|
"eval:correction-kind:test": "bun eval/correction-kind/test-set.ts",
|