agents-handoff 0.0.0-stage → 2.0.3

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 (56) hide show
  1. package/CHANGELOG.md +192 -0
  2. package/LICENSE +21 -0
  3. package/README.md +150 -2
  4. package/SKILL.md +147 -0
  5. package/capability-registry.json +27 -0
  6. package/docs/ARCHITECTURE.md +187 -0
  7. package/docs/CHANGELOG.md +196 -0
  8. package/docs/CLI.md +299 -0
  9. package/docs/COMPATIBILITY.md +124 -0
  10. package/docs/CONTRIBUTING.md +134 -0
  11. package/docs/FORMAT.md +185 -0
  12. package/docs/INSTALL.md +394 -0
  13. package/docs/INTEGRATION.md +188 -0
  14. package/docs/LEVEL4.md +202 -0
  15. package/docs/LEVEL5.md +96 -0
  16. package/docs/PERMISSIONS.md +145 -0
  17. package/docs/PROVENANCE.md +110 -0
  18. package/docs/SECURITY.md +93 -0
  19. package/docs/SESSIONS.md +97 -0
  20. package/docs/TROUBLESHOOTING.md +158 -0
  21. package/docs/UNINSTALL.md +148 -0
  22. package/docs/UPGRADE.md +177 -0
  23. package/docs/_config.yml +18 -0
  24. package/docs/_data/nav.yml +36 -0
  25. package/docs/_layouts/default.html +31 -0
  26. package/docs/assets/style.css +88 -0
  27. package/docs/index.md +92 -0
  28. package/docs/sessions.json +34 -0
  29. package/handoff.config.example.json +35 -0
  30. package/handoff.config.schema.json +117 -0
  31. package/install/CHANGELOG.md +48 -0
  32. package/install/README.md +76 -0
  33. package/install/install.mjs +1455 -0
  34. package/install/package.json +39 -0
  35. package/package.json +66 -4
  36. package/permission-policy.json +33 -0
  37. package/refs/ADAPTERS.md +33 -0
  38. package/refs/bootstrap.md +59 -0
  39. package/refs/brief-checklist.md +79 -0
  40. package/refs/handbook.md +58 -0
  41. package/refs/protocol.md +117 -0
  42. package/refs/roles.md +75 -0
  43. package/refs/validator.md +73 -0
  44. package/schemas/handoff.schema.json +275 -0
  45. package/skill.json +147 -0
  46. package/templates/HANDOFF.llm.schema.json +144 -0
  47. package/templates/HANDOFF.template.md +40 -0
  48. package/tests/acceptance/acceptance.yaml +209 -0
  49. package/tests/fixtures/minimal-transcript.jsonl +2 -0
  50. package/tools/agent-handoff.mjs +22 -0
  51. package/tools/agents-handoff.mjs +410 -0
  52. package/tools/capability-registry.mjs +120 -0
  53. package/tools/handoff.mjs +398 -0
  54. package/tools/handoff.test.mjs +668 -0
  55. package/tools/lib/handoff-root.mjs +161 -0
  56. package/tools/runtime-engine.mjs +330 -0
