claude-code-session-manager 0.62.0 → 0.62.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/assets/{TiptapBody-Bn-LMce4.js → TiptapBody-BwN4pNC0.js} +1 -1
- package/dist/assets/{index-DOXvph-Z.css → index-CVCCMw5o.css} +1 -1
- package/dist/assets/{index-BfiMRkln.js → index-ZjTDk5b7.js} +1083 -1111
- package/dist/index.html +2 -2
- package/package.json +1 -1
- package/src/main/__tests__/runVerify-transcript-commit-evidence.test.cjs +215 -0
- package/src/main/__tests__/scheduler-investigation-prompt.test.cjs +42 -1
- package/src/main/__tests__/scheduler-prd-missing-skip.test.cjs +54 -1
- package/src/main/__tests__/transcripts-doFlush-array.test.cjs +6 -0
- package/src/main/__tests__/transcripts-paged-reads.test.cjs +6 -0
- package/src/main/__tests__/transcriptsUsageFor.test.cjs +6 -0
- package/src/main/config.cjs +1 -22
- package/src/main/files.cjs +20 -0
- package/src/main/index.cjs +1 -63
- package/src/main/ipcSchemas.cjs +11 -209
- package/src/main/lib/opsOwnership.cjs +0 -2
- package/src/main/lib/summarize.cjs +0 -1
- package/src/main/runVerify.cjs +55 -6
- package/src/main/scheduler.cjs +46 -7
- package/src/preload/api.d.ts +2 -197
- package/src/preload/index.cjs +1 -100
- package/src/main/__tests__/browserAgentServer.test.cjs +0 -235
- package/src/main/__tests__/browserView-destroyed-handler.test.cjs +0 -85
- package/src/main/__tests__/browserView-oauth-popup.test.cjs +0 -126
- package/src/main/__tests__/web-remote-e2e-pinning.test.cjs +0 -181
- package/src/main/browserAgentServer.cjs +0 -224
- package/src/main/browserCapture.cjs +0 -728
- package/src/main/browserView.cjs +0 -916
- package/src/main/lib/__tests__/browserAgentActions.test.cjs +0 -76
- package/src/main/lib/browserAgentActions.cjs +0 -114
- package/src/main/lib/e2eStateMachine.cjs +0 -39
- package/src/main/webRemote.cjs +0 -1367
- package/src/preload/browserViewPreload.cjs +0 -118
package/src/main/ipcSchemas.cjs
CHANGED
|
@@ -25,17 +25,6 @@ const ptyTabId = z.object({ tabId: z.string().min(1).max(128) });
|
|
|
25
25
|
// have a live PTY? Bounded so the renderer can't ask about unbounded lists.
|
|
26
26
|
const ptyAlive = z.object({ tabIds: z.array(z.string().min(1).max(128)).max(500) });
|
|
27
27
|
|
|
28
|
-
// v2 mobile: subscribe to a session's live state + summary. cwd is needed to
|
|
29
|
-
// locate the transcript JSONL (transcriptPath); validated against home-dir boundary
|
|
30
|
-
// in webRemote before any fs access.
|
|
31
|
-
const sessionSubscribe = z.object({
|
|
32
|
-
// tabId becomes a transcript FILENAME (`<tabId>.jsonl`) — restrict to a
|
|
33
|
-
// session-id charset (no '/', no '.', so it can't traverse out of the project
|
|
34
|
-
// transcript dir). sessionId is a UUID, which satisfies this.
|
|
35
|
-
tabId: z.string().min(1).max(128).regex(/^[A-Za-z0-9][A-Za-z0-9_-]*$/),
|
|
36
|
-
cwd: z.string().min(1).max(4096),
|
|
37
|
-
});
|
|
38
|
-
|
|
39
28
|
// 64 KiB cap per pty:write — typewriter input is bounded; a renderer firing
|
|
40
29
|
// megabytes per call is either a bug or an attack. Block it at the boundary.
|
|
41
30
|
const PTY_WRITE_MAX_BYTES = 64 * 1024;
|
|
@@ -50,115 +39,12 @@ const ptyResize = z.object({
|
|
|
50
39
|
rows: z.number().int().min(3).max(1000),
|
|
51
40
|
});
|
|
52
41
|
|
|
53
|
-
// ──────────────────────────────────────────── Browser (WebContentsView embed)
|
|
54
|
-
// viewId is a renderer-generated identifier; restrict to a safe charset (no
|
|
55
|
-
// '/', no '.') since it keys the in-process Map<viewId, WebContentsView> —
|
|
56
|
-
// not used as a filesystem path, but kept consistent with tabId conventions.
|
|
57
|
-
const BROWSER_VIEW_ID_RE = /^[A-Za-z0-9][A-Za-z0-9_-]*$/;
|
|
58
|
-
const browserViewId = z.object({
|
|
59
|
-
viewId: z.string().min(1).max(128).regex(BROWSER_VIEW_ID_RE),
|
|
60
|
-
});
|
|
61
|
-
|
|
62
|
-
const browserCreate = z.object({
|
|
63
|
-
viewId: z.string().min(1).max(128).regex(BROWSER_VIEW_ID_RE),
|
|
64
|
-
// Non-persistent partition string (PRD 400 run-mode isolation). No leading
|
|
65
|
-
// 'persist:' enforced here — callers choose persistence explicitly.
|
|
66
|
-
partition: z.string().min(1).max(256),
|
|
67
|
-
});
|
|
68
|
-
|
|
69
|
-
const BOUNDS_INT = z.number().int().min(0).max(100000);
|
|
70
|
-
const browserSetBounds = z.object({
|
|
71
|
-
viewId: z.string().min(1).max(128).regex(BROWSER_VIEW_ID_RE),
|
|
72
|
-
x: BOUNDS_INT,
|
|
73
|
-
y: BOUNDS_INT,
|
|
74
|
-
width: BOUNDS_INT,
|
|
75
|
-
height: BOUNDS_INT,
|
|
76
|
-
});
|
|
77
|
-
|
|
78
|
-
const browserNavigate = z.object({
|
|
79
|
-
viewId: z.string().min(1).max(128).regex(BROWSER_VIEW_ID_RE),
|
|
80
|
-
url: z.string().min(1).max(8192),
|
|
81
|
-
});
|
|
82
|
-
|
|
83
|
-
// PRD 407: DOM/text capture from the active browser sub-tab.
|
|
84
|
-
const browserCaptureDom = z.object({
|
|
85
|
-
viewId: z.string().min(1).max(128).regex(BROWSER_VIEW_ID_RE),
|
|
86
|
-
kind: z.enum(['text', 'html']),
|
|
87
|
-
});
|
|
88
|
-
|
|
89
|
-
// PRD 404: filter -> prune -> summarize -> chunk capture of a picked
|
|
90
|
-
// selection (browser:capture). selectors comes from the PRD 403 picker.
|
|
91
|
-
const browserCaptureSelection = z.object({
|
|
92
|
-
viewId: z.string().min(1).max(128).regex(BROWSER_VIEW_ID_RE),
|
|
93
|
-
selectors: z.array(z.string().min(1).max(2048)).min(1).max(50),
|
|
94
|
-
mode: z.enum(['agent', 'html', 'a11y', 'selector']),
|
|
95
|
-
});
|
|
96
|
-
|
|
97
|
-
// PRD 407: clipboard image write (browser:copy-image). dataUrl is a PNG data
|
|
98
|
-
// URL from webContents.capturePage() — capped well above any realistic
|
|
99
|
-
// screenshot so a malformed/huge payload can't wedge the IPC channel.
|
|
100
|
-
const browserCopyImage = z.object({
|
|
101
|
-
dataUrl: z.string().min(1).max(50_000_000),
|
|
102
|
-
});
|
|
103
|
-
|
|
104
42
|
// Recorder export (PRD 412): write arbitrary recorded-flow text to the OS
|
|
105
|
-
// clipboard
|
|
43
|
+
// clipboard.
|
|
106
44
|
const clipboardWriteText = z.object({
|
|
107
45
|
text: z.string().max(1_000_000),
|
|
108
46
|
});
|
|
109
47
|
|
|
110
|
-
// Recorder export (PRD 412): native "Save As" dialog write, bypassing the
|
|
111
|
-
// config.cjs write-boundary since the path is user-chosen via OS dialog.
|
|
112
|
-
const browserSaveRecording = z.object({
|
|
113
|
-
defaultName: z.string().min(1).max(255),
|
|
114
|
-
text: z.string().max(1_000_000),
|
|
115
|
-
});
|
|
116
|
-
|
|
117
|
-
// PRD 407: binary-safe atomic write (browser:save-binary) for screenshot
|
|
118
|
-
// captures — config:write-text is utf8-only.
|
|
119
|
-
const browserSaveBinary = z.object({
|
|
120
|
-
path: z.string().min(1).max(4096),
|
|
121
|
-
base64: z.string().min(1).max(50_000_000),
|
|
122
|
-
// Single-writer law (lib/opsOwnership.cjs) — required for ops-root paths.
|
|
123
|
-
writer: z.string().min(1).max(64).optional(),
|
|
124
|
-
});
|
|
125
|
-
|
|
126
|
-
// PRD 410: replay a recorded step list against a live view. The renderer
|
|
127
|
-
// owns the step list (main never persists recorded steps), so every call is
|
|
128
|
-
// self-contained. `select` is accepted for forward-compat even though the
|
|
129
|
-
// live recorder engine doesn't emit it yet.
|
|
130
|
-
const browserReplayStep = z.object({
|
|
131
|
-
n: z.number().int().min(1),
|
|
132
|
-
verb: z.enum(['navigate', 'click', 'type', 'select', 'wait-for']),
|
|
133
|
-
target: z.string().max(2000),
|
|
134
|
-
value: z.string().max(2000).optional(),
|
|
135
|
-
variable: z.string().max(64).nullable().optional(),
|
|
136
|
-
kind: z.enum(['nav', 'assert']).optional(),
|
|
137
|
-
masked: z.boolean().optional(),
|
|
138
|
-
variableSuggestion: z.string().max(64).optional(),
|
|
139
|
-
});
|
|
140
|
-
|
|
141
|
-
const browserReplay = z.object({
|
|
142
|
-
viewId: z.string().min(1).max(128).regex(BROWSER_VIEW_ID_RE),
|
|
143
|
-
steps: z.array(browserReplayStep).max(500),
|
|
144
|
-
values: z.record(z.string().max(64), z.string().max(2000)).optional(),
|
|
145
|
-
continueOnError: z.boolean().optional(),
|
|
146
|
-
});
|
|
147
|
-
|
|
148
|
-
// PRD 402: address-bar zoom control. factor is clamped again in
|
|
149
|
-
// browserView.cjs's setZoom — this just bounds the wire payload.
|
|
150
|
-
const browserSetZoom = z.object({
|
|
151
|
-
viewId: z.string().min(1).max(128).regex(BROWSER_VIEW_ID_RE),
|
|
152
|
-
factor: z.number().min(0.1).max(10),
|
|
153
|
-
});
|
|
154
|
-
|
|
155
|
-
// PRD 402: Cmd/Ctrl+F find bar.
|
|
156
|
-
const browserFind = z.object({
|
|
157
|
-
viewId: z.string().min(1).max(128).regex(BROWSER_VIEW_ID_RE),
|
|
158
|
-
text: z.string().max(2000),
|
|
159
|
-
forward: z.boolean().optional(),
|
|
160
|
-
});
|
|
161
|
-
|
|
162
48
|
// ──────────────────────────────────────────── Transcripts
|
|
163
49
|
const SESSION_UUID_RE = /^[a-zA-Z0-9-]{1,64}$/;
|
|
164
50
|
|
|
@@ -680,6 +566,15 @@ const exchangesList = z.object({
|
|
|
680
566
|
// this one lives here per PRD 638 so it's reusable without importing files.cjs.
|
|
681
567
|
const filesDuplicate = z.object({ path: z.string().min(1).max(4096) });
|
|
682
568
|
|
|
569
|
+
// files:save-binary — binary-safe atomic write (formerly browser:save-binary,
|
|
570
|
+
// re-homed off the Browser tab). Mirrors the retired browserSaveBinary shape.
|
|
571
|
+
const filesSaveBinary = z.object({
|
|
572
|
+
path: z.string().min(1).max(4096),
|
|
573
|
+
base64: z.string().min(1).max(50_000_000),
|
|
574
|
+
// Single-writer law (lib/opsOwnership.cjs) — required for ops-root paths.
|
|
575
|
+
writer: z.string().min(1).max(64).optional(),
|
|
576
|
+
});
|
|
577
|
+
|
|
683
578
|
// ──────────────────────────────────────────── Doc Edit (PRD 638 rewrite runner)
|
|
684
579
|
// docedit:run — consumed by docEdit.cjs's registerDocEditHandlers.
|
|
685
580
|
const docEditRun = z.object({
|
|
@@ -744,23 +639,6 @@ const chatExternalSend = z.object({
|
|
|
744
639
|
),
|
|
745
640
|
});
|
|
746
641
|
|
|
747
|
-
// ──────────────────────────────────────────── Web Remote
|
|
748
|
-
// OTP is 8 uppercase alphanumeric chars (case-insensitive entry, normalised to upper in handler).
|
|
749
|
-
const WEB_REMOTE_OTP_RE = /^[A-Z0-9]{8}$/i;
|
|
750
|
-
const DEVICE_ID_RE = /^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i;
|
|
751
|
-
|
|
752
|
-
const webRemotePair = z.object({
|
|
753
|
-
otp: z.string().regex(WEB_REMOTE_OTP_RE),
|
|
754
|
-
}).strict();
|
|
755
|
-
|
|
756
|
-
const webRemoteRevokeDevice = z.object({
|
|
757
|
-
deviceId: z.string().regex(DEVICE_ID_RE),
|
|
758
|
-
}).strict();
|
|
759
|
-
|
|
760
|
-
const webRemoteAuditTail = z.object({
|
|
761
|
-
lines: z.number().int().min(1).max(500).optional(),
|
|
762
|
-
}).strict();
|
|
763
|
-
|
|
764
642
|
// ──────────────────────────────────────────── History
|
|
765
643
|
const DATE_YYYY_MM_DD = /^\d{4}-\d{2}-\d{2}$/;
|
|
766
644
|
|
|
@@ -892,96 +770,19 @@ function validated(schema, handler) {
|
|
|
892
770
|
};
|
|
893
771
|
}
|
|
894
772
|
|
|
895
|
-
// ──────────────────────────────────────────── Web Remote command allowlist
|
|
896
|
-
// Commands are split into three tiers:
|
|
897
|
-
// READ_COMMANDS — return data; allowed when remoteEnabled=true.
|
|
898
|
-
// SAS_GATED_READS — return sensitive user data (sessions, PRDs, logs,
|
|
899
|
-
// transcript summaries); additionally require
|
|
900
|
-
// _e2eAuthenticated=true (SAS confirmed by user).
|
|
901
|
-
// A compromised relay cannot exfiltrate this data from
|
|
902
|
-
// a session that has not been SAS-confirmed.
|
|
903
|
-
// MUTATE_COMMANDS — write files, spawn processes, or mutate persisted
|
|
904
|
-
// state; gated behind remoteControlEnabled=true AND
|
|
905
|
-
// _e2eAuthenticated=true.
|
|
906
|
-
// ALLOWED_COMMANDS is the union, kept for existing import compatibility.
|
|
907
|
-
//
|
|
908
|
-
// Ungated READ_COMMANDS (justify each):
|
|
909
|
-
// cmd:app:version — exposes only the app semver string; no user data.
|
|
910
|
-
// cmd:session:unsubscribe — teardown lifecycle; returns nothing sensitive.
|
|
911
|
-
const READ_COMMANDS = new Set([
|
|
912
|
-
'cmd:app:version',
|
|
913
|
-
// v2 mobile: unsubscribe is a teardown lifecycle call with no data payload.
|
|
914
|
-
'cmd:session:unsubscribe',
|
|
915
|
-
]);
|
|
916
|
-
|
|
917
|
-
// Sensitive reads — return user data; require SAS confirmation same as MUTATE.
|
|
918
|
-
const SAS_GATED_READS = new Set([
|
|
919
|
-
'cmd:sessions:load',
|
|
920
|
-
'cmd:history:aggregate',
|
|
921
|
-
// subscribe initiates a live stream of session state/summary — sensitive.
|
|
922
|
-
'cmd:session:subscribe',
|
|
923
|
-
// NOTE: cmd:exchanges:list is intentionally NOT allowlisted — webRemote.cjs has
|
|
924
|
-
// no dispatch handler for it, so an allowlist entry would only fail closed with
|
|
925
|
-
// an opaque reject. Re-add here together with the handler when remote exchanges
|
|
926
|
-
// are wired, so the allowlist always mirrors an actual capability.
|
|
927
|
-
//
|
|
928
|
-
// Scheduler/Epics commands (cmd:schedule:*) were removed here deliberately
|
|
929
|
-
// (core scheduler/Epics redesign) — Remote no longer reaches into scheduler
|
|
930
|
-
// internals at all; a future Remote rebuild will talk to Scheduler/Epics
|
|
931
|
-
// through whatever higher-level surface that redesign lands on, not this
|
|
932
|
-
// per-command allowlist.
|
|
933
|
-
]);
|
|
934
|
-
|
|
935
|
-
const MUTATE_COMMANDS = new Set([
|
|
936
|
-
'cmd:sessions:save',
|
|
937
|
-
'cmd:pty:spawn',
|
|
938
|
-
'cmd:pty:write',
|
|
939
|
-
// pty:kill terminates a live session; pty:resize drives the geometry of the
|
|
940
|
-
// user's interactive PTY — both write live process state, so they are gated
|
|
941
|
-
// behind remoteControlEnabled + SAS like every other mutation. A read-only
|
|
942
|
-
// mobile mirror has no business killing or resizing the desktop's session.
|
|
943
|
-
'cmd:pty:kill',
|
|
944
|
-
'cmd:pty:resize',
|
|
945
|
-
// Pushes a prompt into an open tab's chat queue — a real mutation of that
|
|
946
|
-
// tab's session, same tier as pty:write.
|
|
947
|
-
'cmd:chat:send',
|
|
948
|
-
]);
|
|
949
|
-
|
|
950
|
-
const ALLOWED_COMMANDS = new Set([...READ_COMMANDS, ...SAS_GATED_READS, ...MUTATE_COMMANDS]);
|
|
951
|
-
|
|
952
773
|
module.exports = {
|
|
953
774
|
// Centralized slug regex — used by scheduler.cjs and queueOps.cjs for
|
|
954
775
|
// direct test()/match() containment checks alongside the zod parses.
|
|
955
776
|
SCHEDULE_SLUG_RE,
|
|
956
777
|
SCHEDULE_RUN_ID_RE,
|
|
957
778
|
PRD_CREATE_SLUG_RE,
|
|
958
|
-
READ_COMMANDS,
|
|
959
|
-
SAS_GATED_READS,
|
|
960
|
-
MUTATE_COMMANDS,
|
|
961
|
-
ALLOWED_COMMANDS,
|
|
962
779
|
schemas: {
|
|
963
|
-
webRemotePair,
|
|
964
|
-
webRemoteRevokeDevice,
|
|
965
|
-
webRemoteAuditTail,
|
|
966
780
|
ptySpawn,
|
|
967
781
|
ptyTabId,
|
|
968
782
|
ptyAlive,
|
|
969
783
|
ptyWrite,
|
|
970
784
|
ptyResize,
|
|
971
|
-
sessionSubscribe,
|
|
972
|
-
browserViewId,
|
|
973
|
-
browserCreate,
|
|
974
|
-
browserSetBounds,
|
|
975
|
-
browserNavigate,
|
|
976
|
-
browserCaptureDom,
|
|
977
|
-
browserCaptureSelection,
|
|
978
|
-
browserCopyImage,
|
|
979
|
-
browserSaveBinary,
|
|
980
785
|
clipboardWriteText,
|
|
981
|
-
browserSaveRecording,
|
|
982
|
-
browserReplay,
|
|
983
|
-
browserSetZoom,
|
|
984
|
-
browserFind,
|
|
985
786
|
transcriptSubscribe,
|
|
986
787
|
transcriptTabId,
|
|
987
788
|
transcriptPath,
|
|
@@ -1049,6 +850,7 @@ module.exports = {
|
|
|
1049
850
|
watchersRemove,
|
|
1050
851
|
watchersKillTab,
|
|
1051
852
|
filesDuplicate,
|
|
853
|
+
filesSaveBinary,
|
|
1052
854
|
docEditRun,
|
|
1053
855
|
docEditRunInSession,
|
|
1054
856
|
chatRun,
|
|
@@ -48,8 +48,6 @@ const OWNERS = Object.freeze({
|
|
|
48
48
|
'scheduler': 'scheduler',
|
|
49
49
|
// Project Home owns the synthesized Brief (generate + hand-edit).
|
|
50
50
|
'project-brief': 'project-home',
|
|
51
|
-
// Browser tab scratch saves (DOM captures, screenshots, recorded flows).
|
|
52
|
-
'browser': 'browser',
|
|
53
51
|
// Structured per-tab error log lines (JSONL), tagged for tracing/analysis.
|
|
54
52
|
'logs': 'logs',
|
|
55
53
|
// Host on Bilko.run tab's deterministic bundle prep (dist/index.html +
|
|
@@ -5,7 +5,6 @@
|
|
|
5
5
|
*
|
|
6
6
|
* Single source of truth for the Anthropic summarization call. Used by:
|
|
7
7
|
* - exchanges.cjs (records durable per-exchange summaries)
|
|
8
|
-
* - webRemote.cjs (mobile summary push — refactored to import from here)
|
|
9
8
|
*
|
|
10
9
|
* Degrade contracts (never throw — always return a record):
|
|
11
10
|
* no API key → { summary: text.slice(0,600), model:'raw', degraded:'no_api_key' }
|
package/src/main/runVerify.cjs
CHANGED
|
@@ -157,6 +157,7 @@ function parseLog(logPath) {
|
|
|
157
157
|
const events = [];
|
|
158
158
|
let resultEvent = null;
|
|
159
159
|
let seq = 0;
|
|
160
|
+
let transcriptCommitLanded = false;
|
|
160
161
|
|
|
161
162
|
for (const line of text.split('\n')) {
|
|
162
163
|
const trimmed = line.trim();
|
|
@@ -166,6 +167,18 @@ function parseLog(logPath) {
|
|
|
166
167
|
try { obj = JSON.parse(trimmed); } catch { continue; }
|
|
167
168
|
if (!obj || typeof obj !== 'object') continue;
|
|
168
169
|
|
|
170
|
+
// Harness-emitted commit evidence: git itself produced these lines (a
|
|
171
|
+
// `vcs_state_changed` system event, or a tool_use_result carrying a
|
|
172
|
+
// gitOperation), so the model cannot fabricate them. Detected here as a
|
|
173
|
+
// plain boolean scan — never pushed into `events`, so the index-based
|
|
174
|
+
// final-20% math elsewhere is unaffected.
|
|
175
|
+
if (obj.type === 'system' && obj.subtype === 'vcs_state_changed' && obj.kind === 'commit') {
|
|
176
|
+
transcriptCommitLanded = true;
|
|
177
|
+
}
|
|
178
|
+
if (obj.tool_use_result?.gitOperation?.commit?.kind === 'committed') {
|
|
179
|
+
transcriptCommitLanded = true;
|
|
180
|
+
}
|
|
181
|
+
|
|
169
182
|
// Final result event.
|
|
170
183
|
if (obj.type === 'result') {
|
|
171
184
|
const ev = {
|
|
@@ -215,7 +228,7 @@ function parseLog(logPath) {
|
|
|
215
228
|
}
|
|
216
229
|
}
|
|
217
230
|
|
|
218
|
-
return { events, resultEvent, error: null };
|
|
231
|
+
return { events, resultEvent, error: null, transcriptCommitLanded };
|
|
219
232
|
}
|
|
220
233
|
|
|
221
234
|
// ─── self-recovery helpers ────────────────────────────────────────────────────
|
|
@@ -591,6 +604,27 @@ function isAncestorCommit({ cwd, sha, timeoutMs = GH_CHECK_TIMEOUT_MS, execImpl
|
|
|
591
604
|
}
|
|
592
605
|
}
|
|
593
606
|
|
|
607
|
+
/**
|
|
608
|
+
* True when `cwd` is inside a git repository. Used to gate the transcript
|
|
609
|
+
* commit-evidence fallback below: a job whose cwd is not a git repo can never
|
|
610
|
+
* produce committedDuringRun=true (gitHead()/committedInWindow() fail closed
|
|
611
|
+
* there), so the fallback must only apply in that narrow case — never when
|
|
612
|
+
* git ancestry is actually observable.
|
|
613
|
+
*/
|
|
614
|
+
function isGitRepo(cwd) {
|
|
615
|
+
if (!cwd) return false;
|
|
616
|
+
try {
|
|
617
|
+
execFileSync('git', ['-C', cwd, 'rev-parse', '--git-dir'], {
|
|
618
|
+
timeout: 10_000,
|
|
619
|
+
encoding: 'utf8',
|
|
620
|
+
stdio: ['ignore', 'pipe', 'pipe'],
|
|
621
|
+
});
|
|
622
|
+
return true;
|
|
623
|
+
} catch {
|
|
624
|
+
return false;
|
|
625
|
+
}
|
|
626
|
+
}
|
|
627
|
+
|
|
594
628
|
// ─── already-shipped postcondition exemption (original PRD re-runs) ─────────
|
|
595
629
|
|
|
596
630
|
const PRD_DELIVERABLE_PATH_RE = /(?:^|[`\s(])((?:src|scripts|session-manager-operations|test|tests|docs|bin)\/[A-Za-z0-9_./-]*[A-Za-z0-9_-]\.[A-Za-z0-9]+)\b/g;
|
|
@@ -743,12 +777,21 @@ async function verifyRun({ runDir, prdPath, queueEntry, allJobs = [], committedD
|
|
|
743
777
|
}
|
|
744
778
|
|
|
745
779
|
// ── 2. Parse log ───────────────────────────────────────────────────────
|
|
746
|
-
const { events, resultEvent, error: parseError } = parseLog(logPath);
|
|
780
|
+
const { events, resultEvent, error: parseError, transcriptCommitLanded } = parseLog(logPath);
|
|
747
781
|
|
|
748
782
|
if (parseError) {
|
|
749
783
|
return conclude('verify_unavailable', `log unreadable: ${parseError}`, 'needs_review');
|
|
750
784
|
}
|
|
751
785
|
|
|
786
|
+
// A job whose cwd is not a git repo (e.g. an investigation fix-plan that
|
|
787
|
+
// inherited cwd: /tmp) can never produce committedDuringRun=true —
|
|
788
|
+
// gitHead()/committedInWindow() both fail closed there. In that case
|
|
789
|
+
// ONLY, fall back to the harness-emitted commit evidence in the
|
|
790
|
+
// transcript, which git itself produced and the model cannot fabricate.
|
|
791
|
+
const cwdIsGitRepo = isGitRepo(queueEntry?.cwd);
|
|
792
|
+
const commitEvidence = committedDuringRun || (!cwdIsGitRepo && transcriptCommitLanded);
|
|
793
|
+
const commitEvidenceSource = (!committedDuringRun && commitEvidence) ? 'transcript' : null;
|
|
794
|
+
|
|
752
795
|
// ── 3. HALT detection ─────────────────────────────────────────────────
|
|
753
796
|
// Primary: check the final `{"type":"result"}` event's `result` text.
|
|
754
797
|
if (resultEvent) {
|
|
@@ -865,7 +908,7 @@ async function verifyRun({ runDir, prdPath, queueEntry, allJobs = [], committedD
|
|
|
865
908
|
// committed anything nor ever emitted the finish-protocol sentinel likely
|
|
866
909
|
// ended before doing real work (e.g. stopped on a clarifying question).
|
|
867
910
|
// Weaker evidence than a caught transcript error, but still not clean.
|
|
868
|
-
if (sentinel === null && !
|
|
911
|
+
if (sentinel === null && !commitEvidence) {
|
|
869
912
|
issues.push({
|
|
870
913
|
verdict: 'no_verdict_sentinel',
|
|
871
914
|
reason: 'run made no commit and emitted no SCHEDULER_VERDICT sentinel — likely ended before the finish protocol (e.g. stopped on a clarifying question)',
|
|
@@ -894,7 +937,7 @@ async function verifyRun({ runDir, prdPath, queueEntry, allJobs = [], committedD
|
|
|
894
937
|
// bare PASS with no commit from one is still a strong silent-failure
|
|
895
938
|
// signal and stays covered by this check.
|
|
896
939
|
const isFixPlanJob = /^\d+-fix-/.test(queueEntry?.slug || '');
|
|
897
|
-
if (sentinel === 'pass' && !
|
|
940
|
+
if (sentinel === 'pass' && !commitEvidence && !isFixPlanJob) {
|
|
898
941
|
// EXEMPTION: `-merge-main` PRDs (see isMergeMainSlug) can genuinely and
|
|
899
942
|
// correctly find nothing left to do when an out-of-band actor (a human,
|
|
900
943
|
// another agent, a sibling scheduler job) already merged/updated the
|
|
@@ -1015,13 +1058,18 @@ async function verifyRun({ runDir, prdPath, queueEntry, allJobs = [], committedD
|
|
|
1015
1058
|
// MUST NOT apply to halt or deps_unmet — those keep their existing semantics.
|
|
1016
1059
|
if (
|
|
1017
1060
|
sentinel === 'pass'
|
|
1018
|
-
&&
|
|
1061
|
+
&& commitEvidence
|
|
1019
1062
|
&& (top.verdict === 'transcript_errors' || top.verdict === 'verify_unavailable')
|
|
1020
1063
|
) {
|
|
1021
1064
|
return conclude('clean',
|
|
1022
1065
|
`SCHEDULER_VERDICT: PASS + commit landed overrides ${top.verdict}`,
|
|
1023
1066
|
null,
|
|
1024
|
-
{
|
|
1067
|
+
{
|
|
1068
|
+
...(annotations.length ? { annotations } : {}),
|
|
1069
|
+
sentinel,
|
|
1070
|
+
sentinelOverride: top.verdict,
|
|
1071
|
+
...(commitEvidenceSource ? { commitEvidenceSource } : {}),
|
|
1072
|
+
},
|
|
1025
1073
|
);
|
|
1026
1074
|
}
|
|
1027
1075
|
|
|
@@ -1073,4 +1121,5 @@ module.exports = {
|
|
|
1073
1121
|
extractPrdDeliverablePaths,
|
|
1074
1122
|
allDeliverablesAlreadyTracked,
|
|
1075
1123
|
isAncestorCommit,
|
|
1124
|
+
isGitRepo,
|
|
1076
1125
|
};
|
package/src/main/scheduler.cjs
CHANGED
|
@@ -271,6 +271,28 @@ function gitHead(cwd) {
|
|
|
271
271
|
});
|
|
272
272
|
}
|
|
273
273
|
|
|
274
|
+
// True when cwd is inside a git repository. Used to keep a non-git cwd (e.g.
|
|
275
|
+
// a scratch dir like /tmp) from ever being handed to an investigation's
|
|
276
|
+
// fix-plan as its cwd — the commit guard, worktree isolation, and
|
|
277
|
+
// runVerify's PASS+commit sentinel override all read git state from job.cwd,
|
|
278
|
+
// so a fix plan rooted at a non-repo path gets parked in needs_review even
|
|
279
|
+
// when its commit really landed (Incident: 99-fix-e2e-needs-review-test,
|
|
280
|
+
// 2026-08-07 — green, committed 6ecbd40, PASS sentinel, still needs_review).
|
|
281
|
+
// Bounded and never throws.
|
|
282
|
+
function isGitRepoSync(cwd) {
|
|
283
|
+
if (!cwd) return false;
|
|
284
|
+
try {
|
|
285
|
+
execFileSync('git', ['-C', cwd, 'rev-parse', '--git-dir'], {
|
|
286
|
+
timeout: 10_000,
|
|
287
|
+
windowsHide: true,
|
|
288
|
+
stdio: ['ignore', 'pipe', 'pipe'],
|
|
289
|
+
});
|
|
290
|
+
return true;
|
|
291
|
+
} catch {
|
|
292
|
+
return false;
|
|
293
|
+
}
|
|
294
|
+
}
|
|
295
|
+
|
|
274
296
|
// Best-effort `git fetch --all --prune` in cwd, bounded and never throwing.
|
|
275
297
|
// A PRD that does its work in a separate `git worktree add` checkout (per
|
|
276
298
|
// standards.md's own recommended pattern for shared repos) commits and pushes
|
|
@@ -2666,6 +2688,10 @@ ${logTail}
|
|
|
2666
2688
|
estimateMinutes: <your time estimate>
|
|
2667
2689
|
---
|
|
2668
2690
|
\`\`\`
|
|
2691
|
+
\`cwd\` must be the git repo root where the fix will actually land. If the failed job's cwd is
|
|
2692
|
+
not that repo (e.g. a scratch dir like \`/tmp\`), set \`cwd:\` to the correct repo root instead —
|
|
2693
|
+
the scheduler's commit guard and post-run verifier read git state from this path, and a
|
|
2694
|
+
non-repo cwd parks even a fully green, committed run in \`needs_review\`.
|
|
2669
2695
|
6. The PRD body MUST be self-contained — \`claude -p\` runs it on a fresh Sonnet session with NO conversation context. Include:
|
|
2670
2696
|
- Root-cause analysis (what went wrong and why)
|
|
2671
2697
|
- Concrete fix steps (specific files / commands / edits)
|
|
@@ -2803,6 +2829,16 @@ async function spawnInvestigation(failedJob, runDir) {
|
|
|
2803
2829
|
console.warn(`[scheduler] investigation cwd missing (${cwd}); falling back to ${DEFAULT_PROJECT_CWD}`);
|
|
2804
2830
|
cwd = DEFAULT_PROJECT_CWD;
|
|
2805
2831
|
}
|
|
2832
|
+
// A non-git cwd is unusable as a fix-plan cwd: the commit guard, worktree
|
|
2833
|
+
// isolation, and runVerify's PASS+commit sentinel override all read git
|
|
2834
|
+
// state from job.cwd, so a fix plan rooted at e.g. /tmp gets parked in
|
|
2835
|
+
// needs_review even when its commit really landed. (Incident:
|
|
2836
|
+
// 99-fix-e2e-needs-review-test, 2026-08-07 — green, committed 6ecbd40,
|
|
2837
|
+
// PASS sentinel, still needs_review.)
|
|
2838
|
+
if (!isGitRepoSync(cwd)) {
|
|
2839
|
+
console.warn(`[scheduler] investigation cwd is not a git repo (${cwd}); falling back to ${DEFAULT_PROJECT_CWD}`);
|
|
2840
|
+
cwd = DEFAULT_PROJECT_CWD;
|
|
2841
|
+
}
|
|
2806
2842
|
const prompt = buildInvestigationPrompt({ failedJob, cwd, failedLogPath, originalBody, logTail, fixPath, group });
|
|
2807
2843
|
|
|
2808
2844
|
// Phase 1: open log fd for pre-spawn diagnostics.
|
|
@@ -3024,11 +3060,12 @@ async function spawnJob(job, runId, runDir, defaultCwd) {
|
|
|
3024
3060
|
await setPaused('rate_limit', resetIso);
|
|
3025
3061
|
}
|
|
3026
3062
|
|
|
3027
|
-
// Stale queue entry: the PRD already shipped
|
|
3028
|
-
//
|
|
3029
|
-
//
|
|
3030
|
-
//
|
|
3031
|
-
|
|
3063
|
+
// Stale queue entry: the PRD was archived (already shipped) or is gone
|
|
3064
|
+
// from disk entirely (deleted before dispatch). Both are retire-not-fail
|
|
3065
|
+
// results from prdArchivedSkipResult with exitCode 0 and no transcript —
|
|
3066
|
+
// treat any truthy skip reason as a plain completion: no verify pass, no
|
|
3067
|
+
// commit guard, no RCA feedback item.
|
|
3068
|
+
if (res.skipped) {
|
|
3032
3069
|
await mutate((s) => {
|
|
3033
3070
|
const idx = s.jobs.findIndex((x) => x.slug === job.slug);
|
|
3034
3071
|
if (idx >= 0) {
|
|
@@ -4765,7 +4802,9 @@ async function init() {
|
|
|
4765
4802
|
}
|
|
4766
4803
|
}
|
|
4767
4804
|
|
|
4768
|
-
// remote —
|
|
4805
|
+
// remote — in-process (non-IPC) scheduler accessors, used by prdCreate.cjs
|
|
4806
|
+
// and other main-process callers. (Named for the retired web-remote relay,
|
|
4807
|
+
// its original consumer; kept because it still has in-process callers.)
|
|
4769
4808
|
const remote = {
|
|
4770
4809
|
// `cwd` is optional: prdCreate.cjs's create flow passes the target
|
|
4771
4810
|
// project's cwd explicitly (the file may not exist yet, so there's
|
|
@@ -4956,4 +4995,4 @@ function registerAdminRoutes(adminHttp, remoteObj = remote) {
|
|
|
4956
4995
|
});
|
|
4957
4996
|
}
|
|
4958
4997
|
|
|
4959
|
-
module.exports = { registerScheduleHandlers, attachWindow, init, ROOT, PRDS_DIR, healRefusalReason, writeQueue, reconcile, reconcileSourcePromptId, allocateParallelGroup, selectHistoryJobs, parsePorcelain, FINISH_PROTOCOL, remote, pickNextBatch, pickForProject, reapDeadRunningJobs, pollRecoveryClearSource, memoryLimitedBatchSize, availableForJobs, reverifyNeedsReview, isRescanCandidate, isPromotableOriginal, selectAutoFixTargets, isEligibleForImmediateAutoFix, resolveRunId, isUnresolvableNeedsReview, healTargetForFix, buildInvestigationPrompt, committedInWindow, computeCommittedDuringRun, classifySigtermWithCommit, isFixPlanSlug, isFixPlanBeyondDepthCap, MAX_INVESTIGATION_DEPTH, forceTickOutcome, applyPauseCleared, detectNetworkErrorInLog, detectRateLimitInLog, classifyFailureOutcome, commitGuardVerdict, TRANSIENT_RETRY_CAP, buildScheduleStatePayload, partitionBootOrphans, applyOrphanOutcome, BOOT_ORPHAN_KILL_GRACE_MS, registerAdminRoutes, notifyOriginatingTab, notifyNeedsReview, isNotifiableTerminalStatus, extractResultTextFromLog, candidatePrdsDirs, candidateArchivedPrdsDirs, resolveArchivedPrdStatus, prdDirForCwd, prdPathForJob, archivedPrdPathForJob, archivedTwinExists, findPrdDir, resolveVerifyPrdPath, resolveNotifyPrd, runPrdMigration, shouldSkipInvestigationForCleanRun, archiveCompletedPrd, retireCompletedSlugs, SCHEDULER_BOOTED_AT, SCHEDULER_CODE_SHA, resetJobFields, executeJob, prdArchivedSkipResult };
|
|
4998
|
+
module.exports = { registerScheduleHandlers, attachWindow, init, ROOT, PRDS_DIR, healRefusalReason, writeQueue, reconcile, reconcileSourcePromptId, allocateParallelGroup, selectHistoryJobs, parsePorcelain, FINISH_PROTOCOL, remote, pickNextBatch, pickForProject, reapDeadRunningJobs, pollRecoveryClearSource, memoryLimitedBatchSize, availableForJobs, reverifyNeedsReview, isRescanCandidate, isPromotableOriginal, selectAutoFixTargets, isEligibleForImmediateAutoFix, resolveRunId, isUnresolvableNeedsReview, healTargetForFix, buildInvestigationPrompt, isGitRepoSync, committedInWindow, computeCommittedDuringRun, classifySigtermWithCommit, isFixPlanSlug, isFixPlanBeyondDepthCap, MAX_INVESTIGATION_DEPTH, forceTickOutcome, applyPauseCleared, detectNetworkErrorInLog, detectRateLimitInLog, classifyFailureOutcome, commitGuardVerdict, TRANSIENT_RETRY_CAP, buildScheduleStatePayload, partitionBootOrphans, applyOrphanOutcome, BOOT_ORPHAN_KILL_GRACE_MS, registerAdminRoutes, notifyOriginatingTab, notifyNeedsReview, isNotifiableTerminalStatus, extractResultTextFromLog, candidatePrdsDirs, candidateArchivedPrdsDirs, resolveArchivedPrdStatus, prdDirForCwd, prdPathForJob, archivedPrdPathForJob, archivedTwinExists, findPrdDir, resolveVerifyPrdPath, resolveNotifyPrd, runPrdMigration, shouldSkipInvestigationForCleanRun, archiveCompletedPrd, retireCompletedSlugs, SCHEDULER_BOOTED_AT, SCHEDULER_CODE_SHA, resetJobFields, executeJob, prdArchivedSkipResult, spawnJob };
|