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.
- package/CHANGELOG.md +192 -0
- package/LICENSE +21 -0
- package/README.md +150 -2
- package/SKILL.md +147 -0
- package/capability-registry.json +27 -0
- package/docs/ARCHITECTURE.md +187 -0
- package/docs/CHANGELOG.md +196 -0
- package/docs/CLI.md +299 -0
- package/docs/COMPATIBILITY.md +124 -0
- package/docs/CONTRIBUTING.md +134 -0
- package/docs/FORMAT.md +185 -0
- package/docs/INSTALL.md +394 -0
- package/docs/INTEGRATION.md +188 -0
- package/docs/LEVEL4.md +202 -0
- package/docs/LEVEL5.md +96 -0
- package/docs/PERMISSIONS.md +145 -0
- package/docs/PROVENANCE.md +110 -0
- package/docs/SECURITY.md +93 -0
- package/docs/SESSIONS.md +97 -0
- package/docs/TROUBLESHOOTING.md +158 -0
- package/docs/UNINSTALL.md +148 -0
- package/docs/UPGRADE.md +177 -0
- package/docs/_config.yml +18 -0
- package/docs/_data/nav.yml +36 -0
- package/docs/_layouts/default.html +31 -0
- package/docs/assets/style.css +88 -0
- package/docs/index.md +92 -0
- package/docs/sessions.json +34 -0
- package/handoff.config.example.json +35 -0
- package/handoff.config.schema.json +117 -0
- package/install/CHANGELOG.md +48 -0
- package/install/README.md +76 -0
- package/install/install.mjs +1455 -0
- package/install/package.json +39 -0
- package/package.json +66 -4
- package/permission-policy.json +33 -0
- package/refs/ADAPTERS.md +33 -0
- package/refs/bootstrap.md +59 -0
- package/refs/brief-checklist.md +79 -0
- package/refs/handbook.md +58 -0
- package/refs/protocol.md +117 -0
- package/refs/roles.md +75 -0
- package/refs/validator.md +73 -0
- package/schemas/handoff.schema.json +275 -0
- package/skill.json +147 -0
- package/templates/HANDOFF.llm.schema.json +144 -0
- package/templates/HANDOFF.template.md +40 -0
- package/tests/acceptance/acceptance.yaml +209 -0
- package/tests/fixtures/minimal-transcript.jsonl +2 -0
- package/tools/agent-handoff.mjs +22 -0
- package/tools/agents-handoff.mjs +410 -0
- package/tools/capability-registry.mjs +120 -0
- package/tools/handoff.mjs +398 -0
- package/tools/handoff.test.mjs +668 -0
- package/tools/lib/handoff-root.mjs +161 -0
- 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.
|