@@ -0,0 +1,188 @@
1
+ ---
2
+ title: Integrating agents-handoff
3
+ ---
4
+
5
+ # Integrating agents-handoff
6
+
7
+ The engine is a command-line program with a file contract. There is no daemon, no network
8
+ call and no database. Integration means three things: getting a transcript into the canonical
9
+ input shape, running the engine, and reading the artifacts it writes.
10
+
11
+ - Put a transcript in the canonical shape: [Input contract](#input-contract).
12
+ - Run it: [Build a handoff](#build-a-handoff) and [Exit codes](#exit-codes).
13
+ - Read the result: [Output artifacts](#output-artifacts) and [Manifest fields](#manifest-fields).
14
+
15
+ ## Input contract
16
+
17
+ Every input line is one JSON object (JSONL). Any program that can write JSONL can feed the
18
+ engine; no client is required.
19
+
20
+ ```json
21
+ {"seq":0,"ts":"1787669764492","harness":"dsh","source":"<origin path>",
22
+ "session":"<id>","thread":"<project/thread>","role":"user|assistant|system|tool",
23
+ "kind":"<free-form: reasoning|tool_use|text>","text":"<message body>"}
24
+ ```
25
+
26
+ | Field | Used for | Notes |
27
+ |---|---|---|
28
+ | `seq` | ordering, watermark | Falls back to the line index when absent or non-numeric. |
29
+ | `ts` | timeline ordering | `timestamp` is accepted as an alias. |
30
+ | `role` | turn class | `user`, `assistant`, `system`, `tool`. |
31
+ | `kind` | turn class refinement | `tool_use` → TOOL, `reasoning`/`thinking` → THOUGHT. |
32
+ | `text` | the turn body | `content` and `parts[].text` are accepted as aliases. |
33
+ | `session` | handoff id | Used when `--session` is not passed. |
34
+ | `thread` | project inference | A `--tag--` inside the thread name is read as the project. |
35
+ | `harness` | provenance | Not read from the line. Set it with `--harness`; otherwise it is `unknown`. |
36
+ | `source` | provenance | Not read from the line. `source_paths` records the file given to `--source`. |
37
+
38
+ `harness` and `source` are carried for whatever reads the file later. The engine itself reads
39
+ `seq`, `ts`, `role`, `kind` and `text`, plus `session` and `thread` from the first line.
40
+
41
+ Turn classification order (`classify()` in [handoff.mjs](https://github.com/Alot1z/agent-handoff/blob/main/tools/handoff.mjs)):
42
+
43
+ 1. `kind` contains `tool`, or `role` is `tool` → **TOOL**
44
+ 2. `kind` contains `reason` or `think` → **THOUGHT**
45
+ 3. `role` is `user`, or `kind` is `human` → **USER**
46
+ 4. `role` is `assistant`, or `kind` is `ai` → **AGENT**
47
+ 5. anything else → **OTHER**
48
+
49
+ A line whose body is empty after that mapping is skipped. A file whose name ends in `.jsonl`
50
+ is parsed as JSONL; every other file is parsed as text, using role markers:
51
+
52
+ ```
53
+ user: what needs to happen next
54
+ assistant: the plan is in refs/plan.md
55
+ ```
56
+
57
+ The marker is `user|human|assistant|ai|system|tool`, optionally prefixed by `#` and followed
58
+ by `:` or `>`. Lines after a marker are appended to that turn until the next marker. A
59
+ `system:` line becomes OTHER, so it stays in the record without appearing as dialogue.
60
+
61
+ If no usable turn is parsed, the build fails with exit 4 rather than writing an empty handoff.
62
+
63
+ ## Build a handoff
64
+
65
+ ```bash
66
+ node tools/handoff.mjs build --source transcript.jsonl \
67
+ --session my-session --harness claude-code --model <name> \
68
+ --project my-project --objective "what this session was for"
69
+ ```
70
+
71
+ | Flag | Effect |
72
+ |---|---|
73
+ | `--source <file>` | The transcript to ingest. Required. |
74
+ | `--session <id>` | Handoff id. Default: the `session` field, else the file name. |
75
+ | `--harness <name>` | Provenance label. Default `unknown`; the engine reads no harness field. |
76
+ | `--model <name>` | Provenance label. Stored, never invented. |
77
+ | `--project <name>` | Project group. Default: config, then the `--tag--` in the thread, then the harness. |
78
+ | `--objective <text>` | Overrides the objective taken from the transcript. |
79
+ | `--force-harness` | Rewrite an existing harness value. Without it, a set value is kept. |
80
+ | `--force-model` | Rewrite an existing model value. Without it, a set value is kept. |
81
+
82
+ `node tools/handoff.mjs --handoff --source transcript.jsonl` is an alias of `build`.
83
+
84
+ A rebuild of the same session is incremental: only turns above the manifest watermark are
85
+ appended, and a source whose bytes and watermark are unchanged prints `up-to-date` and writes
86
+ nothing.
87
+
88
+ ## Where handoffs are stored
89
+
90
+ The root is resolved by one module, [tools/lib/handoff-root.mjs](https://github.com/Alot1z/agent-handoff/blob/main/tools/lib/handoff-root.mjs),
91
+ in this order:
92
+
93
+ | # | Source | Detail |
94
+ |---|---|---|
95
+ | 1 | `HANDOFFS_ROOT` environment variable | Always wins. This is what makes a CI run hermetic. |
96
+ | 2 | `handoff.config.json` | Found by walking up from the current directory. |
97
+ | 3 | a `handoffs/` directory | Found by walking up from the current directory. |
98
+ | 4 | the skill directory | Final fallback when nothing else exists. |
99
+
100
+ Inside a config, `storage.path` beats `handoff_dir`, and a relative `handoff_dir` resolves
101
+ against the config's own directory. The walk is bounded to ten levels. A config file that is
102
+ present but invalid fails the run with exit 2 — it is never ignored, because falling back
103
+ would write to a different store than the one configured.
104
+
105
+ `node tools/handoff.mjs config` prints the resolved root, the source that decided it, the
106
+ config path, the schema path, the project name and whether cross-linking is on.
107
+
108
+ ## Output artifacts
109
+
110
+ ```
111
+ <root>/
112
+ ├── INDEX.json # rebuilt from every manifest
113
+ ├── projects/<project>/
114
+ │ ├── PROJECT.md # project name, session list
115
+ │ └── <session>/
116
+ │ ├── HANDOFF.md # the brief a human or agent reads
117
+ │ ├── HANDOFF.summary.json # compact payload (schema 2.0.0-summary)
118
+ │ ├── HANDOFF.llm.json # full payload (schema 2.0.0)
119
+ │ ├── timeline.jsonl # append-only, one turn per line
120
+ │ ├── TOOLS.md # every tool call, verbatim
121
+ │ └── manifest.json # provenance and counters
122
+ └── links/<other-project>.md # cross-project relation notes
123
+ ```
124
+
125
+ `TOOLS.md` is the fidelity tier: it carries each tool call in full. The table inside
126
+ `HANDOFF.md` is a digest of the same turns.
127
+
128
+ ## Manifest fields
129
+
130
+ | Field | Meaning |
131
+ |---|---|
132
+ | `session`, `project`, `harness`, `model` | Identity and provenance. |
133
+ | `created_at`, `updated_at` | First write, last write. |
134
+ | `source_paths` | Every source file ingested for this session. |
135
+ | `watermark` | Highest ingested `seq`. Everything below it is already in the timeline. |
136
+ | `raw_sha256` | Hash of the source bytes at the last build. |
137
+ | `revisions` | Build count for this session. |
138
+ | `turn_count` | Lines in `timeline.jsonl`. |
139
+ | `counts` | Turn count per class: USER, AGENT, THOUGHT, TOOL. |
140
+ | `manifest_sha256` | Hash of the manifest without this field. |
141
+
142
+ ## Verify a handoff
143
+
144
+ ```bash
145
+ node tools/handoff.mjs list # all sessions
146
+ node tools/handoff.mjs list <project> # one project
147
+ node tools/handoff.mjs show <id-prefix> # print the brief
148
+ node tools/handoff.mjs verify <id-prefix> # manifest sha, turn count, payload parses
149
+ ```
150
+
151
+ `verify` recomputes the manifest hash, compares the timeline line count against
152
+ `turn_count`, and parses `HANDOFF.llm.json`. It prints `PASS <id> [...]` or fails with exit 1
153
+ and the first broken check. See [PROVENANCE.md](PROVENANCE.md) for what the hash chain does
154
+ and does not detect.
155
+
156
+ ## Exit codes
157
+
158
+ | Code | Meaning |
159
+ |---|---|
160
+ | 0 | Success. |
161
+ | 1 | Integrity failure: tampered manifest, unparsable manifest, unexpected error. |
162
+ | 2 | Usage or configuration error: missing `--source`, missing argument, invalid config. |
163
+ | 3 | Ambiguous id prefix — more than one session matched. |
164
+ | 4 | No usable turns, no handoffs, or no session matching the prefix. |
165
+
166
+ ## CI recipe
167
+
168
+ ```yaml
169
+ - name: Build and verify the handoff
170
+ env:
171
+ HANDOFFS_ROOT: ${{ github.workspace }}/.handoffs
172
+ run: |
173
+ node skills/agents-handoff/tools/handoff.mjs build \
174
+ --source exported-transcript.jsonl --project ci --harness generic
175
+ node skills/agents-handoff/tools/handoff.mjs verify "$(ls .handoffs/projects/ci | head -1)"
176
+ ```
177
+
178
+ `HANDOFFS_ROOT` keeps the run off any configured store, and `verify` returns the exit code a
179
+ pipeline can gate on.
180
+
181
+ ## Limits
182
+
183
+ - Filesystem only: nothing is uploaded, and no network call is made.
184
+ - No watcher: a build happens when it is invoked. See [LEVEL4.md](LEVEL4.md) for the layer
185
+ that probes for staleness and runs the build for you.
186
+ - The engine reads whatever the transcript contains, including secrets. Redaction is the
187
+ caller's job. See [SECURITY.md](SECURITY.md).
188
+ - Adapter routes for common session stores: [../refs/ADAPTERS.md](https://github.com/Alot1z/agent-handoff/blob/main/refs/ADAPTERS.md).
package/docs/LEVEL4.md ADDED
@@ -0,0 +1,202 @@
1
+ ---
2
+ title: Level 4 — the dynamic runtime layer
3
+ ---
4
+
5
+ # Level 4 — the dynamic runtime layer
6
+
7
+ The engine ([handoff.mjs](https://github.com/Alot1z/agent-handoff/blob/main/tools/handoff.mjs)) is passive: something has to invoke it. The
8
+ runtime layer is [tools/agents-handoff.mjs](https://github.com/Alot1z/agent-handoff/blob/main/tools/agents-handoff.mjs), which acts on the
9
+ state of the store — it probes for staleness, checks a handoff against a contract, composes
10
+ sessions, imports other stores and maintains the index.
11
+
12
+ Every mutating command takes a lock, so two runs cannot capture or merge the same session at
13
+ once. Locks live in `<root>/.locks/` and are named after a hash of the operation target.
14
+
15
+ ```bash
16
+ node tools/agents-handoff.mjs <command> [args]
17
+ ```
18
+
19
+ | Command | Purpose |
20
+ |---|---|
21
+ | `auto --source <file>` | Build only when the source is newer than the stored manifest. |
22
+ | `verify-gate <id-prefix>` | Five checks: sha, counts, payload, contract, evidence. |
23
+ | `promote <id-prefix>` | Stamp promotion metadata on a handoff's manifest. |
24
+ | `merge <a> <b>` | Compose two sessions of one project into one handoff. |
25
+ | `dispatch <id-prefix> --task <objective>` | Hand the continuation to a worker (Level 5). |
26
+ | `federated-merge --from <root>` | Import sessions from another store root. |
27
+ | `self-improve` | Collect brief shortfalls into a rules candidate file. |
28
+ | `index` | Rebuild `INDEX.json` and report stale sessions. |
29
+
30
+ An unknown command prints that list and exits 2.
31
+
32
+ ## `auto` — self-triggering capture
33
+
34
+ ```bash
35
+ node tools/agents-handoff.mjs auto --source transcript.jsonl \
36
+ [--session <id>] [--harness <name>] [--project <name>] [--min-fresh-ms <n>]
37
+ ```
38
+
39
+ `--source` is required. Two gates run before any build:
40
+
41
+ 1. **Freshness.** If the session's manifest exists and `source_mtime - manifest.updated_at`
42
+ is below `--min-fresh-ms` (default 60000), nothing is built and the command prints
43
+ `{"action":"skip-fresh"}`.
44
+ 2. **Lock.** A capture already in flight for the same project and session exits 3.
45
+
46
+ It then runs the engine's `build` and prints `{"action":"captured",...}`. A missing source
47
+ file exits 2.
48
+
49
+ ## `verify-gate` — the evidence gate
50
+
51
+ ```bash
52
+ node tools/agents-handoff.mjs verify-gate <id-prefix>
53
+ ```
54
+
55
+ | Check | Passes when |
56
+ |---|---|
57
+ | `sha` | `manifest_sha256` equals the hash of the manifest without that field. |
58
+ | `counts` | Lines in `timeline.jsonl` equal `turn_count`. |
59
+ | `payload` | `HANDOFF.llm.json` parses as JSON. |
60
+ | `contract` | `HANDOFF.md` contains all seven contract fields, each at the start of a line. |
61
+ | `evidence` | Not `RESULT: DONE` with no `EVIDENCE:` line. |
62
+
63
+ The contract fields are `RESULT`, `WHAT_CHANGED`, `VALIDATION`, `EVIDENCE`, `BLOCKERS`,
64
+ `RISKS`, `FOLLOW_UP`. The verdict is `VERIFIED` when every check is `PASS`, otherwise
65
+ `REJECTED`, printed as JSON with the per-check table.
66
+
67
+ **The command exits 0 in both cases.** A caller must read `ok` or `verdict` from the JSON —
68
+ the exit code alone does not report a rejection. Only a bad prefix (exit 3) or a missing
69
+ manifest is visible as a nonzero status.
70
+
71
+ ## `promote` — stamping verified work
72
+
73
+ ```bash
74
+ node tools/agents-handoff.mjs promote <id-prefix>
75
+ ```
76
+
77
+ The manifest is backed up to `manifest.json.bak`, the gate above is run and its verdict is
78
+ printed, and the manifest is then stamped with `promoted_at` and `promoted_by` and re-hashed.
79
+
80
+ Two honest caveats:
81
+
82
+ - The gate result is **printed, not enforced**. The stamp is written even when the verdict is
83
+ `REJECTED`; a caller that wants a gate must run `verify-gate` and branch on `verdict`
84
+ before calling `promote`.
85
+ - Promotion is local. It writes one flag pair into one manifest file; it contacts no external
86
+ system and publishes nothing.
87
+
88
+ ## `merge` — composing two sessions
89
+
90
+ ```bash
91
+ node tools/agents-handoff.mjs merge <id-prefix-a> <id-prefix-b>
92
+ ```
93
+
94
+ Timelines are concatenated and sorted by `ts`, and written to a new session directory named
95
+ `<a>+merge+<b>` under the first project. The merged manifest records `harness: "merged"`,
96
+ `merged_from: [a, b]` and a fresh `raw_sha256` over the joined timeline. A note is appended
97
+ to `links/<project-b>.md` with a `<!-- merged:<id> -->` marker, so a re-run does not duplicate
98
+ it.
99
+
100
+ A merged session has no `HANDOFF.md`, no payload and no `TOOLS.md`, so it fails `verify-gate`
101
+ on `contract` and `payload` until a brief is written for it, and `index` reports it as stale.
102
+
103
+ ## `federated-merge` — importing another store root
104
+
105
+ ```bash
106
+ node tools/agents-handoff.mjs federated-merge --from <remote-root> [--from <root> ...] [--dry-run]
107
+ ```
108
+
109
+ Each remote root is expected to have the same `projects/<project>/<session>/` layout. A
110
+ session is imported when its `manifest_sha256` differs from the local copy, or when no local
111
+ copy exists; identical sessions are counted as skipped, so re-running imports only what
112
+ changed. `--dry-run` lists the actions without writing.
113
+
114
+ On import: an existing local manifest is backed up to `manifest.json.bak-federated`, the
115
+ session directory is copied, and `federated_from` and `federated_at` are stamped on the
116
+ canonical manifest, which is then re-hashed. Every imported session is verified with the
117
+ engine's `verify` afterwards, and the index is rebuilt. Per-session failures are listed in
118
+ the `failed` array rather than aborting the run. A root without a `projects/` directory is
119
+ recorded as failed, and a missing `--from` exits 2.
120
+
121
+ ## `self-improve` — brief shortfalls
122
+
123
+ ```bash
124
+ node tools/agents-handoff.mjs self-improve
125
+ ```
126
+
127
+ Every session is scanned, and a session with 40 or more USER+AGENT turns whose `HANDOFF.md`
128
+ is shorter than 800 characters is reported as a candidate. The output is written to
129
+ `docs/self-improve-candidates.json` inside the skill directory — deliberately skill-relative,
130
+ so a configured store never collects rule candidates. The file lists counts, not content.
131
+
132
+ ## `index` — the store index
133
+
134
+ ```bash
135
+ node tools/agents-handoff.mjs index
136
+ ```
137
+
138
+ Rebuilds `<root>/INDEX.json` from the manifests, with one entry per session (`id`, `uuid`,
139
+ `project`, `harness`, `turns`, `revisions`, `updated`, `manifest_sha256`), and reports
140
+ sessions that have a manifest but no `HANDOFF.md`.
141
+
142
+ ## Bounded execution — `runtime-engine.mjs`
143
+
144
+ [tools/runtime-engine.mjs](https://github.com/Alot1z/agent-handoff/blob/main/tools/runtime-engine.mjs) is the command-execution half of the
145
+ runtime: every operation is evaluated against `permission-policy.json` **before** it runs, and
146
+ a denied operation is recorded but never executed.
147
+
148
+ ```bash
149
+ node tools/runtime-engine.mjs evaluate --risk R0..R4 [--target <path>] [--json]
150
+ node tools/runtime-engine.mjs run --risk R0|R1 --op read --target <path>
151
+ node tools/runtime-engine.mjs run --risk R1 --op spawn --args "<node args>"
152
+ node tools/runtime-engine.mjs policy
153
+ node tools/runtime-engine.mjs job --session <s> --steps <n> [--fail-at <k>] [--json]
154
+ node tools/runtime-engine.mjs resume --session <s> --steps <n> [--json]
155
+ node tools/runtime-engine.mjs status --session <s> [--json]
156
+ ```
157
+
158
+ | Verdict | Meaning |
159
+ |---|---|
160
+ | `ALLOWED` | The grant for the risk class's level is `allow`. |
161
+ | `DENIED` | Refused: an explicit denial, a personal-data path, an unknown risk, or an unknown grant. |
162
+ | `NEEDS_AUTH` | The grant is `needs_auth`; the operation waits for authorization and does not run. |
163
+
164
+ Targets are classified in a fixed order: an explicit denial first, then the engine's own
165
+ state directory, then the approved workspaces, then the broad personal-data roots, then
166
+ system read-only roots, otherwise external. A personal-data or denied path is refused at any
167
+ risk. A read-only level is allowed inside an approved workspace or a system path; every other
168
+ level requires an approved workspace. Levels, risk classes and grants are documented in
169
+ [PERMISSIONS.md](PERMISSIONS.md).
170
+
171
+ The `spawn` operation is bounded: it runs the local node binary with the given arguments, the
172
+ working directory pinned to the repository, a 15000 ms timeout, and the output captured and
173
+ hashed rather than streamed. Each decision — allowed, refused or failed — is written to
174
+ `<state>/executions/<id>.json` with `enforced_before_execution: true`.
175
+
176
+ Checkpoints make a job resumable from disk alone: after each step, `<state>/checkpoints/<session>.json`
177
+ is rewritten with a sha256 seal, and the step is appended to `<state>/jobs/<session>/work.log`.
178
+ Each step is permission-gated at `R1`. `--fail-at <k>` simulates an abrupt kill before step
179
+ `k` executes, leaving the durable state at `k-1`.
180
+
181
+ The state directory is `<repo>/.agents-handoff`, or `AGENT_HANDOFF_STATE_DIR` when set.
182
+
183
+ | Code | Meaning |
184
+ |---|---|
185
+ | 0 | Allowed, executed, or completed. |
186
+ | 1 | Execution error, or the child produced no exit code. |
187
+ | 2 | Usage or configuration error. |
188
+ | 3 | Denied. |
189
+ | 4 | Needs authorization, or no checkpoint to resume from. |
190
+ | 5 | Checkpoint corrupt, or its integrity seal does not match. |
191
+ | 137 | Simulated abrupt kill. |
192
+ | other | The captured exit code of the spawned child. |
193
+
194
+ ## Exit codes
195
+
196
+ | Code | Meaning |
197
+ |---|---|
198
+ | 0 | Success. |
199
+ | 1 | A mutating command failed; the lock was released. |
200
+ | 2 | Usage or configuration error, including an unresolvable store root. |
201
+ | 3 | A lock is held, an id prefix is ambiguous, or a referenced path is missing. |
202
+ | 4 | No session matches the prefix. |
package/docs/LEVEL5.md ADDED
@@ -0,0 +1,96 @@
1
+ ---
2
+ title: Level 5 — collaborative dispatch
3
+ ---
4
+
5
+ # Level 5 — collaborative dispatch
6
+
7
+ A handoff normally ends as files on disk, and something reads it later. The dispatch command
8
+ lets a handoff whose own record is complete hand its continuation to a worker through a local
9
+ broker, instead of waiting for a reader.
10
+
11
+ ```bash
12
+ node tools/agents-handoff.mjs dispatch <id-prefix> --task "<objective>" \
13
+ [--role <role>] [--parent <parentTaskId>] [--broker <broker-root>] [--live]
14
+ ```
15
+
16
+ | Argument | Default | Meaning |
17
+ |---|---|---|
18
+ | `<id-prefix>` | required | The handoff that is handing off its continuation. |
19
+ | `--task <objective>` | required | What the worker is being asked to do. |
20
+ | `--role <role>` | `implementation-agent` | The role the request is addressed to. |
21
+ | `--parent <parentTaskId>` | `handoff:<id>` | The parent the request is attached to. |
22
+ | `--broker <root>` | none | Root of the broker program. |
23
+ | `--live` | off | Enqueue the request instead of printing it. |
24
+
25
+ ## The gate
26
+
27
+ Dispatch refuses to hand over work from a handoff that is not internally complete. Before
28
+ anything is built, `HANDOFF.md` is read and two things are required:
29
+
30
+ 1. all seven contract fields are present, each at the start of a line — `RESULT`,
31
+ `WHAT_CHANGED`, `VALIDATION`, `EVIDENCE`, `BLOCKERS`, `RISKS`, `FOLLOW_UP`;
32
+ 2. a record that says `RESULT: DONE` also carries an `EVIDENCE:` line.
33
+
34
+ When either fails, the command prints the refusal and exits 1:
35
+
36
+ ```json
37
+ { "ok": false, "status": "GATE_REJECTED", "session": "<id>",
38
+ "reason": "handoff fails the evidence gate (missing contract fields: ...; DONE-without-EVIDENCE=true)" }
39
+ ```
40
+
41
+ The gate reads `HANDOFF.md` only. It does not recompute the manifest hash, check the timeline
42
+ count, or parse the payload — those are `verify-gate` checks. A handoff can therefore pass the
43
+ dispatch gate while failing `verify-gate` on `sha`, `counts` or `payload`; run
44
+ `verify-gate <id>` first when that matters.
45
+
46
+ ## The envelope
47
+
48
+ | Field | Value |
49
+ |---|---|
50
+ | `from_handoff` | The session id the request came from. |
51
+ | `project` | The project that session belongs to. |
52
+ | `role` | `--role`. |
53
+ | `parentTaskId` | `--parent`, by default `handoff:<id>`. |
54
+ | `objective` | `--task`. |
55
+ | `evidence` | The session's `manifest_sha256`, so the receiver can check what it was given. |
56
+ | `evidenceRequirements` | `["verified-handoff"]`. |
57
+ | `dispatchedAt` | Timestamp of the envelope. |
58
+ | `broker_mode` | `live` or `dry-run`. |
59
+ | `handoff_dir` | Absolute path of the session directory. |
60
+
61
+ ## States
62
+
63
+ | State | When | Exit |
64
+ |---|---|---|
65
+ | `GATE_REJECTED` | The contract or evidence check failed. Nothing is sent. | 1 |
66
+ | `DRY_RUN` | `--live` was not passed, or no broker root was given. The envelope is printed. | 0 |
67
+ | `DISPATCHED` | `--live` and a broker root were given and the broker answered. | 0 |
68
+
69
+ `DRY_RUN` is the default. It prints the envelope and one line of advice: either re-run with
70
+ `--live`, or pass a broker root and `--live`. A dry run writes nothing and starts no process.
71
+
72
+ ## Live dispatch
73
+
74
+ With both `--live` and a broker root, the command requires
75
+ `<broker-root>/runtime/request-child-worker.mjs`; if that file is missing it exits 3 and
76
+ sends nothing. It then runs:
77
+
78
+ ```
79
+ node <broker-root>/runtime/request-child-worker.mjs <broker-root> <parentTaskId> <role> <task> handoff:<id>
80
+ ```
81
+
82
+ The broker's stdout is parsed as JSON and returned as `broker_output` alongside the envelope.
83
+ A broker that fails to start, or that prints something that is not JSON, exits 1.
84
+
85
+ Two limits worth knowing:
86
+
87
+ - **No dedupe and no lock.** Unlike `auto`, `merge` and `promote`, dispatch takes no lock and
88
+ keeps no record of a previous dispatch. Running a live dispatch twice enqueues the request
89
+ twice; a caller that needs once-only delivery must guard it.
90
+ - **The broker is a separate program.** This skill builds the envelope, checks the gate and
91
+ invokes the broker entry point. What the broker does with the request — how it queues it,
92
+ which worker picks it up, whether it dedupes — is not implemented or verified here.
93
+
94
+ The traceability chain that does exist: `parentTaskId` defaults to `handoff:<id>`, and
95
+ `evidence` carries the handoff's manifest hash, so a request can be traced back to the session
96
+ directory, its manifest, and the source transcripts recorded in `source_paths`.
@@ -0,0 +1,145 @@
1
+ ---
2
+ title: Permissions
3
+ ---
4
+
5
+ # Permissions
6
+
7
+ The runtime layer evaluates a permission policy before it executes anything. The policy is a
8
+ JSON file; the implementation is `tools/runtime-engine.mjs`.
9
+
10
+ ```
11
+ node tools/runtime-engine.mjs run --risk R0 --op read --target tests/fixtures/minimal-transcript.jsonl
12
+ ```
13
+
14
+ Every operation is evaluated first and executed only on an `ALLOWED` verdict. A `DENIED` or
15
+ `NEEDS_AUTH` operation does not run, and its decision record is written with
16
+ `enforced_before_execution: true`.
17
+
18
+ ## The policy file
19
+
20
+ `permission-policy.json` in the skill root is the default. Every runtime verb accepts
21
+ `--policy <path>`; a relative path resolves against the skill root.
22
+
23
+ | Key | Shipped value |
24
+ |---|---|
25
+ | `approved_workspaces` | `.` (the skill root), `repo-upstream`, `.agents-handoff`, `.context` |
26
+ | `system_read_only_roots` | `C:\Windows`, `C:\Program Files`, `C:\Program Files (x86)` |
27
+ | `personal_data_roots` | `C:\Users` |
28
+ | `denied_roots` | empty |
29
+ | `grants` | `DISCOVERY_ONLY` allow, `READ_ONLY` allow, `WORKSPACE_WRITE` allow, `EXTERNAL_EFFECT` needs_auth, `DESTRUCTIVE` needs_auth, `IRREVERSIBLE` needs_auth |
30
+ | `risk_to_level` | `R0` → `READ_ONLY`, `R1` → `WORKSPACE_WRITE`, `R2` → `EXTERNAL_EFFECT`, `R3` → `DESTRUCTIVE`, `R4` → `IRREVERSIBLE` |
31
+
32
+ The shipped roots are Windows-shaped. On macOS and Linux they match no existing path, so
33
+ anything outside `approved_workspaces` classifies as `EXTERNAL_PATH`. Set platform-appropriate
34
+ roots before relying on classification outside the workspace.
35
+
36
+ A policy that is missing, is not valid JSON, or lacks `grants` or `risk_to_level` is a
37
+ configuration error: exit 2, nothing executed.
38
+
39
+ ## Levels and grants
40
+
41
+ | Level | Grant in the shipped policy | Meaning |
42
+ |---|---|---|
43
+ | `DISCOVERY_ONLY` | allow | Read metadata, list, inspect. |
44
+ | `READ_ONLY` | allow | Read files and state. |
45
+ | `WORKSPACE_WRITE` | allow | Write inside an approved workspace. |
46
+ | `EXTERNAL_EFFECT` | needs_auth | Network or external service. |
47
+ | `DESTRUCTIVE` | needs_auth | Delete or overwrite. |
48
+ | `IRREVERSIBLE` | needs_auth | Operations that cannot be undone. |
49
+
50
+ A grant value of `allow` yields `ALLOWED`, `needs_auth` yields `NEEDS_AUTH`, and any other
51
+ value yields `DENIED`. Unknown risk classes are denied, not allowed.
52
+
53
+ Because grants are keyed by level rather than by operation, an operator can permit every
54
+ `EXTERNAL_EFFECT` operation, or require authorization for every workspace write, with one
55
+ edit.
56
+
57
+ ## Target classification
58
+
59
+ `classify()` tests the target against the policy roots, most specific rule first:
60
+
61
+ | Order | Class | Source |
62
+ |---|---|---|
63
+ | 1 | `DENIED_ROOT` | `denied_roots` |
64
+ | 2 | `APPROVED_WORKSPACE` | the runtime's own state directory, wherever `AGENT_HANDOFF_STATE_DIR` points |
65
+ | 3 | `APPROVED_WORKSPACE` | `approved_workspaces` |
66
+ | 4 | `PERSONAL_DATA_PATH` | `personal_data_roots` |
67
+ | 5 | `SYSTEM_PATH` | `system_read_only_roots` |
68
+ | 6 | `EXTERNAL_PATH` | nothing matched |
69
+
70
+ Rule 3 must precede rule 4. A globally installed skill lives under the user's home
71
+ directory, which is normally inside a personal-data root; if the broad root won, every
72
+ operation inside a real installation would be denied at any risk.
73
+
74
+ ## Verdicts
75
+
76
+ | Target class | `READ_ONLY` / `DISCOVERY_ONLY` | `WORKSPACE_WRITE` and above |
77
+ |---|---|---|
78
+ | `APPROVED_WORKSPACE` | allowed | grant applies |
79
+ | `SYSTEM_PATH` | allowed | denied |
80
+ | `PERSONAL_DATA_PATH` | denied | denied |
81
+ | `DENIED_ROOT` | denied | denied |
82
+ | `EXTERNAL_PATH` | denied | denied |
83
+
84
+ Personal data and explicitly denied roots are denied at every risk level. Writes and effects
85
+ are allowed only inside an approved workspace.
86
+
87
+ | Verdict | Exit code |
88
+ |---|---|
89
+ | `ALLOWED` | 0 |
90
+ | `DENIED` | 3 |
91
+ | `NEEDS_AUTH` | 4 |
92
+ | usage or configuration error | 2 |
93
+
94
+ ## What the gateway covers
95
+
96
+ | Operation | How it is gated |
97
+ |---|---|
98
+ | `run --op read --target <path>` | Evaluated against the target. On `ALLOWED` the file is read and the record carries `bytes` and `content_sha256`. |
99
+ | `run --op spawn --args "<node args>"` | Evaluated against the workspace root. The child is the running Node binary with the given arguments, working directory pinned to the skill root, 15-second timeout, stdout and stderr digested into the record, child exit code propagated. |
100
+ | `job` / `resume` steps | Each step is evaluated at `R1` against the job state directory before the work log is appended. |
101
+ | `evaluate` | Decides and prints; executes nothing. |
102
+ | `policy` | Prints the loaded policy and exits 0. |
103
+
104
+ Every decision, allowed or denied, is written to
105
+ `<state dir>/executions/<timestamp>-<pid>-<random>.json`. The state directory is
106
+ `AGENT_HANDOFF_STATE_DIR` when set, and `<skill>/.agents-handoff` otherwise; it is treated as
107
+ an approved workspace wherever it points.
108
+
109
+ ## Declared but not enforced
110
+
111
+ - `DISCOVERY_ONLY` has a grant, but the shipped `risk_to_level` maps no risk to it, so no
112
+ `--risk` value reaches that level. It becomes reachable only if a policy maps a risk class
113
+ to it.
114
+ - The policy gates the runtime engine only. `tools/handoff.mjs`, the capability registry and
115
+ the installer do not consult it: the handoff engine writes to its resolved store without a
116
+ permission decision, and the registry probes are classified by their own declarations.
117
+ - There is no interactive approval prompt. `NEEDS_AUTH` reports that authorization is
118
+ required; granting it means changing the policy, and the decision is recorded either way.
119
+
120
+ ## Changing the policy
121
+
122
+ 1. Decide the level. Operations are keyed by risk class (`R0`–`R4`), which maps to a level.
123
+ 2. Edit `permission-policy.json`, or write a separate policy file and pass
124
+ `--policy <path>`.
125
+ 3. Change `grants` to move a whole level between allow, needs-auth and denial; change
126
+ `risk_to_level` to reclassify a risk class; add entries to `approved_workspaces`,
127
+ `personal_data_roots`, `system_read_only_roots` or `denied_roots` to move a path between
128
+ classes.
129
+ 4. Test one decision without executing it:
130
+
131
+ ```
132
+ node tools/runtime-engine.mjs evaluate --risk R2 --target README.md --json
133
+ ```
134
+
135
+ 5. Confirm what the runtime actually loaded:
136
+
137
+ ```
138
+ node tools/runtime-engine.mjs policy --json
139
+ ```
140
+
141
+ A `denied_roots` entry outranks an approved workspace, so it is the switch to use when a
142
+ path must be off limits regardless of any other rule.
143
+
144
+ See [INTEGRATION.md](INTEGRATION.md) for wiring these commands into another system, and
145
+ [LEVEL4.md](LEVEL4.md) for the runtime verbs as a whole.