claude-code-session-manager 0.74.0 → 0.75.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/AgentLibrary-CGplUKxY.js +3 -0
- package/dist/assets/DataModel-DQ15KxVb.js +1 -0
- package/dist/assets/{History-ShwkUTny.js → History-v2TSC_6A.js} +2 -2
- package/dist/assets/{Hooks-Dc5y1rZH.js → Hooks-Co7s6BNB.js} +2 -2
- package/dist/assets/HostBilko-DJj8FWPK.js +1 -0
- package/dist/assets/{Library-BOL6GqFX.js → Library-DQaC9VQ1.js} +1 -1
- package/dist/assets/ListDetail-BwFRfAZm.js +1 -0
- package/dist/assets/MarkdownEditor-C155gNpc.js +1 -0
- package/dist/assets/{McpServers-DuOTxmc4.js → McpServers-BmLgE_gS.js} +2 -2
- package/dist/assets/{Memory-BYJHDz7W.js → Memory-ISwNcS5j.js} +4 -4
- package/dist/assets/Panel-1adjEpj-.js +1 -0
- package/dist/assets/{Permissions-DhYERDVR.js → Permissions-FrCz5cGa.js} +3 -3
- package/dist/assets/Plugins-zUssKp_b.js +2 -0
- package/dist/assets/ProvenanceBadge-CAXqA_HX.js +1 -0
- package/dist/assets/SaveBar-D9cqms3Y.js +1 -0
- package/dist/assets/{Scheduler-Bu6kSkl3.js → Scheduler-dNNc9S11.js} +6 -6
- package/dist/assets/ScopeSwitcher-1wBTne7S.js +1 -0
- package/dist/assets/Settings-BT_expIg.js +3 -0
- package/dist/assets/{SkillReferenceGraph-rKaTHmOg.js → SkillReferenceGraph-CVccq4Cu.js} +1 -1
- package/dist/assets/{Skills-BHlH2uG5.js → Skills-xFpRY0ld.js} +2 -2
- package/dist/assets/SystemPrompt-CEk_71th.js +1 -0
- package/dist/assets/TagLibrary-C2zhihQG.js +1 -0
- package/dist/assets/{TiptapBody-XoQHiqfF.js → TiptapBody-CQKcHKaD.js} +1 -1
- package/dist/assets/Toggle-CQ-IBb8m.js +1 -0
- package/dist/assets/{index-Cs_Lq9-w.js → index-BgbV3sKX.js} +659 -655
- package/dist/assets/{index-D9C8N3pn.css → index-Dbo9R4e0.css} +1 -1
- package/dist/assets/{settingsSchema-BTDJcu2x.js → settingsSchema-D6ccSKqm.js} +1 -1
- package/dist/index.html +2 -2
- package/package.json +3 -1
- package/scripts/scheduler-mcp-server.cjs +471 -0
- package/src/main/__tests__/scheduler-autofix-outcome.test.cjs +116 -0
- package/src/main/__tests__/scheduler-autofix-select.test.cjs +113 -7
- package/src/main/__tests__/scheduler-fix-plan-path.test.cjs +119 -0
- package/src/main/__tests__/seedAgentPersonas.test.cjs +105 -0
- package/src/main/__tests__/seedSchedulerMcp.test.cjs +145 -0
- package/src/main/__tests__/seedStatus.test.cjs +100 -0
- package/src/main/agentLibrary.cjs +43 -0
- package/src/main/index.cjs +42 -1
- package/src/main/ipcSchemas.cjs +25 -0
- package/src/main/lib/__tests__/delegationReadiness.test.cjs +185 -0
- package/src/main/lib/__tests__/epicDelegationStats.test.cjs +137 -0
- package/src/main/lib/delegationReadiness.cjs +141 -0
- package/src/main/lib/epicDelegationStats.cjs +124 -0
- package/src/main/lib/promptSessionSchema.cjs +1 -1
- package/src/main/scheduler.cjs +136 -27
- package/src/main/seedAgentPersonas.cjs +92 -0
- package/src/main/seedDevPlugin.cjs +14 -2
- package/src/main/seedSchedulerMcp.cjs +148 -0
- package/src/main/seedStatus.cjs +59 -0
- package/src/preload/api.d.ts +40 -1
- package/src/preload/index.cjs +7 -0
- package/src/seed/agents/architect.md +64 -0
- package/src/seed/agents/dev-lead.md +42 -0
- package/src/seed/prompts/code-review/ac-coverage-check.md +8 -0
- package/src/seed/prompts/code-review/correctness-only.md +8 -0
- package/src/seed/prompts/code-review/full-spectrum-high.md +8 -0
- package/src/seed/prompts/code-review/hallucination-check.md +8 -0
- package/src/seed/prompts/code-review/public-api-compat.md +8 -0
- package/src/seed/prompts/code-review/readability-naming.md +8 -0
- package/src/seed/prompts/debugging/bug-as-failing-test.md +8 -0
- package/src/seed/prompts/debugging/git-bisect-regression.md +8 -0
- package/src/seed/prompts/debugging/instrument-intermittent-bug.md +8 -0
- package/src/seed/prompts/debugging/localize-pipeline-failure.md +8 -0
- package/src/seed/prompts/debugging/reproduce-then-diagnose.md +8 -0
- package/src/seed/prompts/documentation/adr-from-change.md +8 -0
- package/src/seed/prompts/documentation/module-readme.md +8 -0
- package/src/seed/prompts/documentation/onboarding-plan.md +8 -0
- package/src/seed/prompts/documentation/refresh-claude-md.md +8 -0
- package/src/seed/prompts/documentation/tsdoc-public-exports.md +8 -0
- package/src/seed/prompts/git-pr/conventional-commit.md +8 -0
- package/src/seed/prompts/git-pr/draft-pr-title-body.md +8 -0
- package/src/seed/prompts/git-pr/pre-commit-safety-sweep.md +8 -0
- package/src/seed/prompts/git-pr/release-notes-block.md +8 -0
- package/src/seed/prompts/git-pr/split-large-pr.md +8 -0
- package/src/seed/prompts/performance/bundle-startup-audit.md +8 -0
- package/src/seed/prompts/performance/complexity-audit.md +8 -0
- package/src/seed/prompts/performance/cpu-profile-hot-path.md +8 -0
- package/src/seed/prompts/performance/db-query-plan-review.md +8 -0
- package/src/seed/prompts/performance/memory-leak-hunt.md +8 -0
- package/src/seed/prompts/qa/api-contract-tests.md +8 -0
- package/src/seed/prompts/qa/e2e-critical-path.md +8 -0
- package/src/seed/prompts/qa/failing-test-for-bug.md +8 -0
- package/src/seed/prompts/qa/find-missing-test-coverage.md +8 -0
- package/src/seed/prompts/qa/stabilize-flaky-test.md +8 -0
- package/src/seed/prompts/qa/tdd-red-first.md +8 -0
- package/src/seed/prompts/qa/visual-regression-review.md +8 -0
- package/src/seed/prompts/qa/wcag-axe-scan.md +8 -0
- package/src/seed/prompts/refactoring/dead-code-sweep.md +8 -0
- package/src/seed/prompts/refactoring/extract-duplicated-pattern.md +8 -0
- package/src/seed/prompts/refactoring/modernize-legacy-file.md +8 -0
- package/src/seed/prompts/refactoring/reduce-cyclomatic-complexity.md +8 -0
- package/src/seed/prompts/refactoring/tighten-module-boundaries.md +8 -0
- package/src/seed/prompts/security/authz-audit.md +8 -0
- package/src/seed/prompts/security/crypto-correctness.md +8 -0
- package/src/seed/prompts/security/cwe-top-25-hunt.md +8 -0
- package/src/seed/prompts/security/dependency-audit.md +8 -0
- package/src/seed/prompts/security/ipc-boundary-hardening.md +8 -0
- package/src/seed/prompts/security/owasp-top-10-staged-diff.md +8 -0
- package/src/seed/prompts/security/secret-credential-scan.md +8 -0
- package/dist/assets/AgentLibrary-CSnWV6uv.js +0 -3
- package/dist/assets/HostBilko-evd1hD8q.js +0 -1
- package/dist/assets/ListDetail-BPJb2et3.js +0 -1
- package/dist/assets/MarkdownEditor-CACsHVuf.js +0 -1
- package/dist/assets/Panel-n-p8OQLc.js +0 -1
- package/dist/assets/Plugins-D1ckVgo3.js +0 -2
- package/dist/assets/ProvenanceBadge-ByTjDwAX.js +0 -1
- package/dist/assets/SaveBar-_ORUCmZw.js +0 -1
- package/dist/assets/ScopeSwitcher-Css6Sg7Y.js +0 -1
- package/dist/assets/Settings-DlyJjexf.js +0 -3
- package/dist/assets/SystemPrompt-DsgF8xsl.js +0 -1
- package/dist/assets/TagLibrary-Q5QsKMes.js +0 -1
- package/dist/assets/Toggle-BJHhHQOq.js +0 -1
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Derived-at-read-time counters answering "did this Epic delegate work into
|
|
5
|
+
* PRDs, or did the interactive session just edit application source
|
|
6
|
+
* inline?" — no new writer, no new namespace; both numbers are computed
|
|
7
|
+
* from state that already exists on disk (single-writer law,
|
|
8
|
+
* opsOwnership.cjs).
|
|
9
|
+
*
|
|
10
|
+
* prdsQueued reads the Epic's own live PRD dir
|
|
11
|
+
* (session-manager-operations/scheduler/epics/<epicId>/prds/) — the
|
|
12
|
+
* "prds-archived" sibling deliberately does NOT count here: this is a
|
|
13
|
+
* live-delegation signal, not a lifetime PRD count (that's epicPrds() in
|
|
14
|
+
* the renderer, sourced from the scheduler's own snapshot).
|
|
15
|
+
*
|
|
16
|
+
* inlineEdits reads the Epic's OWN interactive claude session transcript —
|
|
17
|
+
* ~/.claude/projects/<encodeCwd(cwd)>/<claudeSessionId>.jsonl, the same file
|
|
18
|
+
* transcripts.cjs already tails for the live feed — and counts Write/Edit/
|
|
19
|
+
* NotebookEdit tool_use blocks whose file_path resolves under this repo's
|
|
20
|
+
* src/, scripts/, plugins/, or bin/. That transcript already carries real
|
|
21
|
+
* tool_use blocks (classifyTranscriptLine.cjs); the durable
|
|
22
|
+
* session-manager-operations/prompt-sessions/transcripts/<epicId>.jsonl
|
|
23
|
+
* store does NOT (it holds only rendered {role, text} turns, verified by
|
|
24
|
+
* reading one), so it is not a usable source for this counter.
|
|
25
|
+
*/
|
|
26
|
+
|
|
27
|
+
const fs = require('node:fs');
|
|
28
|
+
const os = require('node:os');
|
|
29
|
+
const path = require('node:path');
|
|
30
|
+
const { encodeCwd } = require('./encodeCwd.cjs');
|
|
31
|
+
const { classifyLine } = require('./classifyTranscriptLine.cjs');
|
|
32
|
+
|
|
33
|
+
// Same cap transcripts.cjs's readDelta uses for a single pass — bounds
|
|
34
|
+
// memory/time on a very large transcript instead of reading it whole.
|
|
35
|
+
const MAX_TRANSCRIPT_BYTES = 8 * 1024 * 1024;
|
|
36
|
+
|
|
37
|
+
const INLINE_EDIT_TOOL_NAMES = new Set(['Write', 'Edit', 'NotebookEdit']);
|
|
38
|
+
const APPLICATION_SOURCE_PREFIXES = ['src/', 'scripts/', 'plugins/', 'bin/'];
|
|
39
|
+
|
|
40
|
+
function countPrdsQueued(cwd, epicId, deps = {}) {
|
|
41
|
+
const readdirSync = deps.readdirSync || fs.readdirSync;
|
|
42
|
+
try {
|
|
43
|
+
const dir = path.join(cwd, 'session-manager-operations', 'scheduler', 'epics', epicId, 'prds');
|
|
44
|
+
return readdirSync(dir).filter((name) => name.endsWith('.md')).length;
|
|
45
|
+
} catch {
|
|
46
|
+
return 0;
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/** True when `filePath` (absolute or relative) resolves to a path under
|
|
51
|
+
* cwd's src/, scripts/, plugins/, or bin/ — never a dotfile/root config,
|
|
52
|
+
* never a path outside the repo entirely. */
|
|
53
|
+
function isApplicationSourcePath(cwd, filePath) {
|
|
54
|
+
if (typeof filePath !== 'string' || !filePath) return false;
|
|
55
|
+
const rel = path.relative(cwd, path.resolve(cwd, filePath)).split(path.sep).join('/');
|
|
56
|
+
if (rel.startsWith('..')) return false;
|
|
57
|
+
return APPLICATION_SOURCE_PREFIXES.some((prefix) => rel === prefix.slice(0, -1) || rel.startsWith(prefix));
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/** Reads at most the trailing MAX_TRANSCRIPT_BYTES of `filePath`, dropping
|
|
61
|
+
* a truncated leading partial line when the read didn't start at byte 0. */
|
|
62
|
+
function readTranscriptTail(filePath, deps = {}) {
|
|
63
|
+
const statSync = deps.statSync || fs.statSync;
|
|
64
|
+
const openSync = deps.openSync || fs.openSync;
|
|
65
|
+
const readSync = deps.readSync || fs.readSync;
|
|
66
|
+
const closeSync = deps.closeSync || fs.closeSync;
|
|
67
|
+
|
|
68
|
+
const size = statSync(filePath).size;
|
|
69
|
+
const start = Math.max(0, size - MAX_TRANSCRIPT_BYTES);
|
|
70
|
+
const length = size - start;
|
|
71
|
+
const buffer = Buffer.alloc(length);
|
|
72
|
+
const fd = openSync(filePath, 'r');
|
|
73
|
+
try {
|
|
74
|
+
readSync(fd, buffer, 0, length, start);
|
|
75
|
+
} finally {
|
|
76
|
+
closeSync(fd);
|
|
77
|
+
}
|
|
78
|
+
const text = buffer.toString('utf8');
|
|
79
|
+
return start > 0 ? text.slice(text.indexOf('\n') + 1) : text;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
function countInlineEdits(cwd, claudeSessionId, deps = {}) {
|
|
83
|
+
if (!claudeSessionId) return 0;
|
|
84
|
+
const readFn = deps.readTranscriptTail || readTranscriptTail;
|
|
85
|
+
const homeDir = deps.homeDir || os.homedir();
|
|
86
|
+
try {
|
|
87
|
+
const filePath = path.join(homeDir, '.claude', 'projects', encodeCwd(cwd), `${claudeSessionId}.jsonl`);
|
|
88
|
+
const text = readFn(filePath, deps);
|
|
89
|
+
let count = 0;
|
|
90
|
+
for (const line of text.split('\n')) {
|
|
91
|
+
const trimmed = line.trim();
|
|
92
|
+
if (!trimmed) continue;
|
|
93
|
+
let obj;
|
|
94
|
+
try {
|
|
95
|
+
obj = JSON.parse(trimmed);
|
|
96
|
+
} catch {
|
|
97
|
+
continue;
|
|
98
|
+
}
|
|
99
|
+
for (const event of classifyLine(obj)) {
|
|
100
|
+
if (event.kind !== 'tool_use') continue;
|
|
101
|
+
const { name, input } = event.data || {};
|
|
102
|
+
if (!INLINE_EDIT_TOOL_NAMES.has(name)) continue;
|
|
103
|
+
if (isApplicationSourcePath(cwd, input?.file_path)) count += 1;
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
return count;
|
|
107
|
+
} catch {
|
|
108
|
+
return 0;
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
function computeEpicDelegationStats(cwd, epicId, claudeSessionId, deps = {}) {
|
|
113
|
+
return {
|
|
114
|
+
prdsQueued: countPrdsQueued(cwd, epicId, deps),
|
|
115
|
+
inlineEdits: countInlineEdits(cwd, claudeSessionId, deps),
|
|
116
|
+
};
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
module.exports = {
|
|
120
|
+
computeEpicDelegationStats,
|
|
121
|
+
countPrdsQueued,
|
|
122
|
+
countInlineEdits,
|
|
123
|
+
isApplicationSourcePath,
|
|
124
|
+
};
|
|
@@ -41,7 +41,7 @@ const EpicSourceSchema = z.object({
|
|
|
41
41
|
// data so the Epic's first turn can render a structured AIM briefing card
|
|
42
42
|
// instead of regex-parsing the flat string back apart.
|
|
43
43
|
const EpicIntakeSectionSchema = z.object({
|
|
44
|
-
kind: z.enum(['actor', 'injection', 'input', 'mission', 'goal', 'reference']),
|
|
44
|
+
kind: z.enum(['actor', 'persona-body', 'injection', 'input', 'mission', 'goal', 'reference']),
|
|
45
45
|
label: z.string(),
|
|
46
46
|
text: z.string(),
|
|
47
47
|
source: z.string().optional(),
|
package/src/main/scheduler.cjs
CHANGED
|
@@ -2289,6 +2289,37 @@ async function resolveVerifyPrdPath(job) {
|
|
|
2289
2289
|
return liveDir ? safeSlugPathIn(liveDir, job.slug) : null;
|
|
2290
2290
|
}
|
|
2291
2291
|
|
|
2292
|
+
/**
|
|
2293
|
+
* resolveFixPlanPath(failedJob) → { fixPath, fixSlug, group, livePrdDir }
|
|
2294
|
+
*
|
|
2295
|
+
* Where spawnInvestigation's auto-fix plan for `failedJob` must be written.
|
|
2296
|
+
* The plan belongs in the SAME dir the original PRD lives in right now
|
|
2297
|
+
* (`livePrdDir`, from resolveVerifyPrdPath's live-dir search — usually the
|
|
2298
|
+
* originating Epic's `epics/<epicId>/prds/`), never the retired flat dir:
|
|
2299
|
+
* consolidateFlatPrds (prdMigration.cjs) sweeps only the flat dir and
|
|
2300
|
+
* archives any `.md` there with no live queue row, which a freshly-authored
|
|
2301
|
+
* fix plan never has yet — so writing it flat means the very next
|
|
2302
|
+
* reconcile() pass (src/main/scheduler.cjs's consolidateAllFlatPrds call)
|
|
2303
|
+
* archives it before it can ever be enqueued. `livePrdDir` is null (and the
|
|
2304
|
+
* flat dir, `prdDirForCwd`, is used instead) only when the original PRD
|
|
2305
|
+
* can't be found live anywhere on disk — an archived twin is deliberately
|
|
2306
|
+
* NOT treated as a live location for this purpose.
|
|
2307
|
+
*
|
|
2308
|
+
* Exported as a pure(ish) helper (one fs-free branch, one await) so tests
|
|
2309
|
+
* can prove the round trip — fix plan lands beside its original and
|
|
2310
|
+
* survives a consolidateFlatPrds(cwd) pass — without spawning a real
|
|
2311
|
+
* investigation.
|
|
2312
|
+
*/
|
|
2313
|
+
async function resolveFixPlanPath(failedJob) {
|
|
2314
|
+
const livePrdPath = await resolveVerifyPrdPath(failedJob);
|
|
2315
|
+
const livePrdDir = livePrdPath ? path.dirname(livePrdPath) : null;
|
|
2316
|
+
const baseSlug = failedJob.slug.replace(/^\d+-/, '');
|
|
2317
|
+
const group = failedJob.parallelGroup ?? 99;
|
|
2318
|
+
const fixSlug = `${String(group).padStart(2, '0')}-fix-${baseSlug}`;
|
|
2319
|
+
const fixPath = path.join(livePrdDir ?? prdDirForCwd(failedJob.cwd), `${fixSlug}.md`);
|
|
2320
|
+
return { fixPath, fixSlug, group, livePrdDir };
|
|
2321
|
+
}
|
|
2322
|
+
|
|
2292
2323
|
/**
|
|
2293
2324
|
* notifyOriginatingTab(job) → void
|
|
2294
2325
|
*
|
|
@@ -2980,6 +3011,16 @@ function isFixPlanSlug(slug) {
|
|
|
2980
3011
|
return /^\d+-fix-/.test(slug);
|
|
2981
3012
|
}
|
|
2982
3013
|
|
|
3014
|
+
/**
|
|
3015
|
+
* The fix-plan slug spawnInvestigation authors for a given failed job —
|
|
3016
|
+
* shared by selectAutoFixTargets (queue-membership check) and
|
|
3017
|
+
* reverifyNeedsReview's autofix_plan_unqueued annotation (same check, run
|
|
3018
|
+
* one or more passes later) so both use one slug formula.
|
|
3019
|
+
*/
|
|
3020
|
+
function fixSlugFor(job) {
|
|
3021
|
+
return `${String(job.parallelGroup ?? 99).padStart(2, '0')}-fix-${job.slug.replace(/^\d+-/, '')}`;
|
|
3022
|
+
}
|
|
3023
|
+
|
|
2983
3024
|
/**
|
|
2984
3025
|
* Returns true for statuses that a fix-plan completion should promote
|
|
2985
3026
|
* (clear) on the original job. Both 'failed' and 'needs_review' are
|
|
@@ -3165,9 +3206,18 @@ async function spawnInvestigation(failedJob, runDir) {
|
|
|
3165
3206
|
const failedLogPath = path.join(runDir, `${failedJob.slug}.log`);
|
|
3166
3207
|
const investigationLogPath = path.join(runDir, `${failedJob.slug}.investigation.log`);
|
|
3167
3208
|
|
|
3209
|
+
// livePrdDir is the SAME dir the original PRD lives in right now (usually
|
|
3210
|
+
// its Epic-scoped `epics/<epicId>/prds/`) — the fix plan must land there
|
|
3211
|
+
// too, since consolidateFlatPrds only ever sweeps the retired flat dir
|
|
3212
|
+
// (prdDirForCwd) and would archive a freshly-authored fix plan on the very
|
|
3213
|
+
// next reconcile() pass before it ever gets a queue row. Only fall back to
|
|
3214
|
+
// the flat dir when the original PRD can't be found on disk at all — an
|
|
3215
|
+
// archived twin (archivedPrdPathForJob) is deliberately NOT used for the
|
|
3216
|
+
// write dir, only for reading originalBody below.
|
|
3168
3217
|
let originalBody = '';
|
|
3218
|
+
const { fixPath, fixSlug, group, livePrdDir } = await resolveFixPlanPath(failedJob);
|
|
3169
3219
|
try {
|
|
3170
|
-
const originalPath = (
|
|
3220
|
+
const originalPath = livePrdDir ? path.join(livePrdDir, `${failedJob.slug}.md`) : archivedPrdPathForJob(failedJob);
|
|
3171
3221
|
originalBody = (await parsePrd(originalPath)).body;
|
|
3172
3222
|
} catch {
|
|
3173
3223
|
originalBody = failedJob.bodyPreview || '(original PRD missing from disk)';
|
|
@@ -3175,11 +3225,6 @@ async function spawnInvestigation(failedJob, runDir) {
|
|
|
3175
3225
|
|
|
3176
3226
|
const logTail = readTail(failedLogPath, 16 * 1024) || '(failed to read log)';
|
|
3177
3227
|
|
|
3178
|
-
const baseSlug = failedJob.slug.replace(/^\d+-/, '');
|
|
3179
|
-
const group = failedJob.parallelGroup ?? 99;
|
|
3180
|
-
const fixSlug = `${String(group).padStart(2, '0')}-fix-${baseSlug}`;
|
|
3181
|
-
const fixPath = path.join(prdDirForCwd(failedJob.cwd), `${fixSlug}.md`);
|
|
3182
|
-
|
|
3183
3228
|
if (fs.existsSync(fixPath)) {
|
|
3184
3229
|
console.log(`[scheduler] skip investigation: fix plan already exists at ${fixPath}`);
|
|
3185
3230
|
releaseSlot();
|
|
@@ -3281,6 +3326,13 @@ async function spawnInvestigation(failedJob, runDir) {
|
|
|
3281
3326
|
? `investigation spawn failed: ${error?.message ?? String(error)}`
|
|
3282
3327
|
: `investigation error: ${error.message}`;
|
|
3283
3328
|
sl(`\n[scheduler] ${errMsg}\n`);
|
|
3329
|
+
// Stamp the outcome even on this early exit — otherwise the job is
|
|
3330
|
+
// left with autoFixAttempted: true and no outcome, which used to
|
|
3331
|
+
// read as a permanent dead-end (see selectAutoFixTargets).
|
|
3332
|
+
mutate((s) => {
|
|
3333
|
+
const j = s.jobs.find((x) => x.slug === failedJob.slug);
|
|
3334
|
+
if (j) j.autoFixOutcome = 'error';
|
|
3335
|
+
}).catch(() => {});
|
|
3284
3336
|
return;
|
|
3285
3337
|
}
|
|
3286
3338
|
sl(`\n[scheduler] investigation exit code=${exitCode}\n`);
|
|
@@ -3301,6 +3353,10 @@ async function spawnInvestigation(failedJob, runDir) {
|
|
|
3301
3353
|
}
|
|
3302
3354
|
if (fs.existsSync(fixPath)) {
|
|
3303
3355
|
console.log(`[scheduler] investigation produced fix plan: ${fixSlug}`);
|
|
3356
|
+
mutate((s) => {
|
|
3357
|
+
const j = s.jobs.find((x) => x.slug === failedJob.slug);
|
|
3358
|
+
if (j) j.autoFixOutcome = 'plan';
|
|
3359
|
+
}).catch(() => {});
|
|
3304
3360
|
} else {
|
|
3305
3361
|
console.log(`[scheduler] investigation finished WITHOUT producing fix plan (slug=${failedJob.slug}, code=${exitCode})`);
|
|
3306
3362
|
// Record the no-plan outcome so selectAutoFixTargets can offer one
|
|
@@ -3711,10 +3767,10 @@ async function spawnJob(job, runId, runDir, defaultCwd) {
|
|
|
3711
3767
|
process.env.SM_AUTOFIX_DISABLE !== '1' &&
|
|
3712
3768
|
isEligibleForImmediateAutoFix(s.jobs[i2], s.jobs, fixSlugExists)
|
|
3713
3769
|
) {
|
|
3714
|
-
const
|
|
3770
|
+
const isRetryAttempt = s.jobs[i2].autoFixAttempted === true;
|
|
3715
3771
|
s.jobs[i2].autoFixAttempted = true;
|
|
3716
3772
|
if (!s.jobs[i2].runId) s.jobs[i2].runId = runId;
|
|
3717
|
-
if (
|
|
3773
|
+
if (isRetryAttempt) {
|
|
3718
3774
|
s.jobs[i2].autoFixRetries = (s.jobs[i2].autoFixRetries ?? 0) + 1;
|
|
3719
3775
|
delete s.jobs[i2].autoFixOutcome;
|
|
3720
3776
|
}
|
|
@@ -4337,6 +4393,35 @@ function isUnresolvableNeedsReview(job, { hasRunDir }) {
|
|
|
4337
4393
|
return !!job && job.status === 'needs_review' && !job.runId && !hasRunDir;
|
|
4338
4394
|
}
|
|
4339
4395
|
|
|
4396
|
+
/**
|
|
4397
|
+
* Pure predicate: an auto-fix attempt whose one bounded retry is already
|
|
4398
|
+
* spent, and whose outcome was NOT 'plan' (a produced plan is judged by
|
|
4399
|
+
* isPlanUnqueued instead — a plan is never retried, only annotated if it
|
|
4400
|
+
* never reaches the queue). Covers all three exhaustible outcomes
|
|
4401
|
+
* ('no-plan', 'error', and unstamped/undefined) — mirrors the retry
|
|
4402
|
+
* eligibility rule in selectAutoFixTargets so a job can never be retry-
|
|
4403
|
+
* eligible there and simultaneously un-annotatable here.
|
|
4404
|
+
*/
|
|
4405
|
+
function isExhaustedAutoFix(job) {
|
|
4406
|
+
return !!job && job.status === 'needs_review'
|
|
4407
|
+
&& job.autoFixAttempted === true
|
|
4408
|
+
&& job.autoFixOutcome !== 'plan'
|
|
4409
|
+
&& (job.autoFixRetries ?? 0) >= 1;
|
|
4410
|
+
}
|
|
4411
|
+
|
|
4412
|
+
/**
|
|
4413
|
+
* Pure predicate: an investigation produced a fix plan (autoFixOutcome ===
|
|
4414
|
+
* 'plan') but its fix-plan slug is not present among `queuedSlugs` — the
|
|
4415
|
+
* plan file failed to become a queue row (write failure, or some other
|
|
4416
|
+
* reason reconcile() never ingested it). `queuedSlugs` is the caller's
|
|
4417
|
+
* current job-slug set, passed in rather than read here so this stays pure.
|
|
4418
|
+
*/
|
|
4419
|
+
function isPlanUnqueued(job, queuedSlugs) {
|
|
4420
|
+
if (!job || job.status !== 'needs_review') return false;
|
|
4421
|
+
if (job.autoFixOutcome !== 'plan') return false;
|
|
4422
|
+
return !queuedSlugs.has(fixSlugFor(job));
|
|
4423
|
+
}
|
|
4424
|
+
|
|
4340
4425
|
/**
|
|
4341
4426
|
* Pure predicate: is this job eligible for the boot re-verify self-heal? Only
|
|
4342
4427
|
* needs_review jobs with a run log (own or backfilled via resolveRunId) AND a
|
|
@@ -4417,9 +4502,13 @@ function isRescanCandidate(job) {
|
|
|
4417
4502
|
* - a runId, own or backfilled via resolveJobRunId (need a run log to investigate)
|
|
4418
4503
|
* - not itself a fix-plan slug (avoids infinite recursion)
|
|
4419
4504
|
* - autoFixAttempted cap: a job with no prior attempt is eligible; a job
|
|
4420
|
-
* whose prior attempt outcome was 'no-plan'
|
|
4421
|
-
* (
|
|
4422
|
-
*
|
|
4505
|
+
* whose prior attempt outcome was 'no-plan', 'error', or UNSTAMPED
|
|
4506
|
+
* (investigation exited without recording any outcome — unknown is not
|
|
4507
|
+
* "succeeded") gets ONE bounded retry (autoFixRetries < 1); a job whose
|
|
4508
|
+
* outcome was 'plan' (a fix plan was produced) is never retried here —
|
|
4509
|
+
* it either lands as a queue row on its own or gets flagged
|
|
4510
|
+
* autofix_plan_unqueued by reverifyNeedsReview's annotation pass; an
|
|
4511
|
+
* exhausted retry is excluded
|
|
4423
4512
|
* - no fix sibling on disk (fixSlugExists) or already in the queue
|
|
4424
4513
|
*/
|
|
4425
4514
|
function selectAutoFixTargets(jobs, { fixSlugExists, resolveJobRunId = resolveRunId }) {
|
|
@@ -4430,10 +4519,13 @@ function selectAutoFixTargets(jobs, { fixSlugExists, resolveJobRunId = resolveRu
|
|
|
4430
4519
|
if (!runId) return false;
|
|
4431
4520
|
if (isFixPlanBeyondDepthCap(job.slug, job.investigationDepth)) return false;
|
|
4432
4521
|
if (job.autoFixAttempted) {
|
|
4433
|
-
|
|
4522
|
+
const retryEligible = job.autoFixOutcome === 'no-plan'
|
|
4523
|
+
|| job.autoFixOutcome === 'error'
|
|
4524
|
+
|| job.autoFixOutcome == null;
|
|
4525
|
+
if (!retryEligible) return false;
|
|
4434
4526
|
if ((job.autoFixRetries ?? 0) >= 1) return false;
|
|
4435
4527
|
}
|
|
4436
|
-
const fixSlug =
|
|
4528
|
+
const fixSlug = fixSlugFor(job);
|
|
4437
4529
|
if (fixSlugExists(fixSlug)) return false;
|
|
4438
4530
|
if (slugsInQueue.has(fixSlug)) return false;
|
|
4439
4531
|
return true;
|
|
@@ -4561,20 +4653,29 @@ async function reverifyNeedsReview() {
|
|
|
4561
4653
|
|
|
4562
4654
|
// Surface needs_review jobs that can never self-heal or get an auto-fix
|
|
4563
4655
|
// investigation (no runId, no backfillable run dir) instead of leaving
|
|
4564
|
-
// them silently stranded. Also surface
|
|
4565
|
-
//
|
|
4656
|
+
// them silently stranded. Also surface auto-fix jobs whose one bounded
|
|
4657
|
+
// retry is already exhausted (outcome 'no-plan', 'error', or unstamped —
|
|
4658
|
+
// selectAutoFixTargets treats all three as retry-eligible, so all three
|
|
4659
|
+
// must also be annotatable once the retry is spent), and jobs whose
|
|
4660
|
+
// investigation DID produce a fix plan that never became a queue row
|
|
4661
|
+
// (write failed, or reconcile hasn't picked it up for some other reason).
|
|
4662
|
+
// All three classes are annotated, never looped on — a needs_review job
|
|
4663
|
+
// must not exit this pass both un-retried and un-annotated.
|
|
4566
4664
|
const afterHealForAnnotate = await readQueue();
|
|
4567
4665
|
const unresolvable = afterHealForAnnotate.jobs.filter(
|
|
4568
4666
|
(j) => isUnresolvableNeedsReview(j, { hasRunDir: !!resolveRunId(j) }) && j.verifierVerdict !== 'no_run_artifacts',
|
|
4569
4667
|
);
|
|
4570
|
-
const
|
|
4571
|
-
(j) => j.
|
|
4572
|
-
|
|
4573
|
-
|
|
4668
|
+
const exhaustedAutoFix = afterHealForAnnotate.jobs.filter(
|
|
4669
|
+
(j) => isExhaustedAutoFix(j) && j.verifierVerdict !== 'autofix_no_plan',
|
|
4670
|
+
);
|
|
4671
|
+
const queuedSlugs = new Set(afterHealForAnnotate.jobs.map((j) => j.slug));
|
|
4672
|
+
const planUnqueued = afterHealForAnnotate.jobs.filter(
|
|
4673
|
+
(j) => isPlanUnqueued(j, queuedSlugs) && j.verifierVerdict !== 'autofix_plan_unqueued',
|
|
4574
4674
|
);
|
|
4575
|
-
if (unresolvable.length ||
|
|
4675
|
+
if (unresolvable.length || exhaustedAutoFix.length || planUnqueued.length) {
|
|
4576
4676
|
const unresolvableSet = new Set(unresolvable.map((j) => j.slug));
|
|
4577
|
-
const exhaustedSet = new Set(
|
|
4677
|
+
const exhaustedSet = new Set(exhaustedAutoFix.map((j) => j.slug));
|
|
4678
|
+
const planUnqueuedSet = new Set(planUnqueued.map((j) => j.slug));
|
|
4578
4679
|
await mutate((s) => {
|
|
4579
4680
|
for (const j of s.jobs) {
|
|
4580
4681
|
if (unresolvableSet.has(j.slug)) {
|
|
@@ -4583,14 +4684,22 @@ async function reverifyNeedsReview() {
|
|
|
4583
4684
|
} else if (exhaustedSet.has(j.slug)) {
|
|
4584
4685
|
j.verifierVerdict = 'autofix_no_plan';
|
|
4585
4686
|
j.error = 'auto-fix investigation produced no fix plan after retry — manual review';
|
|
4687
|
+
} else if (planUnqueuedSet.has(j.slug)) {
|
|
4688
|
+
j.verifierVerdict = 'autofix_plan_unqueued';
|
|
4689
|
+
j.error = 'auto-fix investigation produced a fix plan that never reached the queue — manual review';
|
|
4586
4690
|
}
|
|
4587
4691
|
}
|
|
4588
4692
|
});
|
|
4589
4693
|
if (unresolvable.length) {
|
|
4590
4694
|
console.log(`[scheduler] boot reverify: no run artifacts for ${unresolvable.map((j) => j.slug).join(', ')} — flagged for manual review`);
|
|
4591
4695
|
}
|
|
4592
|
-
if (
|
|
4593
|
-
console.log(`[scheduler] boot reverify: auto-fix retry exhausted for ${
|
|
4696
|
+
if (exhaustedAutoFix.length) {
|
|
4697
|
+
console.log(`[scheduler] boot reverify: auto-fix retry exhausted for ${exhaustedAutoFix.map((j) => j.slug).join(', ')} — flagged for manual review`);
|
|
4698
|
+
}
|
|
4699
|
+
if (planUnqueued.length) {
|
|
4700
|
+
for (const j of planUnqueued) {
|
|
4701
|
+
console.log(`[scheduler] boot reverify: auto-fix plan never queued for ${j.slug} — flagged for manual review`);
|
|
4702
|
+
}
|
|
4594
4703
|
}
|
|
4595
4704
|
await broadcast();
|
|
4596
4705
|
}
|
|
@@ -4608,7 +4717,7 @@ async function reverifyNeedsReview() {
|
|
|
4608
4717
|
for (const job of targets) {
|
|
4609
4718
|
const runId = job.runId || resolveRunId(job);
|
|
4610
4719
|
const runDir = path.join(RUNS_DIR, runId);
|
|
4611
|
-
const
|
|
4720
|
+
const isRetryAttempt = job.autoFixAttempted === true;
|
|
4612
4721
|
// Persist the attempt BEFORE spawning — a crash mid-investigation still
|
|
4613
4722
|
// counts it (mirrors orphanRetries). Safe even when the slot is busy: the
|
|
4614
4723
|
// investigation is queued and drained as slots free, so it is genuinely
|
|
@@ -4618,13 +4727,13 @@ async function reverifyNeedsReview() {
|
|
|
4618
4727
|
if (j) {
|
|
4619
4728
|
j.autoFixAttempted = true;
|
|
4620
4729
|
if (!j.runId && runId) j.runId = runId;
|
|
4621
|
-
if (
|
|
4730
|
+
if (isRetryAttempt) {
|
|
4622
4731
|
j.autoFixRetries = (j.autoFixRetries ?? 0) + 1;
|
|
4623
4732
|
delete j.autoFixOutcome;
|
|
4624
4733
|
}
|
|
4625
4734
|
}
|
|
4626
4735
|
});
|
|
4627
|
-
console.log(`[scheduler] auto-fix: needs_review ${job.slug} → authoring fix-plan (${
|
|
4736
|
+
console.log(`[scheduler] auto-fix: needs_review ${job.slug} → authoring fix-plan (${isRetryAttempt ? 'retry' : '1/1'})`);
|
|
4628
4737
|
spawnInvestigation(job, runDir).catch((e) => {
|
|
4629
4738
|
console.error('[scheduler] auto-fix spawnInvestigation error', job.slug, e);
|
|
4630
4739
|
});
|
|
@@ -5712,4 +5821,4 @@ function registerAdminRoutes(adminHttp, remoteObj = remote) {
|
|
|
5712
5821
|
});
|
|
5713
5822
|
}
|
|
5714
5823
|
|
|
5715
|
-
module.exports = { findOverrunningJobs, JOB_OVERRUN_FACTOR, JOB_OVERRUN_FLOOR_MS, 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, consolidateAllFlatPrds, shouldSkipInvestigationForCleanRun, archiveCompletedPrd, retireCompletedSlugs, SCHEDULER_BOOTED_AT, SCHEDULER_CODE_SHA, resetJobFields, executeJob, prdArchivedSkipResult, spawnJob, listPrdsInternal, computeStallSummary, findStaleQuarantinedJobs, QUARANTINE_ESCALATE_MS };
|
|
5824
|
+
module.exports = { findOverrunningJobs, JOB_OVERRUN_FACTOR, JOB_OVERRUN_FLOOR_MS, 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, isExhaustedAutoFix, isPlanUnqueued, fixSlugFor, 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, resolveFixPlanPath, resolveNotifyPrd, runPrdMigration, consolidateAllFlatPrds, shouldSkipInvestigationForCleanRun, archiveCompletedPrd, retireCompletedSlugs, SCHEDULER_BOOTED_AT, SCHEDULER_CODE_SHA, resetJobFields, executeJob, prdArchivedSkipResult, spawnJob, listPrdsInternal, computeStallSummary, findStaleQuarantinedJobs, QUARANTINE_ESCALATE_MS };
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* First-boot seeder for the bundled Agent personas (Architect, Dev Lead).
|
|
3
|
+
*
|
|
4
|
+
* On a fresh install `~/.claude/agents/` is empty, so the Agent Library has
|
|
5
|
+
* nothing to offer — no persona to select for an Epic's Actor. This seeder
|
|
6
|
+
* copies the personas bundled at `src/seed/agents/*.md` into `~/.claude/agents/`
|
|
7
|
+
* on first boot, one file per persona, and NEVER overwrites a file that
|
|
8
|
+
* already exists there (hand-edited or user-authored personas win).
|
|
9
|
+
*
|
|
10
|
+
* Idempotent the same way as seedDevPlugin.cjs:
|
|
11
|
+
* 1. A marker file (`~/.claude/session-manager/.agent-personas-seeded`)
|
|
12
|
+
* records seed state. Once seeding SUCCEEDS we write `done` and never
|
|
13
|
+
* touch it again. A FAILED attempt only bumps an attempt counter and
|
|
14
|
+
* retries on the next few boots (bounded by MAX_ATTEMPTS), so a
|
|
15
|
+
* transient first-boot failure doesn't permanently skip the personas.
|
|
16
|
+
*
|
|
17
|
+
* Fire-and-forget: called post-window from index.cjs. Errors are logged,
|
|
18
|
+
* never thrown. Kill-switch: SM_SEED_AGENT_PERSONAS_DISABLE=1.
|
|
19
|
+
*/
|
|
20
|
+
|
|
21
|
+
const fs = require('node:fs');
|
|
22
|
+
const path = require('node:path');
|
|
23
|
+
const os = require('node:os');
|
|
24
|
+
const { writeJsonSync } = require('./config.cjs');
|
|
25
|
+
|
|
26
|
+
const PERSONAS = ['architect', 'dev-lead'];
|
|
27
|
+
const MAX_ATTEMPTS = 3; // give a transient first-boot failure a few chances
|
|
28
|
+
|
|
29
|
+
function bundledSourceDir() {
|
|
30
|
+
return path.join(__dirname, '..', 'seed', 'agents');
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function destAgentsDir() {
|
|
34
|
+
return path.join(os.homedir(), '.claude', 'agents');
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function markerPath() {
|
|
38
|
+
return path.join(os.homedir(), '.claude', 'session-manager', '.agent-personas-seeded');
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/** Read the marker → { done:boolean, attempts:number }. Absent = fresh. */
|
|
42
|
+
function readMarker() {
|
|
43
|
+
try {
|
|
44
|
+
const raw = fs.readFileSync(markerPath(), 'utf8').trim();
|
|
45
|
+
const m = JSON.parse(raw);
|
|
46
|
+
return { done: !!m.done, attempts: Number(m.attempts) || 0 };
|
|
47
|
+
} catch {
|
|
48
|
+
return { done: false, attempts: 0 };
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function writeMarker(state) {
|
|
53
|
+
try {
|
|
54
|
+
writeJsonSync(markerPath(), { ...state, ts: new Date().toISOString() });
|
|
55
|
+
} catch (err) {
|
|
56
|
+
console.warn('[seedAgentPersonas] could not write marker:', err?.message ?? err);
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
async function seedAgentPersonas({ logger = console, writeLog = () => {} } = {}) {
|
|
61
|
+
if (process.env.SM_SEED_AGENT_PERSONAS_DISABLE === '1') return;
|
|
62
|
+
const marker = readMarker();
|
|
63
|
+
if (marker.done) return; // succeeded before — leave it alone.
|
|
64
|
+
if (marker.attempts >= MAX_ATTEMPTS) return; // gave up — manual copy only.
|
|
65
|
+
|
|
66
|
+
try {
|
|
67
|
+
const srcDir = bundledSourceDir();
|
|
68
|
+
const destDir = destAgentsDir();
|
|
69
|
+
fs.mkdirSync(destDir, { recursive: true });
|
|
70
|
+
|
|
71
|
+
for (const name of PERSONAS) {
|
|
72
|
+
const src = path.join(srcDir, `${name}.md`);
|
|
73
|
+
const dest = path.join(destDir, `${name}.md`);
|
|
74
|
+
if (fs.existsSync(dest)) continue; // never overwrite an existing persona
|
|
75
|
+
fs.copyFileSync(src, dest);
|
|
76
|
+
logger.log?.(`[seedAgentPersonas] seeded ${name}.md`);
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
writeMarker({ done: true, attempts: marker.attempts });
|
|
80
|
+
} catch (err) {
|
|
81
|
+
logger.warn?.('[seedAgentPersonas] error:', err?.message ?? err);
|
|
82
|
+
writeLog({
|
|
83
|
+
scope: 'seed-agent-personas',
|
|
84
|
+
level: 'error',
|
|
85
|
+
message: 'seed error',
|
|
86
|
+
meta: { error: err?.message ?? String(err), attempt: marker.attempts + 1, maxAttempts: MAX_ATTEMPTS },
|
|
87
|
+
});
|
|
88
|
+
writeMarker({ done: false, attempts: marker.attempts + 1 });
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
module.exports = { seedAgentPersonas, markerPath, MAX_ATTEMPTS };
|
|
@@ -65,7 +65,7 @@ function writeMarker(state) {
|
|
|
65
65
|
}
|
|
66
66
|
}
|
|
67
67
|
|
|
68
|
-
async function seedDevPlugin({ logger = console } = {}) {
|
|
68
|
+
async function seedDevPlugin({ logger = console, writeLog = () => {} } = {}) {
|
|
69
69
|
if (process.env.SM_SEED_DEV_PLUGIN_DISABLE === '1') return;
|
|
70
70
|
const marker = readMarker();
|
|
71
71
|
if (marker.done) return; // succeeded before — leave it alone.
|
|
@@ -84,12 +84,24 @@ async function seedDevPlugin({ logger = console } = {}) {
|
|
|
84
84
|
} else {
|
|
85
85
|
// Failure: bump the attempt counter so the next boot can retry (bounded).
|
|
86
86
|
logger.warn?.(`[seedDevPlugin] install failed (exit ${r.exitCode})${r.error ? ` — ${r.error}` : ''}; attempt ${marker.attempts + 1}/${MAX_ATTEMPTS}`);
|
|
87
|
+
writeLog({
|
|
88
|
+
scope: 'seed-dev-plugin',
|
|
89
|
+
level: 'error',
|
|
90
|
+
message: 'install failed',
|
|
91
|
+
meta: { exitCode: r.exitCode, error: r.error, attempt: marker.attempts + 1, maxAttempts: MAX_ATTEMPTS },
|
|
92
|
+
});
|
|
87
93
|
writeMarker({ done: false, attempts: marker.attempts + 1 });
|
|
88
94
|
}
|
|
89
95
|
} catch (err) {
|
|
90
96
|
logger.warn?.('[seedDevPlugin] error:', err?.message ?? err);
|
|
97
|
+
writeLog({
|
|
98
|
+
scope: 'seed-dev-plugin',
|
|
99
|
+
level: 'error',
|
|
100
|
+
message: 'seed error',
|
|
101
|
+
meta: { error: err?.message ?? String(err), attempt: marker.attempts + 1, maxAttempts: MAX_ATTEMPTS },
|
|
102
|
+
});
|
|
91
103
|
writeMarker({ done: false, attempts: marker.attempts + 1 });
|
|
92
104
|
}
|
|
93
105
|
}
|
|
94
106
|
|
|
95
|
-
module.exports = { seedDevPlugin };
|
|
107
|
+
module.exports = { seedDevPlugin, markerPath, MAX_ATTEMPTS };
|