claude-code-session-manager 0.88.1 → 0.90.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-kQF8_wAw.js → AgentLibrary-CrmoSn9-.js} +1 -1
- package/dist/assets/DataModel-SPLQTR8E.js +1 -0
- package/dist/assets/{History-Sji2brb0.js → History-DT6S3abz.js} +2 -2
- package/dist/assets/{Hooks-CtqpO-p5.js → Hooks-CC2iv5Oq.js} +3 -3
- package/dist/assets/{HostBilko-DN0ydOCr.js → HostBilko-DqY7dA2a.js} +1 -1
- package/dist/assets/{Library-CoiLZTKh.js → Library-CUoOC77T.js} +1 -1
- package/dist/assets/{ListDetail-CmqiQ_io.js → ListDetail-DgrG72f4.js} +1 -1
- package/dist/assets/MarkdownEditor-C8dB3ZWU.js +1 -0
- package/dist/assets/{McpServers-Bj1X3_NT.js → McpServers-DupdjvFt.js} +2 -2
- package/dist/assets/{Memory-1PKjsM3b.js → Memory-C_4LhC2r.js} +6 -6
- package/dist/assets/{Panel-I1s6ghPo.js → Panel-DWmE5Fu9.js} +1 -1
- package/dist/assets/{Permissions-BhiiZjHQ.js → Permissions-hxiN2IWM.js} +3 -3
- package/dist/assets/{Plugins-gv3_fkPX.js → Plugins-fttEgZqy.js} +2 -2
- package/dist/assets/{ProvenanceBadge-BGcgBd8V.js → ProvenanceBadge-DROCzlbx.js} +1 -1
- package/dist/assets/{SaveBar-BGr9eZ4g.js → SaveBar-BZs0IQHq.js} +1 -1
- package/dist/assets/Scheduler-Bi4oZ8gg.js +16 -0
- package/dist/assets/{ScopeSwitcher-ukl0qWaA.js → ScopeSwitcher-C0538XU8.js} +1 -1
- package/dist/assets/{Settings-DJkFj22G.js → Settings-CcW6x-wu.js} +2 -2
- package/dist/assets/{SkillReferenceGraph-0t1Qblp6.js → SkillReferenceGraph-BtCcPAYO.js} +1 -1
- package/dist/assets/{Skills-CeVdBGE-.js → Skills-Daf_1sQR.js} +2 -2
- package/dist/assets/SystemPrompt-rYIla_wt.js +1 -0
- package/dist/assets/{TagLibrary-DAzNaUPd.js → TagLibrary-81Mr1RLU.js} +1 -1
- package/dist/assets/{TiptapBody--S_rGXoA.js → TiptapBody-BxuLNg1e.js} +1 -1
- package/dist/assets/{Toggle-Dszm0xnp.js → Toggle-CN8EVEai.js} +1 -1
- package/dist/assets/{index-Dp4Rc--q.js → index-A9TWmB7_.js} +345 -345
- package/dist/assets/{index-BHTX4OTc.css → index-Dh7BLxdA.css} +1 -1
- package/dist/assets/{settingsSchema-BLbssMYo.js → settingsSchema-C9I9a0jv.js} +1 -1
- package/dist/index.html +2 -2
- package/package.json +1 -1
- package/scripts/README.md +1 -0
- package/src/main/__tests__/chatRunner-session-flag-retry.test.cjs +159 -0
- package/src/main/__tests__/health-delegation-chain.test.cjs +3 -1
- package/src/main/__tests__/pollLoop-dispatch-on-failure.test.cjs +16 -4
- package/src/main/__tests__/scheduler-default-eligible-heal.test.cjs +10 -3
- package/src/main/__tests__/scheduler-integration-failure-stamp.test.cjs +41 -0
- package/src/main/__tests__/scheduler-looks-done.test.cjs +9 -2
- package/src/main/__tests__/scheduler-mechanical-recovery.test.cjs +23 -0
- package/src/main/__tests__/scheduler-needs-review-autoresolve.test.cjs +14 -6
- package/src/main/__tests__/scheduler-shard-quarantine.test.cjs +2 -2
- package/src/main/__tests__/scheduler-stranded-autofix-park.test.cjs +8 -1
- package/src/main/__tests__/transcripts-paged-reads.test.cjs +4 -2
- package/src/main/__tests__/transcripts-worktree-epic-path.test.cjs +154 -0
- package/src/main/__tests__/usageSingleFlight.test.cjs +3 -1
- package/src/main/build-info.json +4 -4
- package/src/main/chatRunner.cjs +108 -45
- package/src/main/health.cjs +20 -3
- package/src/main/ipcSchemas.cjs +3 -0
- package/src/main/lib/__tests__/epicSpawnCwd.test.cjs +34 -0
- package/src/main/lib/__tests__/epicSpawnPlan.test.cjs +117 -0
- package/src/main/lib/__tests__/epicTranscriptPath.test.cjs +163 -0
- package/src/main/lib/__tests__/gitWorktree.test.cjs +172 -0
- package/src/main/lib/__tests__/localAdminHttp.test.cjs +5 -1
- package/src/main/lib/__tests__/schedulerPathsWorktree.test.cjs +40 -1
- package/src/main/lib/__tests__/telemetryCountersMetadataColumn.test.cjs +8 -5
- package/src/main/lib/effectiveModelInfo.cjs +3 -3
- package/src/main/lib/epicDelegationStats.cjs +3 -2
- package/src/main/lib/epicSpawnCwd.cjs +36 -5
- package/src/main/lib/epicSpawnPlan.cjs +119 -0
- package/src/main/lib/epicTranscriptDiagnostic.cjs +79 -0
- package/src/main/lib/epicTranscriptPath.cjs +184 -0
- package/src/main/lib/gitWorktree.cjs +196 -4
- package/src/main/lib/mcpToolCatalog.cjs +6 -1
- package/src/main/lib/prdCreate.cjs +27 -1
- package/src/main/lib/prdFrontmatter.cjs +16 -5
- package/src/main/lib/rcaReport.cjs +35 -4
- package/src/main/lib/schedulerPaths.cjs +27 -4
- package/src/main/lib/telemetryClient.cjs +11 -4
- package/src/main/lib/terminalRunOutcome.cjs +1 -0
- package/src/main/pty.cjs +1 -1
- package/src/main/runVerify.cjs +89 -1
- package/src/main/scheduler.cjs +106 -18
- package/src/main/templates/PRD_AUTHORING.md +11 -0
- package/src/main/transcripts.cjs +28 -7
- package/src/preload/api.d.ts +2 -0
- package/src/preload/index.cjs +1 -0
- package/dist/assets/DataModel-CY_mMnsr.js +0 -1
- package/dist/assets/MarkdownEditor-HPoNeKhT.js +0 -1
- package/dist/assets/Scheduler-Bdo_awPq.js +0 -14
- package/dist/assets/SystemPrompt-CfKvNHi3.js +0 -1
|
@@ -374,12 +374,19 @@ async function persistSent() {
|
|
|
374
374
|
await config.writeJson(sentPath(), { ids: S.sentOrder, updatedAt: new Date().toISOString() }, { mode: 0o600 });
|
|
375
375
|
}
|
|
376
376
|
|
|
377
|
+
// Records are immutable once queued, so their serialized size is computed once. Without
|
|
378
|
+
// this, enforceCap() re-stringified the whole queue on every append: O(n^2) stringifies
|
|
379
|
+
// (~15s idle for 5000 appends) and the reason the cap tests blew their budget under load.
|
|
380
|
+
const recordBytesCache = new WeakMap();
|
|
377
381
|
function recordBytes(r) {
|
|
382
|
+
const hit = recordBytesCache.get(r);
|
|
383
|
+
if (hit !== undefined) return hit;
|
|
384
|
+
let n = 0;
|
|
378
385
|
try {
|
|
379
|
-
|
|
380
|
-
} catch {
|
|
381
|
-
|
|
382
|
-
|
|
386
|
+
n = Buffer.byteLength(JSON.stringify(r), 'utf8');
|
|
387
|
+
} catch { /* unserializable -> 0 */ }
|
|
388
|
+
recordBytesCache.set(r, n);
|
|
389
|
+
return n;
|
|
383
390
|
}
|
|
384
391
|
|
|
385
392
|
/** Oldest-first eviction, bounded by count and total bytes. O(n) per call, n <= QUEUE_CAP_COUNT. */
|
|
@@ -21,6 +21,7 @@ const COMPLETED_EQUIVALENT_VERDICTS = new Set([
|
|
|
21
21
|
'pass_no_commit_target_verified',
|
|
22
22
|
'pass_no_commit_already_shipped',
|
|
23
23
|
'pass_no_commit_prior_run_verified',
|
|
24
|
+
'pass_no_commit_artifact_verified',
|
|
24
25
|
]);
|
|
25
26
|
|
|
26
27
|
// Bounds the scan: only the newest few run dirs are stat'd per slug, never
|
package/src/main/pty.cjs
CHANGED
|
@@ -181,7 +181,7 @@ class PtyManager {
|
|
|
181
181
|
// returns its isolated worktree dir when one exists — `cwd` itself is
|
|
182
182
|
// NEVER repointed; only this actual PTY spawn option is. See
|
|
183
183
|
// epicSpawnCwd.cjs's header comment (the ops-root hazard) for why.
|
|
184
|
-
const spawnCwd = resolveEpicSpawnCwd({ cwd, claudeSessionId: tabId });
|
|
184
|
+
const spawnCwd = resolveEpicSpawnCwd({ cwd, claudeSessionId: tabId, deps: { restore: true } });
|
|
185
185
|
if (spawnCwd !== cwd) {
|
|
186
186
|
console.log(`[pty] tabId=${tabId} isolated in worktree ${spawnCwd}`);
|
|
187
187
|
}
|
package/src/main/runVerify.cjs
CHANGED
|
@@ -40,6 +40,7 @@
|
|
|
40
40
|
const fs = require('node:fs');
|
|
41
41
|
const path = require('node:path');
|
|
42
42
|
const { execFileSync } = require('node:child_process');
|
|
43
|
+
const { parsePrdFile } = require('./lib/prdFrontmatter.cjs');
|
|
43
44
|
|
|
44
45
|
const VERDICTS_SCHEMA_VERSION = 1;
|
|
45
46
|
|
|
@@ -778,6 +779,57 @@ function allDeliverablesAlreadyTracked({ cwd, paths, execImpl = execFileSync, ti
|
|
|
778
779
|
}
|
|
779
780
|
}
|
|
780
781
|
|
|
782
|
+
|
|
783
|
+
/**
|
|
784
|
+
* Material check for a DECLARED artifact-only PRD (`deliverable: artifact` +
|
|
785
|
+
* `artifactPaths`). Returns true only when every declared path resolves inside
|
|
786
|
+
* `cwd`, is a non-empty regular file, and has an mtime inside the run window
|
|
787
|
+
* (startedAt - 60s .. (finishedAt ?? now) + 120s). Never throws: any doubt
|
|
788
|
+
* (no paths, `..`, escape, missing, non-file, empty, stale, fs error) → false.
|
|
789
|
+
* `cwd` may be a list of roots; a path is accepted if it verifies under any.
|
|
790
|
+
* Complexity O(paths).
|
|
791
|
+
*/
|
|
792
|
+
function artifactsVerifiedOnDisk({ cwd, paths, startedAt, finishedAt, fsImpl = fs } = {}) {
|
|
793
|
+
return artifactsCheck({ cwd, paths, startedAt, finishedAt, fsImpl }).ok;
|
|
794
|
+
}
|
|
795
|
+
|
|
796
|
+
function artifactsCheck({ cwd, paths, startedAt, finishedAt, fsImpl = fs }) {
|
|
797
|
+
const fail = { ok: false, verified: [] };
|
|
798
|
+
try {
|
|
799
|
+
const roots = (Array.isArray(cwd) ? cwd : [cwd]).filter((r) => typeof r === 'string' && r);
|
|
800
|
+
if (!roots.length || !Array.isArray(paths) || paths.length === 0) return fail;
|
|
801
|
+
const start = Date.parse(startedAt ?? '');
|
|
802
|
+
if (Number.isNaN(start)) return fail;
|
|
803
|
+
const endParsed = finishedAt ? Date.parse(finishedAt) : NaN;
|
|
804
|
+
const end = Number.isNaN(endParsed) ? Date.now() : endParsed;
|
|
805
|
+
const lo = start - 60_000;
|
|
806
|
+
const hi = end + 120_000;
|
|
807
|
+
const verified = [];
|
|
808
|
+
for (const p of paths) {
|
|
809
|
+
if (typeof p !== 'string' || !p || p.includes('\0')) return fail;
|
|
810
|
+
if (p.split(/[\\/]/).includes('..')) return fail;
|
|
811
|
+
let hit = null;
|
|
812
|
+
for (const root of roots) {
|
|
813
|
+
const base = path.resolve(root);
|
|
814
|
+
const abs = path.resolve(base, p);
|
|
815
|
+
const rel = path.relative(base, abs);
|
|
816
|
+
if (!rel || rel.startsWith('..') || path.isAbsolute(rel)) continue;
|
|
817
|
+
let st;
|
|
818
|
+
try { st = fsImpl.statSync(abs); } catch { continue; }
|
|
819
|
+
if (!st.isFile() || st.size <= 0) continue;
|
|
820
|
+
if (st.mtimeMs < lo || st.mtimeMs > hi) continue;
|
|
821
|
+
hit = { path: p, size: st.size, mtime: new Date(st.mtimeMs).toISOString() };
|
|
822
|
+
break;
|
|
823
|
+
}
|
|
824
|
+
if (!hit) return fail;
|
|
825
|
+
verified.push(hit);
|
|
826
|
+
}
|
|
827
|
+
return { ok: true, verified };
|
|
828
|
+
} catch {
|
|
829
|
+
return fail;
|
|
830
|
+
}
|
|
831
|
+
}
|
|
832
|
+
|
|
781
833
|
// ─── main verifier ────────────────────────────────────────────────────────────
|
|
782
834
|
|
|
783
835
|
/**
|
|
@@ -830,7 +882,7 @@ function allDeliverablesAlreadyTracked({ cwd, paths, execImpl = execFileSync, ti
|
|
|
830
882
|
*/
|
|
831
883
|
async function verifyRun({
|
|
832
884
|
runDir, prdPath, queueEntry, allJobs = [], committedDuringRun = false, allowPreSentinelHeal = false,
|
|
833
|
-
ghExecImpl, priorLandedCommit = null, jobLandedCommitThisRun = null, exitCode = null,
|
|
885
|
+
ghExecImpl, priorLandedCommit = null, jobLandedCommitThisRun = null, exitCode = null, worktreeDir = null,
|
|
834
886
|
}) {
|
|
835
887
|
const { slug } = queueEntry;
|
|
836
888
|
const logPath = path.join(runDir, `${slug}.log`);
|
|
@@ -1202,7 +1254,42 @@ async function verifyRun({
|
|
|
1202
1254
|
);
|
|
1203
1255
|
}
|
|
1204
1256
|
}
|
|
1257
|
+
// EXEMPTION: a PRD that DECLARES `deliverable: artifact` + `artifactPaths`
|
|
1258
|
+
// (all deliverables live in a git-excluded dir, so "no commit" is
|
|
1259
|
+
// correct). Reads the declared list from frontmatter — never scrapes
|
|
1260
|
+
// prose — and stat-checks each file on disk (non-empty, in the run
|
|
1261
|
+
// window). Paths resolve against queueEntry.cwd, then retry against the
|
|
1262
|
+
// run's worktree dir when the caller supplies one. Any doubt falls
|
|
1263
|
+
// through to pass_no_commit below. The commit guard separately requires
|
|
1264
|
+
// a clean tree for this verdict (scheduler.cjs commitGuardVerdict).
|
|
1265
|
+
let artifactVerified = false;
|
|
1205
1266
|
if (!mergeMainVerified && !priorRunVerified && !alreadyShipped) {
|
|
1267
|
+
let fm = null;
|
|
1268
|
+
try { fm = parsePrdFile(prdFullText).frontmatter; } catch { fm = null; }
|
|
1269
|
+
if (fm && fm.deliverable === 'artifact' && Array.isArray(fm.artifactPaths) && fm.artifactPaths.length > 0) {
|
|
1270
|
+
const check = artifactsCheck({
|
|
1271
|
+
cwd: [queueEntry?.cwd, worktreeDir],
|
|
1272
|
+
paths: fm.artifactPaths,
|
|
1273
|
+
startedAt: queueEntry?.startedAt,
|
|
1274
|
+
finishedAt: queueEntry?.finishedAt,
|
|
1275
|
+
});
|
|
1276
|
+
if (check.ok) {
|
|
1277
|
+
artifactVerified = true;
|
|
1278
|
+
return conclude(
|
|
1279
|
+
'pass_no_commit_artifact_verified',
|
|
1280
|
+
'SCHEDULER_VERDICT: PASS with no commit, but every declared artifact exists, is non-empty, and was '
|
|
1281
|
+
+ `written inside the run window (${check.verified.map((v) => v.path).join(', ')})`,
|
|
1282
|
+
null,
|
|
1283
|
+
{
|
|
1284
|
+
...(annotations.length ? { annotations } : {}),
|
|
1285
|
+
sentinel,
|
|
1286
|
+
verifiedArtifacts: check.verified,
|
|
1287
|
+
},
|
|
1288
|
+
);
|
|
1289
|
+
}
|
|
1290
|
+
}
|
|
1291
|
+
}
|
|
1292
|
+
if (!mergeMainVerified && !priorRunVerified && !alreadyShipped && !artifactVerified) {
|
|
1206
1293
|
issues.push({
|
|
1207
1294
|
verdict: 'pass_no_commit',
|
|
1208
1295
|
reason: 'SCHEDULER_VERDICT: PASS but no commit landed during the run window — the run claims success but produced no code change',
|
|
@@ -1294,6 +1381,7 @@ module.exports = {
|
|
|
1294
1381
|
checkMergeablePr,
|
|
1295
1382
|
extractPrdDeliverablePaths,
|
|
1296
1383
|
allDeliverablesAlreadyTracked,
|
|
1384
|
+
artifactsVerifiedOnDisk,
|
|
1297
1385
|
isAncestorCommit,
|
|
1298
1386
|
isGitRepo,
|
|
1299
1387
|
hasAbandonedBackgroundTask,
|
package/src/main/scheduler.cjs
CHANGED
|
@@ -4540,11 +4540,18 @@ async function findSatisfyingCommitOnMain(job) {
|
|
|
4540
4540
|
* newlyDirty is empty: a fix-plan job that left real dirt behind is
|
|
4541
4541
|
* still a genuine finish-protocol violation (incident:
|
|
4542
4542
|
* 523-fix-bounded-fix-plan-retry, 2026-07-12).
|
|
4543
|
-
|
|
4544
|
-
|
|
4543
|
+
* - artifactVerified (zero-edit case only): runVerify.cjs proved a PRD that
|
|
4544
|
+
* DECLARED `deliverable: artifact` wrote its git-excluded artifacts inside
|
|
4545
|
+
* the run window (pass_no_commit_artifact_verified). That verdict is
|
|
4546
|
+
* deliberately NOT blanket-excused via legitimateNoOp (the call site
|
|
4547
|
+
* excludes it) — it stands the guard down only on a clean tree, like the
|
|
4548
|
+
* fix-plan carve-out, so stray uncommitted tracked edits still park as
|
|
4549
|
+
* uncommitted_changes.
|
|
4550
|
+
*/
|
|
4551
|
+
function commitGuardVerdict({ newlyDirty, siblingRunning, ranInWorktree, jobSelfCommitted, legitimateNoOp, isFixPlanJob, artifactVerified, verifyResult, salvagePatch }) {
|
|
4545
4552
|
if ((siblingRunning && !ranInWorktree) || jobSelfCommitted || legitimateNoOp) return null;
|
|
4546
4553
|
const dirty = newlyDirty || [];
|
|
4547
|
-
if (dirty.length === 0 && isFixPlanJob) return null;
|
|
4554
|
+
if (dirty.length === 0 && (isFixPlanJob || artifactVerified)) return null;
|
|
4548
4555
|
|
|
4549
4556
|
const carried = [...(verifyResult?.annotations ?? [])];
|
|
4550
4557
|
if (verifyResult && verifyResult.verdict !== 'clean') {
|
|
@@ -4797,6 +4804,25 @@ As the LAST LINE of your final result text, emit exactly one of:
|
|
|
4797
4804
|
Print PASS only once the commit above has actually landed.`;
|
|
4798
4805
|
}
|
|
4799
4806
|
|
|
4807
|
+
/**
|
|
4808
|
+
* Stamps integrateBranch's failure subtype (`failureKind`) and conflicted
|
|
4809
|
+
* paths onto a job row at finalize; clears both when there is no failure so
|
|
4810
|
+
* a re-run never carries a stale subtype. Plain JSON values only.
|
|
4811
|
+
*/
|
|
4812
|
+
function stampIntegrationFailure(row, integration) {
|
|
4813
|
+
if (integration && integration.failureKind) {
|
|
4814
|
+
row.integrationFailureKind = integration.failureKind;
|
|
4815
|
+
if (Array.isArray(integration.conflictedPaths)) row.integrationConflictPaths = integration.conflictedPaths;
|
|
4816
|
+
else delete row.integrationConflictPaths;
|
|
4817
|
+
if (integration.baseHeadSha) row.integrationBaseHeadSha = integration.baseHeadSha;
|
|
4818
|
+
else delete row.integrationBaseHeadSha;
|
|
4819
|
+
} else {
|
|
4820
|
+
delete row.integrationFailureKind;
|
|
4821
|
+
delete row.integrationConflictPaths;
|
|
4822
|
+
delete row.integrationBaseHeadSha;
|
|
4823
|
+
}
|
|
4824
|
+
}
|
|
4825
|
+
|
|
4800
4826
|
/**
|
|
4801
4827
|
* Mechanical recovery (PRD 1130). isFixPlanBeyondDepthCap (below) is the
|
|
4802
4828
|
* ONLY gate on re-investigating a fix-plan job at investigationDepth >= 2 —
|
|
@@ -4809,14 +4835,21 @@ Print PASS only once the commit above has actually landed.`;
|
|
|
4809
4835
|
* auto-fix it authors no plan and spawns no model; it is pure git.
|
|
4810
4836
|
*
|
|
4811
4837
|
* The closed set of mechanically-resolvable verdicts starts at exactly
|
|
4812
|
-
* 'worktree_integration_failed'
|
|
4813
|
-
*
|
|
4814
|
-
*
|
|
4815
|
-
*
|
|
4816
|
-
*
|
|
4817
|
-
*
|
|
4818
|
-
*
|
|
4819
|
-
*
|
|
4838
|
+
* 'worktree_integration_failed', which covers two subtypes (job.
|
|
4839
|
+
* integrationFailureKind): `blocking_paths` — PRD 1125 taught integrateBranch
|
|
4840
|
+
* to parse git's "would be overwritten by merge" stderr, verify the blocking
|
|
4841
|
+
* paths are byte-identical to the branch, discard the proven duplicates and
|
|
4842
|
+
* retry once, so a re-call can genuinely succeed once the dirty files are
|
|
4843
|
+
* gone — and `content_conflict`, which PRD 1125 explicitly does NOT handle
|
|
4844
|
+
* (parseBlockingMergePaths returns null for it). Only pure-addition text
|
|
4845
|
+
* conflicts are auto-resolved (mc-02, gitWorktree.cjs, kill switch
|
|
4846
|
+
* SM_PURE_ADDITION_MERGE_DISABLE=1) — and that already ran inside the
|
|
4847
|
+
* original failed integration. Re-calling integrateBranch for a content
|
|
4848
|
+
* conflict that survived it, against an unmoved base HEAD, fails
|
|
4849
|
+
* deterministically, so the selector skips it (WITHOUT spending the one
|
|
4850
|
+
* attempt) until HEAD moves — a sibling merge may make the retry succeed.
|
|
4851
|
+
* A job parked with this verdict has its `sm-job/<slug>` branch preserved
|
|
4852
|
+
* (cleanupJobWorktree's `keepBranch: !integration.ok`).
|
|
4820
4853
|
*
|
|
4821
4854
|
* Bounded to exactly one attempt via job.mechanicalRecoveryAttempted,
|
|
4822
4855
|
* stamped in the SAME mutate as the outcome (performMechanicalRecovery,
|
|
@@ -4828,11 +4861,41 @@ Print PASS only once the commit above has actually landed.`;
|
|
|
4828
4861
|
*/
|
|
4829
4862
|
const MECHANICALLY_RESOLVABLE_VERDICTS = new Set(['worktree_integration_failed']);
|
|
4830
4863
|
|
|
4831
|
-
|
|
4864
|
+
/**
|
|
4865
|
+
* True when a retry is provably futile: a content_conflict whose base HEAD is
|
|
4866
|
+
* the same as at failure time. Unknown state (no stamp, null current HEAD)
|
|
4867
|
+
* is NOT futile — falls back to attempting the retry.
|
|
4868
|
+
*/
|
|
4869
|
+
function isMechanicalRecoveryFutile(job, currentHeadSha) {
|
|
4870
|
+
return job.integrationFailureKind === 'content_conflict'
|
|
4871
|
+
&& !!job.integrationBaseHeadSha
|
|
4872
|
+
&& !!currentHeadSha
|
|
4873
|
+
&& job.integrationBaseHeadSha === currentHeadSha;
|
|
4874
|
+
}
|
|
4875
|
+
|
|
4876
|
+
/**
|
|
4877
|
+
* Resolves the current HEAD once per distinct cwd for a pass. Returns a
|
|
4878
|
+
* `(job) => sha|null` lookup over `jobs`; the memo lives only as long as the
|
|
4879
|
+
* returned closure. O(distinct cwds) git calls, not O(jobs).
|
|
4880
|
+
*/
|
|
4881
|
+
async function resolveHeadShasForPass(jobs) {
|
|
4882
|
+
const byCwd = new Map();
|
|
4883
|
+
for (const j of jobs || []) {
|
|
4884
|
+
if (!j || j.status !== 'needs_review' || j.integrationFailureKind !== 'content_conflict') continue;
|
|
4885
|
+
const cwd = j.cwd || DEFAULT_PROJECT_CWD;
|
|
4886
|
+
if (!byCwd.has(cwd)) byCwd.set(cwd, gitHead(cwd));
|
|
4887
|
+
}
|
|
4888
|
+
const resolved = new Map();
|
|
4889
|
+
for (const [cwd, p] of byCwd) resolved.set(cwd, await p);
|
|
4890
|
+
return (j) => (j && resolved.get(j.cwd || DEFAULT_PROJECT_CWD)) || null;
|
|
4891
|
+
}
|
|
4892
|
+
|
|
4893
|
+
function selectMechanicalRecoveryTarget(job, currentHeadSha = null) {
|
|
4832
4894
|
if (process.env.SM_MECHANICAL_RECOVERY_DISABLE === '1') return null;
|
|
4833
4895
|
if (!job || job.status !== 'needs_review') return null;
|
|
4834
4896
|
if (!MECHANICALLY_RESOLVABLE_VERDICTS.has(job.verifierVerdict)) return null;
|
|
4835
4897
|
if (job.mechanicalRecoveryAttempted === true) return null;
|
|
4898
|
+
if (isMechanicalRecoveryFutile(job, currentHeadSha)) return null;
|
|
4836
4899
|
const cwd = job.cwd || DEFAULT_PROJECT_CWD;
|
|
4837
4900
|
return { slug: job.slug, cwd, branch: jobWorktree.branchNameFor(job.slug), carriedPaths: job.carriedPaths || [] };
|
|
4838
4901
|
}
|
|
@@ -6044,7 +6107,7 @@ async function spawnInvestigation(failedJob, runDir, { deadChild = null } = {})
|
|
|
6044
6107
|
// Mechanical recovery (PRD 1130): same first-refusal treatment — a job
|
|
6045
6108
|
// eligible for a pure-git retry must never also get a cold-read fix-plan
|
|
6046
6109
|
// PRD authored in the same pass.
|
|
6047
|
-
if (selectMechanicalRecoveryTarget(failedJob)) {
|
|
6110
|
+
if (selectMechanicalRecoveryTarget(failedJob, failedJob.integrationFailureKind === 'content_conflict' ? await gitHead(failedJob.cwd || DEFAULT_PROJECT_CWD) : null)) {
|
|
6048
6111
|
console.log(`[scheduler] skip investigation: ${failedJob.slug} is mechanical-recovery eligible`);
|
|
6049
6112
|
return { deferred: false };
|
|
6050
6113
|
}
|
|
@@ -6859,6 +6922,7 @@ async function spawnJob(job, runId, runDir, defaultCwd, resumeTarget = null) {
|
|
|
6859
6922
|
let res;
|
|
6860
6923
|
let worktreeLeftoverDirty = [];
|
|
6861
6924
|
let worktreeIntegrationFailure = null;
|
|
6925
|
+
let worktreeIntegrationDetail = null;
|
|
6862
6926
|
// Set only when integrateJobBranch's stderr-parsing auto-resolve fired
|
|
6863
6927
|
// (PRD 1125) — surfaced on the job row so the Queue UI can say the merge
|
|
6864
6928
|
// self-healed rather than silently looking like an ordinary merge.
|
|
@@ -6919,6 +6983,7 @@ async function spawnJob(job, runId, runDir, defaultCwd, resumeTarget = null) {
|
|
|
6919
6983
|
}
|
|
6920
6984
|
if (!integration.ok) {
|
|
6921
6985
|
worktreeIntegrationFailure = integration.reason;
|
|
6986
|
+
worktreeIntegrationDetail = integration;
|
|
6922
6987
|
console.error(`[scheduler] ${job.slug}: worktree branch integration FAILED (${integration.reason}) — branch ${worktree.branch} preserved in ${guardCwd} for manual recovery`);
|
|
6923
6988
|
} else if (integration.integrated) {
|
|
6924
6989
|
console.log(`[scheduler] ${job.slug}: worktree branch ${worktree.branch} integrated into ${guardCwd}${integration.mergeCommit ? ' (merge commit)' : ' (fast-forward)'}`);
|
|
@@ -7117,6 +7182,7 @@ async function spawnJob(job, runId, runDir, defaultCwd, resumeTarget = null) {
|
|
|
7117
7182
|
priorLandedCommit,
|
|
7118
7183
|
jobLandedCommitThisRun,
|
|
7119
7184
|
exitCode: res.exitCode,
|
|
7185
|
+
worktreeDir: worktree.ok ? worktree.dir : null,
|
|
7120
7186
|
}).catch((e) => ({
|
|
7121
7187
|
verdict: 'verify_unavailable',
|
|
7122
7188
|
reason: `verifier threw: ${e?.message ?? String(e)}`,
|
|
@@ -7151,7 +7217,8 @@ async function spawnJob(job, runId, runDir, defaultCwd, resumeTarget = null) {
|
|
|
7151
7217
|
// annotation, so a real "finish protocol incomplete" is distinguishable from
|
|
7152
7218
|
// transcript noise in the queue (feedback 2026-06-10 addendum).
|
|
7153
7219
|
const guardWillRefire = verifyResult && verifyResult.downgradeTo === 'pending';
|
|
7154
|
-
const
|
|
7220
|
+
const guardArtifactVerified = verifyResult?.verdict === 'pass_no_commit_artifact_verified';
|
|
7221
|
+
const guardIsLegitimateNoOp = verifyResult && !guardArtifactVerified && COMPLETED_EQUIVALENT_VERDICTS.has(verifyResult.verdict);
|
|
7155
7222
|
if (res.exitCode === 0 && !res.rateLimited && !guardWillRefire && !guardIsLegitimateNoOp) {
|
|
7156
7223
|
// afterGuardCwd === null means non-git cwd (or git errored) —
|
|
7157
7224
|
// best-effort skip, same as always; only a git-status result (even an
|
|
@@ -7172,6 +7239,7 @@ async function spawnJob(job, runId, runDir, defaultCwd, resumeTarget = null) {
|
|
|
7172
7239
|
jobSelfCommitted,
|
|
7173
7240
|
legitimateNoOp: guardIsLegitimateNoOp,
|
|
7174
7241
|
isFixPlanJob: resolveIsFixPlan(job.slug, job.isFixPlan),
|
|
7242
|
+
artifactVerified: guardArtifactVerified,
|
|
7175
7243
|
verifyResult,
|
|
7176
7244
|
salvagePatch,
|
|
7177
7245
|
});
|
|
@@ -7205,7 +7273,9 @@ async function spawnJob(job, runId, runDir, defaultCwd, resumeTarget = null) {
|
|
|
7205
7273
|
if (worktreeIntegrationFailure) {
|
|
7206
7274
|
verifyResult = {
|
|
7207
7275
|
verdict: 'worktree_integration_failed',
|
|
7208
|
-
reason:
|
|
7276
|
+
reason: worktreeIntegrationDetail && worktreeIntegrationDetail.failureKind === 'content_conflict'
|
|
7277
|
+
? `Integration blocked by a content conflict in ${(worktreeIntegrationDetail.conflictedPaths || []).join(', ') || 'unknown paths'} — branch ${jobWorktree.branchNameFor(job.slug)} preserved; needs a manual merge.`
|
|
7278
|
+
: `worktree branch integration failed: ${worktreeIntegrationFailure} — branch preserved for manual merge`,
|
|
7209
7279
|
downgradeTo: 'needs_review',
|
|
7210
7280
|
};
|
|
7211
7281
|
}
|
|
@@ -7320,6 +7390,9 @@ async function spawnJob(job, runId, runDir, defaultCwd, resumeTarget = null) {
|
|
|
7320
7390
|
let mechanicalRecoveryTarget = null;
|
|
7321
7391
|
let terminalNotifySnapshot = null;
|
|
7322
7392
|
const newlyCompletedPrds = [];
|
|
7393
|
+
// Base HEAD for the mechanical-recovery futility check — read here, never inside mutate() (no I/O).
|
|
7394
|
+
const finalizeHeadSha = worktreeIntegrationDetail && worktreeIntegrationDetail.failureKind === 'content_conflict'
|
|
7395
|
+
? await gitHead(guardCwd) : null;
|
|
7323
7396
|
await mutate((s) => {
|
|
7324
7397
|
const i2 = s.jobs.findIndex((x) => x.slug === job.slug);
|
|
7325
7398
|
// A job already moved off 'running' by someone else (namely
|
|
@@ -7511,6 +7584,7 @@ async function spawnJob(job, runId, runDir, defaultCwd, resumeTarget = null) {
|
|
|
7511
7584
|
} else if (!['blocked_by_foreign_wip_streak', 'budget_exceeded'].includes(s.jobs[i2].verifierVerdict)) {
|
|
7512
7585
|
delete s.jobs[i2].verifierVerdict;
|
|
7513
7586
|
}
|
|
7587
|
+
stampIntegrationFailure(s.jobs[i2], worktreeIntegrationFailure ? worktreeIntegrationDetail : null);
|
|
7514
7588
|
transitionJob(s.jobs[i2], effectiveStatus, { reason: finalizeReason, source: 'spawnJob:finalize' });
|
|
7515
7589
|
s.jobs[i2].finishedAt = new Date().toISOString();
|
|
7516
7590
|
s.jobs[i2].exitCode = res.exitCode;
|
|
@@ -7620,7 +7694,7 @@ async function spawnJob(job, runId, runDir, defaultCwd, resumeTarget = null) {
|
|
|
7620
7694
|
// depth-cap check, so it must never fall through to either.
|
|
7621
7695
|
// Snapshot only (no I/O inside mutate()); the actual git retry
|
|
7622
7696
|
// happens outside mutate(), below.
|
|
7623
|
-
const mTarget = selectMechanicalRecoveryTarget(s.jobs[i2]);
|
|
7697
|
+
const mTarget = selectMechanicalRecoveryTarget(s.jobs[i2], finalizeHeadSha);
|
|
7624
7698
|
if (mTarget) {
|
|
7625
7699
|
mechanicalRecoveryJob = { ...s.jobs[i2] };
|
|
7626
7700
|
mechanicalRecoveryTarget = mTarget;
|
|
@@ -10828,9 +10902,17 @@ async function reverifyNeedsReview() {
|
|
|
10828
10902
|
// check in spawnJob missed because the app restarted in between). Depth
|
|
10829
10903
|
// never disqualifies it, so it runs regardless of investigationDepth.
|
|
10830
10904
|
{
|
|
10905
|
+
const headFor = await resolveHeadShasForPass(queueForResumeAndAutofix.jobs);
|
|
10831
10906
|
for (const job of queueForResumeAndAutofix.jobs) {
|
|
10832
|
-
const
|
|
10833
|
-
|
|
10907
|
+
const head = headFor(job);
|
|
10908
|
+
const target = selectMechanicalRecoveryTarget(job, head);
|
|
10909
|
+
if (!target) {
|
|
10910
|
+
if (job.status === 'needs_review' && job.mechanicalRecoveryAttempted !== true
|
|
10911
|
+
&& MECHANICALLY_RESOLVABLE_VERDICTS.has(job.verifierVerdict) && isMechanicalRecoveryFutile(job, head)) {
|
|
10912
|
+
console.log(`[scheduler] mechanical-recovery: skip ${job.slug} — content conflict, base HEAD unchanged since failure`);
|
|
10913
|
+
}
|
|
10914
|
+
continue;
|
|
10915
|
+
}
|
|
10834
10916
|
console.log(`[scheduler] mechanical-recovery: needs_review ${job.slug} → re-integrating ${target.branch}`);
|
|
10835
10917
|
performMechanicalRecovery(job, target).catch((e) => {
|
|
10836
10918
|
console.error('[scheduler] performMechanicalRecovery error', job.slug, e);
|
|
@@ -10984,6 +11066,9 @@ function registerScheduleHandlers() {
|
|
|
10984
11066
|
// configuration tab.
|
|
10985
11067
|
ipcMain.handle('schedule:session-slots', () => sessionSlots.snapshot());
|
|
10986
11068
|
|
|
11069
|
+
// Read-only: where Epic/job git worktrees (and so their CLI transcript encodings) live.
|
|
11070
|
+
ipcMain.handle('schedule:worktree-base', () => require('./lib/schedulerPaths.cjs').worktreeBase());
|
|
11071
|
+
|
|
10987
11072
|
// Home-tab control for the same pool: user-set cap in [0, 10], default 5.
|
|
10988
11073
|
// 0 pauses new claude -p launches machine-wide without killing anything
|
|
10989
11074
|
// already running. SM_SESSION_SLOTS (if set) still overrides this at read
|
|
@@ -12598,6 +12683,9 @@ module.exports = {
|
|
|
12598
12683
|
buildClaudeSpawnArgs,
|
|
12599
12684
|
spawnResumeRecovery,
|
|
12600
12685
|
selectMechanicalRecoveryTarget,
|
|
12686
|
+
isMechanicalRecoveryFutile,
|
|
12687
|
+
resolveHeadShasForPass,
|
|
12688
|
+
stampIntegrationFailure,
|
|
12601
12689
|
performMechanicalRecovery,
|
|
12602
12690
|
MECHANICALLY_RESOLVABLE_VERDICTS,
|
|
12603
12691
|
selectLeftoverQuarantineTarget,
|
|
@@ -141,6 +141,17 @@ estimateMinutes: 60
|
|
|
141
141
|
- `estimateMinutes` is used for ETA display; include a realistic estimate (note: empirical median is ~10 min, p90 ~20 min — avoid wildly inflated estimates that hide real outliers).
|
|
142
142
|
- `parallelGroup` in frontmatter, when present, IS honored by the scheduler (`pickNextBatch` reads `parallelGroup ?? 99` and overrides the filename NN). Use this only for cross-stream interleaving — e.g., the cellar series `122-`, `123-`, `124-` overrides to groups `113`, `114`, `115` so cellar steps fire alongside the parallel etch steps. Do NOT use it to reorder within a single stream; rename the file instead.
|
|
143
143
|
|
|
144
|
+
### Artifact-only PRDs
|
|
145
|
+
|
|
146
|
+
Use `deliverable: artifact` + `artifactPaths: [a, b]` (both passed to `scheduler_create_prd`) ONLY when every deliverable is a file the target repo deliberately git-excludes (e.g. patches/notes under `session-manager-operations/review-records/`, matched by `.git/info/exclude`), so "no commit" is the correct outcome.
|
|
147
|
+
|
|
148
|
+
- Every artifact path must be named in `artifactPaths`; an unlisted file is invisible to the verifier. Each path is relative, never contains `..`.
|
|
149
|
+
- The artifact must be non-empty and written during the run window — the verifier stat-checks it on disk (verdict `pass_no_commit_artifact_verified`; window = start−60s..finish+120s).
|
|
150
|
+
- The run must still leave the working tree clean — declaring artifacts excuses the missing commit, not stray tracked edits. The commit guard enforces this: the `pass_no_commit_artifact_verified` verdict stands the guard down only when nothing tracked was left dirty; any uncommitted tracked change still parks as `uncommitted_changes`.
|
|
151
|
+
- Both fields are required together: the write is refused if only one is given.
|
|
152
|
+
|
|
153
|
+
Incident: sigma PRD `816-prepare-157-copy-citation-extras-patch` (2026-09-20) wrote its patch into a git-excluded folder; with no way to declare that, two runs both parked in `needs_review`.
|
|
154
|
+
|
|
144
155
|
---
|
|
145
156
|
|
|
146
157
|
## §7 Self-containment
|
package/src/main/transcripts.cjs
CHANGED
|
@@ -38,9 +38,22 @@ function attachWindow(w) {
|
|
|
38
38
|
|
|
39
39
|
const { encodeCwd } = require('./lib/encodeCwd.cjs');
|
|
40
40
|
const { classifyLine } = require('./lib/classifyTranscriptLine.cjs');
|
|
41
|
+
const { resolveEpicTranscriptPath } = require('./lib/epicTranscriptPath.cjs');
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Where the CLI's JSONL for this session lives. Epic sessions run in a
|
|
45
|
+
* worktree, so the resolver checks the worktree encoding as well as the
|
|
46
|
+
* project one; a non-Epic tab resolves to the plain project-encoded path.
|
|
47
|
+
* Sync + absolute always (falls back to the plain path on invalid input).
|
|
48
|
+
*/
|
|
49
|
+
function resolveTranscript(cwd, sessionUuid) {
|
|
50
|
+
const r = resolveEpicTranscriptPath({ cwd, claudeSessionId: sessionUuid });
|
|
51
|
+
const filePath = r.path || path.join(os.homedir(), '.claude', 'projects', encodeCwd(cwd), `${sessionUuid}.jsonl`);
|
|
52
|
+
return { filePath, existsAnywhere: !!r.existsAnywhere };
|
|
53
|
+
}
|
|
41
54
|
|
|
42
55
|
function transcriptPath(cwd, sessionUuid) {
|
|
43
|
-
return
|
|
56
|
+
return resolveTranscript(cwd, sessionUuid).filePath;
|
|
44
57
|
}
|
|
45
58
|
|
|
46
59
|
// Cap bytes read per readDelta pass. Bounds memory on first attach to a very
|
|
@@ -191,8 +204,11 @@ async function readPage(sub, startLine, endLine) {
|
|
|
191
204
|
const from = Math.max(0, Math.min(startLine, total - 1));
|
|
192
205
|
const to = Math.max(0, Math.min(endLine, total - 1));
|
|
193
206
|
if (total === 0 || from > to) return { events: [], totalLines: total };
|
|
194
|
-
|
|
195
|
-
|
|
207
|
+
// Snapshot the entries synchronously: the awaits below can interleave with a
|
|
208
|
+
// rotation/truncation rebuild that shrinks sub.lineIndex, so never re-index it after.
|
|
209
|
+
const entries = sub.lineIndex.slice(from, to + 1);
|
|
210
|
+
const first = entries[0];
|
|
211
|
+
const last = entries[entries.length - 1];
|
|
196
212
|
const spanOffset = first.byteOffset;
|
|
197
213
|
const spanLength = last.byteOffset + last.byteLength - spanOffset;
|
|
198
214
|
const events = [];
|
|
@@ -202,7 +218,7 @@ async function readPage(sub, startLine, endLine) {
|
|
|
202
218
|
const buf = Buffer.alloc(spanLength);
|
|
203
219
|
await fd.read(buf, 0, spanLength, spanOffset);
|
|
204
220
|
for (let lineNo = from; lineNo <= to; lineNo++) {
|
|
205
|
-
const entry =
|
|
221
|
+
const entry = entries[lineNo - from];
|
|
206
222
|
const relOffset = entry.byteOffset - spanOffset;
|
|
207
223
|
const text = buf.toString('utf8', relOffset, relOffset + entry.byteLength);
|
|
208
224
|
let obj;
|
|
@@ -361,9 +377,14 @@ async function subscribe({ tabId, cwd, sessionUuid }) {
|
|
|
361
377
|
});
|
|
362
378
|
return { ok: false, path: null, error: 'too many active subscriptions' };
|
|
363
379
|
}
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
380
|
+
// Known limitation: filePath is latched here and never re-resolved. Fine
|
|
381
|
+
// because nothing relocates a transcript under an open subscription.
|
|
382
|
+
const { filePath, existsAnywhere } = resolveTranscript(cwd, sessionUuid);
|
|
383
|
+
// Only create the dir when no transcript exists anywhere (it is the dir the
|
|
384
|
+
// CLI is about to write); otherwise we'd mint empty project dirs it never made.
|
|
385
|
+
if (!existsAnywhere) {
|
|
386
|
+
await fsp.mkdir(path.dirname(filePath), { recursive: true }).catch(() => {});
|
|
387
|
+
}
|
|
367
388
|
const sub = {
|
|
368
389
|
tabId,
|
|
369
390
|
cwd,
|
package/src/preload/api.d.ts
CHANGED
|
@@ -1780,6 +1780,8 @@ export interface SessionManagerAPI {
|
|
|
1780
1780
|
total: number; inUse: number; holders: { owner: string; at: string }[];
|
|
1781
1781
|
min: number; max: number; default: number; envOverride: boolean;
|
|
1782
1782
|
}>;
|
|
1783
|
+
/** Resolved base dir of managed Epic/job worktrees (SM_WORKTREE_ROOT, else the persistent per-user state dir). */
|
|
1784
|
+
worktreeBase: () => Promise<string>;
|
|
1783
1785
|
/** Set the slot pool's user cap (Home tab), [0, 10]; 0 pauses new launches. Ignored if SM_SESSION_SLOTS is set. */
|
|
1784
1786
|
setSessionSlots: (cap: number) => Promise<{
|
|
1785
1787
|
total: number; inUse: number; holders: { owner: string; at: string }[];
|
package/src/preload/index.cjs
CHANGED
|
@@ -231,6 +231,7 @@ contextBridge.exposeInMainWorld('api', {
|
|
|
231
231
|
schedule: {
|
|
232
232
|
state: () => ipcRenderer.invoke('schedule:state'),
|
|
233
233
|
sessionSlots: () => ipcRenderer.invoke('schedule:session-slots'),
|
|
234
|
+
worktreeBase: () => ipcRenderer.invoke('schedule:worktree-base'),
|
|
234
235
|
setSessionSlots: (cap) => ipcRenderer.invoke('schedule:set-session-slots', { cap }),
|
|
235
236
|
setConfig: (partial) => ipcRenderer.invoke('schedule:set-config', partial),
|
|
236
237
|
resetJob: (slug) => ipcRenderer.invoke('schedule:reset-job', { slug }),
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
import{al as d,f as j,ab as $,U as g,aa as e,j as N,V as E,ad as S,W as b,an as T,b as p,I as D,g as v,E as R}from"./index-Dp4Rc--q.js";import{P as V}from"./Panel-I1s6ghPo.js";import"./monaco-editor-BW5C4Iv1.js";const C=[{key:"diagram",label:"Diagram"},{key:"tables",label:"Tables"}];function G(){const[s,t]=d.useState("diagram"),[a,i]=d.useState(null),[l,n]=d.useState(()=>new Set),c=d.useMemo(()=>j.filter(r=>!l.has(r.group)),[l]),m=d.useMemo(()=>$(c),[c]),o=r=>{n(x=>{const h=new Set(x);return h.has(r)?h.delete(r):h.add(r),h}),i(null)},u=a?g(a):null,y=r=>c.some(x=>x.id===r);return e.jsx(V,{toolbar:e.jsxs(e.Fragment,{children:[e.jsx(E,{options:C,active:s,onChange:t}),e.jsx(M,{hiddenGroups:l,onToggle:o}),e.jsxs("span",{className:"ml-auto text-fg-faint",children:[j.length," entities"]})]}),children:s==="diagram"?e.jsxs("div",{className:"h-full flex",children:[e.jsx("div",{className:"flex-1 min-w-0 overflow-auto",children:e.jsx(O,{layout:m,selectedId:a,onSelect:r=>i(x=>x===r?null:r)})}),e.jsx("div",{className:"w-96 shrink-0 border-l border-line overflow-y-auto",children:u?e.jsx(F,{entity:u,onSelectRelated:r=>y(r)?i(r):void 0}):e.jsx(N,{title:"select an entity",hint:"click a box in the diagram to see its storage, fields, and relations"})})]}):e.jsx(P,{})})}const L=d.memo(G);function M({hiddenGroups:s,onToggle:t}){return e.jsx("div",{className:"flex items-center gap-1.5 flex-wrap",children:v.map(a=>{const i=s.has(a.id);return e.jsxs("button",{onClick:()=>t(a.id),title:`${a.blurb} (${a.realm})`,className:`flex items-center gap-1 px-1.5 py-0.5 rounded border text-[10.5px] ${i?"border-line text-fg-faint opacity-50":"border-line text-fg-dim hover:text-fg"}`,children:[e.jsx("span",{className:`w-1.5 h-1.5 rounded-full ${a.tone.dot}`}),a.label,e.jsx("span",{className:"text-fg-faint",children:a.realm})]},a.id)})})}function O({layout:s,selectedId:t,onSelect:a}){const i=d.useMemo(()=>t?new Set(S(t)):null,[t]),l=n=>!!t&&(n.relation.from===t||n.relation.to===t);return s.boxes.length===0?e.jsx(N,{title:"no entities in view",hint:"toggle a group in the legend to show its entities"}):e.jsxs("div",{className:"relative p-4",style:{width:s.width+32,height:s.height+32},children:[e.jsxs("svg",{width:s.width,height:s.height,className:"absolute left-4 top-4 overflow-visible",children:[s.edges.map(n=>{const c=l(n);return e.jsx("path",{"data-testid":`erd-edge-${n.relation.from}-${n.relation.to}`,d:n.d,fill:"none",className:c?"stroke-accent":"stroke-line",strokeWidth:c?2:1,opacity:t&&!c?.25:1},`${n.relation.from}-${n.relation.to}-${n.relation.label}`)}),s.edges.map(n=>l(n)?e.jsx("text",{x:n.mx,y:n.my,textAnchor:"middle",className:"fill-fg-dim text-[10px]",children:n.relation.label},`label-${n.relation.from}-${n.relation.to}-${n.relation.label}`):null)]}),s.boxes.map(n=>e.jsx(_,{box:n,selected:n.id===t,dimmed:!!t&&n.id!==t&&!(i!=null&&i.has(n.id)),onClick:()=>a(n.id)},n.id))]})}function _({box:s,selected:t,dimmed:a,onClick:i}){const l=g(s.id),n=b(s.group),c=l.fields.slice(0,R),m=l.fields.length-c.length;return e.jsxs("button",{type:"button","data-testid":`erd-box-${s.id}`,onClick:i,className:`absolute text-left rounded border overflow-hidden bg-bg-hi ${t?"border-accent ring-1 ring-accent":"border-line"} ${a?"opacity-30":""}`,style:{left:s.x,top:s.y,width:s.w,height:s.h},children:[e.jsxs("div",{className:"px-2 py-1 border-b border-line flex items-center gap-1.5",children:[e.jsx("span",{className:`w-1.5 h-1.5 rounded-full shrink-0 ${n.tone.dot}`}),e.jsx("span",{className:"text-[11px] font-semibold text-fg truncate",children:l.name})]}),l.aka&&e.jsx("div",{className:"px-2 pt-0.5 text-[9.5px] text-fg-faint truncate",children:l.aka}),e.jsxs("div",{className:"px-2 py-0.5",children:[c.map(o=>e.jsxs("div",{className:"text-[9.5px] font-mono text-fg-dim truncate flex items-center gap-1",children:[o.key&&e.jsx("span",{className:"text-fg-faint",children:o.key}),e.jsx("span",{className:"truncate",children:o.name})]},o.name)),m>0&&e.jsxs("div",{className:"text-[9.5px] text-fg-faint italic",children:["+",m," more"]})]})]})}function w({fields:s}){return e.jsxs("table",{className:"w-full text-[11px] font-mono",children:[e.jsx("thead",{className:"text-fg-faint uppercase tracking-wide text-[9.5px]",children:e.jsxs("tr",{className:"border-b border-line",children:[e.jsx("th",{className:"text-left font-normal px-2 py-1",children:"name"}),e.jsx("th",{className:"text-left font-normal px-2 py-1",children:"type"}),e.jsx("th",{className:"text-left font-normal px-2 py-1",children:"key"}),e.jsx("th",{className:"text-left font-normal px-2 py-1",children:"note"})]})}),e.jsx("tbody",{children:s.map(t=>e.jsxs("tr",{className:"border-b border-line/50",children:[e.jsxs("td",{className:"px-2 py-1 text-fg",children:[t.name,t.optional&&e.jsx("span",{className:"text-fg-faint",children:"?"})]}),e.jsx("td",{className:"px-2 py-1 text-fg-dim",children:t.type}),e.jsx("td",{className:"px-2 py-1",children:t.key&&e.jsxs(p,{tone:t.key==="pk"?"accent":"dim",children:[t.key,t.ref?` → ${t.ref}`:""]})}),e.jsx("td",{className:"px-2 py-1 text-fg-faint",children:t.note})]},t.name))})]})}function k({entity:s}){return e.jsxs("div",{className:"space-y-1 text-[11px]",children:[e.jsxs("div",{className:"flex items-center gap-1.5",children:[e.jsx("span",{className:"text-fg-faint w-20 shrink-0",children:"path"}),e.jsx("span",{className:"font-mono text-fg-dim break-all",children:s.store.path})]}),e.jsxs("div",{className:"flex items-center gap-1.5",children:[e.jsx("span",{className:"text-fg-faint w-20 shrink-0",children:"format"}),e.jsx("span",{className:"font-mono text-fg-dim",children:s.store.format})]}),s.store.writer&&e.jsxs("div",{className:"flex items-center gap-1.5",children:[e.jsx("span",{className:"text-fg-faint w-20 shrink-0",children:"writer"}),e.jsxs(p,{tone:"accent",children:["single-writer: ",s.store.writer]})]}),e.jsxs("div",{className:"flex items-center gap-1.5",children:[e.jsx("span",{className:"text-fg-faint w-20 shrink-0",children:"definedIn"}),e.jsx("span",{className:"font-mono text-fg-dim break-all",children:s.store.definedIn})]})]})}function F({entity:s,onSelectRelated:t}){const a=b(s.group),i=T(s.id);return e.jsxs("div",{className:"p-3 space-y-3",children:[e.jsxs("div",{children:[e.jsxs("div",{className:"flex items-center gap-1.5",children:[e.jsx("span",{className:`w-2 h-2 rounded-full ${a.tone.dot}`}),e.jsx("span",{className:"text-sm font-semibold text-fg",children:s.name})]}),s.aka&&e.jsx("div",{className:"text-[10.5px] text-fg-faint",children:s.aka}),e.jsx(p,{tone:"dim",className:"mt-1",children:a.label})]}),e.jsx("div",{className:"text-xs text-fg-dim leading-relaxed",children:s.summary}),e.jsxs("div",{children:[e.jsx(f,{children:"storage"}),e.jsx(k,{entity:s})]}),e.jsxs("div",{children:[e.jsx(f,{children:"fields"}),e.jsx(w,{fields:s.fields})]}),s.notes&&s.notes.length>0&&e.jsxs("div",{children:[e.jsx(f,{children:"notes"}),e.jsx("ul",{className:"list-disc pl-4 space-y-0.5 text-xs text-fg-dim",children:s.notes.map(l=>e.jsx("li",{children:l},l))})]}),e.jsxs("div",{children:[e.jsx(f,{children:"relations"}),i.length===0?e.jsx("div",{className:"text-xs text-fg-faint italic",children:"no relations"}):e.jsx("ul",{className:"space-y-1",children:i.map(l=>{const n=l.from===s.id?l.to:l.from,c=g(n);return e.jsxs("li",{className:"flex items-center gap-1.5 text-xs",children:[e.jsx("span",{className:"font-mono text-fg-faint w-10 shrink-0",children:D(l.cardinality)}),e.jsx("span",{className:"text-fg-dim",children:l.label}),l.via&&e.jsxs("span",{className:"font-mono text-[10px] text-fg-faint",children:["via ",l.via]}),e.jsx("button",{type:"button",onClick:()=>t(n),className:"ml-auto text-accent hover:underline font-mono",children:c.name})]},`${l.from}-${l.to}-${l.label}`)})})]})]})}function f({children:s}){return e.jsx("div",{className:"text-[10.5px] uppercase tracking-wide text-fg-faint mb-1 font-mono",children:s})}function P(){return e.jsx("div",{className:"p-4 space-y-6",children:v.map(s=>{const t=j.filter(a=>a.group===s.id);return t.length===0?null:e.jsxs("div",{children:[e.jsxs("div",{className:"flex items-center gap-1.5 mb-2",children:[e.jsx("span",{className:`w-2 h-2 rounded-full ${s.tone.dot}`}),e.jsx("span",{className:"text-xs font-semibold text-fg",children:s.label}),e.jsx("span",{className:"text-[10.5px] text-fg-faint",children:s.realm})]}),e.jsx("div",{className:"grid grid-cols-1 xl:grid-cols-2 gap-3",children:t.map(a=>e.jsxs("div",{className:"border border-line rounded bg-bg-hi p-3 space-y-2",children:[e.jsxs("div",{children:[e.jsxs("div",{className:"flex items-center gap-2",children:[e.jsx("span",{className:"text-sm font-semibold text-fg",children:a.name}),a.aka&&e.jsx("span",{className:"text-[10.5px] text-fg-faint",children:a.aka})]}),e.jsx(p,{tone:"dim",className:"mt-1",children:s.label})]}),e.jsx("div",{className:"text-xs text-fg-dim leading-relaxed",children:a.summary}),e.jsx(k,{entity:a}),e.jsx(w,{fields:a.fields}),a.notes&&a.notes.length>0&&e.jsx("ul",{className:"list-disc pl-4 space-y-0.5 text-xs text-fg-dim",children:a.notes.map(i=>e.jsx("li",{children:i},i))})]},a.id))})]},s.id)})})}export{L as DataModel};
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
import{aa as i,F as r}from"./index-Dp4Rc--q.js";function l({value:e,onChange:a,path:n,readOnly:o}){return i.jsx(r,{path:`file://${n}`,defaultLanguage:"markdown",value:e,onChange:t=>a(t??""),theme:"vs-dark",options:{minimap:{enabled:!1},fontSize:13,fontFamily:"JetBrains Mono, ui-monospace, Menlo, monospace",lineNumbers:"on",renderLineHighlight:"line",scrollBeyondLastLine:!1,tabSize:2,wordWrap:"on",readOnly:o,automaticLayout:!0}})}export{l as M};
|