harnery 0.31.6 → 0.33.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +2 -0
- package/dist/commander.d.ts.map +1 -1
- package/dist/commander.js +5 -1
- package/dist/commands/agents.d.ts +0 -9
- package/dist/commands/agents.d.ts.map +1 -1
- package/dist/commands/agents.js +280 -94
- package/dist/commands/browse-session.d.ts +21 -0
- package/dist/commands/browse-session.d.ts.map +1 -0
- package/dist/commands/browse-session.js +157 -0
- package/dist/commands/browse.d.ts.map +1 -1
- package/dist/commands/browse.js +205 -37
- package/dist/commands/checkpoint.js +1 -1
- package/dist/commands/claude-desktop.d.ts +2 -2
- package/dist/commands/claude-desktop.d.ts.map +1 -1
- package/dist/commands/claude-desktop.js +74 -3
- package/dist/commands/deinit.d.ts.map +1 -1
- package/dist/commands/deinit.js +4 -0
- package/dist/commands/docs.d.ts.map +1 -1
- package/dist/commands/docs.js +40 -0
- package/dist/commands/doctor.d.ts +4 -2
- package/dist/commands/doctor.d.ts.map +1 -1
- package/dist/commands/doctor.js +141 -19
- package/dist/commands/init.d.ts +2 -2
- package/dist/commands/init.d.ts.map +1 -1
- package/dist/commands/init.js +26 -5
- package/dist/commands/instructions.d.ts +4 -0
- package/dist/commands/instructions.d.ts.map +1 -0
- package/dist/commands/instructions.js +100 -0
- package/dist/commands/presence.d.ts.map +1 -1
- package/dist/commands/presence.js +4 -1
- package/dist/commands/tunnel.d.ts.map +1 -1
- package/dist/commands/tunnel.js +2 -3
- package/dist/core/agents/canonical-emit.d.ts +2 -3
- package/dist/core/agents/canonical-emit.d.ts.map +1 -1
- package/dist/core/agents/canonical-emit.js +6 -5
- package/dist/core/agents/cli.js +128 -89
- package/dist/core/agents/coord-client.d.ts +18 -3
- package/dist/core/agents/coord-client.d.ts.map +1 -1
- package/dist/core/agents/coord-client.js +5 -4
- package/dist/core/agents/finalization.d.ts +68 -0
- package/dist/core/agents/finalization.d.ts.map +1 -0
- package/dist/core/agents/finalization.js +443 -0
- package/dist/core/agents/git-hook.d.ts +51 -0
- package/dist/core/agents/git-hook.d.ts.map +1 -0
- package/dist/core/agents/git-hook.js +118 -0
- package/dist/core/agents/index.d.ts +1 -0
- package/dist/core/agents/index.d.ts.map +1 -1
- package/dist/core/agents/index.js +1 -0
- package/dist/core/agents/render/prompt-context.d.ts +12 -5
- package/dist/core/agents/render/prompt-context.d.ts.map +1 -1
- package/dist/core/agents/render/prompt-context.js +70 -16
- package/dist/core/agents/render/session-context.d.ts.map +1 -1
- package/dist/core/agents/render/session-context.js +23 -4
- package/dist/core/agents/rules/claim-conflict.js +3 -3
- package/dist/core/agents/rules/commit-conflict.d.ts +15 -6
- package/dist/core/agents/rules/commit-conflict.d.ts.map +1 -1
- package/dist/core/agents/rules/commit-conflict.js +21 -5
- package/dist/core/agents/rules/stop-hook.d.ts +3 -0
- package/dist/core/agents/rules/stop-hook.d.ts.map +1 -1
- package/dist/core/agents/rules/stop-hook.js +59 -23
- package/dist/core/agents/session-events.d.ts +8 -16
- package/dist/core/agents/session-events.d.ts.map +1 -1
- package/dist/core/agents/session-events.js +12 -26
- package/dist/core/agents/state/heartbeat-projector.d.ts +14 -0
- package/dist/core/agents/state/heartbeat-projector.d.ts.map +1 -1
- package/dist/core/agents/state/heartbeat-projector.js +98 -1
- package/dist/core/agents/state/heartbeat-writer.d.ts +47 -5
- package/dist/core/agents/state/heartbeat-writer.d.ts.map +1 -1
- package/dist/core/agents/state/heartbeat-writer.js +86 -17
- package/dist/core/agents/state/names.d.ts +22 -1
- package/dist/core/agents/state/names.d.ts.map +1 -1
- package/dist/core/agents/state/names.js +40 -2
- package/dist/core/agents/state/session-state.d.ts +44 -0
- package/dist/core/agents/state/session-state.d.ts.map +1 -0
- package/dist/core/agents/state/session-state.js +81 -0
- package/dist/core/config.d.ts +30 -2
- package/dist/core/config.d.ts.map +1 -1
- package/dist/core/config.js +74 -11
- package/dist/core/governor/planning.d.ts.map +1 -1
- package/dist/core/governor/planning.js +12 -13
- package/dist/core/hooks/adapter/detect.d.ts +2 -4
- package/dist/core/hooks/adapter/detect.d.ts.map +1 -1
- package/dist/core/hooks/adapter/detect.js +4 -14
- package/dist/core/hooks/adapter/events.d.ts.map +1 -1
- package/dist/core/hooks/adapter/events.js +1 -0
- package/dist/core/hooks/adapter/output.d.ts +1 -1
- package/dist/core/hooks/adapter/output.d.ts.map +1 -1
- package/dist/core/hooks/adapter/output.js +5 -4
- package/dist/core/hooks/adapter/parse.d.ts +1 -1
- package/dist/core/hooks/adapter/parse.d.ts.map +1 -1
- package/dist/core/hooks/adapter/parse.js +2 -0
- package/dist/core/hooks/adapter/wiring.d.ts +23 -0
- package/dist/core/hooks/adapter/wiring.d.ts.map +1 -1
- package/dist/core/hooks/adapter/wiring.js +32 -0
- package/dist/core/hooks/cli.d.ts +1 -2
- package/dist/core/hooks/cli.d.ts.map +1 -1
- package/dist/core/hooks/cli.js +258 -32
- package/dist/core/hooks/codex-authorization.d.ts +30 -0
- package/dist/core/hooks/codex-authorization.d.ts.map +1 -0
- package/dist/core/hooks/codex-authorization.js +199 -0
- package/dist/core/hooks/codex-wsl-bridge.d.ts +46 -0
- package/dist/core/hooks/codex-wsl-bridge.d.ts.map +1 -0
- package/dist/core/hooks/codex-wsl-bridge.js +137 -0
- package/dist/core/hooks/effects/index.d.ts +1 -1
- package/dist/core/hooks/effects/index.js +3 -3
- package/dist/core/hooks/events/schema.d.ts +57 -1
- package/dist/core/hooks/events/schema.d.ts.map +1 -1
- package/dist/core/hooks/resolve/owner.d.ts +7 -1
- package/dist/core/hooks/resolve/owner.d.ts.map +1 -1
- package/dist/core/hooks/resolve/owner.js +15 -0
- package/dist/core/hooks/resolve/transcript.d.ts +44 -0
- package/dist/core/hooks/resolve/transcript.d.ts.map +1 -1
- package/dist/core/hooks/resolve/transcript.js +176 -1
- package/dist/core/hooks/session-name-presence.d.ts +30 -0
- package/dist/core/hooks/session-name-presence.d.ts.map +1 -0
- package/dist/core/hooks/session-name-presence.js +43 -0
- package/dist/core/hooks/unsafe-cross-shell.d.ts +13 -0
- package/dist/core/hooks/unsafe-cross-shell.d.ts.map +1 -0
- package/dist/core/hooks/unsafe-cross-shell.js +151 -0
- package/dist/core/presence/blob.d.ts +4 -0
- package/dist/core/presence/blob.d.ts.map +1 -1
- package/dist/core/presence/blob.js +12 -0
- package/dist/core/presence/index.js +15 -1
- package/dist/core/work/state.d.ts +4 -5
- package/dist/core/work/state.d.ts.map +1 -1
- package/dist/core/work/state.js +5 -8
- package/dist/core/workflow/index.d.ts +1 -1
- package/dist/core/workflow/index.d.ts.map +1 -1
- package/dist/core/workflow/proof.d.ts +2 -2
- package/dist/core/workflow/proof.d.ts.map +1 -1
- package/dist/core/workflow/proof.js +2 -2
- package/dist/core/workflow/run-state.d.ts +2 -2
- package/dist/core/workflow/run-state.d.ts.map +1 -1
- package/dist/core/workflow/run-state.js +2 -2
- package/dist/core/workflow/types.d.ts +3 -3
- package/dist/core/workflow/types.d.ts.map +1 -1
- package/dist/core/workflow/workspaces/execution.d.ts +2 -2
- package/dist/core/workflow/workspaces/execution.d.ts.map +1 -1
- package/dist/core/workflow/workspaces/execution.js +5 -0
- package/dist/core/workflow/workspaces/index.d.ts +1 -1
- package/dist/core/workflow/workspaces/index.d.ts.map +1 -1
- package/dist/core/workflow/workspaces/local-git.d.ts.map +1 -1
- package/dist/core/workflow/workspaces/local-git.js +1 -2
- package/dist/core/workflow/workspaces/paths.d.ts +2 -2
- package/dist/core/workflow/workspaces/types.d.ts +0 -6
- package/dist/core/workflow/workspaces/types.d.ts.map +1 -1
- package/dist/core/workflow/workspaces/validate.d.ts +0 -2
- package/dist/core/workflow/workspaces/validate.d.ts.map +1 -1
- package/dist/core/workflow/workspaces/validate.js +0 -2
- package/dist/lib/browser/client.d.ts +118 -1
- package/dist/lib/browser/client.d.ts.map +1 -1
- package/dist/lib/browser/client.js +435 -6
- package/dist/lib/browser/geometry.d.ts.map +1 -1
- package/dist/lib/browser/geometry.js +180 -37
- package/dist/lib/browser/index.d.ts +5 -1
- package/dist/lib/browser/index.d.ts.map +1 -1
- package/dist/lib/browser/index.js +5 -1
- package/dist/lib/browser/netscape-cookies.d.ts +6 -0
- package/dist/lib/browser/netscape-cookies.d.ts.map +1 -0
- package/dist/lib/browser/netscape-cookies.js +31 -0
- package/dist/lib/browser/proxy.d.ts +24 -0
- package/dist/lib/browser/proxy.d.ts.map +1 -0
- package/dist/lib/browser/proxy.js +84 -0
- package/dist/lib/browser/runts.d.ts +6 -0
- package/dist/lib/browser/runts.d.ts.map +1 -1
- package/dist/lib/browser/runts.js +21 -4
- package/dist/lib/browser/session-control.d.ts +112 -0
- package/dist/lib/browser/session-control.d.ts.map +1 -0
- package/dist/lib/browser/session-control.js +670 -0
- package/dist/lib/claude-desktop.d.ts +47 -0
- package/dist/lib/claude-desktop.d.ts.map +1 -1
- package/dist/lib/claude-desktop.js +169 -1
- package/dist/lib/docs-links.d.ts +108 -0
- package/dist/lib/docs-links.d.ts.map +1 -0
- package/dist/lib/docs-links.js +555 -0
- package/dist/lib/exec.d.ts +1 -1
- package/dist/lib/exec.js +1 -1
- package/dist/lib/identities/assume.d.ts +4 -2
- package/dist/lib/identities/assume.d.ts.map +1 -1
- package/dist/lib/identities/assume.js +15 -2
- package/dist/lib/instructions/bundle.d.ts +33 -0
- package/dist/lib/instructions/bundle.d.ts.map +1 -0
- package/dist/lib/instructions/bundle.js +149 -0
- package/dist/lib/instructions/git-hooks.d.ts +75 -0
- package/dist/lib/instructions/git-hooks.d.ts.map +1 -0
- package/dist/lib/instructions/git-hooks.js +238 -0
- package/dist/lib/instructions/splice.d.ts +10 -4
- package/dist/lib/instructions/splice.d.ts.map +1 -1
- package/dist/lib/instructions/splice.js +19 -11
- package/package.json +2 -2
- package/schemas/config.schema.json +32 -0
- package/src/commander.ts +5 -1
- package/src/commands/agents.ts +341 -113
- package/src/commands/browse-session.ts +245 -0
- package/src/commands/browse.ts +287 -44
- package/src/commands/checkpoint.ts +1 -1
- package/src/commands/claude-desktop.ts +106 -5
- package/src/commands/deinit.ts +7 -1
- package/src/commands/docs.ts +56 -0
- package/src/commands/doctor.ts +155 -21
- package/src/commands/init.ts +28 -9
- package/src/commands/instructions.ts +129 -0
- package/src/commands/presence.ts +7 -1
- package/src/commands/tunnel.ts +2 -5
- package/src/core/agents/canonical-emit.ts +7 -6
- package/src/core/agents/cli.ts +143 -89
- package/src/core/agents/coord-client.ts +21 -5
- package/src/core/agents/finalization.ts +595 -0
- package/src/core/agents/git-hook.ts +126 -0
- package/src/core/agents/index.ts +1 -0
- package/src/core/agents/render/prompt-context.ts +96 -16
- package/src/core/agents/render/session-context.ts +29 -4
- package/src/core/agents/rules/claim-conflict.ts +3 -3
- package/src/core/agents/rules/commit-conflict.ts +35 -8
- package/src/core/agents/rules/stop-hook.ts +71 -23
- package/src/core/agents/session-events.ts +14 -41
- package/src/core/agents/state/heartbeat-projector.ts +108 -1
- package/src/core/agents/state/heartbeat-writer.ts +129 -16
- package/src/core/agents/state/names.ts +54 -2
- package/src/core/agents/state/session-state.ts +131 -0
- package/src/core/config.ts +92 -12
- package/src/core/governor/planning.ts +17 -12
- package/src/core/hooks/adapter/detect.ts +4 -13
- package/src/core/hooks/adapter/events.ts +1 -0
- package/src/core/hooks/adapter/output.ts +9 -4
- package/src/core/hooks/adapter/parse.ts +3 -0
- package/src/core/hooks/adapter/wiring.ts +43 -0
- package/src/core/hooks/cli.ts +296 -27
- package/src/core/hooks/codex-authorization.ts +253 -0
- package/src/core/hooks/codex-wsl-bridge.ts +188 -0
- package/src/core/hooks/effects/index.ts +5 -5
- package/src/core/hooks/events/schema.ts +71 -0
- package/src/core/hooks/resolve/owner.ts +22 -1
- package/src/core/hooks/resolve/transcript.ts +164 -1
- package/src/core/hooks/session-name-presence.ts +52 -0
- package/src/core/hooks/unsafe-cross-shell.ts +160 -0
- package/src/core/presence/blob.ts +18 -0
- package/src/core/presence/index.ts +18 -1
- package/src/core/work/state.ts +8 -12
- package/src/core/workflow/engine.ts +2 -2
- package/src/core/workflow/index.ts +0 -1
- package/src/core/workflow/proof.ts +4 -4
- package/src/core/workflow/run-state.ts +5 -5
- package/src/core/workflow/types.ts +3 -3
- package/src/core/workflow/workspaces/execution.ts +11 -4
- package/src/core/workflow/workspaces/index.ts +0 -3
- package/src/core/workflow/workspaces/local-git.ts +1 -2
- package/src/core/workflow/workspaces/paths.ts +2 -2
- package/src/core/workflow/workspaces/types.ts +0 -9
- package/src/core/workflow/workspaces/validate.ts +1 -5
- package/src/lib/browser/client.ts +528 -6
- package/src/lib/browser/geometry.ts +197 -37
- package/src/lib/browser/index.ts +39 -0
- package/src/lib/browser/netscape-cookies.ts +39 -0
- package/src/lib/browser/proxy.ts +105 -0
- package/src/lib/browser/runts.ts +27 -3
- package/src/lib/browser/session-control.ts +892 -0
- package/src/lib/claude-desktop.ts +232 -1
- package/src/lib/docs-links.ts +674 -0
- package/src/lib/exec.ts +1 -1
- package/src/lib/identities/assume.ts +22 -1
- package/src/lib/instructions/bundle.ts +199 -0
- package/src/lib/instructions/git-hooks.ts +259 -0
- package/src/lib/instructions/splice.ts +41 -11
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
import { existsSync, statSync } from "node:fs";
|
|
1
|
+
import { existsSync, readdirSync, readFileSync, statSync } from "node:fs";
|
|
2
|
+
import { basename, dirname, join } from "node:path";
|
|
2
3
|
|
|
3
4
|
/**
|
|
4
5
|
* Scan a CC-style JSONL transcript for the `┌─ agent-` status-box prefix in
|
|
@@ -20,6 +21,56 @@ export function scanStatusBoxPresent(transcriptPath: string | undefined): boolea
|
|
|
20
21
|
return text.includes("┌─ agent-");
|
|
21
22
|
}
|
|
22
23
|
|
|
24
|
+
/**
|
|
25
|
+
* Scan a CC-style JSONL transcript for a needle in ASSISTANT message text
|
|
26
|
+
* blocks only. Used by `turn.stop` to detect the suggested session name in the
|
|
27
|
+
* reply.
|
|
28
|
+
*
|
|
29
|
+
* The row filter is load-bearing: the transcript tail also carries the needle
|
|
30
|
+
* inside `tool_result` rows (the `agents set-task` JSON output) and hook
|
|
31
|
+
* context rows, so a raw `includes()` over the tail — the way the status-box
|
|
32
|
+
* scan works — would false-pass the moment set-task runs. Verified empirically
|
|
33
|
+
* 2026-08-09 against a live transcript.
|
|
34
|
+
*
|
|
35
|
+
* Same 256KB tail window as the other scans; the window's first line may be a
|
|
36
|
+
* truncated JSON row, which the per-line parse guard skips.
|
|
37
|
+
*/
|
|
38
|
+
export function scanAssistantTextIncludes(
|
|
39
|
+
transcriptPath: string | undefined,
|
|
40
|
+
needle: string,
|
|
41
|
+
): boolean {
|
|
42
|
+
if (!transcriptPath || !needle || !existsSync(transcriptPath)) return false;
|
|
43
|
+
const text = tailText(transcriptPath);
|
|
44
|
+
if (!text) return false;
|
|
45
|
+
for (const line of text.split("\n")) {
|
|
46
|
+
// Cheap gate before parsing: most lines don't contain the needle at all.
|
|
47
|
+
if (!line.includes(needle)) continue;
|
|
48
|
+
try {
|
|
49
|
+
const row = JSON.parse(line) as {
|
|
50
|
+
type?: unknown;
|
|
51
|
+
message?: { content?: unknown };
|
|
52
|
+
};
|
|
53
|
+
if (row.type !== "assistant") continue;
|
|
54
|
+
const content = row.message?.content;
|
|
55
|
+
if (typeof content === "string") {
|
|
56
|
+
if (content.includes(needle)) return true;
|
|
57
|
+
continue;
|
|
58
|
+
}
|
|
59
|
+
if (!Array.isArray(content)) continue;
|
|
60
|
+
for (const block of content) {
|
|
61
|
+
if (typeof block !== "object" || block === null) continue;
|
|
62
|
+
const b = block as { type?: unknown; text?: unknown };
|
|
63
|
+
if (b.type === "text" && typeof b.text === "string" && b.text.includes(needle)) {
|
|
64
|
+
return true;
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
} catch {
|
|
68
|
+
// Truncated/partial line (tail-window start or flush race); skip it.
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
return false;
|
|
72
|
+
}
|
|
73
|
+
|
|
23
74
|
/**
|
|
24
75
|
* Resolve the agent's model from a CC-style JSONL transcript by reading the
|
|
25
76
|
* most-recent assistant message's `message.model`. Claude Code's SessionStart
|
|
@@ -56,6 +107,118 @@ export function scanTranscriptModel(transcriptPath: string | undefined): string
|
|
|
56
107
|
return undefined;
|
|
57
108
|
}
|
|
58
109
|
|
|
110
|
+
const SESSION_FILE_RE = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}\.jsonl$/i;
|
|
111
|
+
/** Skip absurdly large transcripts rather than buffering them. */
|
|
112
|
+
const FORK_SCAN_MAX_BYTES = 128 * 1024 * 1024;
|
|
113
|
+
/** How many message uuids to sample across the fork's copied prefix. */
|
|
114
|
+
const FORK_SAMPLE_SIZE = 24;
|
|
115
|
+
|
|
116
|
+
/**
|
|
117
|
+
* Detect the parent session of a forked CC conversation from its transcript.
|
|
118
|
+
*
|
|
119
|
+
* Empirical basis (verified 2026-08-05 against `claude --resume <id>
|
|
120
|
+
* --fork-session`): the fork copies the parent's rows into a new
|
|
121
|
+
* `<new-session-id>.jsonl`, rewriting every row's `sessionId` to the new id
|
|
122
|
+
* but PRESERVING each copied message row's `uuid`. So a sibling transcript in
|
|
123
|
+
* the same project directory that contains the fork's message uuids is an
|
|
124
|
+
* ancestor of the fork.
|
|
125
|
+
*
|
|
126
|
+
* Ancestor vs. parent: a grandparent (when the parent was itself a fork)
|
|
127
|
+
* shares the fork's early uuids too, but only the true parent contains the
|
|
128
|
+
* whole copied prefix. At detection time the fork's transcript is almost
|
|
129
|
+
* entirely copied prefix, so uuids sampled across the whole file separate the
|
|
130
|
+
* two: score candidates by containment, highest wins.
|
|
131
|
+
*
|
|
132
|
+
* Sibling forks force a second rule: a PRIOR fork of the same parent also
|
|
133
|
+
* contains the whole copied prefix, so it ties the true parent on score. The
|
|
134
|
+
* sibling additionally carries its own post-fork turns, which the parent
|
|
135
|
+
* lacks, so among tied scores the MINIMAL container (fewest message rows)
|
|
136
|
+
* wins. A wrong pick under this rule stays within the ancestry (grandparent
|
|
137
|
+
* at worst); a recency tie-break can name a non-ancestor sibling (observed
|
|
138
|
+
* 2026-08-05 with two probes forked off one parent).
|
|
139
|
+
*
|
|
140
|
+
* Fail-soft by design: any read error, an unflushed transcript, or no
|
|
141
|
+
* qualifying sibling returns undefined, which downgrades the caller to the
|
|
142
|
+
* no-lineage behavior. Never throws.
|
|
143
|
+
*/
|
|
144
|
+
export function detectForkParent(
|
|
145
|
+
transcriptPath: string | undefined,
|
|
146
|
+
sessionId: string,
|
|
147
|
+
): string | undefined {
|
|
148
|
+
try {
|
|
149
|
+
if (!transcriptPath || !sessionId || !existsSync(transcriptPath)) return undefined;
|
|
150
|
+
if (statSync(transcriptPath).size > FORK_SCAN_MAX_BYTES) return undefined;
|
|
151
|
+
const own = readFileSync(transcriptPath, "utf8");
|
|
152
|
+
const uuids = messageUuids(own);
|
|
153
|
+
if (uuids.length === 0) return undefined;
|
|
154
|
+
const sample = sampleEvenly(uuids, FORK_SAMPLE_SIZE);
|
|
155
|
+
|
|
156
|
+
const dir = dirname(transcriptPath);
|
|
157
|
+
const ownFile = basename(transcriptPath);
|
|
158
|
+
let best: { id: string; score: number; rows: number; mtimeMs: number } | null = null;
|
|
159
|
+
for (const file of readdirSync(dir)) {
|
|
160
|
+
if (file === ownFile || !SESSION_FILE_RE.test(file)) continue;
|
|
161
|
+
const candidateId = file.slice(0, -".jsonl".length);
|
|
162
|
+
if (candidateId === sessionId) continue;
|
|
163
|
+
const path = join(dir, file);
|
|
164
|
+
let stat: ReturnType<typeof statSync>;
|
|
165
|
+
try {
|
|
166
|
+
stat = statSync(path);
|
|
167
|
+
} catch {
|
|
168
|
+
continue;
|
|
169
|
+
}
|
|
170
|
+
if (stat.size > FORK_SCAN_MAX_BYTES) continue;
|
|
171
|
+
let text: string;
|
|
172
|
+
try {
|
|
173
|
+
text = readFileSync(path, "utf8");
|
|
174
|
+
} catch {
|
|
175
|
+
continue;
|
|
176
|
+
}
|
|
177
|
+
// Cheap gate before scoring: an ancestor must contain the fork's first
|
|
178
|
+
// copied message uuid (the copy always starts at the beginning).
|
|
179
|
+
if (!text.includes(sample[0]!)) continue;
|
|
180
|
+
let score = 0;
|
|
181
|
+
for (const u of sample) if (text.includes(u)) score++;
|
|
182
|
+
if (score < Math.max(1, Math.ceil(sample.length / 2))) continue;
|
|
183
|
+
const rows = messageUuids(text).length;
|
|
184
|
+
const wins =
|
|
185
|
+
!best ||
|
|
186
|
+
score > best.score ||
|
|
187
|
+
(score === best.score &&
|
|
188
|
+
(rows < best.rows || (rows === best.rows && stat.mtimeMs < best.mtimeMs)));
|
|
189
|
+
if (wins) best = { id: candidateId, score, rows, mtimeMs: stat.mtimeMs };
|
|
190
|
+
}
|
|
191
|
+
return best?.id;
|
|
192
|
+
} catch {
|
|
193
|
+
return undefined;
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
/** Message-row uuids (user/assistant rows only), in file order. */
|
|
198
|
+
function messageUuids(text: string): string[] {
|
|
199
|
+
const out: string[] = [];
|
|
200
|
+
for (const line of text.split("\n")) {
|
|
201
|
+
if (!line.includes('"uuid"')) continue;
|
|
202
|
+
try {
|
|
203
|
+
const row = JSON.parse(line) as { type?: unknown; uuid?: unknown };
|
|
204
|
+
if (row.type !== "user" && row.type !== "assistant") continue;
|
|
205
|
+
if (typeof row.uuid === "string" && row.uuid.length > 0) out.push(row.uuid);
|
|
206
|
+
} catch {
|
|
207
|
+
// Truncated/partial line (flush race); skip it.
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
return out;
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
/** Up to <n> items spread evenly across the list, always including the first. */
|
|
214
|
+
function sampleEvenly(items: string[], n: number): string[] {
|
|
215
|
+
if (items.length <= n) return items;
|
|
216
|
+
const out: string[] = [];
|
|
217
|
+
const step = items.length / n;
|
|
218
|
+
for (let i = 0; i < n; i++) out.push(items[Math.floor(i * step)]!);
|
|
219
|
+
return out;
|
|
220
|
+
}
|
|
221
|
+
|
|
59
222
|
/** Tail-read up to 256KB from the end of a file as UTF-8, or undefined on error. */
|
|
60
223
|
function tailText(path: string): string | undefined {
|
|
61
224
|
try {
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Session-name presence for `turn.stop`.
|
|
3
|
+
*
|
|
4
|
+
* While the heartbeat carries a `suggested_session_name`, report whether the
|
|
5
|
+
* naming ritual is satisfied: either this turn's reply shows the name, or an
|
|
6
|
+
* earlier reply already did. Scanning covers assistant text blocks only (the
|
|
7
|
+
* raw transcript tail also carries the name inside the set-task tool_result,
|
|
8
|
+
* which must not count) plus the adapter-supplied last assistant message, for
|
|
9
|
+
* stops that arrive without a transcript.
|
|
10
|
+
*
|
|
11
|
+
* Two properties the stop-hook naming rule depends on:
|
|
12
|
+
*
|
|
13
|
+
* 1. Once a name is satisfied, every later stop keeps reporting `true` rather
|
|
14
|
+
* than omitting the field. The rule wants an in-window `turn.stop` carrying
|
|
15
|
+
* `session_name_present: true`; omitting it after the heartbeat stamp meant
|
|
16
|
+
* nothing could ever re-emit the flag, so every subsequent reply blocked --
|
|
17
|
+
* including the replies reproducing the exact name the rule asked for.
|
|
18
|
+
* 2. The result names WHICH suggested name it covered, so a projector rebuild
|
|
19
|
+
* attributes the sighting to that name instead of to whichever name is
|
|
20
|
+
* current during the replay. Without it, a re-minted name inherited an older
|
|
21
|
+
* name's sighting and looked satisfied without ever having been shown.
|
|
22
|
+
*
|
|
23
|
+
* Never throws: coordination telemetry must not take down a hook.
|
|
24
|
+
*/
|
|
25
|
+
|
|
26
|
+
import { readHeartbeat, stampSessionNameSeen } from "../agents/state/heartbeat-writer.ts";
|
|
27
|
+
|
|
28
|
+
export interface SessionNamePresence {
|
|
29
|
+
session_name_present?: boolean;
|
|
30
|
+
session_name_present_for?: string;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export function sessionNamePresence(
|
|
34
|
+
coordRoot: string,
|
|
35
|
+
instanceId: string,
|
|
36
|
+
lastAssistantMessage: string,
|
|
37
|
+
scanAssistantText: (name: string) => boolean,
|
|
38
|
+
): SessionNamePresence {
|
|
39
|
+
try {
|
|
40
|
+
const hb = readHeartbeat(coordRoot, instanceId);
|
|
41
|
+
const name = hb?.suggested_session_name;
|
|
42
|
+
if (!name) return {};
|
|
43
|
+
if (hb?.session_name_seen_for === name) {
|
|
44
|
+
return { session_name_present: true, session_name_present_for: name };
|
|
45
|
+
}
|
|
46
|
+
const present = scanAssistantText(name) || lastAssistantMessage.includes(name);
|
|
47
|
+
if (present) stampSessionNameSeen(coordRoot, instanceId, name);
|
|
48
|
+
return { session_name_present: present, session_name_present_for: name };
|
|
49
|
+
} catch {
|
|
50
|
+
return {};
|
|
51
|
+
}
|
|
52
|
+
}
|
|
@@ -0,0 +1,160 @@
|
|
|
1
|
+
import { isWslUncPath } from "./codex-wsl-bridge.ts";
|
|
2
|
+
import type { Adapter } from "./events/schema.ts";
|
|
3
|
+
|
|
4
|
+
export interface CrossShellInspection {
|
|
5
|
+
adapter: Adapter;
|
|
6
|
+
cwd: unknown;
|
|
7
|
+
toolName: unknown;
|
|
8
|
+
toolInput: unknown;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
const DENY_REASON =
|
|
12
|
+
"Unsafe Windows-to-WSL shell-string construction blocked. Use the host's argv-preserving " +
|
|
13
|
+
"WSL bridge for a single executable, or its literal-script mode for compound Bash. Do not " +
|
|
14
|
+
"invoke wsl.exe with bash -c or bash -lc.";
|
|
15
|
+
|
|
16
|
+
function shellCommand(toolName: unknown, toolInput: unknown): string | null {
|
|
17
|
+
if (typeof toolName !== "string") return null;
|
|
18
|
+
const normalized = toolName.toLowerCase();
|
|
19
|
+
if (
|
|
20
|
+
normalized !== "bash" &&
|
|
21
|
+
normalized !== "shell" &&
|
|
22
|
+
normalized !== "shell_command" &&
|
|
23
|
+
normalized !== "functions.shell_command"
|
|
24
|
+
) {
|
|
25
|
+
return null;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
let input = toolInput;
|
|
29
|
+
if (typeof input === "string") {
|
|
30
|
+
try {
|
|
31
|
+
input = JSON.parse(input) as unknown;
|
|
32
|
+
} catch {
|
|
33
|
+
return null;
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
if (!input || typeof input !== "object") return null;
|
|
37
|
+
const command = (input as Record<string, unknown>).command;
|
|
38
|
+
return typeof command === "string" ? command : null;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function quotedToken(command: string, start: number): { token: string; end: number } {
|
|
42
|
+
const quote = command[start]!;
|
|
43
|
+
let token = "";
|
|
44
|
+
for (let index = start + 1; index < command.length; index += 1) {
|
|
45
|
+
const char = command[index]!;
|
|
46
|
+
if (char === "`" && quote === '"' && index + 1 < command.length) {
|
|
47
|
+
token += command[index + 1]!;
|
|
48
|
+
index += 1;
|
|
49
|
+
continue;
|
|
50
|
+
}
|
|
51
|
+
if (char === quote) {
|
|
52
|
+
if (quote === "'" && command[index + 1] === "'") {
|
|
53
|
+
token += "'";
|
|
54
|
+
index += 1;
|
|
55
|
+
continue;
|
|
56
|
+
}
|
|
57
|
+
return { token, end: index + 1 };
|
|
58
|
+
}
|
|
59
|
+
token += char;
|
|
60
|
+
}
|
|
61
|
+
return { token, end: command.length };
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/** Find direct PowerShell command invocations of wsl/wsl.exe outside quoted strings. */
|
|
65
|
+
function wslInvocationOffsets(command: string): number[] {
|
|
66
|
+
const offsets: number[] = [];
|
|
67
|
+
let expectCommand = true;
|
|
68
|
+
let callOperator = false;
|
|
69
|
+
|
|
70
|
+
for (let index = 0; index < command.length; ) {
|
|
71
|
+
const char = command[index]!;
|
|
72
|
+
if (/\s/.test(char)) {
|
|
73
|
+
if (char === "\n" || char === "\r") {
|
|
74
|
+
expectCommand = true;
|
|
75
|
+
callOperator = false;
|
|
76
|
+
}
|
|
77
|
+
index += 1;
|
|
78
|
+
continue;
|
|
79
|
+
}
|
|
80
|
+
if (char === "#" && expectCommand) {
|
|
81
|
+
const newline = command.indexOf("\n", index + 1);
|
|
82
|
+
index = newline === -1 ? command.length : newline + 1;
|
|
83
|
+
expectCommand = true;
|
|
84
|
+
callOperator = false;
|
|
85
|
+
continue;
|
|
86
|
+
}
|
|
87
|
+
if (";|{}=()".includes(char)) {
|
|
88
|
+
expectCommand = true;
|
|
89
|
+
callOperator = false;
|
|
90
|
+
index += 1;
|
|
91
|
+
continue;
|
|
92
|
+
}
|
|
93
|
+
if (char === "&") {
|
|
94
|
+
expectCommand = true;
|
|
95
|
+
callOperator = true;
|
|
96
|
+
index += command[index + 1] === "&" ? 2 : 1;
|
|
97
|
+
continue;
|
|
98
|
+
}
|
|
99
|
+
if (char === "'" || char === '"') {
|
|
100
|
+
const parsed = quotedToken(command, index);
|
|
101
|
+
if (expectCommand && callOperator && /^wsl(?:\.exe)?$/i.test(parsed.token)) {
|
|
102
|
+
offsets.push(index);
|
|
103
|
+
}
|
|
104
|
+
expectCommand = false;
|
|
105
|
+
callOperator = false;
|
|
106
|
+
index = parsed.end;
|
|
107
|
+
continue;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
const start = index;
|
|
111
|
+
while (index < command.length && !/[\s;|{}=()&]/.test(command[index]!)) index += 1;
|
|
112
|
+
const token = command.slice(start, index);
|
|
113
|
+
if (expectCommand && /^wsl(?:\.exe)?$/i.test(token)) offsets.push(start);
|
|
114
|
+
expectCommand = false;
|
|
115
|
+
callOperator = false;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
return offsets;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
function statementEnd(command: string, start: number): number {
|
|
122
|
+
let quote: string | null = null;
|
|
123
|
+
for (let index = start; index < command.length; index += 1) {
|
|
124
|
+
const char = command[index]!;
|
|
125
|
+
if (quote) {
|
|
126
|
+
if (char === "`" && quote === '"') {
|
|
127
|
+
index += 1;
|
|
128
|
+
} else if (char === quote) {
|
|
129
|
+
if (quote === "'" && command[index + 1] === "'") index += 1;
|
|
130
|
+
else quote = null;
|
|
131
|
+
}
|
|
132
|
+
continue;
|
|
133
|
+
}
|
|
134
|
+
if (char === "'" || char === '"') quote = char;
|
|
135
|
+
else if (char === ";" || char === "\n" || char === "\r" || char === "|") return index;
|
|
136
|
+
else if (char === "&" && command[index + 1] === "&") return index;
|
|
137
|
+
}
|
|
138
|
+
return command.length;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
function invokesBashCommandString(command: string): boolean {
|
|
142
|
+
for (const offset of wslInvocationOffsets(command)) {
|
|
143
|
+
const segment = command.slice(offset, statementEnd(command, offset));
|
|
144
|
+
if (/(?:^|\s)(?:\/bin\/)?bash(?=\s|$)[\s\S]*?\s-[a-z]*c[a-z]*(?=\s|$)/i.test(segment)) {
|
|
145
|
+
return true;
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
return false;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
/**
|
|
152
|
+
* Deny only the unsafe hybrid shape: Windows-native Codex, WSL UNC workspace,
|
|
153
|
+
* shell tool, and a direct wsl.exe -> bash -c/-lc command-string invocation.
|
|
154
|
+
*/
|
|
155
|
+
export function unsafeCrossShellReason(input: CrossShellInspection): string | null {
|
|
156
|
+
if (input.adapter !== "codex" || !isWslUncPath(input.cwd)) return null;
|
|
157
|
+
const command = shellCommand(input.toolName, input.toolInput);
|
|
158
|
+
if (!command || !invokesBashCommandString(command)) return null;
|
|
159
|
+
return DENY_REASON;
|
|
160
|
+
}
|
|
@@ -9,6 +9,7 @@ import { createHash } from "node:crypto";
|
|
|
9
9
|
import { existsSync, readdirSync, readFileSync } from "node:fs";
|
|
10
10
|
import { join } from "node:path";
|
|
11
11
|
import { resolveMachineLabel } from "../../lib/machine.ts";
|
|
12
|
+
import type { AgentActivity, TaskState } from "../agents/state/session-state.ts";
|
|
12
13
|
|
|
13
14
|
/** Mirror of the local heartbeat freshness window (commands/agents.ts). */
|
|
14
15
|
const FRESHNESS_SECS = 600;
|
|
@@ -24,6 +25,9 @@ export interface PresenceAgent {
|
|
|
24
25
|
session_id?: string;
|
|
25
26
|
platform?: string;
|
|
26
27
|
task?: string;
|
|
28
|
+
activity: AgentActivity;
|
|
29
|
+
task_state: TaskState;
|
|
30
|
+
task_state_reason?: string;
|
|
27
31
|
turn_summary?: string;
|
|
28
32
|
files_touched?: string[];
|
|
29
33
|
last_tool?: string;
|
|
@@ -78,6 +82,9 @@ export function buildPresenceBlob(coordRoot: string, now: Date = new Date()): Bu
|
|
|
78
82
|
session_id: strOr(hb.session_id),
|
|
79
83
|
platform: strOr(hb.platform),
|
|
80
84
|
task: strOr(hb.task),
|
|
85
|
+
activity: activityOrUnknown(hb.activity),
|
|
86
|
+
task_state: taskStateOrActive(hb.task_state),
|
|
87
|
+
task_state_reason: clamp(strOr(hb.task_state_reason), 160),
|
|
81
88
|
turn_summary: clamp(strOr(hb.turn_summary), 160),
|
|
82
89
|
files_touched: Array.isArray(hb.files_touched)
|
|
83
90
|
? (hb.files_touched as string[]).slice(0, MAX_FILES_PER_AGENT)
|
|
@@ -107,6 +114,9 @@ export function buildPresenceBlob(coordRoot: string, now: Date = new Date()): Bu
|
|
|
107
114
|
n: a.name ?? null,
|
|
108
115
|
k: a.kind ?? null,
|
|
109
116
|
t: a.task ?? null,
|
|
117
|
+
a: a.activity,
|
|
118
|
+
l: a.task_state,
|
|
119
|
+
r: a.task_state_reason ?? null,
|
|
110
120
|
s: a.turn_summary ?? null,
|
|
111
121
|
f: [...(a.files_touched ?? [])].sort(),
|
|
112
122
|
}));
|
|
@@ -119,6 +129,14 @@ function strOr(v: unknown): string | undefined {
|
|
|
119
129
|
return typeof v === "string" && v.length > 0 ? v : undefined;
|
|
120
130
|
}
|
|
121
131
|
|
|
132
|
+
function activityOrUnknown(v: unknown): AgentActivity {
|
|
133
|
+
return v === "working" || v === "needs_input" || v === "idle" ? v : "unknown";
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
function taskStateOrActive(v: unknown): TaskState {
|
|
137
|
+
return v === "blocked" || v === "done" ? v : "active";
|
|
138
|
+
}
|
|
139
|
+
|
|
122
140
|
function clamp(v: string | undefined, max: number): string | undefined {
|
|
123
141
|
if (!v) return undefined;
|
|
124
142
|
return v.length > max ? v.slice(0, max) : v;
|
|
@@ -263,7 +263,7 @@ export function readRemoteMachines(
|
|
|
263
263
|
machine,
|
|
264
264
|
published_at: blob.published_at,
|
|
265
265
|
age_secs: ageSecs,
|
|
266
|
-
agents: blob.agents,
|
|
266
|
+
agents: blob.agents.map(normalizePresenceAgent),
|
|
267
267
|
});
|
|
268
268
|
}
|
|
269
269
|
out.sort((a, b) => a.machine.localeCompare(b.machine));
|
|
@@ -272,3 +272,20 @@ export function readRemoteMachines(
|
|
|
272
272
|
return [];
|
|
273
273
|
}
|
|
274
274
|
}
|
|
275
|
+
|
|
276
|
+
function normalizePresenceAgent(agent: PresenceAgent): PresenceAgent {
|
|
277
|
+
const legacy = agent as Partial<PresenceAgent>;
|
|
278
|
+
return {
|
|
279
|
+
...agent,
|
|
280
|
+
activity:
|
|
281
|
+
legacy.activity === "working" ||
|
|
282
|
+
legacy.activity === "needs_input" ||
|
|
283
|
+
legacy.activity === "idle"
|
|
284
|
+
? legacy.activity
|
|
285
|
+
: "unknown",
|
|
286
|
+
task_state:
|
|
287
|
+
legacy.task_state === "blocked" || legacy.task_state === "done"
|
|
288
|
+
? legacy.task_state
|
|
289
|
+
: "active",
|
|
290
|
+
};
|
|
291
|
+
}
|
package/src/core/work/state.ts
CHANGED
|
@@ -21,7 +21,7 @@ import { readWorkflowRunManifest, workflowScriptDigest } from "../workflow/run-s
|
|
|
21
21
|
import { WORKFLOW_TRANSCRIPT_EVENT_BYTES, workflowTranscriptPath } from "../workflow/transcript.ts";
|
|
22
22
|
import { isWorkspaceBoundExecutionEvidence } from "../workflow/workspaces/validate.ts";
|
|
23
23
|
|
|
24
|
-
export const WORK_INTENT_SCHEMA_VERSION =
|
|
24
|
+
export const WORK_INTENT_SCHEMA_VERSION = 2 as const;
|
|
25
25
|
export const WORK_EVENT_SCHEMA_VERSION = 1 as const;
|
|
26
26
|
|
|
27
27
|
const WORK_ID = /^[A-Za-z0-9][A-Za-z0-9._-]{0,99}$/;
|
|
@@ -77,12 +77,11 @@ export interface WorkIntent {
|
|
|
77
77
|
dependencies: string[];
|
|
78
78
|
workflow: { path: string; sha256: string };
|
|
79
79
|
max_attempts: number;
|
|
80
|
-
/** Ceiling on
|
|
80
|
+
/** Ceiling on consecutive uncharged attempts (ADR 0046), separate from
|
|
81
81
|
* max_attempts. An upstream outage produces uncharged attempt after uncharged
|
|
82
82
|
* attempt; without a bound it would retry forever. At the bound the item stops
|
|
83
|
-
* and reports it is blocked on an outside service.
|
|
84
|
-
|
|
85
|
-
max_uncharged_attempts?: number;
|
|
83
|
+
* and reports it is blocked on an outside service. */
|
|
84
|
+
max_uncharged_attempts: number;
|
|
86
85
|
source?: { kind: "human" | "workflow" | "external"; ref?: string };
|
|
87
86
|
created_at: string;
|
|
88
87
|
}
|
|
@@ -551,7 +550,7 @@ function deriveWorkProjection(
|
|
|
551
550
|
// Count trailing consecutive uncharged attempts in the current window. The
|
|
552
551
|
// bound is the only brake on an outage that never ends, so at the limit the
|
|
553
552
|
// item stops and names the outside service — distinct from work-blocked.
|
|
554
|
-
const maxUncharged = intent.max_uncharged_attempts
|
|
553
|
+
const maxUncharged = intent.max_uncharged_attempts;
|
|
555
554
|
let trailingUncharged = 0;
|
|
556
555
|
for (let index = currentAttempts.length - 1; index >= 0; index--) {
|
|
557
556
|
if (currentAttempts[index]!.uncharged === undefined) break;
|
|
@@ -1041,12 +1040,9 @@ function validateWorkIntent(intent: WorkIntent, workId: string): void {
|
|
|
1041
1040
|
!Number.isSafeInteger(intent.max_attempts) ||
|
|
1042
1041
|
intent.max_attempts < 1 ||
|
|
1043
1042
|
intent.max_attempts > 100 ||
|
|
1044
|
-
|
|
1045
|
-
|
|
1046
|
-
|
|
1047
|
-
(!Number.isSafeInteger(intent.max_uncharged_attempts) ||
|
|
1048
|
-
intent.max_uncharged_attempts < 1 ||
|
|
1049
|
-
intent.max_uncharged_attempts > 100))
|
|
1043
|
+
!Number.isSafeInteger(intent.max_uncharged_attempts) ||
|
|
1044
|
+
intent.max_uncharged_attempts < 1 ||
|
|
1045
|
+
intent.max_uncharged_attempts > 100
|
|
1050
1046
|
) {
|
|
1051
1047
|
throw new Error(`work intent ${workId} has an unsupported or mismatched schema`);
|
|
1052
1048
|
}
|
|
@@ -87,7 +87,7 @@ import {
|
|
|
87
87
|
import type {
|
|
88
88
|
WorkspaceAttestation,
|
|
89
89
|
WorkspaceBinding,
|
|
90
|
-
|
|
90
|
+
WorkspaceCompatibilityExecutionEvidence,
|
|
91
91
|
} from "./workspaces/types.ts";
|
|
92
92
|
import { isWorkspaceAttestation } from "./workspaces/validate.ts";
|
|
93
93
|
|
|
@@ -324,7 +324,7 @@ async function executeWorkflow(
|
|
|
324
324
|
const fallbackMeta = normalizeWorkflowMeta(undefined, fallbackName);
|
|
325
325
|
const workspaceProvider = isolation === "shared" ? undefined : opts.workspace?.provider;
|
|
326
326
|
let workspaceBinding: WorkspaceBinding | undefined;
|
|
327
|
-
let workspaceFallback:
|
|
327
|
+
let workspaceFallback: WorkspaceCompatibilityExecutionEvidence | undefined =
|
|
328
328
|
resumeState?.manifest.execution.workspace_fallback;
|
|
329
329
|
try {
|
|
330
330
|
workspaceBinding = await resolveWorkspaceBinding({
|
|
@@ -40,13 +40,13 @@ import { isCanonicalWorkflowWorkContext } from "./work-context.ts";
|
|
|
40
40
|
import type {
|
|
41
41
|
WorkspaceAttestation,
|
|
42
42
|
WorkspaceBinding,
|
|
43
|
+
WorkspaceCompatibilityExecutionEvidence,
|
|
43
44
|
WorkspaceExecutionEvidence,
|
|
44
|
-
WorkspaceUnsupportedExecutionEvidence,
|
|
45
45
|
} from "./workspaces/index.ts";
|
|
46
46
|
import {
|
|
47
47
|
isWorkspaceBoundExecutionEvidence,
|
|
48
|
+
isWorkspaceCompatibilityExecutionEvidence,
|
|
48
49
|
isWorkspaceExecutionEvidence,
|
|
49
|
-
isWorkspaceUnsupportedExecutionEvidence,
|
|
50
50
|
workspaceProofLifecycle,
|
|
51
51
|
} from "./workspaces/validate.ts";
|
|
52
52
|
|
|
@@ -101,7 +101,7 @@ export interface BuildWorkflowProofInput {
|
|
|
101
101
|
blocked?: { reason: string; decisionId?: string };
|
|
102
102
|
workspaceBinding?: WorkspaceBinding;
|
|
103
103
|
workspaceAttestation?: WorkspaceAttestation;
|
|
104
|
-
workspaceFallback?:
|
|
104
|
+
workspaceFallback?: WorkspaceCompatibilityExecutionEvidence;
|
|
105
105
|
result?: unknown;
|
|
106
106
|
error?: string;
|
|
107
107
|
}
|
|
@@ -401,7 +401,7 @@ export function readWorkflowProof(coordRoot: string, runId: string): WorkflowPro
|
|
|
401
401
|
? isWorkspaceBoundExecutionEvidence(proofExecution, runId) &&
|
|
402
402
|
stableDigest(proofExecution.binding) === stableDigest(manifestBinding)
|
|
403
403
|
: manifestFallback !== undefined
|
|
404
|
-
?
|
|
404
|
+
? isWorkspaceCompatibilityExecutionEvidence(proofExecution, runId) &&
|
|
405
405
|
stableDigest(proofExecution) === stableDigest(manifestFallback)
|
|
406
406
|
: proofExecution === undefined;
|
|
407
407
|
if (!executionMatches) {
|
|
@@ -33,12 +33,12 @@ import { isCanonicalWorkflowWorkContext } from "./work-context.ts";
|
|
|
33
33
|
import type {
|
|
34
34
|
WorkspaceAllocationRequest,
|
|
35
35
|
WorkspaceBinding,
|
|
36
|
-
|
|
36
|
+
WorkspaceCompatibilityExecutionEvidence,
|
|
37
37
|
} from "./workspaces/index.ts";
|
|
38
38
|
import {
|
|
39
39
|
assertWorkspaceManifestAuthority,
|
|
40
40
|
isWorkspaceBinding,
|
|
41
|
-
|
|
41
|
+
isWorkspaceCompatibilityExecutionEvidence,
|
|
42
42
|
} from "./workspaces/validate.ts";
|
|
43
43
|
|
|
44
44
|
export const WORKFLOW_RUN_MANIFEST_SCHEMA_VERSION = 1 as const;
|
|
@@ -70,7 +70,7 @@ export interface WorkflowRunManifest {
|
|
|
70
70
|
policy?: NormalizedPolicy;
|
|
71
71
|
specialists?: Record<string, WorkflowSpecialistProfile>;
|
|
72
72
|
workspace_binding?: WorkspaceBinding;
|
|
73
|
-
workspace_fallback?:
|
|
73
|
+
workspace_fallback?: WorkspaceCompatibilityExecutionEvidence;
|
|
74
74
|
};
|
|
75
75
|
}
|
|
76
76
|
|
|
@@ -307,9 +307,9 @@ function validWorkspaceBinding(binding: WorkspaceBinding | undefined): boolean {
|
|
|
307
307
|
}
|
|
308
308
|
|
|
309
309
|
function validWorkspaceFallback(
|
|
310
|
-
fallback:
|
|
310
|
+
fallback: WorkspaceCompatibilityExecutionEvidence | undefined,
|
|
311
311
|
): boolean {
|
|
312
|
-
return fallback === undefined ||
|
|
312
|
+
return fallback === undefined || isWorkspaceCompatibilityExecutionEvidence(fallback);
|
|
313
313
|
}
|
|
314
314
|
|
|
315
315
|
function validSpecialists(
|
|
@@ -425,11 +425,11 @@ export type Spawner = (req: SpawnRequest) => Promise<SpawnResult>;
|
|
|
425
425
|
/** The API surface injected into a workflow script's default export. Explicit
|
|
426
426
|
* injection (no ambient globals): keeps scripts portable and unit-testable. */
|
|
427
427
|
export interface WorkflowContext {
|
|
428
|
-
/** Frozen durable-work assignment for work-linked runs. Standalone
|
|
429
|
-
*
|
|
428
|
+
/** Frozen durable-work assignment for work-linked runs. Standalone workflows
|
|
429
|
+
* have no work context. */
|
|
430
430
|
work?: Readonly<WorkflowWorkContext>;
|
|
431
431
|
/** Frozen attempt identity and, on retry, a bounded synopsis of prior
|
|
432
|
-
* terminal evidence. Absent for standalone
|
|
432
|
+
* terminal evidence. Absent for standalone workflows. */
|
|
433
433
|
attempt?: Readonly<WorkflowAttemptContext>;
|
|
434
434
|
/** Spawn one subagent; resolves to validated JSON (schema) or reply text. */
|
|
435
435
|
agent: (prompt: string, opts?: AgentOpts) => Promise<unknown>;
|