claude-code-session-manager 0.86.0 → 0.87.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/dist/assets/AgentLibrary-DyLWzZDf.js +3 -0
- package/dist/assets/{DataModel-Q4jhl24R.js → DataModel--mISIJ6h.js} +1 -1
- package/dist/assets/{History-Cj2FejEo.js → History-C2ahUXTg.js} +2 -2
- package/dist/assets/{Hooks-CaelQI6t.js → Hooks-BiC6oyR2.js} +3 -3
- package/dist/assets/{HostBilko--v7cMR8I.js → HostBilko-BPleEOld.js} +1 -1
- package/dist/assets/{Library-DgI9oCCZ.js → Library-Dc8Qst1R.js} +1 -1
- package/dist/assets/{ListDetail-DYUZN-x-.js → ListDetail-DIXh-OLX.js} +1 -1
- package/dist/assets/MarkdownEditor-C90bkLXK.js +1 -0
- package/dist/assets/{McpServers-ypCYURh3.js → McpServers-DqcbLOLZ.js} +2 -2
- package/dist/assets/{Memory-C2qYp-3M.js → Memory-CW62MXlh.js} +4 -4
- package/dist/assets/{Panel-Cj2kw-Zv.js → Panel-Bw1FhRuF.js} +1 -1
- package/dist/assets/Permissions-BcUC-5y8.js +3 -0
- package/dist/assets/{Plugins-C1Vj8_dU.js → Plugins-BnKx9flD.js} +2 -2
- package/dist/assets/{ProvenanceBadge-DczPNM5U.js → ProvenanceBadge-Bw5vNVPT.js} +1 -1
- package/dist/assets/{SaveBar-Cd_7U6Gb.js → SaveBar-CWr0O_w-.js} +1 -1
- package/dist/assets/Scheduler-DYdLuUqq.js +14 -0
- package/dist/assets/{ScopeSwitcher-DVSyI44-.js → ScopeSwitcher-CrBLbg8s.js} +1 -1
- package/dist/assets/Settings-DluB-vN1.js +3 -0
- package/dist/assets/{SkillReferenceGraph-CUv1_Q2c.js → SkillReferenceGraph-CHLSseay.js} +1 -1
- package/dist/assets/{Skills-C_YHkAy-.js → Skills-gNdo_HNK.js} +2 -2
- package/dist/assets/{SystemPrompt-B8R7T9xn.js → SystemPrompt-Cru05-Ia.js} +1 -1
- package/dist/assets/{TagLibrary-dj9YHWyy.js → TagLibrary-DNHY0xou.js} +1 -1
- package/dist/assets/{TiptapBody-DnSBUjHE.js → TiptapBody-I4lmbCgP.js} +1 -1
- package/dist/assets/{Toggle-CjV_BJn6.js → Toggle-bWMHjmRh.js} +1 -1
- package/dist/assets/{index-CDo9xBR9.css → index-DV3PorRY.css} +1 -1
- package/dist/assets/{index-CXFQIPhO.js → index-fc_JjdxL.js} +724 -724
- package/dist/assets/settingsSchema-BfhtZnGD.js +3 -0
- package/dist/index.html +2 -2
- package/package.json +14 -14
- package/plugins/CLAUDE.md +61 -0
- package/plugins/session-manager-dev/.claude-plugin/plugin.json +1 -1
- package/plugins/session-manager-dev/skills/builder/4-manual/SKILL.md +1 -1
- package/plugins/session-manager-dev/skills/ops-sweep/SKILL.md +1 -1
- package/scripts/scheduler-mcp-server.cjs +7 -0
- package/src/main/__tests__/agentModelResolve.test.cjs +100 -9
- package/src/main/__tests__/broadcastCoalescer.test.cjs +18 -0
- package/src/main/__tests__/epicMint.test.cjs +2 -2
- package/src/main/__tests__/health-delegation-chain.test.cjs +2 -1
- package/src/main/__tests__/needsReviewLedger.test.cjs +162 -0
- package/src/main/__tests__/opsErrorLogTelemetryTap.test.cjs +3 -3
- package/src/main/__tests__/pollLoop-dispatch-on-failure.test.cjs +15 -1
- package/src/main/__tests__/prdCreateDisposition.test.cjs +201 -0
- package/src/main/__tests__/prdFrontmatterDisposition.test.cjs +125 -0
- package/src/main/__tests__/prdLocations.test.cjs +100 -2
- package/src/main/__tests__/prdLocationsArchived.test.cjs +43 -1
- package/src/main/__tests__/prdSetDisposition.test.cjs +222 -0
- package/src/main/__tests__/queue-health-verdict.test.cjs +170 -0
- package/src/main/__tests__/queue-starvation-per-project.test.cjs +14 -2
- package/src/main/__tests__/queueHistory.test.cjs +63 -0
- package/src/main/__tests__/reconcileTiming.test.cjs +135 -0
- package/src/main/__tests__/scheduleJobTransitions.test.cjs +101 -1
- package/src/main/__tests__/scheduler-boot-orphans.test.cjs +2 -2
- package/src/main/__tests__/scheduler-broadcast-reconcile.test.cjs +121 -0
- package/src/main/__tests__/scheduler-cross-project-batch.test.cjs +43 -0
- package/src/main/__tests__/scheduler-guard-verdict-autoresolve.test.cjs +344 -0
- package/src/main/__tests__/scheduler-job-budget.test.cjs +172 -0
- package/src/main/__tests__/scheduler-looks-done.test.cjs +93 -3
- package/src/main/__tests__/scheduler-porcelain-rename.test.cjs +164 -0
- package/src/main/__tests__/scheduler-reap-dead-running-jobs.test.cjs +160 -0
- package/src/main/__tests__/scheduler-reaper-helpers-basics.test.cjs +87 -0
- package/src/main/__tests__/scheduler-shared-tree-guard.test.cjs +88 -0
- package/src/main/__tests__/scheduler-starve-escalation.test.cjs +14 -4
- package/src/main/__tests__/scheduler-worktree-cap-defer.test.cjs +14 -0
- package/src/main/chatRunner.cjs +8 -5
- package/src/main/health.cjs +1 -1
- package/src/main/historyAggregator.cjs +5 -0
- package/src/main/index.cjs +95 -44
- package/src/main/ipcSchemas.cjs +47 -0
- package/src/main/lib/__tests__/active-sessions.test.cjs +251 -0
- package/src/main/lib/__tests__/bootSelfHeal.test.cjs +107 -0
- package/src/main/lib/__tests__/delegationReadiness.test.cjs +322 -43
- package/src/main/lib/__tests__/effectiveModelInfo.test.cjs +239 -0
- package/src/main/lib/__tests__/gitWorktree.test.cjs +89 -0
- package/src/main/lib/__tests__/guardShims.test.cjs +151 -0
- package/src/main/lib/__tests__/opsRootAbsoluteCwd.test.cjs +5 -5
- package/src/main/lib/__tests__/prdDisposition.test.cjs +224 -0
- package/src/main/lib/__tests__/reaperHelpers.test.cjs +179 -1
- package/src/main/lib/__tests__/usageCircuit.test.cjs +224 -0
- package/src/main/lib/__tests__/watchdog-helpers.test.cjs +312 -0
- package/src/main/lib/__tests__/watchdog-relaunch.test.cjs +193 -0
- package/{scripts → src/main}/lib/activeSessions.cjs +50 -4
- package/src/main/lib/agentModelResolve.cjs +65 -27
- package/src/main/lib/bootSelfHeal.cjs +88 -0
- package/src/main/lib/delegationReadiness.cjs +290 -225
- package/src/main/lib/effectiveModelInfo.cjs +333 -0
- package/src/main/lib/ephemeralCwd.cjs +1 -1
- package/src/main/lib/epicMint.cjs +3 -3
- package/src/main/lib/gitWorktree.cjs +42 -12
- package/src/main/lib/guardShims.cjs +156 -0
- package/src/main/lib/jobDirtFilter.cjs +7 -2
- package/src/main/lib/launchFailure.cjs +2 -1
- package/src/main/lib/mcpToolCatalog.cjs +4 -1
- package/src/main/lib/needsReviewLedger.cjs +205 -0
- package/src/main/lib/opsErrorLog.cjs +1 -1
- package/src/main/lib/opsOwnership.cjs +1 -1
- package/src/main/lib/prdCreate.cjs +56 -1
- package/src/main/lib/prdDisposition.cjs +199 -0
- package/src/main/lib/prdFrontmatter.cjs +8 -2
- package/src/main/lib/prdLocations.cjs +167 -45
- package/src/main/lib/projectHomeAdminRoutes.cjs +4 -4
- package/src/main/lib/projectPageSummarySchema.cjs +1 -1
- package/src/main/lib/projectRootResolve.cjs +1 -1
- package/src/main/lib/queueHistory.cjs +19 -1
- package/src/main/lib/queueStore.cjs +6 -1
- package/src/main/lib/reaperHelpers.cjs +181 -15
- package/src/main/lib/scheduleJobSchema.cjs +8 -0
- package/src/main/lib/scheduleJobTransitions.cjs +33 -0
- package/src/main/lib/schedulerConfig.cjs +24 -0
- package/src/main/lib/usageCircuit.cjs +159 -0
- package/{scripts → src/main}/lib/watchdogHelpers.cjs +1 -1
- package/src/main/scheduler/prdParser.cjs +13 -0
- package/src/main/scheduler.cjs +1285 -143
- package/src/main/templates/PRD_AUTHORING.md +50 -0
- package/src/main/templates/project-pages-catalog.json +1 -1
- package/src/main/usage.cjs +21 -3
- package/src/preload/api.d.ts +92 -1
- package/src/preload/index.cjs +10 -0
- package/web/README.md +41 -0
- package/{scripts/render-project-pages.cjs → web/project-pages/render.cjs} +4 -4
- package/{scripts/render-project-pages → web/project-pages/renderer}/dist/renderer.cjs +1 -1
- package/{scripts/validate-project-pages-summary.cjs → web/project-pages/validate-summary.cjs} +5 -5
- package/dist/assets/AgentLibrary-DTFL7y8G.js +0 -3
- package/dist/assets/MarkdownEditor-DCIubYWf.js +0 -1
- package/dist/assets/Permissions-BiYZNGYW.js +0 -3
- package/dist/assets/Scheduler-DcLBiJBq.js +0 -14
- package/dist/assets/Settings-Cv-pRyms.js +0 -3
- package/dist/assets/settingsSchema-BJVciriw.js +0 -3
- /package/{scripts/project-pages-logic → web/project-pages/logic}/dist/logic.cjs +0 -0
|
@@ -0,0 +1,333 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* effectiveModelInfo.cjs — read-only observer answering "what will this
|
|
5
|
+
* Epic's agentType actually run as": the persona's model alias, the
|
|
6
|
+
* concrete model id evidence points at, and the provenance of each. Sits
|
|
7
|
+
* BESIDE agentModelResolve.cjs (which decides what a launch actually passes
|
|
8
|
+
* to `--model`) rather than replacing it — nothing here feeds a spawn
|
|
9
|
+
* decision, and agentModelResolve.cjs's own resolution is unchanged.
|
|
10
|
+
*
|
|
11
|
+
* Provenance is the entire point: a caller must be able to tell "we KNOW
|
|
12
|
+
* this alias last ran as claude-opus-5 because a real run resolved it" from
|
|
13
|
+
* "we don't know; the CLI resolves this alias at launch." The two must
|
|
14
|
+
* never collapse into one optimistic string.
|
|
15
|
+
*
|
|
16
|
+
* Effort-level resolution is deliberately NOT here — it already exists,
|
|
17
|
+
* unused, in the renderer's `useEffectiveSettings.ts` (a scope-chain reader
|
|
18
|
+
* with a file watcher). This module supplies only the half that genuinely
|
|
19
|
+
* needs main-process filesystem access: persona lookup (overlay-aware) and
|
|
20
|
+
* concrete-model evidence (scheduler run logs, session transcripts).
|
|
21
|
+
*
|
|
22
|
+
* Never throws — every lookup failure degrades a field to null, same
|
|
23
|
+
* contract as agentModelResolve.cjs's resolveEpicModel.
|
|
24
|
+
*/
|
|
25
|
+
|
|
26
|
+
const fs = require('node:fs');
|
|
27
|
+
const path = require('node:path');
|
|
28
|
+
const os = require('node:os');
|
|
29
|
+
const { splitFrontmatter } = require('./prdFrontmatter.cjs');
|
|
30
|
+
const { encodeCwd } = require('./encodeCwd.cjs');
|
|
31
|
+
|
|
32
|
+
// Mirrors rawSessionModel.ts's RAW_MODELS — duplicated rather than imported
|
|
33
|
+
// because that file is a renderer ES module and this is a main-process CJS
|
|
34
|
+
// module (CLAUDE.md: "No CommonJS in renderer, no ES modules in main").
|
|
35
|
+
// Anything NOT in this set is treated as an already-concrete pinned model id
|
|
36
|
+
// (e.g. 'claude-opus-5'), never as an alias needing evidence.
|
|
37
|
+
const KNOWN_MODEL_ALIASES = new Set(['opus', 'sonnet', 'haiku', 'fable']);
|
|
38
|
+
|
|
39
|
+
// Run-log evidence (scheduler.cjs's executeJob, ~line 4473) is written
|
|
40
|
+
// essentially at dispatch time, before any of the run's own stdout streams
|
|
41
|
+
// in — so a bounded HEAD read (not a tail) is both correct and cheap even
|
|
42
|
+
// against a run log that grows to tens of MB. O(min(size, cap)) time/space.
|
|
43
|
+
const MAX_RUN_LOG_HEAD_BYTES = 64 * 1024;
|
|
44
|
+
|
|
45
|
+
// Same bound epicDelegationStats.cjs's readTranscriptTail uses for a single
|
|
46
|
+
// pass over a transcript that can reach tens of MB. O(min(size, cap)).
|
|
47
|
+
const MAX_TRANSCRIPT_TAIL_BYTES = 8 * 1024 * 1024;
|
|
48
|
+
|
|
49
|
+
// Mirrors scheduler.cjs's own ROOT/RUNS_DIR constants (same convention as
|
|
50
|
+
// queueStore.cjs's LEGACY_QUEUE_PATH, which duplicates this literal rather
|
|
51
|
+
// than requiring scheduler.cjs's ~530KB module graph for one path).
|
|
52
|
+
function runsDir(homeDir) {
|
|
53
|
+
return path.join(homeDir, '.claude', 'session-manager', 'scheduled-plans', 'runs');
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function isConcreteModelId(modelAlias) {
|
|
57
|
+
return Boolean(modelAlias) && !KNOWN_MODEL_ALIASES.has(modelAlias);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* True when this app's spawn path (cleanEnv.cjs's cleanChildEnv) would strip
|
|
62
|
+
* an effort env var before a child ever saw it — i.e. whether the settings
|
|
63
|
+
* schema's env-var effort configuration path is actually reachable for an
|
|
64
|
+
* app-launched session. Derived by probing cleanChildEnv itself rather than
|
|
65
|
+
* hardcoding a boolean, so this self-corrects if that deletion list ever
|
|
66
|
+
* changes instead of silently drifting stale.
|
|
67
|
+
*/
|
|
68
|
+
function computeEffortReachable(deps) {
|
|
69
|
+
try {
|
|
70
|
+
const { cleanChildEnv } = deps.cleanEnv || require('./cleanEnv.cjs');
|
|
71
|
+
const probe = cleanChildEnv({ CLAUDE_EFFORT: 'high', CLAUDE_CODE_EFFORT_LEVEL: 'high' });
|
|
72
|
+
return Boolean(probe.CLAUDE_EFFORT || probe.CLAUDE_CODE_EFFORT_LEVEL);
|
|
73
|
+
} catch {
|
|
74
|
+
return false;
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* Resolves a persona's model alias, overlay-aware (agentLibrary.cjs's
|
|
80
|
+
* getPersonaBody: project overlay at `<cwd>/.claude/agents/<name>.md` wins
|
|
81
|
+
* over the global `~/.claude/agents/<name>.md` — the same precedence
|
|
82
|
+
* `resolvePrdPersonaForSpawn` already uses, unlike `listPersonas`/
|
|
83
|
+
* `readPersonaModel`, which only ever see the global directory).
|
|
84
|
+
*
|
|
85
|
+
* Never throws: a missing/unreadable persona (dangling agentType) resolves
|
|
86
|
+
* to `{ modelAlias: null, modelSource: 'fallback' }` without logging —
|
|
87
|
+
* agentModelResolve.cjs's own `logDanglingPersonaOnce`/`reportDanglingAgentTypeOnce`
|
|
88
|
+
* dedup-per-(cwd,agentType) already covers that diagnostic on the LAUNCH
|
|
89
|
+
* path; this read-only observer deliberately does not add a second logger
|
|
90
|
+
* for the same dangling reference.
|
|
91
|
+
*/
|
|
92
|
+
async function resolvePersonaModelAlias(cwd, agentType, deps) {
|
|
93
|
+
const getPersonaBody = deps.getPersonaBody || require('../agentLibrary.cjs').getPersonaBody;
|
|
94
|
+
const resolvePersonaPaths = deps.resolvePersonaPaths || require('./epicMint.cjs').resolvePersonaPaths;
|
|
95
|
+
|
|
96
|
+
let persona;
|
|
97
|
+
try {
|
|
98
|
+
persona = await getPersonaBody({ cwd, name: agentType });
|
|
99
|
+
} catch {
|
|
100
|
+
persona = null;
|
|
101
|
+
}
|
|
102
|
+
if (!persona) return { modelAlias: null, modelSource: 'fallback' };
|
|
103
|
+
|
|
104
|
+
const { fm } = splitFrontmatter(persona.text);
|
|
105
|
+
|
|
106
|
+
let fromOverlay = false;
|
|
107
|
+
try {
|
|
108
|
+
const { projectPath } = resolvePersonaPaths(cwd, agentType);
|
|
109
|
+
fromOverlay = Boolean(projectPath) && path.resolve(projectPath) === path.resolve(persona.path);
|
|
110
|
+
} catch {
|
|
111
|
+
// Best-effort overlay signal only — a resolution failure here just means
|
|
112
|
+
// "assume global", never a reason to fail the whole lookup.
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
if (fm.model && fm.model !== 'inherit') {
|
|
116
|
+
return { modelAlias: fm.model, modelSource: fromOverlay ? 'persona-overlay' : 'persona' };
|
|
117
|
+
}
|
|
118
|
+
return { modelAlias: null, modelSource: 'inherit' };
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
/** Best-effort read of `<cwd>/session-manager-operations/scheduler/state/queue.json`'s hot jobs[]. */
|
|
122
|
+
function readHotJobsForCwd(cwd, deps) {
|
|
123
|
+
const { projectQueuePath } = deps.queueStore || require('./queueStore.cjs');
|
|
124
|
+
try {
|
|
125
|
+
const data = JSON.parse(fs.readFileSync(projectQueuePath(cwd), 'utf8'));
|
|
126
|
+
return Array.isArray(data.jobs) ? data.jobs : [];
|
|
127
|
+
} catch {
|
|
128
|
+
return [];
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
/** Best-effort read of `<cwd>/session-manager-operations/scheduler/state/history.jsonl`'s archived rows. */
|
|
133
|
+
function readHistoryEntriesForCwd(cwd, deps) {
|
|
134
|
+
const { projectHistoryPath } = deps.queueStore || require('./queueStore.cjs');
|
|
135
|
+
let text;
|
|
136
|
+
try {
|
|
137
|
+
text = fs.readFileSync(projectHistoryPath(cwd), 'utf8');
|
|
138
|
+
} catch {
|
|
139
|
+
return [];
|
|
140
|
+
}
|
|
141
|
+
const entries = [];
|
|
142
|
+
for (const line of text.split('\n')) {
|
|
143
|
+
if (!line.trim()) continue;
|
|
144
|
+
try {
|
|
145
|
+
const j = JSON.parse(line);
|
|
146
|
+
// needs_review_entry/needs_review_resolution lines (needsReviewLedger.cjs)
|
|
147
|
+
// share history.jsonl with terminal job rows but carry no `agentType` —
|
|
148
|
+
// findLatestSchedulerModel's own `j.agentType === agentType` filter
|
|
149
|
+
// already excludes them, but skip explicitly here too so this reader
|
|
150
|
+
// stays correct even if a future ledger line shape happens to pick up
|
|
151
|
+
// an agentType-shaped field.
|
|
152
|
+
if (j?.kind && j.kind !== 'terminal') continue;
|
|
153
|
+
entries.push(j);
|
|
154
|
+
} catch {
|
|
155
|
+
// corrupt/partial line — skip
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
return entries;
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
function jobTimestampMs(job) {
|
|
162
|
+
return Date.parse(job?.finishedAt || job?.startedAt || job?.queuedAt || 0) || 0;
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
/** Bounded HEAD read of an absolute file path; null on any read failure. */
|
|
166
|
+
function readHead(filePath, maxBytes, deps) {
|
|
167
|
+
const statSync = deps.statSync || fs.statSync;
|
|
168
|
+
const openSync = deps.openSync || fs.openSync;
|
|
169
|
+
const readSync = deps.readSync || fs.readSync;
|
|
170
|
+
const closeSync = deps.closeSync || fs.closeSync;
|
|
171
|
+
try {
|
|
172
|
+
const size = statSync(filePath).size;
|
|
173
|
+
const length = Math.min(maxBytes, size);
|
|
174
|
+
const buffer = Buffer.alloc(length);
|
|
175
|
+
const fd = openSync(filePath, 'r');
|
|
176
|
+
try {
|
|
177
|
+
readSync(fd, buffer, 0, length, 0);
|
|
178
|
+
} finally {
|
|
179
|
+
closeSync(fd);
|
|
180
|
+
}
|
|
181
|
+
return buffer.toString('utf8');
|
|
182
|
+
} catch {
|
|
183
|
+
return null;
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
/** Bounded TAIL read of an absolute file path; null on any read failure. */
|
|
188
|
+
function readTail(filePath, maxBytes, deps) {
|
|
189
|
+
const statSync = deps.statSync || fs.statSync;
|
|
190
|
+
const openSync = deps.openSync || fs.openSync;
|
|
191
|
+
const readSync = deps.readSync || fs.readSync;
|
|
192
|
+
const closeSync = deps.closeSync || fs.closeSync;
|
|
193
|
+
try {
|
|
194
|
+
const size = statSync(filePath).size;
|
|
195
|
+
const start = Math.max(0, size - maxBytes);
|
|
196
|
+
const length = size - start;
|
|
197
|
+
const buffer = Buffer.alloc(length);
|
|
198
|
+
const fd = openSync(filePath, 'r');
|
|
199
|
+
try {
|
|
200
|
+
readSync(fd, buffer, 0, length, start);
|
|
201
|
+
} finally {
|
|
202
|
+
closeSync(fd);
|
|
203
|
+
}
|
|
204
|
+
const text = buffer.toString('utf8');
|
|
205
|
+
return start > 0 ? text.slice(text.indexOf('\n') + 1) : text;
|
|
206
|
+
} catch {
|
|
207
|
+
return null;
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
/**
|
|
212
|
+
* Evidence source 1: the scheduler already logs the persona-resolved model
|
|
213
|
+
* at dispatch (scheduler.cjs's executeJob, ~line 4473: `[scheduler]
|
|
214
|
+
* agentType=... persona=... model=...`). Scans this project's hot queue.json
|
|
215
|
+
* jobs[] AND its archived history.jsonl for the most recent job whose
|
|
216
|
+
* agentType matches, then reads that job's own run log for the line. O(J log
|
|
217
|
+
* J) to sort J candidate jobs, O(min(logSize, cap)) per log read.
|
|
218
|
+
*/
|
|
219
|
+
function findLatestSchedulerModel(cwd, agentType, deps) {
|
|
220
|
+
const jobs = [...readHotJobsForCwd(cwd, deps), ...readHistoryEntriesForCwd(cwd, deps)];
|
|
221
|
+
const matches = jobs.filter((j) => j && j.agentType === agentType && j.runId && j.slug);
|
|
222
|
+
matches.sort((a, b) => jobTimestampMs(b) - jobTimestampMs(a));
|
|
223
|
+
|
|
224
|
+
const homeDir = deps.homeDir || os.homedir();
|
|
225
|
+
const re = /\[scheduler\] agentType=\S+ persona=.*? model=(\S+)/g;
|
|
226
|
+
for (const job of matches) {
|
|
227
|
+
const logPath = path.join(runsDir(homeDir), job.runId, `${job.slug}.log`);
|
|
228
|
+
const head = readHead(logPath, MAX_RUN_LOG_HEAD_BYTES, deps);
|
|
229
|
+
if (!head) continue;
|
|
230
|
+
let match;
|
|
231
|
+
let last = null;
|
|
232
|
+
while ((match = re.exec(head))) last = match[1];
|
|
233
|
+
if (last) return last;
|
|
234
|
+
}
|
|
235
|
+
return null;
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
/**
|
|
239
|
+
* Evidence source 2: the most recent Epic (active-index.json entry) created
|
|
240
|
+
* with this agentType, read via its own claude-session transcript's last
|
|
241
|
+
* `"model":"..."` occurrence. O(E log E) to sort E candidate Epics,
|
|
242
|
+
* O(min(transcriptSize, cap)) per transcript read.
|
|
243
|
+
*/
|
|
244
|
+
function findLatestTranscriptModel(cwd, agentType, deps) {
|
|
245
|
+
const readActiveIndex = deps.readActiveIndex || require('./epicMint.cjs').readActiveIndex;
|
|
246
|
+
let sessions;
|
|
247
|
+
try {
|
|
248
|
+
({ sessions } = readActiveIndex(cwd));
|
|
249
|
+
} catch {
|
|
250
|
+
return null;
|
|
251
|
+
}
|
|
252
|
+
const matches = Object.values(sessions || {}).filter((s) => s && s.agentType === agentType && s.claudeSessionId);
|
|
253
|
+
matches.sort((a, b) => (Date.parse(b.createdAt || 0) || 0) - (Date.parse(a.createdAt || 0) || 0));
|
|
254
|
+
|
|
255
|
+
const homeDir = deps.homeDir || os.homedir();
|
|
256
|
+
const encodeCwdFn = deps.encodeCwd || encodeCwd;
|
|
257
|
+
const re = /"model":"([^"]+)"/g;
|
|
258
|
+
for (const session of matches) {
|
|
259
|
+
const filePath = path.join(homeDir, '.claude', 'projects', encodeCwdFn(cwd), `${session.claudeSessionId}.jsonl`);
|
|
260
|
+
const tail = readTail(filePath, MAX_TRANSCRIPT_TAIL_BYTES, deps);
|
|
261
|
+
if (!tail) continue;
|
|
262
|
+
let match;
|
|
263
|
+
let last = null;
|
|
264
|
+
while ((match = re.exec(tail))) last = match[1];
|
|
265
|
+
if (last) return last;
|
|
266
|
+
}
|
|
267
|
+
return null;
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
/**
|
|
271
|
+
* resolveEffectiveModelInfo({ cwd, agentType, deps? }) →
|
|
272
|
+
* { agentType, modelAlias, modelSource, resolvedModelId, resolvedFrom, effortReachable }
|
|
273
|
+
*
|
|
274
|
+
* Never throws. `cwd` is normalized through the same worktree/ops-internal
|
|
275
|
+
* -aware resolver every ops-root reader goes through, so a job worktree cwd
|
|
276
|
+
* resolves to its main project tree before anything is looked up.
|
|
277
|
+
*/
|
|
278
|
+
async function resolveEffectiveModelInfo({ cwd, agentType, deps = {} } = {}) {
|
|
279
|
+
const effortReachable = computeEffortReachable(deps);
|
|
280
|
+
const miss = {
|
|
281
|
+
agentType: agentType || null,
|
|
282
|
+
modelAlias: null,
|
|
283
|
+
modelSource: 'fallback',
|
|
284
|
+
resolvedModelId: null,
|
|
285
|
+
resolvedFrom: null,
|
|
286
|
+
effortReachable,
|
|
287
|
+
};
|
|
288
|
+
if (!cwd || !agentType) return miss;
|
|
289
|
+
|
|
290
|
+
try {
|
|
291
|
+
const { projectRootOf } = deps.activeSessions || require('./activeSessions.cjs');
|
|
292
|
+
const normalizedCwd = projectRootOf(path.resolve(cwd));
|
|
293
|
+
if (!normalizedCwd) return miss;
|
|
294
|
+
|
|
295
|
+
const { modelAlias, modelSource } = await resolvePersonaModelAlias(normalizedCwd, agentType, deps);
|
|
296
|
+
const result = { agentType, modelAlias, modelSource, resolvedModelId: null, resolvedFrom: null, effortReachable };
|
|
297
|
+
|
|
298
|
+
if (isConcreteModelId(modelAlias)) {
|
|
299
|
+
// Already a concrete pinned id — echo it back, fabricate no evidence.
|
|
300
|
+
result.resolvedModelId = modelAlias;
|
|
301
|
+
return result;
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
// Evidence is never derived via historyAggregator.cjs's resolvePricingKey
|
|
305
|
+
// (which buckets any unknown id to 'sonnet' with estimated:true) —
|
|
306
|
+
// deliberately: that is a pricing heuristic, and borrowing it here would
|
|
307
|
+
// turn "we don't know" into a confident wrong answer.
|
|
308
|
+
const schedulerHit = findLatestSchedulerModel(normalizedCwd, agentType, deps);
|
|
309
|
+
if (schedulerHit) {
|
|
310
|
+
result.resolvedModelId = schedulerHit;
|
|
311
|
+
result.resolvedFrom = 'scheduler-run';
|
|
312
|
+
return result;
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
const transcriptHit = findLatestTranscriptModel(normalizedCwd, agentType, deps);
|
|
316
|
+
if (transcriptHit) {
|
|
317
|
+
result.resolvedModelId = transcriptHit;
|
|
318
|
+
result.resolvedFrom = 'transcript';
|
|
319
|
+
return result;
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
return result;
|
|
323
|
+
} catch {
|
|
324
|
+
return miss;
|
|
325
|
+
}
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
module.exports = {
|
|
329
|
+
resolveEffectiveModelInfo,
|
|
330
|
+
isConcreteModelId,
|
|
331
|
+
computeEffortReachable,
|
|
332
|
+
KNOWN_MODEL_ALIASES,
|
|
333
|
+
};
|
|
@@ -37,7 +37,7 @@
|
|
|
37
37
|
|
|
38
38
|
const os = require('node:os');
|
|
39
39
|
const path = require('node:path');
|
|
40
|
-
const { worktreeMainRootOf } = require('
|
|
40
|
+
const { worktreeMainRootOf } = require('./activeSessions.cjs');
|
|
41
41
|
const { KIND_CONFIG } = require('./gitWorktree.cjs');
|
|
42
42
|
|
|
43
43
|
const TMPDIR = path.resolve(os.tmpdir());
|
|
@@ -161,7 +161,7 @@ function resolvePersonaPaths(cwd, agentType, deps = {}) {
|
|
|
161
161
|
* a readable `.md` persona file, checking the project overlay before the
|
|
162
162
|
* global agents dir. Never throws (a traversal attempt or missing file is
|
|
163
163
|
* treated as "doesn't exist", same fail-closed spirit as
|
|
164
|
-
* agentModelResolve.cjs's
|
|
164
|
+
* agentModelResolve.cjs's readOverlayAwarePersonaModel).
|
|
165
165
|
*/
|
|
166
166
|
function personaFileExists(cwd, agentType, deps = {}) {
|
|
167
167
|
const projectDir = deps.projectDir || path.join(cwd, '.claude', 'agents');
|
|
@@ -289,8 +289,8 @@ function ensureEpic(cwd, { goalText, tag, epicId: explicitEpicId, status = 'prop
|
|
|
289
289
|
}
|
|
290
290
|
|
|
291
291
|
// WRITE-TIME FK CHECK (throw on write, report on read — see this file's
|
|
292
|
-
// header + agentModelResolve.cjs's
|
|
293
|
-
// side of this same FK and stays permissive on purpose). A mint whose
|
|
292
|
+
// header + agentModelResolve.cjs's readOverlayAwarePersonaModel, which is
|
|
293
|
+
// the READ side of this same FK and stays permissive on purpose). A mint whose
|
|
294
294
|
// agentType names a persona that doesn't resolve is a bug in the caller
|
|
295
295
|
// (a typo, or a persona deleted between the picker loading and the mint
|
|
296
296
|
// landing) — refuse it now rather than silently orphaning the Epic.
|
|
@@ -54,6 +54,7 @@ const path = require('node:path');
|
|
|
54
54
|
const os = require('node:os');
|
|
55
55
|
const crypto = require('node:crypto');
|
|
56
56
|
const { execFile } = require('node:child_process');
|
|
57
|
+
const { OPS_ROOT_DIR } = require('./opsOwnership.cjs');
|
|
57
58
|
|
|
58
59
|
// Per-kind configuration. Roots are kept OUTSIDE any project's own tree
|
|
59
60
|
// (os.tmpdir(), not `<cwd>/.git/...`) so a managed worktree never shows up in
|
|
@@ -304,7 +305,7 @@ async function removeWorktreeDir(cwd, dir) {
|
|
|
304
305
|
* <name>` segment stripped off.
|
|
305
306
|
*
|
|
306
307
|
* Round-trip verified before being trusted, mirroring the same defense
|
|
307
|
-
* `
|
|
308
|
+
* `src/main/lib/activeSessions.cjs`'s `worktreeMainRootOf` already applies: a
|
|
308
309
|
* `.git` pointer file is a plain text file, so its content must not be
|
|
309
310
|
* trusted to redirect `git worktree remove --force` at an arbitrary path
|
|
310
311
|
* without the candidate main tree's OWN admin dir
|
|
@@ -481,29 +482,58 @@ async function sweepStaleWorktreeCheckouts(kind, opts = {}) {
|
|
|
481
482
|
* copy of the human's WIP stays in `cwd` the whole time. Nothing here ever
|
|
482
483
|
* `git add`s or commits the carried paths on the job's behalf, and nothing
|
|
483
484
|
* ever restores them back into `cwd` — they were never removed from it.
|
|
485
|
+
*
|
|
486
|
+
* The diff itself is captured via `git diff --output=<file>` so git writes
|
|
487
|
+
* the patch straight to disk — never through Node's stdout buffer. A prior
|
|
488
|
+
* version piped the patch through `execFile`'s default-`maxBuffer` stdout
|
|
489
|
+
* capture, which overflowed on a real dirty tree of ~240 changed paths
|
|
490
|
+
* (`stdout maxBuffer length exceeded`, job 1192) and silently fell back to
|
|
491
|
+
* running the job in the SHARED working tree — the exact moment isolation
|
|
492
|
+
* matters most. `--output` removes the buffer ceiling entirely regardless of
|
|
493
|
+
* how large the tree's outstanding diff is. The temp patch file lives
|
|
494
|
+
* inside `dir` (the job's own fresh worktree checkout, not a bare
|
|
495
|
+
* `os.tmpdir()` path) so it is guaranteed to be cleaned up either by the
|
|
496
|
+
* explicit `fsp.rm` below or, on any earlier failure, by the caller tearing
|
|
497
|
+
* the whole `dir` down.
|
|
484
498
|
*/
|
|
485
499
|
async function captureAndCarryBaseDiff({ cwd, dir }) {
|
|
486
500
|
let paths = [];
|
|
487
|
-
let patch = '';
|
|
488
501
|
try {
|
|
489
502
|
const nameOut = await execGit(['diff', 'HEAD', '--name-only'], { cwd, timeout: 15_000 });
|
|
490
503
|
paths = nameOut.split('\n').map((l) => l.trim()).filter(Boolean);
|
|
491
|
-
patch = await execGit(['diff', 'HEAD', '--binary'], { cwd, timeout: 30_000 });
|
|
492
504
|
} catch (e) {
|
|
493
|
-
return { ok: false, reason: `
|
|
505
|
+
return { ok: false, reason: `listing changed paths failed: ${(e && (e.stderrText || e.message)) || e}` };
|
|
494
506
|
}
|
|
495
|
-
if (!patch || !patch.trim()) return { ok: true, paths: [] };
|
|
496
507
|
|
|
497
|
-
|
|
508
|
+
// Everything from here on operates on a real (or attempted) file at
|
|
509
|
+
// `patchFile` — wrapped in one try/finally so the temp file is removed on
|
|
510
|
+
// EVERY exit path, including a `git diff` that errors after partially
|
|
511
|
+
// writing it, not just the clean-success path.
|
|
512
|
+
const patchFile = path.join(dir, `.sm-worktree-carry-${crypto.randomBytes(8).toString('hex')}.patch`);
|
|
498
513
|
try {
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
514
|
+
try {
|
|
515
|
+
await execGit(['diff', 'HEAD', '--binary', `--output=${patchFile}`], { cwd, timeout: 120_000 });
|
|
516
|
+
} catch (e) {
|
|
517
|
+
return { ok: false, reason: `capturing base diff failed: ${(e && (e.stderrText || e.message)) || e}` };
|
|
518
|
+
}
|
|
519
|
+
|
|
520
|
+
let stat;
|
|
521
|
+
try {
|
|
522
|
+
stat = await fsp.stat(patchFile);
|
|
523
|
+
} catch (e) {
|
|
524
|
+
return { ok: false, reason: `reading captured diff file failed: ${(e && e.message) || e}` };
|
|
525
|
+
}
|
|
526
|
+
if (stat.size === 0) return { ok: true, paths: [] };
|
|
527
|
+
|
|
528
|
+
try {
|
|
529
|
+
await execGit(['apply', '--binary', patchFile], { cwd: dir, timeout: 30_000 });
|
|
530
|
+
} catch (e) {
|
|
531
|
+
return { ok: false, reason: `git apply failed: ${(e && (e.stderrText || e.message)) || e}` };
|
|
532
|
+
}
|
|
533
|
+
return { ok: true, paths };
|
|
503
534
|
} finally {
|
|
504
535
|
try { await fsp.rm(patchFile, { force: true }); } catch { /* best-effort */ }
|
|
505
536
|
}
|
|
506
|
-
return { ok: true, paths };
|
|
507
537
|
}
|
|
508
538
|
|
|
509
539
|
/**
|
|
@@ -1273,7 +1303,7 @@ async function reclaimTerminalJobOrphans({ cwd, terminalSlugs, isLive }) {
|
|
|
1273
1303
|
}
|
|
1274
1304
|
const dirtyPaths = statusOut.split('\n').map((l) => l.slice(3).trim()).filter(Boolean);
|
|
1275
1305
|
const opsOnly = dirtyPaths.every(
|
|
1276
|
-
(p) => p ===
|
|
1306
|
+
(p) => p === OPS_ROOT_DIR || p.startsWith(`${OPS_ROOT_DIR}/`)
|
|
1277
1307
|
);
|
|
1278
1308
|
if (!opsOnly) continue; // real non-ops dirty content present — leave for a human
|
|
1279
1309
|
|
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* guardShims.cjs — stable, app-upgrade-proof indirection for the three
|
|
3
|
+
* PreToolUse guard hooks that delegationReadiness.cjs installs into an
|
|
4
|
+
* adopting project's `.claude/settings.json`.
|
|
5
|
+
*
|
|
6
|
+
* Why this exists: those installers used to bake
|
|
7
|
+
* `path.resolve(__dirname, '../../../scripts/hooks/guard-*.cjs')` — THIS
|
|
8
|
+
* app's own copy of the script — directly into the hook command. Run via
|
|
9
|
+
* `npx claude-code-session-manager@latest` (the normal install path), that
|
|
10
|
+
* resolves under `~/.npm/_npx/<hash>/node_modules/claude-code-session-manager/`
|
|
11
|
+
* — a directory scoped to ONE npx resolution. The next `npx ...@latest` (a
|
|
12
|
+
* new release) or an npm cache prune creates/removes a different hash dir, so
|
|
13
|
+
* every previously-installed hook decays into a dead path. A PreToolUse
|
|
14
|
+
* command pointing at a missing script exits non-zero WITHOUT exit code 2 (a
|
|
15
|
+
* non-blocking error to the harness), so it silently guards nothing — while
|
|
16
|
+
* checkPrdWriteGuard correctly flips the readiness banner back to red. That
|
|
17
|
+
* decay is why "Delegation not ready" reappears across projects even after a
|
|
18
|
+
* successful install.
|
|
19
|
+
*
|
|
20
|
+
* Fix: install a STABLE shim under the user's home
|
|
21
|
+
* (`~/.claude/session-manager/hooks/guard-<name>.cjs`) that never moves
|
|
22
|
+
* across app upgrades. The shim contains no guard logic of its own — CLAUDE.md's
|
|
23
|
+
* "adopt by REFERENCE, never vendor" law — it reads a pointer file
|
|
24
|
+
* (`app-root.json`, rewritten on every app boot with the CURRENTLY running
|
|
25
|
+
* app's root) and `require()`s the real `scripts/hooks/guard-<name>.cjs` from
|
|
26
|
+
* there. An app upgrade (a new npx hash dir) just rewrites the pointer on the
|
|
27
|
+
* next boot; every already-installed project hook keeps resolving through
|
|
28
|
+
* the same shim path with no re-install.
|
|
29
|
+
*
|
|
30
|
+
* The guard scripts run their logic as a require-time side effect (stdin ->
|
|
31
|
+
* decision -> process.exit, not gated on `require.main === module`), so a
|
|
32
|
+
* plain `require()` from the shim is enough to invoke them.
|
|
33
|
+
*/
|
|
34
|
+
'use strict';
|
|
35
|
+
|
|
36
|
+
const fs = require('node:fs');
|
|
37
|
+
const path = require('node:path');
|
|
38
|
+
const os = require('node:os');
|
|
39
|
+
const { writeJson, writeTextAtomic, addAllowedRoot } = require('../config.cjs');
|
|
40
|
+
|
|
41
|
+
// src/main/lib -> app root (three levels up) — the same resolution
|
|
42
|
+
// delegationReadiness.cjs's *_GUARD_SCRIPT constants use.
|
|
43
|
+
const APP_ROOT = path.resolve(__dirname, '..', '..', '..');
|
|
44
|
+
|
|
45
|
+
const GUARD_NAMES = ['guard-prd-writes.cjs', 'guard-destructive-git.cjs', 'guard-inline-implementation.cjs', 'guard-self-schedule.cjs'];
|
|
46
|
+
|
|
47
|
+
function hooksDir(homeDir = os.homedir()) {
|
|
48
|
+
return path.join(homeDir, '.claude', 'session-manager', 'hooks');
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function pointerPath(homeDir = os.homedir()) {
|
|
52
|
+
return path.join(hooksDir(homeDir), 'app-root.json');
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function shimPath(guardName, homeDir = os.homedir()) {
|
|
56
|
+
return path.join(hooksDir(homeDir), guardName);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function shimBody(guardName) {
|
|
60
|
+
return `'use strict';
|
|
61
|
+
const fs = require('node:fs');
|
|
62
|
+
const path = require('node:path');
|
|
63
|
+
const ptr = JSON.parse(fs.readFileSync(path.join(__dirname, 'app-root.json'), 'utf8'));
|
|
64
|
+
require(path.join(ptr.appRoot, 'scripts', 'hooks', '${guardName}'));
|
|
65
|
+
`;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* Write the pointer file + all three shims under `homeDir`. Idempotent: a
|
|
70
|
+
* re-run with the same `appRoot` produces byte-identical files; a re-run with
|
|
71
|
+
* a DIFFERENT `appRoot` (an app upgrade) rewrites only the pointer, which is
|
|
72
|
+
* exactly what lets already-installed project hooks follow the upgrade with
|
|
73
|
+
* no re-install. Never throws — returns `{ ok, error }` so a boot-time caller
|
|
74
|
+
* can fire-and-forget and an installer can refuse cleanly rather than writing
|
|
75
|
+
* a hook command that points at a shim that doesn't exist.
|
|
76
|
+
*/
|
|
77
|
+
async function writeGuardShims({ homeDir = os.homedir(), appRoot = APP_ROOT } = {}) {
|
|
78
|
+
try {
|
|
79
|
+
addAllowedRoot(homeDir);
|
|
80
|
+
await writeJson(pointerPath(homeDir), { appRoot });
|
|
81
|
+
for (const name of GUARD_NAMES) {
|
|
82
|
+
await writeTextAtomic(shimPath(name, homeDir), shimBody(name));
|
|
83
|
+
}
|
|
84
|
+
return { ok: true };
|
|
85
|
+
} catch (err) {
|
|
86
|
+
return { ok: false, error: err?.message ?? String(err) };
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/** True when every shim + the pointer file already exist on disk under `homeDir`. */
|
|
91
|
+
function guardShimsExist(homeDir = os.homedir()) {
|
|
92
|
+
return fs.existsSync(pointerPath(homeDir)) && GUARD_NAMES.every((name) => fs.existsSync(shimPath(name, homeDir)));
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
* Given an absolute script path a hook command resolved to, determine
|
|
97
|
+
* whether it's actually a guard SHIM (its directory carries a sibling
|
|
98
|
+
* `app-root.json`) rather than a direct app-root script. A direct script has
|
|
99
|
+
* no pointer and is trivially "real" once `fs.existsSync` is true — the
|
|
100
|
+
* caller doesn't need this function for that case.
|
|
101
|
+
*
|
|
102
|
+
* For a shim, existence of the shim FILE is not enough: the shim's own
|
|
103
|
+
* require() target is `ptr.appRoot/scripts/hooks/<name>.cjs`, resolved only
|
|
104
|
+
* at hook-invocation time. If the pointer is missing, unparseable, or now
|
|
105
|
+
* names a script that's gone, the shim silently guards nothing the next time
|
|
106
|
+
* a hook actually fires — decay one hop downstream of the hook command
|
|
107
|
+
* itself. Returns `null` when `scriptAbsPath` is not a shim (no sibling
|
|
108
|
+
* pointer) — the caller's plain existence check already covers that case.
|
|
109
|
+
* Throws when it IS a shim and the pointer has decayed.
|
|
110
|
+
*/
|
|
111
|
+
function resolveShimTarget(scriptAbsPath) {
|
|
112
|
+
const dir = path.dirname(scriptAbsPath);
|
|
113
|
+
const ptrFile = path.join(dir, 'app-root.json');
|
|
114
|
+
if (!fs.existsSync(ptrFile)) return null;
|
|
115
|
+
|
|
116
|
+
const guardName = path.basename(scriptAbsPath);
|
|
117
|
+
let appRoot;
|
|
118
|
+
try {
|
|
119
|
+
appRoot = JSON.parse(fs.readFileSync(ptrFile, 'utf8'))?.appRoot;
|
|
120
|
+
} catch (err) {
|
|
121
|
+
throw new Error(`guard shim pointer ${ptrFile} is unparseable: ${err?.message ?? err}`);
|
|
122
|
+
}
|
|
123
|
+
if (typeof appRoot !== 'string' || !appRoot) {
|
|
124
|
+
throw new Error(`guard shim pointer ${ptrFile} has no appRoot`);
|
|
125
|
+
}
|
|
126
|
+
const real = path.join(appRoot, 'scripts', 'hooks', guardName);
|
|
127
|
+
if (!fs.existsSync(real)) {
|
|
128
|
+
throw new Error(`guard shim pointer ${ptrFile} resolves to a script that no longer exists: ${real}`);
|
|
129
|
+
}
|
|
130
|
+
return real;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
/**
|
|
134
|
+
* `writeGuardShims({ homeDir })`, but returns `null` on success and an
|
|
135
|
+
* install-function-shaped error object (`{ ok:false, action:'error', error }`)
|
|
136
|
+
* on failure — the one call every `install*Guard` in delegationReadiness.cjs
|
|
137
|
+
* needs before it can safely write a hook command pointing at the shim.
|
|
138
|
+
*/
|
|
139
|
+
async function ensureGuardShimsOrError(homeDir) {
|
|
140
|
+
const result = await writeGuardShims({ homeDir });
|
|
141
|
+
if (result.ok) return null;
|
|
142
|
+
return { ok: false, action: 'error', error: `could not write the stable guard shim under ${hooksDir(homeDir)}: ${result.error}` };
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
module.exports = {
|
|
146
|
+
APP_ROOT,
|
|
147
|
+
GUARD_NAMES,
|
|
148
|
+
hooksDir,
|
|
149
|
+
pointerPath,
|
|
150
|
+
shimPath,
|
|
151
|
+
shimBody,
|
|
152
|
+
writeGuardShims,
|
|
153
|
+
guardShimsExist,
|
|
154
|
+
resolveShimTarget,
|
|
155
|
+
ensureGuardShimsOrError,
|
|
156
|
+
};
|
|
@@ -27,8 +27,13 @@
|
|
|
27
27
|
* must still park.
|
|
28
28
|
*/
|
|
29
29
|
|
|
30
|
-
/**
|
|
31
|
-
|
|
30
|
+
/**
|
|
31
|
+
* The one app-owned root. Matches at any depth, and only as a path segment.
|
|
32
|
+
* Sourced from opsOwnership.cjs (the single-writer law's own root name)
|
|
33
|
+
* rather than a second hardcoded literal — this file matches git-porcelain
|
|
34
|
+
* RELATIVE paths, never builds an fs path, so opsPath() itself doesn't apply.
|
|
35
|
+
*/
|
|
36
|
+
const { OPS_ROOT_DIR: OPS_ROOT_SEGMENT } = require('./opsOwnership.cjs');
|
|
32
37
|
|
|
33
38
|
/** True when `p` lives under an app-owned operations root. Pure. */
|
|
34
39
|
function isAppOwnedChurn(p) {
|
|
@@ -309,9 +309,10 @@ function evaluateLaunchGate(block, { now, claudeVersion } = {}) {
|
|
|
309
309
|
* closed set so operators never have to open a transcript to tell a
|
|
310
310
|
* non-start from an implementation failure from a verifier downgrade.
|
|
311
311
|
*/
|
|
312
|
-
function deriveTerminalReason({ effectiveStatus, exitCode, verifyResult, sigtermOverride, worktreeIntegrationFailure }) {
|
|
312
|
+
function deriveTerminalReason({ effectiveStatus, exitCode, verifyResult, sigtermOverride, worktreeIntegrationFailure, budgetKill }) {
|
|
313
313
|
if (worktreeIntegrationFailure) return 'worktree_integration_failed';
|
|
314
314
|
if (effectiveStatus === 'completed') return 'completed';
|
|
315
|
+
if (budgetKill) return 'budget_exceeded';
|
|
315
316
|
if (sigtermOverride) return 'signal_kill_with_commit';
|
|
316
317
|
if (exitCode === 143 || exitCode === 137) return 'signal_kill';
|
|
317
318
|
if (typeof exitCode === 'number' && exitCode !== 0) return `impl_failed:exit_${exitCode}`;
|
|
@@ -75,7 +75,10 @@ const MCP_TOOL_CATALOG = [
|
|
|
75
75
|
+ '`tag` and `agentType` are two DIFFERENT, independent fields — `tag` is WHAT KIND '
|
|
76
76
|
+ 'of work this PRD is (feature/bug/build/...); `agentType` is WHO executes it (a '
|
|
77
77
|
+ 'persona name, e.g. "dev-lead"). `agentType` defaults to "dev-lead" when omitted, '
|
|
78
|
-
+ 'and is rejected at write time if it does not name a real persona file.'
|
|
78
|
+
+ 'and is rejected at write time if it does not name a real persona file. '
|
|
79
|
+
+ 'If this PRD has no dependsOn of its own AND the target Epic already has incomplete PRDs, '
|
|
80
|
+
+ '`disposition` ("append" or "new-head") is REQUIRED — the write is refused without it (a '
|
|
81
|
+
+ 'headless job caller instead gets a logged "append" default, since no human is present to ask).',
|
|
79
82
|
whenToUse: 'Use whenever new work should be queued into an already-approved Epic — this is the /develop path.',
|
|
80
83
|
whenNotToUse: 'TWO DISTINCT FAILURE MODES if this tool is not usable — do not conflate them: '
|
|
81
84
|
+ '(a) this tool call is PRESENT in your tool list but ERRORS as app-not-running / admin '
|