@staff0rd/assist 0.437.0 → 0.438.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/commands/sessions/web/bundle.js +459 -458
- package/dist/index.js +491 -183
- package/package.json +3 -1
package/dist/index.js
CHANGED
|
@@ -6,7 +6,7 @@ import { Command } from "commander";
|
|
|
6
6
|
// package.json
|
|
7
7
|
var package_default = {
|
|
8
8
|
name: "@staff0rd/assist",
|
|
9
|
-
version: "0.
|
|
9
|
+
version: "0.438.0",
|
|
10
10
|
type: "module",
|
|
11
11
|
main: "dist/index.js",
|
|
12
12
|
bin: {
|
|
@@ -98,6 +98,7 @@ var package_default = {
|
|
|
98
98
|
"@types/pg-copy-streams": "^1.2.5",
|
|
99
99
|
"@types/react": "^19.2.14",
|
|
100
100
|
"@types/react-dom": "^19.2.3",
|
|
101
|
+
"@types/react-file-icon": "^1.0.5",
|
|
101
102
|
"@types/semver": "^7.7.1",
|
|
102
103
|
"@types/shell-quote": "^1.7.5",
|
|
103
104
|
"@types/ws": "^8.18.1",
|
|
@@ -110,6 +111,7 @@ var package_default = {
|
|
|
110
111
|
oxlint: "^1.69.0",
|
|
111
112
|
react: "^19.2.4",
|
|
112
113
|
"react-dom": "^19.2.4",
|
|
114
|
+
"react-file-icon": "^1.6.0",
|
|
113
115
|
"react-router": "^7.14.0",
|
|
114
116
|
"semantic-release": "^25.0.2",
|
|
115
117
|
skott: "^0.35.11",
|
|
@@ -4101,12 +4103,12 @@ function getChangedFiles() {
|
|
|
4101
4103
|
function filterByChangedFiles(entries) {
|
|
4102
4104
|
const hasFilters = entries.some((e) => e.filter);
|
|
4103
4105
|
if (!hasFilters) return entries;
|
|
4104
|
-
const
|
|
4106
|
+
const changedFiles2 = getChangedFiles();
|
|
4105
4107
|
return entries.filter((entry) => {
|
|
4106
4108
|
const { filter } = entry;
|
|
4107
4109
|
if (!filter) return true;
|
|
4108
|
-
if (
|
|
4109
|
-
return
|
|
4110
|
+
if (changedFiles2.length === 0) return false;
|
|
4111
|
+
return changedFiles2.some((file) => minimatch4(file, filter));
|
|
4110
4112
|
});
|
|
4111
4113
|
}
|
|
4112
4114
|
|
|
@@ -6380,10 +6382,10 @@ async function prepareRun(id, resumeSessionId) {
|
|
|
6380
6382
|
import chalk39 from "chalk";
|
|
6381
6383
|
function reportDuplicateRun(itemId2, holder) {
|
|
6382
6384
|
const since = holder.timestamp ? `, started ${holder.timestamp}` : "";
|
|
6383
|
-
const
|
|
6385
|
+
const reason2 = `another session is already running it (pid ${holder.pid}${since}); close that session before running it again`;
|
|
6384
6386
|
console.log(chalk39.yellow(`Already running: ${formatItemId(itemId2)}`));
|
|
6385
|
-
console.log(chalk39.dim(`Refusing to start a second run \u2014 ${
|
|
6386
|
-
appendDaemonLog(`backlog run ${itemId2}: refused \u2014 ${
|
|
6387
|
+
console.log(chalk39.dim(`Refusing to start a second run \u2014 ${reason2}.`));
|
|
6388
|
+
appendDaemonLog(`backlog run ${itemId2}: refused \u2014 ${reason2}`);
|
|
6387
6389
|
}
|
|
6388
6390
|
|
|
6389
6391
|
// src/commands/backlog/consumePause.ts
|
|
@@ -8030,11 +8032,11 @@ import {
|
|
|
8030
8032
|
var SPAWN_TIMEOUT_MS = 1e4;
|
|
8031
8033
|
var RETRY_DELAY_MS2 = 200;
|
|
8032
8034
|
var STALE_LOCK_MS = SPAWN_TIMEOUT_MS + 5e3;
|
|
8033
|
-
async function ensureDaemonRunning(
|
|
8035
|
+
async function ensureDaemonRunning(reason2 = "unspecified") {
|
|
8034
8036
|
if (await isDaemonRunning()) return;
|
|
8035
8037
|
mkdirSync10(daemonPaths.dir, { recursive: true });
|
|
8036
8038
|
const holdsLock = acquireSpawnLock();
|
|
8037
|
-
if (holdsLock) spawnDaemon(
|
|
8039
|
+
if (holdsLock) spawnDaemon(reason2);
|
|
8038
8040
|
try {
|
|
8039
8041
|
await waitForDaemon();
|
|
8040
8042
|
} finally {
|
|
@@ -8083,12 +8085,12 @@ function releaseSpawnLock() {
|
|
|
8083
8085
|
} catch {
|
|
8084
8086
|
}
|
|
8085
8087
|
}
|
|
8086
|
-
function spawnDaemon(
|
|
8088
|
+
function spawnDaemon(reason2) {
|
|
8087
8089
|
const log = openSync2(daemonPaths.log, "a");
|
|
8088
8090
|
const child = spawn4(process.execPath, [process.argv[1], "daemon", "run"], {
|
|
8089
8091
|
detached: true,
|
|
8090
8092
|
stdio: ["ignore", log, log],
|
|
8091
|
-
env: { ...process.env, ASSIST_DAEMON_SPAWN_REASON:
|
|
8093
|
+
env: { ...process.env, ASSIST_DAEMON_SPAWN_REASON: reason2 }
|
|
8092
8094
|
});
|
|
8093
8095
|
child.unref();
|
|
8094
8096
|
}
|
|
@@ -8485,7 +8487,7 @@ async function patchSubtaskStatus(req, res, id, idx) {
|
|
|
8485
8487
|
}
|
|
8486
8488
|
|
|
8487
8489
|
// src/commands/backlog/rewindItemToPhase.ts
|
|
8488
|
-
async function rewindItemToPhase(orm, item, phaseNumber,
|
|
8490
|
+
async function rewindItemToPhase(orm, item, phaseNumber, reason2) {
|
|
8489
8491
|
const plan2 = resolveRewindPlan(item);
|
|
8490
8492
|
if (phaseNumber < 1 || phaseNumber > plan2.length) {
|
|
8491
8493
|
return {
|
|
@@ -8504,7 +8506,7 @@ async function rewindItemToPhase(orm, item, phaseNumber, reason) {
|
|
|
8504
8506
|
await appendComment(
|
|
8505
8507
|
orm,
|
|
8506
8508
|
item.id,
|
|
8507
|
-
`Rewound to phase ${phaseNumber} (${phaseName}): ${
|
|
8509
|
+
`Rewound to phase ${phaseNumber} (${phaseName}): ${reason2}`,
|
|
8508
8510
|
{ phase: phaseNumber }
|
|
8509
8511
|
);
|
|
8510
8512
|
await updateCurrentPhase(orm, item.id, phaseNumber);
|
|
@@ -8515,11 +8517,11 @@ async function rewindItemToPhase(orm, item, phaseNumber, reason) {
|
|
|
8515
8517
|
|
|
8516
8518
|
// src/commands/backlog/web/rewindItemPhase.ts
|
|
8517
8519
|
async function rewindItemPhase(req, res, id) {
|
|
8518
|
-
const { phase, reason } = await parseRewindBody(req);
|
|
8520
|
+
const { phase, reason: reason2 } = await parseRewindBody(req);
|
|
8519
8521
|
const result = await findItemOr404(res, id);
|
|
8520
8522
|
if (!result) return;
|
|
8521
8523
|
const { orm, item } = result;
|
|
8522
|
-
const outcome = await rewindItemToPhase(orm, item, phase,
|
|
8524
|
+
const outcome = await rewindItemToPhase(orm, item, phase, reason2);
|
|
8523
8525
|
if (!outcome.ok) {
|
|
8524
8526
|
respondJson(res, 400, { error: outcome.error });
|
|
8525
8527
|
return;
|
|
@@ -8977,6 +8979,85 @@ function jiraSite(_req, res) {
|
|
|
8977
8979
|
respondJson(res, 200, { site: config.site ?? null });
|
|
8978
8980
|
}
|
|
8979
8981
|
|
|
8982
|
+
// src/commands/sessions/web/scoreFilePath.ts
|
|
8983
|
+
var BASENAME_WEIGHT = 1e3;
|
|
8984
|
+
var START_BONUS = 15;
|
|
8985
|
+
var BOUNDARY_BONUS = 10;
|
|
8986
|
+
var CONSECUTIVE_BONUS = 8;
|
|
8987
|
+
var MAX_GAP_PENALTY = 6;
|
|
8988
|
+
var MAX_LENGTH_PENALTY = 100;
|
|
8989
|
+
function isBoundary(text17, index3) {
|
|
8990
|
+
if (index3 === 0) return true;
|
|
8991
|
+
const previous = text17[index3 - 1] ?? "";
|
|
8992
|
+
if (!/[A-Za-z0-9]/.test(previous)) return true;
|
|
8993
|
+
return /[a-z0-9]/.test(previous) && /[A-Z]/.test(text17[index3] ?? "");
|
|
8994
|
+
}
|
|
8995
|
+
function subsequenceScore(text17, query) {
|
|
8996
|
+
const lower = text17.toLowerCase();
|
|
8997
|
+
let score = 0;
|
|
8998
|
+
let cursor = 0;
|
|
8999
|
+
let previous = -1;
|
|
9000
|
+
for (const char of query) {
|
|
9001
|
+
const at2 = lower.indexOf(char, cursor);
|
|
9002
|
+
if (at2 === -1) return null;
|
|
9003
|
+
if (at2 === previous + 1) score += CONSECUTIVE_BONUS;
|
|
9004
|
+
if (isBoundary(text17, at2)) score += BOUNDARY_BONUS;
|
|
9005
|
+
if (at2 === 0) score += START_BONUS;
|
|
9006
|
+
score -= Math.min(at2 - cursor, MAX_GAP_PENALTY);
|
|
9007
|
+
previous = at2;
|
|
9008
|
+
cursor = at2 + 1;
|
|
9009
|
+
}
|
|
9010
|
+
return score - Math.min(text17.length, MAX_LENGTH_PENALTY);
|
|
9011
|
+
}
|
|
9012
|
+
function scoreFilePath(path71, query) {
|
|
9013
|
+
const needle = query.trim().toLowerCase();
|
|
9014
|
+
if (!needle) return 0;
|
|
9015
|
+
const basename18 = path71.slice(path71.lastIndexOf("/") + 1);
|
|
9016
|
+
const inBasename = subsequenceScore(basename18, needle);
|
|
9017
|
+
if (inBasename !== null) return BASENAME_WEIGHT + inBasename;
|
|
9018
|
+
return subsequenceScore(path71, needle);
|
|
9019
|
+
}
|
|
9020
|
+
|
|
9021
|
+
// src/commands/sessions/web/rankFilePaths.ts
|
|
9022
|
+
function rankFilePaths(paths, query, limit) {
|
|
9023
|
+
const scored = [];
|
|
9024
|
+
for (const path71 of paths) {
|
|
9025
|
+
const score = scoreFilePath(path71, query);
|
|
9026
|
+
if (score !== null) scored.push({ path: path71, score });
|
|
9027
|
+
}
|
|
9028
|
+
scored.sort((a, b) => b.score - a.score || a.path.localeCompare(b.path));
|
|
9029
|
+
return scored.slice(0, limit).map((entry) => entry.path);
|
|
9030
|
+
}
|
|
9031
|
+
|
|
9032
|
+
// src/commands/sessions/web/listFiles.ts
|
|
9033
|
+
var MAX_RESULTS = 20;
|
|
9034
|
+
var CACHE_TTL_MS = 5e3;
|
|
9035
|
+
var LS_FILES_MAX_BUFFER = 32 * 1024 * 1024;
|
|
9036
|
+
var fileListCache = /* @__PURE__ */ new Map();
|
|
9037
|
+
async function repoFiles(cwd) {
|
|
9038
|
+
const cached = fileListCache.get(cwd);
|
|
9039
|
+
if (cached && cached.expires > Date.now()) return cached.files;
|
|
9040
|
+
const output = await execGit(
|
|
9041
|
+
cwd,
|
|
9042
|
+
["ls-files", "--cached", "--others", "--exclude-standard"],
|
|
9043
|
+
{ maxBuffer: LS_FILES_MAX_BUFFER }
|
|
9044
|
+
);
|
|
9045
|
+
const files = [...new Set(output.split(/\r?\n/).filter(Boolean))];
|
|
9046
|
+
fileListCache.set(cwd, { files, expires: Date.now() + CACHE_TTL_MS });
|
|
9047
|
+
return files;
|
|
9048
|
+
}
|
|
9049
|
+
async function listFiles(req, res) {
|
|
9050
|
+
const cwd = getCwdParam(req, res);
|
|
9051
|
+
if (!cwd) return;
|
|
9052
|
+
const query = new URL(req.url ?? "/", "http://localhost").searchParams.get("q") ?? "";
|
|
9053
|
+
try {
|
|
9054
|
+
const files = await repoFiles(cwd);
|
|
9055
|
+
respondJson(res, 200, { files: rankFilePaths(files, query, MAX_RESULTS) });
|
|
9056
|
+
} catch {
|
|
9057
|
+
respondJson(res, 500, { error: "Failed to list files" });
|
|
9058
|
+
}
|
|
9059
|
+
}
|
|
9060
|
+
|
|
8980
9061
|
// src/commands/news/shared.ts
|
|
8981
9062
|
import { decodeHTML } from "entities";
|
|
8982
9063
|
function extractText(xml, tag) {
|
|
@@ -9173,7 +9254,7 @@ async function openInCode(req, res) {
|
|
|
9173
9254
|
import { execFile as execFile4 } from "child_process";
|
|
9174
9255
|
import { promisify as promisify5 } from "util";
|
|
9175
9256
|
var execFileAsync4 = promisify5(execFile4);
|
|
9176
|
-
var
|
|
9257
|
+
var CACHE_TTL_MS2 = 3e4;
|
|
9177
9258
|
function createCachedGhJson(args, parse3, fallback, options2 = {}) {
|
|
9178
9259
|
const { cacheFallback = true } = options2;
|
|
9179
9260
|
const cache2 = /* @__PURE__ */ new Map();
|
|
@@ -9193,7 +9274,7 @@ function createCachedGhJson(args, parse3, fallback, options2 = {}) {
|
|
|
9193
9274
|
value = fallback;
|
|
9194
9275
|
}
|
|
9195
9276
|
if (cacheFallback || value !== fallback)
|
|
9196
|
-
cache2.set(key, { value, expires: now +
|
|
9277
|
+
cache2.set(key, { value, expires: now + CACHE_TTL_MS2 });
|
|
9197
9278
|
return value;
|
|
9198
9279
|
};
|
|
9199
9280
|
}
|
|
@@ -9561,6 +9642,7 @@ var routes2 = {
|
|
|
9561
9642
|
"GET /api/git-status": gitStatus,
|
|
9562
9643
|
"GET /api/diff": diff2,
|
|
9563
9644
|
"GET /api/file": fileContent,
|
|
9645
|
+
"GET /api/files": listFiles,
|
|
9564
9646
|
"GET /api/jira-site": jiraSite,
|
|
9565
9647
|
"GET /api/harness": harnessCapabilities,
|
|
9566
9648
|
"GET /api/pr-status": prStatus,
|
|
@@ -10364,8 +10446,8 @@ function countCopyRows(data) {
|
|
|
10364
10446
|
|
|
10365
10447
|
// src/commands/backlog/dump/parseDump.ts
|
|
10366
10448
|
var NEWLINE = 10;
|
|
10367
|
-
function invalid(
|
|
10368
|
-
throw new Error(`Invalid dump: ${
|
|
10449
|
+
function invalid(reason2) {
|
|
10450
|
+
throw new Error(`Invalid dump: ${reason2}`);
|
|
10369
10451
|
}
|
|
10370
10452
|
function readLine(dump, start3) {
|
|
10371
10453
|
const eol = dump.indexOf(NEWLINE, start3);
|
|
@@ -11462,14 +11544,14 @@ async function unstar(id) {
|
|
|
11462
11544
|
|
|
11463
11545
|
// src/commands/backlog/wontdo/index.ts
|
|
11464
11546
|
import chalk96 from "chalk";
|
|
11465
|
-
async function wontdo(id,
|
|
11547
|
+
async function wontdo(id, reason2) {
|
|
11466
11548
|
const found = await findOneItem(id);
|
|
11467
11549
|
if (!found) return;
|
|
11468
11550
|
const { orm, item } = found;
|
|
11469
11551
|
await updateStatus(orm, item.id, "wontdo");
|
|
11470
|
-
if (
|
|
11552
|
+
if (reason2) {
|
|
11471
11553
|
const phase = item.currentPhase ?? 1;
|
|
11472
|
-
await appendComment(orm, item.id,
|
|
11554
|
+
await appendComment(orm, item.id, reason2, { phase, type: "summary" });
|
|
11473
11555
|
}
|
|
11474
11556
|
console.log(
|
|
11475
11557
|
chalk96.red(`Won't do item ${formatItemId(item.id)}: ${item.name}`)
|
|
@@ -12795,9 +12877,9 @@ function resolvePermission(toolName, parts) {
|
|
|
12795
12877
|
if (denied) return denied;
|
|
12796
12878
|
const reasons = [];
|
|
12797
12879
|
for (const part of parts) {
|
|
12798
|
-
const
|
|
12799
|
-
if (!
|
|
12800
|
-
reasons.push(
|
|
12880
|
+
const reason2 = isApprovedRead(part, toolName);
|
|
12881
|
+
if (!reason2) return void 0;
|
|
12882
|
+
reasons.push(reason2);
|
|
12801
12883
|
}
|
|
12802
12884
|
return {
|
|
12803
12885
|
permissionDecision: "allow",
|
|
@@ -12921,13 +13003,13 @@ function cliHookCheck(command, toolName = "Bash") {
|
|
|
12921
13003
|
if (reportDeny(toolName, parts)) return;
|
|
12922
13004
|
const reasons = [];
|
|
12923
13005
|
for (const part of parts) {
|
|
12924
|
-
const
|
|
12925
|
-
if (!
|
|
13006
|
+
const reason2 = isApprovedRead(part, toolName);
|
|
13007
|
+
if (!reason2) {
|
|
12926
13008
|
console.log(`not approved (unrecognised: ${part})`);
|
|
12927
13009
|
process.exitCode = 1;
|
|
12928
13010
|
return;
|
|
12929
13011
|
}
|
|
12930
|
-
reasons.push(` ${part} -> ${
|
|
13012
|
+
reasons.push(` ${part} -> ${reason2}`);
|
|
12931
13013
|
}
|
|
12932
13014
|
console.log(`approved
|
|
12933
13015
|
${reasons.join("\n")}`);
|
|
@@ -15665,9 +15747,9 @@ function resolveSolution(sln) {
|
|
|
15665
15747
|
}
|
|
15666
15748
|
|
|
15667
15749
|
// src/commands/dotnet/filterToChangedFiles.ts
|
|
15668
|
-
function filterToChangedFiles(issues,
|
|
15750
|
+
function filterToChangedFiles(issues, changedFiles2) {
|
|
15669
15751
|
const normalize = (f) => f.replace(/\\/g, "/").replace(/^file:\/\/\//, "");
|
|
15670
|
-
const changed =
|
|
15752
|
+
const changed = changedFiles2.map(normalize);
|
|
15671
15753
|
return issues.filter((i) => {
|
|
15672
15754
|
const file = normalize(i.file);
|
|
15673
15755
|
return changed.some((c) => file === c || file.endsWith(`/${c}`));
|
|
@@ -15785,23 +15867,23 @@ function runRoslynInspect(slnPath) {
|
|
|
15785
15867
|
}
|
|
15786
15868
|
|
|
15787
15869
|
// src/commands/dotnet/runEngine.ts
|
|
15788
|
-
function runEngine(resolved,
|
|
15870
|
+
function runEngine(resolved, changedFiles2, options2) {
|
|
15789
15871
|
if (options2.roslyn) {
|
|
15790
15872
|
const issues = runRoslynInspect(resolved);
|
|
15791
|
-
return
|
|
15873
|
+
return changedFiles2 ? filterToChangedFiles(issues, changedFiles2) : issues;
|
|
15792
15874
|
}
|
|
15793
15875
|
return parseInspectReport(
|
|
15794
|
-
runInspectCode(resolved,
|
|
15876
|
+
runInspectCode(resolved, changedFiles2?.join(";") ?? null, !!options2.swea)
|
|
15795
15877
|
);
|
|
15796
15878
|
}
|
|
15797
15879
|
|
|
15798
15880
|
// src/commands/dotnet/inspect.ts
|
|
15799
|
-
function logScope(
|
|
15800
|
-
if (
|
|
15881
|
+
function logScope(changedFiles2) {
|
|
15882
|
+
if (changedFiles2 === null) {
|
|
15801
15883
|
console.log(chalk149.dim("Inspecting full solution..."));
|
|
15802
15884
|
} else {
|
|
15803
15885
|
console.log(
|
|
15804
|
-
chalk149.dim(`Inspecting ${
|
|
15886
|
+
chalk149.dim(`Inspecting ${changedFiles2.length} changed file(s)...`)
|
|
15805
15887
|
);
|
|
15806
15888
|
}
|
|
15807
15889
|
}
|
|
@@ -15817,14 +15899,14 @@ async function inspect(sln, options2) {
|
|
|
15817
15899
|
if (options2.roslyn) assertMsbuildInstalled();
|
|
15818
15900
|
else assertJbInstalled();
|
|
15819
15901
|
const scope = parseScope(options2.scope);
|
|
15820
|
-
const
|
|
15821
|
-
if (
|
|
15902
|
+
const changedFiles2 = getChangedCsFiles(scope);
|
|
15903
|
+
if (changedFiles2 !== null && changedFiles2.length === 0) {
|
|
15822
15904
|
console.log(chalk149.green("No changed .cs files found"));
|
|
15823
15905
|
return;
|
|
15824
15906
|
}
|
|
15825
|
-
logScope(
|
|
15907
|
+
logScope(changedFiles2);
|
|
15826
15908
|
const start3 = Date.now();
|
|
15827
|
-
const issues = runEngine(resolved,
|
|
15909
|
+
const issues = runEngine(resolved, changedFiles2, options2);
|
|
15828
15910
|
const elapsed = Date.now() - start3;
|
|
15829
15911
|
const { all = false, only = [], suppress = [] } = options2;
|
|
15830
15912
|
reportResults(filterIssues(issues, all, only, suppress), elapsed);
|
|
@@ -16076,14 +16158,14 @@ async function editHook() {
|
|
|
16076
16158
|
const input = tryParseInput2(await readStdin());
|
|
16077
16159
|
if (!input) return;
|
|
16078
16160
|
const existing = input.tool_name === "Write" ? readExisting(input.tool_input.file_path) : void 0;
|
|
16079
|
-
const
|
|
16080
|
-
if (!
|
|
16161
|
+
const reason2 = decideOverrideGuard(input, existing) ?? decideCommentGuard(input, existing) ?? decideMigrationGuard(input, existing);
|
|
16162
|
+
if (!reason2) return;
|
|
16081
16163
|
console.log(
|
|
16082
16164
|
JSON.stringify({
|
|
16083
16165
|
hookSpecificOutput: {
|
|
16084
16166
|
hookEventName: "PreToolUse",
|
|
16085
16167
|
permissionDecision: "deny",
|
|
16086
|
-
permissionDecisionReason:
|
|
16168
|
+
permissionDecisionReason: reason2
|
|
16087
16169
|
}
|
|
16088
16170
|
})
|
|
16089
16171
|
);
|
|
@@ -18775,16 +18857,16 @@ function reply(commentId, body) {
|
|
|
18775
18857
|
|
|
18776
18858
|
// src/commands/prs/wontfix.ts
|
|
18777
18859
|
import { execSync as execSync48 } from "child_process";
|
|
18778
|
-
function validateReason(
|
|
18779
|
-
const lowerReason =
|
|
18860
|
+
function validateReason(reason2) {
|
|
18861
|
+
const lowerReason = reason2.toLowerCase();
|
|
18780
18862
|
if (lowerReason.includes("claude") || lowerReason.includes("opus")) {
|
|
18781
18863
|
console.error('Error: Reason must not contain "claude" or "opus"');
|
|
18782
18864
|
process.exit(1);
|
|
18783
18865
|
}
|
|
18784
18866
|
}
|
|
18785
|
-
function validateShaReferences(
|
|
18867
|
+
function validateShaReferences(reason2) {
|
|
18786
18868
|
const commitLinkPattern = /\[[0-9a-f]{7}\]\(https:\/\/github\.com\/[^/]+\/[^/]+\/commit\/([0-9a-f]{40})\)/gi;
|
|
18787
|
-
const shas = Array.from(
|
|
18869
|
+
const shas = Array.from(reason2.matchAll(commitLinkPattern), (m) => m[1]);
|
|
18788
18870
|
if (shas.length === 0) {
|
|
18789
18871
|
return;
|
|
18790
18872
|
}
|
|
@@ -18803,11 +18885,11 @@ function validateShaReferences(reason) {
|
|
|
18803
18885
|
process.exit(1);
|
|
18804
18886
|
}
|
|
18805
18887
|
}
|
|
18806
|
-
function wontfix(commentId,
|
|
18807
|
-
validateReason(
|
|
18808
|
-
validateShaReferences(
|
|
18888
|
+
function wontfix(commentId, reason2) {
|
|
18889
|
+
validateReason(reason2);
|
|
18890
|
+
validateShaReferences(reason2);
|
|
18809
18891
|
try {
|
|
18810
|
-
resolveCommentWithReply(commentId,
|
|
18892
|
+
resolveCommentWithReply(commentId, reason2);
|
|
18811
18893
|
} catch (error) {
|
|
18812
18894
|
if (isGhNotInstalled(error)) {
|
|
18813
18895
|
console.error("Error: GitHub CLI (gh) is not installed.");
|
|
@@ -18986,8 +19068,8 @@ function registerPrs(program2) {
|
|
|
18986
19068
|
prsCommand.command("fixed <comment-id> <sha>").description("Reply with commit link and resolve thread").action((commentId, sha) => {
|
|
18987
19069
|
fixed(Number.parseInt(commentId, 10), sha);
|
|
18988
19070
|
});
|
|
18989
|
-
prsCommand.command("wontfix <comment-id> <reason>").description("Reply with reason and resolve thread").action((commentId,
|
|
18990
|
-
wontfix(Number.parseInt(commentId, 10),
|
|
19071
|
+
prsCommand.command("wontfix <comment-id> <reason>").description("Reply with reason and resolve thread").action((commentId, reason2) => {
|
|
19072
|
+
wontfix(Number.parseInt(commentId, 10), reason2);
|
|
18991
19073
|
});
|
|
18992
19074
|
prsCommand.command("reply <comment-id> <body>").description("Reply to a comment thread without resolving it").action((commentId, body) => {
|
|
18993
19075
|
reply(Number.parseInt(commentId, 10), body);
|
|
@@ -20820,8 +20902,8 @@ function findRootParent(file, importedBy, visited) {
|
|
|
20820
20902
|
function clusterFiles(graph) {
|
|
20821
20903
|
const clusters = /* @__PURE__ */ new Map();
|
|
20822
20904
|
for (const file of graph.files) {
|
|
20823
|
-
const
|
|
20824
|
-
if (
|
|
20905
|
+
const basename18 = path52.basename(file, path52.extname(file));
|
|
20906
|
+
if (basename18 === "index") continue;
|
|
20825
20907
|
const importers = graph.importedBy.get(file);
|
|
20826
20908
|
if (!importers || importers.size !== 1) continue;
|
|
20827
20909
|
const parent = [...importers][0];
|
|
@@ -20968,10 +21050,10 @@ function listFilesRecursive(dir) {
|
|
|
20968
21050
|
}
|
|
20969
21051
|
return results;
|
|
20970
21052
|
}
|
|
20971
|
-
function addDirectoryFileMoves(moves, childDir, newLocation,
|
|
21053
|
+
function addDirectoryFileMoves(moves, childDir, newLocation, reason2) {
|
|
20972
21054
|
for (const file of listFilesRecursive(childDir)) {
|
|
20973
21055
|
const rel = path55.relative(childDir, file);
|
|
20974
|
-
moves.push({ from: file, to: path55.join(newLocation, rel), reason });
|
|
21056
|
+
moves.push({ from: file, to: path55.join(newLocation, rel), reason: reason2 });
|
|
20975
21057
|
}
|
|
20976
21058
|
}
|
|
20977
21059
|
function resolveChildDest(parentDir, childDir) {
|
|
@@ -20982,8 +21064,8 @@ function childMoveReason(parentDir) {
|
|
|
20982
21064
|
}
|
|
20983
21065
|
function registerDirectoryMove(result, childDir, dest, parentDir) {
|
|
20984
21066
|
result.directories.push(dest);
|
|
20985
|
-
const
|
|
20986
|
-
addDirectoryFileMoves(result.moves, childDir, dest,
|
|
21067
|
+
const reason2 = childMoveReason(parentDir);
|
|
21068
|
+
addDirectoryFileMoves(result.moves, childDir, dest, reason2);
|
|
20987
21069
|
}
|
|
20988
21070
|
function planChildDirectoryMove(result, parentDir, childDir) {
|
|
20989
21071
|
const dest = resolveChildDest(parentDir, childDir);
|
|
@@ -21383,7 +21465,7 @@ function gatherContext() {
|
|
|
21383
21465
|
encoding: "utf8"
|
|
21384
21466
|
}).trim();
|
|
21385
21467
|
const prInfo = fetchPrDiffInfo();
|
|
21386
|
-
const
|
|
21468
|
+
const changedFiles2 = fetchPrChangedFiles(prInfo.prNumber);
|
|
21387
21469
|
const diff3 = fetchPrDiff(prInfo.prNumber, prInfo.baseSha, prInfo.headSha);
|
|
21388
21470
|
return {
|
|
21389
21471
|
branch: branch2,
|
|
@@ -21394,7 +21476,7 @@ function gatherContext() {
|
|
|
21394
21476
|
baseSha: prInfo.baseSha,
|
|
21395
21477
|
headRef: prInfo.headRef,
|
|
21396
21478
|
headSha: prInfo.headSha,
|
|
21397
|
-
changedFiles,
|
|
21479
|
+
changedFiles: changedFiles2,
|
|
21398
21480
|
diff: diff3
|
|
21399
21481
|
};
|
|
21400
21482
|
}
|
|
@@ -25904,6 +25986,16 @@ async function gitResult(cwd, args) {
|
|
|
25904
25986
|
};
|
|
25905
25987
|
}
|
|
25906
25988
|
}
|
|
25989
|
+
function gitSyncResult(cwd, args) {
|
|
25990
|
+
try {
|
|
25991
|
+
return { ok: true, out: gitSync(cwd, args).trim() };
|
|
25992
|
+
} catch (error) {
|
|
25993
|
+
return {
|
|
25994
|
+
ok: false,
|
|
25995
|
+
error: error instanceof Error ? error.message : String(error)
|
|
25996
|
+
};
|
|
25997
|
+
}
|
|
25998
|
+
}
|
|
25907
25999
|
function gitSync(cwd, args) {
|
|
25908
26000
|
const { file, argv, options: options2 } = gitInvocation(cwd, args);
|
|
25909
26001
|
return execFileSync12(file, argv, {
|
|
@@ -25918,6 +26010,8 @@ function gitSyncOrNull(cwd, args) {
|
|
|
25918
26010
|
return null;
|
|
25919
26011
|
}
|
|
25920
26012
|
}
|
|
26013
|
+
|
|
26014
|
+
// src/commands/sessions/daemon/worktree/listWorktreePaths.ts
|
|
25921
26015
|
function listWorktreePaths(cwd) {
|
|
25922
26016
|
const out = gitSyncOrNull(cwd, ["worktree", "list", "--porcelain"]);
|
|
25923
26017
|
if (!out) return [];
|
|
@@ -25942,6 +26036,59 @@ function gitCommonDir(cwd) {
|
|
|
25942
26036
|
]);
|
|
25943
26037
|
}
|
|
25944
26038
|
|
|
26039
|
+
// src/commands/sessions/daemon/worktree/readWorktreeRegistry.ts
|
|
26040
|
+
var REGISTRY_FILE = "worktrees.json";
|
|
26041
|
+
function readWorktreeRegistry() {
|
|
26042
|
+
const data = loadJson(REGISTRY_FILE);
|
|
26043
|
+
return Object.entries(data).map(([path71, record]) => ({ path: path71, ...record }));
|
|
26044
|
+
}
|
|
26045
|
+
function recordWorktree(path71, clone, origin) {
|
|
26046
|
+
const data = loadJson(REGISTRY_FILE);
|
|
26047
|
+
data[path71] = { clone, origin };
|
|
26048
|
+
saveJson(REGISTRY_FILE, data);
|
|
26049
|
+
}
|
|
26050
|
+
function forgetWorktree(path71) {
|
|
26051
|
+
const data = loadJson(REGISTRY_FILE);
|
|
26052
|
+
if (!(path71 in data)) return;
|
|
26053
|
+
delete data[path71];
|
|
26054
|
+
saveJson(REGISTRY_FILE, data);
|
|
26055
|
+
}
|
|
26056
|
+
|
|
26057
|
+
// src/commands/sessions/daemon/worktree/removeTree.ts
|
|
26058
|
+
async function removeTree(clone, worktreePath, force) {
|
|
26059
|
+
const remove2 = (forced) => git3(clone, [
|
|
26060
|
+
"worktree",
|
|
26061
|
+
"remove",
|
|
26062
|
+
...forced ? ["--force"] : [],
|
|
26063
|
+
worktreePath
|
|
26064
|
+
]);
|
|
26065
|
+
try {
|
|
26066
|
+
await remove2(force);
|
|
26067
|
+
return true;
|
|
26068
|
+
} catch (error) {
|
|
26069
|
+
daemonLog(`worktree ${worktreePath} removal failed: ${reason(error)}`);
|
|
26070
|
+
}
|
|
26071
|
+
if (force) return false;
|
|
26072
|
+
return await removeIgnoringLeftovers(remove2, worktreePath);
|
|
26073
|
+
}
|
|
26074
|
+
async function removeIgnoringLeftovers(remove2, worktreePath) {
|
|
26075
|
+
try {
|
|
26076
|
+
await remove2(true);
|
|
26077
|
+
daemonLog(
|
|
26078
|
+
`worktree ${worktreePath} removed on retry; only ignored files remained, its work is landed`
|
|
26079
|
+
);
|
|
26080
|
+
return true;
|
|
26081
|
+
} catch (error) {
|
|
26082
|
+
daemonLog(
|
|
26083
|
+
`worktree ${worktreePath} left in place for the next reconcile: ${reason(error)}`
|
|
26084
|
+
);
|
|
26085
|
+
return false;
|
|
26086
|
+
}
|
|
26087
|
+
}
|
|
26088
|
+
function reason(error) {
|
|
26089
|
+
return error instanceof Error ? error.message : String(error);
|
|
26090
|
+
}
|
|
26091
|
+
|
|
25945
26092
|
// src/commands/sessions/daemon/worktree/treeDurability.ts
|
|
25946
26093
|
import { existsSync as existsSync58 } from "fs";
|
|
25947
26094
|
function treeDurability(state) {
|
|
@@ -25950,28 +26097,29 @@ function treeDurability(state) {
|
|
|
25950
26097
|
return { durable: false, reason: "unpushed commits" };
|
|
25951
26098
|
return { durable: true };
|
|
25952
26099
|
}
|
|
25953
|
-
|
|
25954
|
-
const
|
|
26100
|
+
function* durabilityProbes(cwd) {
|
|
26101
|
+
const status3 = yield ["status", "--porcelain"];
|
|
26102
|
+
if (!status3.ok)
|
|
26103
|
+
return { durable: false, reason: `tree state unreadable: ${status3.error}` };
|
|
26104
|
+
const upstream = yield [
|
|
25955
26105
|
"rev-parse",
|
|
25956
26106
|
"--abbrev-ref",
|
|
25957
26107
|
"--symbolic-full-name",
|
|
25958
26108
|
"@{upstream}"
|
|
25959
|
-
]
|
|
26109
|
+
];
|
|
26110
|
+
let localOnlyCommits;
|
|
25960
26111
|
if (upstream.ok && upstream.out) {
|
|
25961
|
-
const ahead =
|
|
25962
|
-
|
|
25963
|
-
|
|
25964
|
-
|
|
25965
|
-
|
|
25966
|
-
return !ahead.ok || Number(ahead.out) > 0;
|
|
26112
|
+
const ahead = yield ["rev-list", "--count", "@{upstream}..HEAD"];
|
|
26113
|
+
localOnlyCommits = !ahead.ok || Number(ahead.out) > 0;
|
|
26114
|
+
} else {
|
|
26115
|
+
const remoteBranches = yield ["branch", "-r", "--contains", "HEAD"];
|
|
26116
|
+
localOnlyCommits = !remoteBranches.ok || remoteBranches.out === "";
|
|
25967
26117
|
}
|
|
25968
|
-
|
|
25969
|
-
"
|
|
25970
|
-
|
|
25971
|
-
|
|
25972
|
-
|
|
25973
|
-
]);
|
|
25974
|
-
return !remoteBranches.ok || remoteBranches.out === "";
|
|
26118
|
+
return treeDurability({
|
|
26119
|
+
dirty: status3.out !== "",
|
|
26120
|
+
push: pushOnCommit(cwd),
|
|
26121
|
+
localOnlyCommits
|
|
26122
|
+
});
|
|
25975
26123
|
}
|
|
25976
26124
|
function pushOnCommit(cwd) {
|
|
25977
26125
|
try {
|
|
@@ -25982,14 +26130,17 @@ function pushOnCommit(cwd) {
|
|
|
25982
26130
|
}
|
|
25983
26131
|
async function checkDurability(cwd) {
|
|
25984
26132
|
if (!existsSync58(cwd)) return { durable: true };
|
|
25985
|
-
const
|
|
25986
|
-
|
|
25987
|
-
|
|
25988
|
-
return
|
|
25989
|
-
|
|
25990
|
-
|
|
25991
|
-
|
|
25992
|
-
|
|
26133
|
+
const probes = durabilityProbes(cwd);
|
|
26134
|
+
let step = probes.next();
|
|
26135
|
+
while (!step.done) step = probes.next(await gitResult(cwd, step.value));
|
|
26136
|
+
return step.value;
|
|
26137
|
+
}
|
|
26138
|
+
function checkDurabilitySync(cwd) {
|
|
26139
|
+
if (!existsSync58(cwd)) return { durable: true };
|
|
26140
|
+
const probes = durabilityProbes(cwd);
|
|
26141
|
+
let step = probes.next();
|
|
26142
|
+
while (!step.done) step = probes.next(gitSyncResult(cwd, step.value));
|
|
26143
|
+
return step.value;
|
|
25993
26144
|
}
|
|
25994
26145
|
|
|
25995
26146
|
// src/commands/sessions/daemon/worktree/reapWorktree.ts
|
|
@@ -26011,22 +26162,11 @@ async function reapWorktree(worktreePath, force = false) {
|
|
|
26011
26162
|
"--short",
|
|
26012
26163
|
"HEAD"
|
|
26013
26164
|
]);
|
|
26014
|
-
|
|
26015
|
-
|
|
26016
|
-
|
|
26017
|
-
|
|
26018
|
-
|
|
26019
|
-
worktreePath
|
|
26020
|
-
]);
|
|
26021
|
-
if (branch2) await deleteBranch(clone, branch2);
|
|
26022
|
-
daemonLog(`worktree ${worktreePath} reaped${force ? " (forced)" : ""}`);
|
|
26023
|
-
return true;
|
|
26024
|
-
} catch (error) {
|
|
26025
|
-
daemonLog(
|
|
26026
|
-
`worktree ${worktreePath} reap failed: ${error instanceof Error ? error.message : String(error)}`
|
|
26027
|
-
);
|
|
26028
|
-
return false;
|
|
26029
|
-
}
|
|
26165
|
+
if (!await removeTree(clone, worktreePath, force)) return false;
|
|
26166
|
+
if (branch2) await deleteBranch(clone, branch2);
|
|
26167
|
+
forgetWorktree(worktreePath);
|
|
26168
|
+
daemonLog(`worktree ${worktreePath} reaped${force ? " (forced)" : ""}`);
|
|
26169
|
+
return true;
|
|
26030
26170
|
}
|
|
26031
26171
|
async function deleteBranch(clone, branch2) {
|
|
26032
26172
|
const current = await gitOrNull(clone, ["symbolic-ref", "--short", "HEAD"]);
|
|
@@ -26096,22 +26236,42 @@ function setStatus2(session, newStatus) {
|
|
|
26096
26236
|
// src/commands/sessions/daemon/worktree/watchGitState.ts
|
|
26097
26237
|
import { existsSync as existsSync60, watch } from "fs";
|
|
26098
26238
|
var DEBOUNCE_MS = 500;
|
|
26239
|
+
var POLL_MS = 3e4;
|
|
26099
26240
|
function watchGitState(cwd, onChange) {
|
|
26100
26241
|
const common = gitCommonDir(cwd);
|
|
26101
26242
|
if (!common || !existsSync60(common)) return void 0;
|
|
26243
|
+
const watchers = [
|
|
26244
|
+
watchGitDir(common, onChange),
|
|
26245
|
+
pollGitState(cwd, onChange)
|
|
26246
|
+
].filter((w) => w !== void 0);
|
|
26247
|
+
return { close: () => watchers.forEach((w) => w.close()) };
|
|
26248
|
+
}
|
|
26249
|
+
function watchGitDir(common, onChange) {
|
|
26102
26250
|
let timer = null;
|
|
26103
26251
|
try {
|
|
26104
|
-
|
|
26252
|
+
const watcher = watch(common, { recursive: true }, () => {
|
|
26105
26253
|
if (timer) clearTimeout(timer);
|
|
26106
26254
|
timer = setTimeout(onChange, DEBOUNCE_MS);
|
|
26107
26255
|
});
|
|
26256
|
+
return {
|
|
26257
|
+
close: () => {
|
|
26258
|
+
if (timer) clearTimeout(timer);
|
|
26259
|
+
watcher.close();
|
|
26260
|
+
}
|
|
26261
|
+
};
|
|
26108
26262
|
} catch (error) {
|
|
26109
26263
|
daemonLog(
|
|
26110
|
-
`git-state watch
|
|
26264
|
+
`git-state watch unavailable for ${common}, polling only: ${error instanceof Error ? error.message : String(error)}`
|
|
26111
26265
|
);
|
|
26112
26266
|
return void 0;
|
|
26113
26267
|
}
|
|
26114
26268
|
}
|
|
26269
|
+
function pollGitState(cwd, onChange) {
|
|
26270
|
+
const timer = setInterval(onChange, POLL_MS);
|
|
26271
|
+
timer.unref?.();
|
|
26272
|
+
daemonLog(`git-state polled every ${POLL_MS / 1e3}s for ${cwd}`);
|
|
26273
|
+
return { close: () => clearInterval(timer) };
|
|
26274
|
+
}
|
|
26115
26275
|
|
|
26116
26276
|
// src/commands/sessions/daemon/worktree/resolveCloseDurability.ts
|
|
26117
26277
|
async function resolveCloseDurability(session, finalize, notify2) {
|
|
@@ -26133,19 +26293,24 @@ async function resolveCloseDurability(session, finalize, notify2) {
|
|
|
26133
26293
|
session.gitWatcher = void 0;
|
|
26134
26294
|
finalize();
|
|
26135
26295
|
}
|
|
26136
|
-
function holdStopped(session, tree,
|
|
26137
|
-
if (session.undurable?.reason !==
|
|
26296
|
+
function holdStopped(session, tree, reason2, finalize, notify2) {
|
|
26297
|
+
if (session.undurable?.reason !== reason2)
|
|
26138
26298
|
daemonLog(
|
|
26139
|
-
`session ${session.id} stopped; ${tree.removable ? "reap" : "close"} blocked in ${tree.path}: ${
|
|
26299
|
+
`session ${session.id} stopped; ${tree.removable ? "reap" : "close"} blocked in ${tree.path}: ${reason2}`
|
|
26140
26300
|
);
|
|
26141
|
-
session.undurable = { reason, removesTree: tree.removable };
|
|
26301
|
+
session.undurable = { reason: reason2, removesTree: tree.removable };
|
|
26142
26302
|
session.closing = void 0;
|
|
26143
26303
|
setStatus2(session, "stopped");
|
|
26144
|
-
if (!session.gitWatcher)
|
|
26304
|
+
if (!session.gitWatcher) {
|
|
26305
|
+
let rechecking = false;
|
|
26145
26306
|
session.gitWatcher = watchGitState(tree.path, () => {
|
|
26146
|
-
if (session.status !== "stopped") return;
|
|
26147
|
-
|
|
26307
|
+
if (session.status !== "stopped" || rechecking) return;
|
|
26308
|
+
rechecking = true;
|
|
26309
|
+
void resolveCloseDurability(session, finalize, notify2).finally(() => {
|
|
26310
|
+
rechecking = false;
|
|
26311
|
+
});
|
|
26148
26312
|
});
|
|
26313
|
+
}
|
|
26149
26314
|
notify2();
|
|
26150
26315
|
}
|
|
26151
26316
|
|
|
@@ -26893,6 +27058,8 @@ function respawnPlan(session) {
|
|
|
26893
27058
|
status: "waiting"
|
|
26894
27059
|
};
|
|
26895
27060
|
if (initialPrompt) return freshClaudePlan(session, initialPrompt, cwd);
|
|
27061
|
+
if (cwd && session.harness !== "pi")
|
|
27062
|
+
return freshClaudePlan(session, void 0, cwd);
|
|
26896
27063
|
return null;
|
|
26897
27064
|
}
|
|
26898
27065
|
if (commandType === "assist" && assistArgs) {
|
|
@@ -26921,7 +27088,7 @@ function freshClaudePlan(session, prompt, cwd) {
|
|
|
26921
27088
|
claudeSessionId
|
|
26922
27089
|
});
|
|
26923
27090
|
},
|
|
26924
|
-
status: "running"
|
|
27091
|
+
status: prompt ? "running" : "waiting"
|
|
26925
27092
|
};
|
|
26926
27093
|
}
|
|
26927
27094
|
|
|
@@ -27346,9 +27513,9 @@ function restoreOne(persisted, spawn12, sessions) {
|
|
|
27346
27513
|
const id = spawn12((sid) => restoreSession(sid, persisted));
|
|
27347
27514
|
logUnresumable(persisted.name, id, sessions.get(id));
|
|
27348
27515
|
} catch (error) {
|
|
27349
|
-
const
|
|
27516
|
+
const reason2 = logRestoreError(persisted.name, error);
|
|
27350
27517
|
try {
|
|
27351
|
-
spawn12((id) => errorSession(id, persisted,
|
|
27518
|
+
spawn12((id) => errorSession(id, persisted, reason2));
|
|
27352
27519
|
} catch {
|
|
27353
27520
|
}
|
|
27354
27521
|
}
|
|
@@ -27360,9 +27527,9 @@ function logUnresumable(name, id, session) {
|
|
|
27360
27527
|
);
|
|
27361
27528
|
}
|
|
27362
27529
|
function logRestoreError(name, error) {
|
|
27363
|
-
const
|
|
27364
|
-
daemonLog(`failed to restore session "${name}": ${
|
|
27365
|
-
return
|
|
27530
|
+
const reason2 = error instanceof Error ? error.message : String(error);
|
|
27531
|
+
daemonLog(`failed to restore session "${name}": ${reason2}`);
|
|
27532
|
+
return reason2;
|
|
27366
27533
|
}
|
|
27367
27534
|
|
|
27368
27535
|
// src/commands/sessions/daemon/sessionLimits.ts
|
|
@@ -27398,22 +27565,42 @@ function restoreAll(spawn12, sessions) {
|
|
|
27398
27565
|
}
|
|
27399
27566
|
|
|
27400
27567
|
// src/commands/sessions/daemon/worktree/rearmStoppedSessions.ts
|
|
27568
|
+
function armStoppedSession(sessions, session, notify2) {
|
|
27569
|
+
void resolveCloseDurability(
|
|
27570
|
+
session,
|
|
27571
|
+
() => {
|
|
27572
|
+
if (dismissSession(sessions, session.id)) notify2();
|
|
27573
|
+
},
|
|
27574
|
+
notify2
|
|
27575
|
+
);
|
|
27576
|
+
}
|
|
27401
27577
|
function rearmStoppedSessions(sessions, notify2) {
|
|
27402
27578
|
for (const s of sessions.values()) {
|
|
27403
27579
|
if (s.status !== "stopped" || !treeUnderClose(s)) continue;
|
|
27404
|
-
|
|
27405
|
-
s,
|
|
27406
|
-
() => {
|
|
27407
|
-
if (dismissSession(sessions, s.id)) notify2();
|
|
27408
|
-
},
|
|
27409
|
-
notify2
|
|
27410
|
-
);
|
|
27580
|
+
armStoppedSession(sessions, s, notify2);
|
|
27411
27581
|
}
|
|
27412
27582
|
}
|
|
27413
27583
|
|
|
27414
27584
|
// src/commands/sessions/daemon/worktree/reconcileWorktreesOnRestore.ts
|
|
27415
|
-
import { existsSync as
|
|
27416
|
-
import { basename as
|
|
27585
|
+
import { existsSync as existsSync67 } from "fs";
|
|
27586
|
+
import { basename as basename15 } from "path";
|
|
27587
|
+
|
|
27588
|
+
// src/commands/sessions/daemon/worktree/accountedTrees.ts
|
|
27589
|
+
function accountedTrees(sessions) {
|
|
27590
|
+
const accounted = /* @__PURE__ */ new Set();
|
|
27591
|
+
const account = (cwd) => {
|
|
27592
|
+
if (!cwd) return;
|
|
27593
|
+
accounted.add(cwd);
|
|
27594
|
+
const root = findRepoRoot(cwd);
|
|
27595
|
+
if (root) accounted.add(root);
|
|
27596
|
+
};
|
|
27597
|
+
for (const s of sessions.values()) {
|
|
27598
|
+
account(s.cwd);
|
|
27599
|
+
account(s.worktree?.path);
|
|
27600
|
+
}
|
|
27601
|
+
for (const s of loadPersistedSessions()) account(s.cwd);
|
|
27602
|
+
return accounted;
|
|
27603
|
+
}
|
|
27417
27604
|
|
|
27418
27605
|
// src/commands/sessions/daemon/worktree/detectExistingWorktree.ts
|
|
27419
27606
|
function detectExistingWorktree(cwd) {
|
|
@@ -27490,12 +27677,6 @@ function message(error) {
|
|
|
27490
27677
|
}
|
|
27491
27678
|
|
|
27492
27679
|
// src/commands/sessions/daemon/worktree/bindNewWorktree.ts
|
|
27493
|
-
function boundTreeRoots(sessions) {
|
|
27494
|
-
const roots = /* @__PURE__ */ new Set();
|
|
27495
|
-
for (const session of sessions.values())
|
|
27496
|
-
if (session.cwd) roots.add(findRepoRoot(session.cwd) ?? session.cwd);
|
|
27497
|
-
return roots;
|
|
27498
|
-
}
|
|
27499
27680
|
function bindNewWorktree(session, alloc, notify2, startHeld = () => {
|
|
27500
27681
|
}) {
|
|
27501
27682
|
if (alloc.kind !== "worktree" || !alloc.cwd || !alloc.clone || !session)
|
|
@@ -27520,66 +27701,169 @@ function bindResumedWorktree(session, cwd, notify2) {
|
|
|
27520
27701
|
notify2();
|
|
27521
27702
|
}
|
|
27522
27703
|
|
|
27523
|
-
// src/commands/sessions/daemon/worktree/
|
|
27524
|
-
var
|
|
27525
|
-
function
|
|
27526
|
-
|
|
27527
|
-
|
|
27704
|
+
// src/commands/sessions/daemon/worktree/describeHeldWork.ts
|
|
27705
|
+
var MAX_ITEMS = 20;
|
|
27706
|
+
async function describeHeldWork(path71, reason2) {
|
|
27707
|
+
if (reason2.startsWith("uncommitted")) return await changedFiles(path71);
|
|
27708
|
+
if (reason2.startsWith("unpushed")) return await unpushedCommits(path71, reason2);
|
|
27709
|
+
return { summary: reason2, items: [] };
|
|
27528
27710
|
}
|
|
27529
|
-
function
|
|
27530
|
-
const
|
|
27531
|
-
|
|
27532
|
-
|
|
27711
|
+
async function changedFiles(path71) {
|
|
27712
|
+
const status3 = await gitResult(path71, ["status", "--porcelain"]);
|
|
27713
|
+
const lines = status3.ok ? nonEmptyLines(status3.out) : [];
|
|
27714
|
+
return {
|
|
27715
|
+
summary: `${lines.length} uncommitted ${lines.length === 1 ? "file" : "files"}`,
|
|
27716
|
+
items: capped(lines)
|
|
27717
|
+
};
|
|
27533
27718
|
}
|
|
27534
|
-
|
|
27535
|
-
|
|
27536
|
-
|
|
27537
|
-
|
|
27538
|
-
|
|
27719
|
+
async function unpushedCommits(path71, reason2) {
|
|
27720
|
+
const log = await gitResult(path71, ["log", "--oneline", "@{upstream}..HEAD"]);
|
|
27721
|
+
if (!log.ok) return { summary: reason2, items: [] };
|
|
27722
|
+
const lines = nonEmptyLines(log.out);
|
|
27723
|
+
return {
|
|
27724
|
+
summary: `${lines.length} unpushed ${lines.length === 1 ? "commit" : "commits"}`,
|
|
27725
|
+
items: capped(lines)
|
|
27726
|
+
};
|
|
27539
27727
|
}
|
|
27540
|
-
|
|
27541
|
-
|
|
27542
|
-
|
|
27543
|
-
|
|
27544
|
-
|
|
27545
|
-
|
|
27546
|
-
|
|
27547
|
-
|
|
27548
|
-
|
|
27549
|
-
|
|
27550
|
-
|
|
27551
|
-
|
|
27552
|
-
|
|
27553
|
-
|
|
27554
|
-
|
|
27728
|
+
function nonEmptyLines(out) {
|
|
27729
|
+
return out.split("\n").map((line) => line.trim()).filter((line) => line !== "");
|
|
27730
|
+
}
|
|
27731
|
+
function capped(lines) {
|
|
27732
|
+
if (lines.length <= MAX_ITEMS) return lines;
|
|
27733
|
+
return [
|
|
27734
|
+
...lines.slice(0, MAX_ITEMS),
|
|
27735
|
+
`\u2026 and ${lines.length - MAX_ITEMS} more`
|
|
27736
|
+
];
|
|
27737
|
+
}
|
|
27738
|
+
|
|
27739
|
+
// src/commands/sessions/daemon/worktree/reclaimVanishedWorktrees.ts
|
|
27740
|
+
import { existsSync as existsSync66 } from "fs";
|
|
27741
|
+
async function reclaimVanishedWorktrees(clone, paths) {
|
|
27742
|
+
if (!existsSync66(clone)) {
|
|
27743
|
+
for (const { path: path71 } of paths) forgetWorktree(path71);
|
|
27744
|
+
daemonLog(
|
|
27745
|
+
`clone ${clone} is gone; forgot ${paths.length} worktree record(s) it owned`
|
|
27746
|
+
);
|
|
27747
|
+
return;
|
|
27555
27748
|
}
|
|
27556
|
-
|
|
27557
|
-
|
|
27558
|
-
|
|
27559
|
-
|
|
27560
|
-
|
|
27561
|
-
|
|
27562
|
-
|
|
27749
|
+
await pruneBookkeeping(clone);
|
|
27750
|
+
for (const { path: path71, branch: branch2 } of paths)
|
|
27751
|
+
if (await reclaimBranch(clone, branch2)) forgetWorktree(path71);
|
|
27752
|
+
}
|
|
27753
|
+
async function pruneBookkeeping(clone) {
|
|
27754
|
+
try {
|
|
27755
|
+
await git3(clone, ["worktree", "prune"]);
|
|
27756
|
+
daemonLog(`worktree bookkeeping pruned for clone ${clone}`);
|
|
27757
|
+
} catch (error) {
|
|
27758
|
+
daemonLog(
|
|
27759
|
+
`worktree bookkeeping prune failed for clone ${clone}: ${error instanceof Error ? error.message : String(error)}`
|
|
27760
|
+
);
|
|
27563
27761
|
}
|
|
27564
|
-
for (const { clone, branch: branch2 } of orphanBranches)
|
|
27565
|
-
if (existsSync66(clone)) await pruneStaleWorktreeBranch(clone, branch2);
|
|
27566
27762
|
}
|
|
27567
|
-
async function
|
|
27568
|
-
if (!listLocalBranches(clone).includes(branch2)) return;
|
|
27763
|
+
async function reclaimBranch(clone, branch2) {
|
|
27764
|
+
if (!listLocalBranches(clone).includes(branch2)) return true;
|
|
27569
27765
|
try {
|
|
27570
27766
|
await git3(clone, ["branch", "-d", branch2]);
|
|
27571
27767
|
daemonLog(`stale worktree branch ${branch2} pruned from clone ${clone}`);
|
|
27768
|
+
return true;
|
|
27572
27769
|
} catch {
|
|
27573
27770
|
daemonLog(
|
|
27574
27771
|
`stale worktree branch ${branch2} retained (unmerged; surfaced not destroyed) in clone ${clone}`
|
|
27575
27772
|
);
|
|
27773
|
+
return false;
|
|
27774
|
+
}
|
|
27775
|
+
}
|
|
27776
|
+
|
|
27777
|
+
// src/commands/sessions/daemon/worktree/resurfaceOrphanedWorktree.ts
|
|
27778
|
+
import { basename as basename14 } from "path";
|
|
27779
|
+
function resurfaceOrphanedWorktree(sessions, spawnWith, recovered, notify2) {
|
|
27780
|
+
const { orphan, reason: reason2, held } = recovered;
|
|
27781
|
+
let id;
|
|
27782
|
+
try {
|
|
27783
|
+
id = spawnWith((sid) => orphanedSession(sid, recovered));
|
|
27784
|
+
} catch (error) {
|
|
27785
|
+
daemonLog(
|
|
27786
|
+
`orphaned worktree ${orphan.path} left in place, no card available: ${error instanceof Error ? error.message : String(error)}`
|
|
27787
|
+
);
|
|
27788
|
+
return;
|
|
27789
|
+
}
|
|
27790
|
+
daemonLog(
|
|
27791
|
+
`orphaned worktree ${orphan.path} resurfaced as stopped session ${id}: ${reason2} (${held.summary})`
|
|
27792
|
+
);
|
|
27793
|
+
const session = sessions.get(id);
|
|
27794
|
+
if (session) armStoppedSession(sessions, session, notify2);
|
|
27795
|
+
notify2();
|
|
27796
|
+
}
|
|
27797
|
+
function orphanedSession(id, recovered) {
|
|
27798
|
+
const { orphan, reason: reason2, held } = recovered;
|
|
27799
|
+
return {
|
|
27800
|
+
...sessionBase(id, "stopped"),
|
|
27801
|
+
name: `recovered ${basename14(orphan.path)}`,
|
|
27802
|
+
subtitle: `${held.summary} in ${orphan.path}`,
|
|
27803
|
+
commandType: "claude",
|
|
27804
|
+
pty: null,
|
|
27805
|
+
cwd: orphan.path,
|
|
27806
|
+
worktree: { path: orphan.path, clone: orphan.clone },
|
|
27807
|
+
undurable: { reason: reason2, removesTree: true },
|
|
27808
|
+
scrollback: recoveryNotice(recovered)
|
|
27809
|
+
};
|
|
27810
|
+
}
|
|
27811
|
+
function recoveryNotice({ orphan, held }) {
|
|
27812
|
+
return [
|
|
27813
|
+
`\r
|
|
27814
|
+
\x1B[33mRecovered workspace ${orphan.path}\x1B[0m\r
|
|
27815
|
+
`,
|
|
27816
|
+
`Its session is gone, but the workspace still holds ${held.summary}:\r
|
|
27817
|
+
`,
|
|
27818
|
+
...held.items.map((item) => ` ${item}\r
|
|
27819
|
+
`),
|
|
27820
|
+
"\r\nThis card is the handle for it: land the work and the workspace is removed on its own,\r\n",
|
|
27821
|
+
"restart to put an agent back in it, or discard to delete it and the work it holds.\r\n"
|
|
27822
|
+
].join("");
|
|
27823
|
+
}
|
|
27824
|
+
|
|
27825
|
+
// src/commands/sessions/daemon/worktree/reconcileWorktreesOnRestore.ts
|
|
27826
|
+
function reconcileWorktreesOnRestore(sessions, spawnWith, notify2) {
|
|
27827
|
+
bindRestoredWorktrees(sessions);
|
|
27828
|
+
void recoverOrphanedWorktrees(sessions, spawnWith, notify2);
|
|
27829
|
+
}
|
|
27830
|
+
async function recoverOrphanedWorktrees(sessions, spawnWith, notify2) {
|
|
27831
|
+
const accounted = accountedTrees(sessions);
|
|
27832
|
+
const vanished = /* @__PURE__ */ new Map();
|
|
27833
|
+
for (const { path: path71, clone } of readWorktreeRegistry()) {
|
|
27834
|
+
if (accounted.has(path71)) continue;
|
|
27835
|
+
if (!existsSync67(path71)) {
|
|
27836
|
+
vanished.set(clone, [
|
|
27837
|
+
...vanished.get(clone) ?? [],
|
|
27838
|
+
{ path: path71, branch: basename15(path71) }
|
|
27839
|
+
]);
|
|
27840
|
+
continue;
|
|
27841
|
+
}
|
|
27842
|
+
await recoverOrphan(sessions, spawnWith, { path: path71, clone }, notify2);
|
|
27576
27843
|
}
|
|
27844
|
+
for (const [clone, paths] of vanished)
|
|
27845
|
+
await reclaimVanishedWorktrees(clone, paths);
|
|
27846
|
+
}
|
|
27847
|
+
async function recoverOrphan(sessions, spawnWith, orphan, notify2) {
|
|
27848
|
+
daemonLog(`worktree ${orphan.path} orphaned across restart; reconciling`);
|
|
27849
|
+
const durability = await checkDurability(orphan.path);
|
|
27850
|
+
if (durability.durable) {
|
|
27851
|
+
await reapWorktree(orphan.path);
|
|
27852
|
+
return;
|
|
27853
|
+
}
|
|
27854
|
+
const held = await describeHeldWork(orphan.path, durability.reason);
|
|
27855
|
+
resurfaceOrphanedWorktree(
|
|
27856
|
+
sessions,
|
|
27857
|
+
spawnWith,
|
|
27858
|
+
{ orphan, reason: durability.reason, held },
|
|
27859
|
+
notify2
|
|
27860
|
+
);
|
|
27577
27861
|
}
|
|
27578
27862
|
|
|
27579
27863
|
// src/commands/sessions/daemon/restoreAllSessions.ts
|
|
27580
27864
|
function restoreAllSessions(spawnWith, sessions, notify2) {
|
|
27581
27865
|
const names = restoreAll(spawnWith, sessions);
|
|
27582
|
-
reconcileWorktreesOnRestore(sessions);
|
|
27866
|
+
reconcileWorktreesOnRestore(sessions, spawnWith, notify2);
|
|
27583
27867
|
rearmStoppedSessions(sessions, notify2);
|
|
27584
27868
|
return names;
|
|
27585
27869
|
}
|
|
@@ -27727,7 +28011,7 @@ function treeSpawnContext(sessions, spawnWith, notify2, clients, onStatusChange)
|
|
|
27727
28011
|
}
|
|
27728
28012
|
|
|
27729
28013
|
// src/commands/sessions/daemon/watchTranscript.ts
|
|
27730
|
-
import { existsSync as
|
|
28014
|
+
import { existsSync as existsSync68, watch as watch3 } from "fs";
|
|
27731
28015
|
|
|
27732
28016
|
// src/commands/sessions/daemon/reconcileTranscriptStatus.ts
|
|
27733
28017
|
async function resolveTranscriptPath(session) {
|
|
@@ -27762,7 +28046,7 @@ function watchTranscript(session, onStatusChange) {
|
|
|
27762
28046
|
if (!session.cwd || !session.claudeSessionId) return;
|
|
27763
28047
|
if (session.watchedTranscriptId === session.claudeSessionId) return;
|
|
27764
28048
|
const dir = projectDirForCwd(session.cwd);
|
|
27765
|
-
if (!
|
|
28049
|
+
if (!existsSync68(dir)) return;
|
|
27766
28050
|
if (session.transcriptWatcher) {
|
|
27767
28051
|
session.transcriptWatcher.close();
|
|
27768
28052
|
session.transcriptWatcher = void 0;
|
|
@@ -27913,13 +28197,13 @@ async function defaultConnect() {
|
|
|
27913
28197
|
}
|
|
27914
28198
|
|
|
27915
28199
|
// src/commands/sessions/daemon/hasPersistedWindowsSessions.ts
|
|
27916
|
-
import { existsSync as
|
|
28200
|
+
import { existsSync as existsSync69, readFileSync as readFileSync49 } from "fs";
|
|
27917
28201
|
import { posix } from "path";
|
|
27918
28202
|
function hasPersistedWindowsSessions() {
|
|
27919
28203
|
const sessionsFile = windowsSessionsFileFromWsl();
|
|
27920
28204
|
if (!sessionsFile) return false;
|
|
27921
28205
|
try {
|
|
27922
|
-
if (!
|
|
28206
|
+
if (!existsSync69(sessionsFile)) return false;
|
|
27923
28207
|
const data = JSON.parse(readFileSync49(sessionsFile, "utf8"));
|
|
27924
28208
|
return Array.isArray(data) && data.length > 0;
|
|
27925
28209
|
} catch (error) {
|
|
@@ -28507,16 +28791,16 @@ function resumeSession(id, sessionId, cwd, name) {
|
|
|
28507
28791
|
}
|
|
28508
28792
|
|
|
28509
28793
|
// src/commands/sessions/daemon/worktree/createWorktree.ts
|
|
28510
|
-
import { existsSync as
|
|
28511
|
-
import { basename as
|
|
28794
|
+
import { existsSync as existsSync70 } from "fs";
|
|
28795
|
+
import { basename as basename17, dirname as dirname31 } from "path";
|
|
28512
28796
|
|
|
28513
28797
|
// src/commands/sessions/daemon/worktree/planAllocation.ts
|
|
28514
|
-
import { basename as
|
|
28798
|
+
import { basename as basename16, join as join72 } from "path";
|
|
28515
28799
|
function planAllocation(clone, boundTreeRoots2) {
|
|
28516
28800
|
return boundTreeRoots2.has(clone) ? "spill" : "primary";
|
|
28517
28801
|
}
|
|
28518
28802
|
function nextWorktreePath(clone, base, isTaken) {
|
|
28519
|
-
const name =
|
|
28803
|
+
const name = basename16(clone);
|
|
28520
28804
|
for (let n = 2; n < 1e3; n++) {
|
|
28521
28805
|
const candidate = join72(base, `${name}-${n}`);
|
|
28522
28806
|
if (!isTaken(candidate)) return candidate;
|
|
@@ -28532,7 +28816,7 @@ function createWorktree(clone, root, boundTreeRoots2) {
|
|
|
28532
28816
|
const path71 = nextWorktreePath(
|
|
28533
28817
|
clone,
|
|
28534
28818
|
base,
|
|
28535
|
-
(candidate) => registered.has(candidate) ||
|
|
28819
|
+
(candidate) => registered.has(candidate) || existsSync70(candidate) || boundTreeRoots2.has(candidate) || branches.has(basename17(candidate))
|
|
28536
28820
|
);
|
|
28537
28821
|
const trunk = cloneTrunkBranch(clone);
|
|
28538
28822
|
gitSync(clone, ["config", "push.default", "upstream"]);
|
|
@@ -28541,13 +28825,13 @@ function createWorktree(clone, root, boundTreeRoots2) {
|
|
|
28541
28825
|
"add",
|
|
28542
28826
|
"--track",
|
|
28543
28827
|
"-b",
|
|
28544
|
-
|
|
28828
|
+
basename17(path71),
|
|
28545
28829
|
path71,
|
|
28546
28830
|
`origin/${trunk}`
|
|
28547
28831
|
]);
|
|
28548
28832
|
recordWorktree(path71, clone, getCurrentOrigin(clone));
|
|
28549
28833
|
daemonLog(
|
|
28550
|
-
`worktree allocated ${path71} (branch ${
|
|
28834
|
+
`worktree allocated ${path71} (branch ${basename17(path71)} tracking origin/${trunk}) for clone ${clone}`
|
|
28551
28835
|
);
|
|
28552
28836
|
return path71;
|
|
28553
28837
|
}
|
|
@@ -28581,6 +28865,30 @@ function allocateTree(requestedCwd, boundTreeRoots2) {
|
|
|
28581
28865
|
return { cwd: path71, kind: "worktree", created: true, clone };
|
|
28582
28866
|
}
|
|
28583
28867
|
|
|
28868
|
+
// src/commands/sessions/daemon/worktree/boundTreeRoots.ts
|
|
28869
|
+
function boundTreeRoots(sessions) {
|
|
28870
|
+
const roots = /* @__PURE__ */ new Set();
|
|
28871
|
+
for (const session of sessions.values()) {
|
|
28872
|
+
const root = treeRootOf(session);
|
|
28873
|
+
if (root && !roots.has(root) && holdsTree(session, root)) roots.add(root);
|
|
28874
|
+
}
|
|
28875
|
+
return roots;
|
|
28876
|
+
}
|
|
28877
|
+
function treeRootOf(session) {
|
|
28878
|
+
if (!session.cwd) return void 0;
|
|
28879
|
+
return findRepoRoot(session.cwd) ?? session.cwd;
|
|
28880
|
+
}
|
|
28881
|
+
function holdsTree(session, root) {
|
|
28882
|
+
if (session.closing) return true;
|
|
28883
|
+
if (session.status !== "done" && session.status !== "error") return true;
|
|
28884
|
+
if (!worktreeConfigFor(root).enabled) return true;
|
|
28885
|
+
const durability = checkDurabilitySync(root);
|
|
28886
|
+
daemonLog(
|
|
28887
|
+
durability.durable ? `tree ${root} free for allocation: session ${session.id} finished and its work is landed` : `tree ${root} still held by finished session ${session.id}: ${durability.reason}`
|
|
28888
|
+
);
|
|
28889
|
+
return !durability.durable;
|
|
28890
|
+
}
|
|
28891
|
+
|
|
28584
28892
|
// src/commands/sessions/daemon/worktree/spawnInTree.ts
|
|
28585
28893
|
function allocateAndBind(ctx, cwd, create) {
|
|
28586
28894
|
const alloc = allocateTree(cwd, boundTreeRoots(ctx.sessions));
|