@gobing-ai/spur 0.4.19 → 0.4.21
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/.claude-plugin/marketplace.json +1 -1
- package/config/config.example.yaml +4 -0
- package/config/workflows/task-pipeline.yaml +28 -10
- package/package.json +9 -9
- package/plugins/sp/plugin.json +1 -1
- package/plugins/sp/scripts/hooks/fleet-guest-stop.ts +32 -12
- package/plugins/sp/skills/fleet-join/SKILL.md +77 -8
- package/plugins/sp/skills/spur-cli/references/agent.md +15 -5
- package/schemas/spur-config.schema.json +5 -0
- package/spur.js +4198 -2001
- package/web/_astro/{BoardApp.DY8fwPfw.js → BoardApp.DFBNysRC.js} +176 -176
- package/web/_astro/{BoardApp.CNF3sCnm.js → BoardApp.DspUVt5b.js} +1 -1
- package/web/_astro/{TaskDetail.8z3DgYux.js → TaskDetail.DEzLL6Fe.js} +1 -1
- package/web/_astro/{arc.CFEYbpFT.js → arc.TAQDSOGC.js} +1 -1
- package/web/_astro/{architectureDiagram-3BPJPVTR.3eDvG_X8.js → architectureDiagram-3BPJPVTR.D2Zyc6nm.js} +1 -1
- package/web/_astro/{blockDiagram-GPEHLZMM.CMbLzMpk.js → blockDiagram-GPEHLZMM.BnA_rJl6.js} +6 -6
- package/web/_astro/{board-facade-react-router-dom.Dy5pjzOw.js → board-facade-react-router-dom.CHNMUrya.js} +1 -1
- package/web/_astro/{board-facade-react-router.DjjqU27_.js → board-facade-react-router.Dob6MDgf.js} +1 -1
- package/web/_astro/{c4Diagram-AAUBKEIU.2lZW8nZ-.js → c4Diagram-AAUBKEIU.DVUo9pxo.js} +1 -1
- package/web/_astro/channel.DpBpvy1k.js +1 -0
- package/web/_astro/{chunk-2J33WTMH.BtM1eI3D.js → chunk-2J33WTMH.BJ-VkV91.js} +1 -1
- package/web/_astro/{chunk-4BX2VUAB.CKeckzAf.js → chunk-4BX2VUAB.xpN1rBQK.js} +1 -1
- package/web/_astro/{chunk-55IACEB6.BBmHet8U.js → chunk-55IACEB6.Dtu4VTmK.js} +1 -1
- package/web/_astro/{chunk-727SXJPM.Bi8v7M_x.js → chunk-727SXJPM.CjXhM1c1.js} +1 -1
- package/web/_astro/{chunk-AQP2D5EJ.DOcBJyb6.js → chunk-AQP2D5EJ.DSCad_Gd.js} +1 -1
- package/web/_astro/{chunk-FMBD7UC4.C-EdUBD7.js → chunk-FMBD7UC4.DxwDIiuY.js} +1 -1
- package/web/_astro/{chunk-ND2GUHAM.IqTyWQhk.js → chunk-ND2GUHAM.D5LgSWYE.js} +1 -1
- package/web/_astro/{chunk-QZHKN3VN.a2DShvIL.js → chunk-QZHKN3VN.CwXZoKZI.js} +1 -1
- package/web/_astro/chunk-YNUBSHFH.B6ndyWdA.js +7 -0
- package/web/_astro/{classDiagram-4FO5ZUOK.CQBnvIjk.js → classDiagram-4FO5ZUOK.DZ-I2QOo.js} +1 -1
- package/web/_astro/{classDiagram-v2-Q7XG4LA2.CQBnvIjk.js → classDiagram-v2-Q7XG4LA2.DZ-I2QOo.js} +1 -1
- package/web/_astro/{cose-bilkent-S5V4N54A.BW9Ryz7c.js → cose-bilkent-S5V4N54A.DszE5dpz.js} +1 -1
- package/web/_astro/{cynefin-OW5HDTMX.BPhLeSep.js → cynefin-OW5HDTMX.c-ctyPLt.js} +1 -1
- package/web/_astro/cytoscape.esm.BDkkwuE4.js +321 -0
- package/web/_astro/{dagre-BM42HDAG._oD9MsDB.js → dagre-BM42HDAG.Boq07Shn.js} +2 -2
- package/web/_astro/{diagram-2AECGRRQ.iciSOlxY.js → diagram-2AECGRRQ.Csap4wVu.js} +1 -1
- package/web/_astro/{diagram-5GNKFQAL.ZzqeC45Q.js → diagram-5GNKFQAL.Cy4jXe_F.js} +1 -1
- package/web/_astro/{diagram-KO2AKTUF.CMqWGOYB.js → diagram-KO2AKTUF.BXEZQbTM.js} +1 -1
- package/web/_astro/{diagram-LMA3HP47.B5MPN99W.js → diagram-LMA3HP47.CFcDobF6.js} +1 -1
- package/web/_astro/{diagram-OG6HWLK6.Zgi-ANAF.js → diagram-OG6HWLK6.CJ8Q1cQd.js} +1 -1
- package/web/_astro/{erDiagram-TEJ5UH35.Bd5aeIyR.js → erDiagram-TEJ5UH35.B409nFLg.js} +1 -1
- package/web/_astro/{flowDiagram-I6XJVG4X.B8LBo_PH.js → flowDiagram-I6XJVG4X.BtF5dGDY.js} +4 -4
- package/web/_astro/{ganttDiagram-6RSMTGT7.Di2HLHk4.js → ganttDiagram-6RSMTGT7.BbflTg4R.js} +1 -1
- package/web/_astro/{gitGraphDiagram-PVQCEYII.BqDuU6fc.js → gitGraphDiagram-PVQCEYII.BLPcOhGS.js} +1 -1
- package/web/_astro/{infoDiagram-5YYISTIA.C_hw98uF.js → infoDiagram-5YYISTIA.DsaY8QU3.js} +1 -1
- package/web/_astro/{ishikawaDiagram-YF4QCWOH.uK7JK9Kk.js → ishikawaDiagram-YF4QCWOH.D9PaTLtO.js} +1 -1
- package/web/_astro/{journeyDiagram-JHISSGLW.D03MNAmn.js → journeyDiagram-JHISSGLW.4_zPJ9m2.js} +1 -1
- package/web/_astro/{kanban-definition-UN3LZRKU.DuiDtKhq.js → kanban-definition-UN3LZRKU.IqD1vq7W.js} +3 -3
- package/web/_astro/{linear.Der6_z5m.js → linear.JJiEkviR.js} +1 -1
- package/web/_astro/{mermaid.core.CaxkEgwh.js → mermaid.core.Brxzivhz.js} +55 -55
- package/web/_astro/{mindmap-definition-RKZ34NQL.DecT082u.js → mindmap-definition-RKZ34NQL.B6pZnBGO.js} +1 -1
- package/web/_astro/{pieDiagram-4H26LBE5.DuAEfZXh.js → pieDiagram-4H26LBE5.MM6Of7YP.js} +1 -1
- package/web/_astro/{quadrantDiagram-W4KKPZXB.CWXczU1k.js → quadrantDiagram-W4KKPZXB.7eWQnH9R.js} +1 -1
- package/web/_astro/{requirementDiagram-4Y6WPE33.BsAHUsR6.js → requirementDiagram-4Y6WPE33.C3T9mbHc.js} +1 -1
- package/web/_astro/{sankeyDiagram-5OEKKPKP.ghBqvXN8.js → sankeyDiagram-5OEKKPKP.CqlwDe_m.js} +1 -1
- package/web/_astro/{sequenceDiagram-3UESZ5HK.BKbwb7ad.js → sequenceDiagram-3UESZ5HK.BNVlPChR.js} +4 -4
- package/web/_astro/{stateDiagram-AJRCARHV.D5geOKAc.js → stateDiagram-AJRCARHV.DSExW-ER.js} +1 -1
- package/web/_astro/{stateDiagram-v2-BHNVJYJU.Dc4Sjs4R.js → stateDiagram-v2-BHNVJYJU.CxE4k_fB.js} +1 -1
- package/web/_astro/{timeline-definition-PNZ67QCA.ve2dW2tR.js → timeline-definition-PNZ67QCA.uFqjG7Z5.js} +1 -1
- package/web/_astro/{vennDiagram-CIIHVFJN.BdJumy3f.js → vennDiagram-CIIHVFJN.DpTrAXYs.js} +5 -5
- package/web/_astro/{wardleyDiagram-YWT4CUSO.X8HgtN8V.js → wardleyDiagram-YWT4CUSO.Dl0KBFZe.js} +1 -1
- package/web/_astro/{xychartDiagram-2RQKCTM6.C30VVwQ6.js → xychartDiagram-2RQKCTM6.CE0syl0o.js} +1 -1
- package/web/board-runtime.json +2 -2
- package/web/index.html +2 -2
- package/web/_astro/channel.CdQ8ZO1m.js +0 -1
- package/web/_astro/chunk-YNUBSHFH.CxeAygfi.js +0 -7
- package/web/_astro/cytoscape.esm.BB4DxJjf.js +0 -321
|
@@ -58,6 +58,10 @@ bootstrap:
|
|
|
58
58
|
command: >-
|
|
59
59
|
spur --no-logo history import --source all &&
|
|
60
60
|
spur --no-logo workflow run config/workflows/history-anatomy.yaml --quiet
|
|
61
|
+
# Without this the chain inherits the 10-minute scheduler default and dies mid-validate.
|
|
62
|
+
# 1h45m: enrich (45-52 min on a slow executor) + validate, still below the server
|
|
63
|
+
# queue's 2h visibility timeout.
|
|
64
|
+
timeoutMs: 6300000
|
|
61
65
|
|
|
62
66
|
# ── Spur app section (validated by spurConfigSchema in
|
|
63
67
|
# @gobing-ai/spur-config) ──
|
|
@@ -660,19 +660,27 @@ states:
|
|
|
660
660
|
# feature credit) is owned by `plugins/sp/agents/super-reviewer.md` Output Format,
|
|
661
661
|
# which this pointer names.
|
|
662
662
|
input: >-
|
|
663
|
-
/sp:dev-review --tasks ${vars.wbs} --auto --focus ${vars.reviewFocus}
|
|
664
|
-
Format
|
|
665
|
-
(`spur task verdict`
|
|
666
|
-
(deriveVerifyConfidence), never the section's disposition level.
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
663
|
+
/sp:dev-review --tasks ${vars.wbs} --auto --focus ${vars.reviewFocus}
|
|
664
|
+
Answer contract: super-reviewer Output Format. Emit the machine-linted artifact, not a prose summary.
|
|
665
|
+
`## Review`: deriveReviewConfidence. Captured answer (`spur task verdict`) has one `Confidence: HIGH|MEDIUM|LOW`
|
|
666
|
+
line (deriveVerifyConfidence), never the section's disposition level.
|
|
667
|
+
End with one `Verdict: PASS|PARTIAL|FAIL` line (plain, final) after
|
|
668
|
+
`| Req | Status | Evidence |` (one row per requirement; MET | PARTIAL | UNMET) and
|
|
669
|
+
`| AC | Status | Evidence Type | Evidence |` (one row per AC; MET | PARTIAL | UNMET | N/A).
|
|
670
|
+
Evidence types (`+` ok): test | command | static-ref | manual-review | llm-judge | n/a.
|
|
671
|
+
Cite repo-root-relative `path:line` (never `/tmp`) or named checks. AC rows use the task's identities or
|
|
672
|
+
a declared `AC-N` alias; a linked scenario covers its task; a task-local criterion needs no feature credit.
|
|
673
|
+
Open P1-P3 blocks completion.
|
|
674
|
+
Also write it to `.spur/run/${vars.__runId}-review-cert.md`.
|
|
672
675
|
# The captured review answer is the run's certification input (and the
|
|
673
676
|
# `task verdict` input below): expectFile fails the step when no answer landed.
|
|
674
677
|
answerFile: .spur/run/${vars.__runId}-review-answer.txt
|
|
675
678
|
expectFile: .spur/run/${vars.__runId}-review-answer.txt
|
|
679
|
+
# The reviewer WRITES this one itself (see (c2)); declared so the composition lint can
|
|
680
|
+
# see the run path the prompt names (1233 R2).
|
|
681
|
+
inputs:
|
|
682
|
+
- path: .spur/run/${vars.__runId}-review-cert.md
|
|
683
|
+
kind: artifact
|
|
676
684
|
# Declared Layer-1 role (0538 R2/0710 R7): routing reason; no executor pin — role routing feeds the distinctness gate.
|
|
677
685
|
role: reviewer
|
|
678
686
|
# B7 R3/R6 (0894): declared fresh policy (reviewer default) — the action
|
|
@@ -683,6 +691,16 @@ states:
|
|
|
683
691
|
priority: ${vars.taskPriority}
|
|
684
692
|
compareExecutorWith: implement
|
|
685
693
|
timeoutMs: ${vars.stepTimeoutMs}
|
|
694
|
+
# (c2) The reviewer is asked to WRITE its canonical artifact to `-review-cert.md`, because
|
|
695
|
+
# the captured answer above is the agent's stdout: a reviewer that writes the artifact and
|
|
696
|
+
# then narrates its summary has the artifact overwritten by the narration, and the verdict
|
|
697
|
+
# lint fails work that is actually complete (four such runs in the G74 batch, 2026-10-24).
|
|
698
|
+
# The agent-written cert wins; when it is absent or empty the captured answer is used, so
|
|
699
|
+
# this is a strict widening of accepted inputs, never a new failure mode.
|
|
700
|
+
- kind: shell
|
|
701
|
+
options:
|
|
702
|
+
command: >-
|
|
703
|
+
A=".spur/run/$__runId-review-answer.txt"; C=".spur/run/$__runId-review-cert.md"; [ -s "$C" ] || cp "$A" "$C" 2>/dev/null; exit 0
|
|
686
704
|
# (d) `spur task verdict` owns the answer lint and the verdict derivation (task 1003
|
|
687
705
|
# R2): one complete requirement row per declared requirement, AC identities resolved,
|
|
688
706
|
# status/evidence vocabularies accepted, hedged or uncited evidence rejected. It
|
|
@@ -694,7 +712,7 @@ states:
|
|
|
694
712
|
options:
|
|
695
713
|
id: review-verdict
|
|
696
714
|
executable: "${vars.spurBin}"
|
|
697
|
-
args: ["task", "verdict", "${vars.wbs}", "--from-answer", ".spur/run/${vars.__runId}-review-
|
|
715
|
+
args: ["task", "verdict", "${vars.wbs}", "--from-answer", ".spur/run/${vars.__runId}-review-cert.md"]
|
|
698
716
|
resultFile: .spur/run/${vars.__runId}-review-verdict.status
|
|
699
717
|
softFail: true
|
|
700
718
|
# (e) one fold, one token: PASS requires the typed verdict PASS, the artifact's own
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@gobing-ai/spur",
|
|
3
|
-
"version": "0.4.
|
|
3
|
+
"version": "0.4.21",
|
|
4
4
|
"description": "Spur CLI — local-first harness for mainstream coding agents: constraint checking, workflow orchestration, agent health, and history analytics. Bun-native; exposes the `spur` command.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"spur",
|
|
@@ -58,14 +58,14 @@
|
|
|
58
58
|
},
|
|
59
59
|
"devDependencies": {
|
|
60
60
|
"@commander-js/extra-typings": "^14.0.0",
|
|
61
|
-
"@gobing-ai/ts-db": "^0.
|
|
62
|
-
"@gobing-ai/ts-ai-runner": "^0.
|
|
63
|
-
"@gobing-ai/ts-dual-workflow-engine": "^0.
|
|
64
|
-
"@gobing-ai/ts-infra": "^0.
|
|
65
|
-
"@gobing-ai/ts-llm-jsonl-importer": "^0.
|
|
66
|
-
"@gobing-ai/ts-rule-engine": "^0.
|
|
67
|
-
"@gobing-ai/ts-runtime": "^0.
|
|
68
|
-
"@gobing-ai/ts-utils": "^0.
|
|
61
|
+
"@gobing-ai/ts-db": "^0.7.1",
|
|
62
|
+
"@gobing-ai/ts-ai-runner": "^0.7.1",
|
|
63
|
+
"@gobing-ai/ts-dual-workflow-engine": "^0.7.1",
|
|
64
|
+
"@gobing-ai/ts-infra": "^0.7.1",
|
|
65
|
+
"@gobing-ai/ts-llm-jsonl-importer": "^0.7.1",
|
|
66
|
+
"@gobing-ai/ts-rule-engine": "^0.7.1",
|
|
67
|
+
"@gobing-ai/ts-runtime": "^0.7.1",
|
|
68
|
+
"@gobing-ai/ts-utils": "^0.7.1",
|
|
69
69
|
"@types/bun": "1.4.3",
|
|
70
70
|
"@types/figlet": "^1.7.0",
|
|
71
71
|
"@types/node-notifier": "8.0.5",
|
package/plugins/sp/plugin.json
CHANGED
|
@@ -37,7 +37,7 @@ export const GUEST_STOP_PROBE_MS = 2000;
|
|
|
37
37
|
/** How many pending messages the delivered reason names. */
|
|
38
38
|
export const GUEST_STOP_MAX_MESSAGES = 5;
|
|
39
39
|
|
|
40
|
-
/**
|
|
40
|
+
/** Preview length for legacy queued snapshots; an owned handoff carries the full body. */
|
|
41
41
|
export const GUEST_STOP_BODY_CHARS = 140;
|
|
42
42
|
|
|
43
43
|
/** Hard per-invocation timeout for a `spur` child the hook shells out to. */
|
|
@@ -108,20 +108,41 @@ export function stopDecisionFor(
|
|
|
108
108
|
} catch {
|
|
109
109
|
return null;
|
|
110
110
|
}
|
|
111
|
-
const
|
|
111
|
+
const payload = rows !== null && typeof rows === 'object' && 'data' in rows ? rows.data : rows;
|
|
112
|
+
const list = Array.isArray(payload)
|
|
113
|
+
? payload
|
|
114
|
+
: payload !== null && typeof payload === 'object' && 'messages' in payload && Array.isArray(payload.messages)
|
|
115
|
+
? payload.messages
|
|
116
|
+
: undefined;
|
|
112
117
|
if (list === undefined) return null;
|
|
118
|
+
const handedOff = payload !== null && typeof payload === 'object' && 'pending' in payload;
|
|
113
119
|
const pending = list
|
|
114
|
-
.
|
|
115
|
-
.
|
|
116
|
-
.
|
|
120
|
+
.filter((row) => row !== null && typeof row === 'object')
|
|
121
|
+
.map((row) => row as { id?: unknown; from_id?: unknown; fromId?: unknown; body?: unknown; status?: unknown })
|
|
122
|
+
.filter(
|
|
123
|
+
(row) =>
|
|
124
|
+
typeof row.body === 'string' &&
|
|
125
|
+
(row.status === undefined ||
|
|
126
|
+
(handedOff
|
|
127
|
+
? ['queued', 'claimed', 'delivered'].includes(String(row.status))
|
|
128
|
+
: row.status === 'queued')),
|
|
129
|
+
)
|
|
130
|
+
.slice(0, handedOff ? undefined : maxMessages);
|
|
117
131
|
if (pending.length === 0) return null;
|
|
118
132
|
const lines = pending.map((row) => {
|
|
119
133
|
const from =
|
|
120
134
|
typeof row.from_id === 'string' ? row.from_id : typeof row.fromId === 'string' ? row.fromId : 'fleet';
|
|
121
|
-
const body =
|
|
122
|
-
.
|
|
123
|
-
.
|
|
124
|
-
|
|
135
|
+
const body = handedOff
|
|
136
|
+
? String(row.body ?? '')
|
|
137
|
+
: String(row.body ?? '')
|
|
138
|
+
.replace(/\s+/g, ' ')
|
|
139
|
+
.trim();
|
|
140
|
+
const id = typeof row.id === 'string' ? ` [${row.id}]` : '';
|
|
141
|
+
const preview =
|
|
142
|
+
!handedOff && body.length > GUEST_STOP_BODY_CHARS
|
|
143
|
+
? `${body.slice(0, GUEST_STOP_BODY_CHARS)}… (read the full message with spur message show ${String(row.id ?? '<msgId>')} --json)`
|
|
144
|
+
: body;
|
|
145
|
+
return `- ${from}${id}: ${preview}`;
|
|
125
146
|
});
|
|
126
147
|
const reason = `Pending fleet messages for ${guestId}:\n${lines.join('\n')}\n\nHandle them, then \`spur message reply <msgId> …\` and continue.`;
|
|
127
148
|
return JSON.stringify({ decision: 'block', reason });
|
|
@@ -155,11 +176,10 @@ export function runFleetGuestStop(payload: GuestStopPayload, deps: GuestStopDeps
|
|
|
155
176
|
guest.id,
|
|
156
177
|
'--timeout',
|
|
157
178
|
String(deps.probeMs ?? GUEST_STOP_PROBE_MS),
|
|
179
|
+
'--json',
|
|
158
180
|
]);
|
|
159
181
|
if (probe.status !== 0) return null;
|
|
160
|
-
|
|
161
|
-
if (inbox.status !== 0) return null;
|
|
162
|
-
return stopDecisionFor(guest.id, inbox.stdout, deps.maxMessages);
|
|
182
|
+
return stopDecisionFor(guest.id, probe.stdout, deps.maxMessages);
|
|
163
183
|
} catch {
|
|
164
184
|
return null;
|
|
165
185
|
}
|
|
@@ -26,17 +26,63 @@ Pick the role whose work this session can actually do. `--session-id` defaults t
|
|
|
26
26
|
`CLAUDE_CODE_SESSION_ID`, which is what the Stop hook matches against — without it the hook cannot
|
|
27
27
|
find this session, though the `wait --inbox` loop still works.
|
|
28
28
|
|
|
29
|
-
|
|
29
|
+
**Renewal is automatic (G75 R1, task 1279).** `join` records the *agent* process that launched this
|
|
30
|
+
session — never the short-lived CLI pid — and starts one detached renewer that heartbeats the lease
|
|
31
|
+
every third of the TTL. Long work with no `wait --inbox` in between no longer lets the lease expire.
|
|
32
|
+
The record says which happened: `identity: 'verified'` with a `pid`/`processStartedAt` means renewal
|
|
33
|
+
is running; `identity: 'unverified'` with `pid: null` means no agent ancestor could be proved, so
|
|
34
|
+
nothing renews and the session must keep looping `wait --inbox` to stay joined. A rejoin from the
|
|
35
|
+
same session reuses the running renewer instead of starting a second one.
|
|
30
36
|
|
|
31
|
-
|
|
32
|
-
|
|
37
|
+
## Binding a declared member (opt-in, fenced)
|
|
38
|
+
|
|
39
|
+
```bash
|
|
40
|
+
spur agent join --role coder --member <declared-member-id> --json
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
`--member` binds this session to one **declared** member instead of joining as a plain guest: the
|
|
44
|
+
record id becomes the member id, so `wait --inbox <member-id>` addresses the declared member's inbox,
|
|
45
|
+
and a supervised spawn or fleet dispatch sees the member busy through its own occupant row. It exits 2
|
|
46
|
+
with a named reason for an unknown/disabled member, a role mismatch, an executor that cannot attest
|
|
47
|
+
the stage capabilities, a live occupant, or a concurrent binder that lost the race. Guests without
|
|
48
|
+
`--member` are unchanged — and still excluded from role selection.
|
|
49
|
+
|
|
50
|
+
### Attached lifecycle reports carry the binding pin (task 1282)
|
|
51
|
+
|
|
52
|
+
A bound occupant's lifecycle reports must present the host binding pin or they are refused (exit 2):
|
|
53
|
+
|
|
54
|
+
```bash
|
|
55
|
+
spur agent report --state working --seq <ns> --pin <ownerEpoch>:<occupancyGeneration> --json
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
The first pin-matched report completes the handshake (the host's strategy then treats this member as
|
|
59
|
+
stage-visible); later reports never move it. A pin mismatch means the occupant generation moved —
|
|
60
|
+
re-read `spur agent list --specs --json` instead of retrying. Plain guests keep reporting without a pin.
|
|
61
|
+
|
|
62
|
+
## Pull loop (the default path — hooks are acceleration only)
|
|
63
|
+
|
|
64
|
+
The durable inbox loop below is what makes a guest work on **every** host, hooks installed or not.
|
|
65
|
+
Never substitute it with repeated model turns or terminal polling while the session is busy: each
|
|
66
|
+
`wait --inbox` call is one bounded pull, and the session does the work between pulls.
|
|
67
|
+
|
|
68
|
+
`spur agent wait --inbox <id> --timeout 540000 --json` returns **0** with up to five claimed messages
|
|
69
|
+
in `{ id, pending, messages }`, settling their delivery after the output handoff. It returns **1**
|
|
70
|
+
on timeout. An output failure releases the exact claims for retry; inbox listing itself never
|
|
71
|
+
consumes messages. Each call heartbeats the lease, so a looping session stays joined. The Bash tool's
|
|
33
72
|
own ~10-minute limit is why the timeout stays under it: one bounded call per turn, re-armed after
|
|
34
73
|
the work is done.
|
|
35
74
|
|
|
36
|
-
1. `spur agent wait --inbox <id> --timeout 540000 --json` → exit 0
|
|
37
|
-
2. `spur message
|
|
75
|
+
1. `spur agent wait --inbox <id> --timeout 540000 --json` → exit 0 hands this session `messages`.
|
|
76
|
+
2. Read the returned messages, including their IDs. `spur message show <id> --json` can reread one;
|
|
77
|
+
`spur message inbox --agent <id> --json` is a non-consuming historical view.
|
|
38
78
|
3. Do the work the message asks for.
|
|
39
|
-
4. `spur message reply <msgId> "<result>"` → reply to each one.
|
|
79
|
+
4. `spur message reply <msgId> "<result>"` → reply to each one. A bound occupant answering a **stage
|
|
80
|
+
request** (a `fleet:task:<requestId>:<attempt>` keyed message) MUST make the reply body the JSON
|
|
81
|
+
correlation payload — `{ "requestId": …, "attempt": …, "ownerEpoch": …, "occupancyGeneration": …,
|
|
82
|
+
"artifact": "<path or summary>" }` — with the values from the request and its own binding. The
|
|
83
|
+
host certifies the turn on that payload alone; a plain-text reply never completes a stage and the
|
|
84
|
+
request is retried to someone else. Do not answer stage requests for a binding you no longer hold:
|
|
85
|
+
a stale epoch/generation in the payload is fenced off, not promoted.
|
|
40
86
|
5. Re-arm: go back to step 1 while the operator wants this session in the fleet.
|
|
41
87
|
|
|
42
88
|
## Leave
|
|
@@ -46,8 +92,29 @@ spur agent leave --json # the guest joined by this session
|
|
|
46
92
|
spur agent leave my-session --json # an explicit id
|
|
47
93
|
```
|
|
48
94
|
|
|
49
|
-
Leaving releases the lease immediately.
|
|
50
|
-
|
|
95
|
+
Leaving releases the lease immediately. A **bound** occupant's leave drains first: it stops new
|
|
96
|
+
assignment, waits up to the drain grace (default 30s, `--drain-timeout <ms>` to bound it) for the
|
|
97
|
+
in-flight turn to settle, then retires the record. A turn that does not settle in time is never
|
|
98
|
+
failed by the leave — it stays outcome-unknown and resumable; nothing is killed. If the session dies
|
|
99
|
+
without leaving, the lease expires and the reconciler pass retires the guest — pending work is never
|
|
100
|
+
lost, it returns to `queued`.
|
|
101
|
+
|
|
102
|
+
## Host capabilities (probed, not assumed)
|
|
103
|
+
|
|
104
|
+
`bun run fleet-e2e --host-probe` writes `docs/reports/fleet-host-capabilities.json`: one row per
|
|
105
|
+
host×mode with the installed binary path/version, the wake channel and the exact `--help` line that
|
|
106
|
+
proves it. Anything unproven is `pull-only` with a reason.
|
|
107
|
+
|
|
108
|
+
| Host | Interactive wake | Headless |
|
|
109
|
+
| --- | --- | --- |
|
|
110
|
+
| Codex | `app-server` (a reachable App Server connection) | `pull-only` |
|
|
111
|
+
| Claude Code | `hook` (the Stop callback below) | `pull-only` |
|
|
112
|
+
| Pi | `rpc` | `pull-only` |
|
|
113
|
+
|
|
114
|
+
These values are **installed-host evidence**, so re-read the report instead of trusting this table
|
|
115
|
+
after an upgrade. A `pull-only` row is not a defect: the loop above is the portable contract, and the
|
|
116
|
+
hook/RPC/App Server channels only shorten the wait. `--real-hosts --agents <csv>` is the opt-in lane
|
|
117
|
+
that drives a real host end to end; the default lane never launches one.
|
|
51
118
|
|
|
52
119
|
## Stop-hook delivery (opportunistic)
|
|
53
120
|
|
|
@@ -63,3 +130,5 @@ so the pull loop above — not the hook — is what makes a guest work anywhere.
|
|
|
63
130
|
- Guests are **not** supervised: do not expect restart, pid ownership, or `--drain` injection.
|
|
64
131
|
- Never dispatch a stage to a guest id: `spur agent run --spec <guest-id>` is refused on purpose.
|
|
65
132
|
- Never hold the write slot through a guest: the guest lease is the only heartbeat (R5).
|
|
133
|
+
- Renewal stops itself on agent-process death, a reused pid, a lost fence or `leave`. There is no
|
|
134
|
+
command to stop it and no daemon behind it: it lives no longer than the agent that joined.
|
|
@@ -26,7 +26,7 @@ that before using `run` for fan-out dispatch.
|
|
|
26
26
|
| `trace <runId>` | Print one execution record's lineage — the run, its dispatch parents, agent session ids and streams (1076 R4, ADR-132) | `--follow` `--timeout <ms>` `--json` |
|
|
27
27
|
| `list` | List detected coding agents, or agent specs with `--specs` (live run status + member session merged from `spur self serve`) | `--specs` `--server <url>` `--json` |
|
|
28
28
|
| `report` | Report a fleet member lifecycle state (`working`/`idle`/`blocked`) from a host hook (G73 R1/R2, task 1080) | `--state <state>` `--seq <ns>` `--spec <id>` `--json` |
|
|
29
|
-
| `join` | Join the fleet as a guest occupant — a live session that pulls its own work (G73 R2, task 1081) | `--role <name>` `--id <id>` `--session-id <sid>` `--pid <n>` `--executor <name>` `--json` |
|
|
29
|
+
| `join` | Join the fleet as a guest occupant — a live session that pulls its own work (G73 R2, task 1081); `--member` binds a declared member instead (G75 R3, task 1281) | `--role <name>` `--id <id>` `--member <id>` `--session-id <sid>` `--pid <n>` `--executor <name>` `--json` |
|
|
30
30
|
| `leave` | Leave the fleet: release the guest lease, retire the occupant, return claimed messages to `queued` | `[id]` `--session-id <sid>` `--json` |
|
|
31
31
|
| `status` | Agent specs with live process status and member session (requires `spur self serve`) | `--server <url>` `--json` |
|
|
32
32
|
| `doctor [agent]` | Check agent readiness | `--json` `--probe-health` `--force-refresh` |
|
|
@@ -228,21 +228,31 @@ returns its claimed messages to `queued` and removes the record.
|
|
|
228
228
|
| `--session-id <sid>` | Host session id (defaults to `CLAUDE_CODE_SESSION_ID`) |
|
|
229
229
|
| `--pid <n>` | Process id to record |
|
|
230
230
|
| `--executor <name>` | Executor name to record (informational — a guest is never dispatched to) |
|
|
231
|
+
| `--member <id>` | Bind a **declared member** instead of joining as a guest (G75 R3, task 1281): resolves the enabled member, checks role/executor, requires a capability proof, and takes the fenced occupant of `guest:<member-id>`. The record id becomes the member id, so `wait --inbox <member-id>` addresses the declared member's inbox. Exits 2 with `unknown-member`, `disabled-member`, `role-mismatch`, `capability-unattested`, `live-occupant` or `collision`. Without it, `join` is unchanged |
|
|
231
232
|
| `--json` | Machine-readable result |
|
|
232
233
|
|
|
233
234
|
A guest carrying no executor attestation is never a stage target: `spur agent run --spec <guest-id>`
|
|
234
235
|
is refused (R3), and `requiresCapabilities` stages therefore can never reach one.
|
|
235
236
|
|
|
237
|
+
`--member <id>` inverts that for exactly one declared id: the binding takes the member's occupant row
|
|
238
|
+
at generation `max+1` and the `guest:<member-id>` claim, so a supervised spawn or strategy dispatch
|
|
239
|
+
sees the member busy through the same `hasRunning` check it already uses — no dual writers. Every
|
|
240
|
+
renew re-checks both the claim epoch and that generation, so a taken-over member refuses the stale
|
|
241
|
+
binder's claim/accept. `memberBinding.stageEligible` is `false` when the binder's host has only the
|
|
242
|
+
`pull-only` wake channel (`bun run fleet-e2e --host-probe`), i.e. it may receive messages but no stage.
|
|
243
|
+
|
|
236
244
|
## `wait --inbox <id>` - the guest pull primitive (G73 R4)
|
|
237
245
|
|
|
238
246
|
```bash
|
|
239
247
|
spur agent wait --inbox reviewer-g1 --timeout 540000 --json
|
|
240
248
|
```
|
|
241
249
|
|
|
242
|
-
Returns `0`
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
250
|
+
Returns `0` after handing off up to five queued messages, with `{ id, pending, messages }` in JSON;
|
|
251
|
+
`pending` is the handed-off batch count. Exact claims settle as delivered after output succeeds and
|
|
252
|
+
release for retry if output fails; further queued work stays for the next pull. Returns `1` on timeout
|
|
253
|
+
or when the id is not a joined guest. Every call heartbeats the guest's lease, so a session that keeps
|
|
254
|
+
looping stays joined; the timeout stays under the Bash tool's ~10-minute limit. The
|
|
255
|
+
`fleet-join` skill loops it (wait → work from returned messages → `spur message reply`),
|
|
246
256
|
and the `fleet-guest-stop` plugin hook delivers the same messages at a turn boundary on hosts that
|
|
247
257
|
support a blocking Stop hook.
|
|
248
258
|
|
|
@@ -120,6 +120,11 @@
|
|
|
120
120
|
"type": "string",
|
|
121
121
|
"minLength": 1,
|
|
122
122
|
"description": "Five-field cron expression (minute hour day-of-month month day-of-week) evaluated in local time. Mutually exclusive with `intervalMinutes`."
|
|
123
|
+
},
|
|
124
|
+
"timeoutMs": {
|
|
125
|
+
"type": ["integer", "null"],
|
|
126
|
+
"minimum": 1,
|
|
127
|
+
"description": "Total child budget for this job in milliseconds. At the deadline the runtime SIGTERMs the owned process group, then SIGKILLs after the scheduler grace. `null` is explicit unlimited; absent falls back to the global scheduler default (10 min). Keep a model-driven chain below the server queue's 2 h visibility timeout so it fails its own attempt instead of being re-delivered."
|
|
123
128
|
}
|
|
124
129
|
},
|
|
125
130
|
"oneOf": [
|