@anthropologies/claudestory 0.1.61 → 0.1.63
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/cli.js +1957 -875
- package/dist/index.d.ts +20 -3
- package/dist/index.js +137 -14
- package/dist/mcp.js +1708 -652
- package/package.json +3 -2
package/dist/mcp.js
CHANGED
|
@@ -133,8 +133,12 @@ var init_session_types = __esm({
|
|
|
133
133
|
title: z.string().optional(),
|
|
134
134
|
commitHash: z.string().optional(),
|
|
135
135
|
risk: z.string().optional(),
|
|
136
|
-
realizedRisk: z.string().optional()
|
|
136
|
+
realizedRisk: z.string().optional(),
|
|
137
|
+
startedAt: z.string().optional(),
|
|
138
|
+
completedAt: z.string().optional()
|
|
137
139
|
})).default([]),
|
|
140
|
+
// T-187: Per-ticket timing -- set when ticket is picked, cleared on commit
|
|
141
|
+
ticketStartedAt: z.string().nullable().default(null),
|
|
138
142
|
// FINALIZE checkpoint
|
|
139
143
|
finalizeCheckpoint: z.enum(["staged", "staged_override", "precommit_passed", "committed"]).nullable().default(null),
|
|
140
144
|
// Git state
|
|
@@ -196,6 +200,10 @@ var init_session_types = __esm({
|
|
|
196
200
|
lastGuideCall: z.string().optional(),
|
|
197
201
|
startedAt: z.string(),
|
|
198
202
|
guideCallCount: z.number().default(0),
|
|
203
|
+
// ISS-098: Codex availability cache -- skip codex after failure
|
|
204
|
+
// ISS-110: Changed from boolean to ISO timestamp with 10-minute TTL
|
|
205
|
+
codexUnavailable: z.boolean().optional(),
|
|
206
|
+
codexUnavailableSince: z.string().optional(),
|
|
199
207
|
// Supersession tracking
|
|
200
208
|
supersededBy: z.string().optional(),
|
|
201
209
|
supersededSession: z.string().optional(),
|
|
@@ -555,12 +563,13 @@ var init_project_state = __esm({
|
|
|
555
563
|
totalTicketCount;
|
|
556
564
|
openTicketCount;
|
|
557
565
|
completeTicketCount;
|
|
558
|
-
|
|
566
|
+
activeIssueCount;
|
|
559
567
|
issuesBySeverity;
|
|
560
568
|
activeNoteCount;
|
|
561
569
|
archivedNoteCount;
|
|
562
570
|
activeLessonCount;
|
|
563
571
|
deprecatedLessonCount;
|
|
572
|
+
lessonTags;
|
|
564
573
|
constructor(input) {
|
|
565
574
|
this.tickets = input.tickets;
|
|
566
575
|
this.issues = input.issues;
|
|
@@ -641,19 +650,19 @@ var init_project_state = __esm({
|
|
|
641
650
|
lByID.set(l.id, l);
|
|
642
651
|
}
|
|
643
652
|
this.lessonsByID = lByID;
|
|
644
|
-
this.totalTicketCount =
|
|
645
|
-
this.openTicketCount =
|
|
653
|
+
this.totalTicketCount = this.leafTickets.length;
|
|
654
|
+
this.openTicketCount = this.leafTickets.filter(
|
|
646
655
|
(t) => t.status !== "complete"
|
|
647
656
|
).length;
|
|
648
|
-
this.completeTicketCount =
|
|
657
|
+
this.completeTicketCount = this.leafTickets.filter(
|
|
649
658
|
(t) => t.status === "complete"
|
|
650
659
|
).length;
|
|
651
|
-
this.
|
|
652
|
-
(i) => i.status
|
|
660
|
+
this.activeIssueCount = input.issues.filter(
|
|
661
|
+
(i) => i.status !== "resolved"
|
|
653
662
|
).length;
|
|
654
663
|
const bySev = /* @__PURE__ */ new Map();
|
|
655
664
|
for (const i of input.issues) {
|
|
656
|
-
if (i.status
|
|
665
|
+
if (i.status !== "resolved") {
|
|
657
666
|
bySev.set(i.severity, (bySev.get(i.severity) ?? 0) + 1);
|
|
658
667
|
}
|
|
659
668
|
}
|
|
@@ -670,6 +679,7 @@ var init_project_state = __esm({
|
|
|
670
679
|
this.deprecatedLessonCount = this.lessons.filter(
|
|
671
680
|
(l) => l.status === "deprecated" || l.status === "superseded"
|
|
672
681
|
).length;
|
|
682
|
+
this.lessonTags = [...new Set(this.lessons.flatMap((l) => l.tags ?? []))].sort();
|
|
673
683
|
}
|
|
674
684
|
// --- Query Methods ---
|
|
675
685
|
isUmbrella(ticket) {
|
|
@@ -1843,7 +1853,7 @@ function formatStatus(state, format, activeSessions = []) {
|
|
|
1843
1853
|
completeTickets: state.completeLeafTicketCount,
|
|
1844
1854
|
openTickets: state.leafTicketCount - state.completeLeafTicketCount,
|
|
1845
1855
|
blockedTickets: state.blockedCount,
|
|
1846
|
-
openIssues: state.
|
|
1856
|
+
openIssues: state.activeIssueCount,
|
|
1847
1857
|
activeNotes: state.activeNoteCount,
|
|
1848
1858
|
archivedNotes: state.archivedNoteCount,
|
|
1849
1859
|
activeLessons: state.activeLessonCount,
|
|
@@ -1865,7 +1875,7 @@ function formatStatus(state, format, activeSessions = []) {
|
|
|
1865
1875
|
`# ${escapeMarkdownInline(state.config.project)}`,
|
|
1866
1876
|
"",
|
|
1867
1877
|
`Tickets: ${state.completeLeafTicketCount}/${state.leafTicketCount} complete, ${state.blockedCount} blocked`,
|
|
1868
|
-
`Issues: ${state.
|
|
1878
|
+
`Issues: ${state.activeIssueCount} open`,
|
|
1869
1879
|
`Notes: ${state.activeNoteCount} active, ${state.archivedNoteCount} archived`,
|
|
1870
1880
|
`Lessons: ${state.activeLessonCount} active, ${state.deprecatedLessonCount} deprecated`,
|
|
1871
1881
|
`Handovers: ${state.handoverFilenames.length}`,
|
|
@@ -2291,7 +2301,7 @@ function formatRecap(recap, state, format) {
|
|
|
2291
2301
|
lines.push("No snapshot found. Run `claudestory snapshot` to enable session diffs.");
|
|
2292
2302
|
lines.push("");
|
|
2293
2303
|
lines.push(`Tickets: ${state.completeLeafTicketCount}/${state.leafTicketCount} complete, ${state.blockedCount} blocked`);
|
|
2294
|
-
lines.push(`Issues: ${state.
|
|
2304
|
+
lines.push(`Issues: ${state.activeIssueCount} open`);
|
|
2295
2305
|
} else {
|
|
2296
2306
|
lines.push(`# ${escapeMarkdownInline(state.config.project)} \u2014 Recap`);
|
|
2297
2307
|
lines.push("");
|
|
@@ -2299,6 +2309,13 @@ function formatRecap(recap, state, format) {
|
|
|
2299
2309
|
if (recap.partial) {
|
|
2300
2310
|
lines.push("**Note:** Snapshot was taken from a project with integrity warnings. Diff may be incomplete.");
|
|
2301
2311
|
}
|
|
2312
|
+
if (recap.staleness) {
|
|
2313
|
+
if (recap.staleness.status === "diverged") {
|
|
2314
|
+
lines.push("**Warning:** Snapshot commit is not an ancestor of current HEAD (history diverged; possible rebase, force-push, or branch switch).");
|
|
2315
|
+
} else if (recap.staleness.status === "behind" && recap.staleness.commitsBehind) {
|
|
2316
|
+
lines.push(`**Warning:** Snapshot is ${recap.staleness.commitsBehind} commit(s) behind HEAD -- context may be stale.`);
|
|
2317
|
+
}
|
|
2318
|
+
}
|
|
2302
2319
|
const changes = recap.changes;
|
|
2303
2320
|
const hasChanges = hasAnyChanges(changes);
|
|
2304
2321
|
if (!hasChanges) {
|
|
@@ -2560,7 +2577,7 @@ function formatFullExport(state, format) {
|
|
|
2560
2577
|
lines.push(`# ${escapeMarkdownInline(state.config.project)} \u2014 Full Export`);
|
|
2561
2578
|
lines.push("");
|
|
2562
2579
|
lines.push(`Tickets: ${state.completeLeafTicketCount}/${state.leafTicketCount} complete`);
|
|
2563
|
-
lines.push(`Issues: ${state.
|
|
2580
|
+
lines.push(`Issues: ${state.activeIssueCount} open`);
|
|
2564
2581
|
lines.push(`Notes: ${state.activeNoteCount} active, ${state.archivedNoteCount} archived`);
|
|
2565
2582
|
lines.push(`Lessons: ${state.activeLessonCount} active, ${state.deprecatedLessonCount} deprecated`);
|
|
2566
2583
|
lines.push("");
|
|
@@ -3460,6 +3477,204 @@ var init_issue2 = __esm({
|
|
|
3460
3477
|
}
|
|
3461
3478
|
});
|
|
3462
3479
|
|
|
3480
|
+
// src/autonomous/git-inspector.ts
|
|
3481
|
+
import { execFile } from "child_process";
|
|
3482
|
+
async function git(cwd, args, parse) {
|
|
3483
|
+
return new Promise((resolve10) => {
|
|
3484
|
+
execFile("git", args, { cwd, timeout: GIT_TIMEOUT, maxBuffer: 10 * 1024 * 1024 }, (err, stdout, stderr) => {
|
|
3485
|
+
if (err) {
|
|
3486
|
+
const message = stderr?.trim() || err.message || "unknown git error";
|
|
3487
|
+
resolve10({ ok: false, reason: "git_error", message });
|
|
3488
|
+
return;
|
|
3489
|
+
}
|
|
3490
|
+
try {
|
|
3491
|
+
resolve10({ ok: true, data: parse(stdout) });
|
|
3492
|
+
} catch (parseErr) {
|
|
3493
|
+
resolve10({ ok: false, reason: "parse_error", message: parseErr.message });
|
|
3494
|
+
}
|
|
3495
|
+
});
|
|
3496
|
+
});
|
|
3497
|
+
}
|
|
3498
|
+
async function gitStatus(cwd) {
|
|
3499
|
+
return git(
|
|
3500
|
+
cwd,
|
|
3501
|
+
["status", "--porcelain"],
|
|
3502
|
+
(out) => out.split("\n").filter((l) => l.length > 0)
|
|
3503
|
+
);
|
|
3504
|
+
}
|
|
3505
|
+
async function gitHead(cwd) {
|
|
3506
|
+
const hashResult = await git(cwd, ["rev-parse", "HEAD"], (out) => out.trim());
|
|
3507
|
+
if (!hashResult.ok) return hashResult;
|
|
3508
|
+
const branchResult = await gitBranch(cwd);
|
|
3509
|
+
return {
|
|
3510
|
+
ok: true,
|
|
3511
|
+
data: {
|
|
3512
|
+
hash: hashResult.data,
|
|
3513
|
+
branch: branchResult.ok ? branchResult.data : null
|
|
3514
|
+
}
|
|
3515
|
+
};
|
|
3516
|
+
}
|
|
3517
|
+
async function gitBranch(cwd) {
|
|
3518
|
+
return git(cwd, ["symbolic-ref", "--short", "HEAD"], (out) => out.trim());
|
|
3519
|
+
}
|
|
3520
|
+
async function gitMergeBase(cwd, base) {
|
|
3521
|
+
return git(cwd, ["merge-base", "HEAD", base], (out) => out.trim());
|
|
3522
|
+
}
|
|
3523
|
+
async function gitDiffStat(cwd, base) {
|
|
3524
|
+
return git(cwd, ["diff", "--numstat", base], parseDiffNumstat);
|
|
3525
|
+
}
|
|
3526
|
+
async function gitDiffNames(cwd, base) {
|
|
3527
|
+
return git(
|
|
3528
|
+
cwd,
|
|
3529
|
+
["diff", "--name-only", base],
|
|
3530
|
+
(out) => out.split("\n").filter((l) => l.length > 0)
|
|
3531
|
+
);
|
|
3532
|
+
}
|
|
3533
|
+
async function gitBlobHash(cwd, file) {
|
|
3534
|
+
return git(cwd, ["hash-object", file], (out) => out.trim());
|
|
3535
|
+
}
|
|
3536
|
+
async function gitDiffCachedNames(cwd) {
|
|
3537
|
+
return git(
|
|
3538
|
+
cwd,
|
|
3539
|
+
["diff", "--cached", "--name-only"],
|
|
3540
|
+
(out) => out.split("\n").filter((l) => l.length > 0)
|
|
3541
|
+
);
|
|
3542
|
+
}
|
|
3543
|
+
async function gitStash(cwd, message) {
|
|
3544
|
+
const pushResult = await git(cwd, ["stash", "push", "-m", message], () => void 0);
|
|
3545
|
+
if (!pushResult.ok) return { ok: false, reason: pushResult.reason, message: pushResult.message };
|
|
3546
|
+
const hashResult = await git(cwd, ["rev-parse", "stash@{0}"], (out) => out.trim());
|
|
3547
|
+
if (!hashResult.ok) {
|
|
3548
|
+
const listResult = await git(
|
|
3549
|
+
cwd,
|
|
3550
|
+
["stash", "list", "--format=%gd %s"],
|
|
3551
|
+
(out) => out.split("\n").filter((l) => l.includes(message))
|
|
3552
|
+
);
|
|
3553
|
+
if (listResult.ok && listResult.data.length > 0) {
|
|
3554
|
+
const ref = listResult.data[0].split(" ")[0];
|
|
3555
|
+
const refHash = await git(cwd, ["rev-parse", ref], (out) => out.trim());
|
|
3556
|
+
if (refHash.ok) return { ok: true, data: refHash.data };
|
|
3557
|
+
}
|
|
3558
|
+
return { ok: false, reason: "stash_hash_failed", message: "Stash created but could not be identified. Run `git stash list` to find and pop it manually." };
|
|
3559
|
+
}
|
|
3560
|
+
return { ok: true, data: hashResult.data };
|
|
3561
|
+
}
|
|
3562
|
+
async function gitStashPop(cwd, commitHash) {
|
|
3563
|
+
if (!commitHash) {
|
|
3564
|
+
return git(cwd, ["stash", "pop"], () => void 0);
|
|
3565
|
+
}
|
|
3566
|
+
const listResult = await git(
|
|
3567
|
+
cwd,
|
|
3568
|
+
["stash", "list", "--format=%gd %H"],
|
|
3569
|
+
(out) => out.split("\n").filter((l) => l.length > 0).map((l) => {
|
|
3570
|
+
const [ref, hash] = l.split(" ", 2);
|
|
3571
|
+
return { ref, hash };
|
|
3572
|
+
})
|
|
3573
|
+
);
|
|
3574
|
+
if (!listResult.ok) {
|
|
3575
|
+
return { ok: false, reason: "stash_list_failed", message: `Cannot list stash entries to find ${commitHash}. Run \`git stash list\` and pop manually.` };
|
|
3576
|
+
}
|
|
3577
|
+
const match = listResult.data.find((e) => e.hash === commitHash);
|
|
3578
|
+
if (!match) {
|
|
3579
|
+
return { ok: false, reason: "stash_not_found", message: `No stash entry with commit hash ${commitHash}` };
|
|
3580
|
+
}
|
|
3581
|
+
return git(cwd, ["stash", "pop", match.ref], () => void 0);
|
|
3582
|
+
}
|
|
3583
|
+
async function gitDiffTreeNames(cwd, commitHash) {
|
|
3584
|
+
return git(
|
|
3585
|
+
cwd,
|
|
3586
|
+
["diff-tree", "--name-only", "--no-commit-id", "-r", commitHash],
|
|
3587
|
+
(out) => out.split("\n").filter((l) => l.trim().length > 0)
|
|
3588
|
+
);
|
|
3589
|
+
}
|
|
3590
|
+
async function gitIsAncestor(cwd, ancestor, descendant) {
|
|
3591
|
+
if (!SAFE_REF.test(ancestor) || !SAFE_REF.test(descendant)) {
|
|
3592
|
+
return { ok: false, reason: "git_error", message: "invalid ref format" };
|
|
3593
|
+
}
|
|
3594
|
+
return new Promise((resolve10) => {
|
|
3595
|
+
execFile(
|
|
3596
|
+
"git",
|
|
3597
|
+
["merge-base", "--is-ancestor", ancestor, descendant],
|
|
3598
|
+
{ cwd, timeout: GIT_TIMEOUT },
|
|
3599
|
+
(err) => {
|
|
3600
|
+
if (!err) {
|
|
3601
|
+
resolve10({ ok: true, data: true });
|
|
3602
|
+
return;
|
|
3603
|
+
}
|
|
3604
|
+
const code = err.code;
|
|
3605
|
+
if (code === 1) {
|
|
3606
|
+
resolve10({ ok: true, data: false });
|
|
3607
|
+
return;
|
|
3608
|
+
}
|
|
3609
|
+
resolve10({ ok: false, reason: "git_error", message: err.message });
|
|
3610
|
+
}
|
|
3611
|
+
);
|
|
3612
|
+
});
|
|
3613
|
+
}
|
|
3614
|
+
async function gitLogRange(cwd, from, to, limit = 20) {
|
|
3615
|
+
if (from && !SAFE_REF.test(from)) {
|
|
3616
|
+
return { ok: false, reason: "invalid_ref", message: `Invalid git ref: ${from}` };
|
|
3617
|
+
}
|
|
3618
|
+
if (to && !SAFE_REF.test(to)) {
|
|
3619
|
+
return { ok: false, reason: "invalid_ref", message: `Invalid git ref: ${to}` };
|
|
3620
|
+
}
|
|
3621
|
+
if (!from || !to) {
|
|
3622
|
+
return { ok: true, data: [] };
|
|
3623
|
+
}
|
|
3624
|
+
return git(
|
|
3625
|
+
cwd,
|
|
3626
|
+
["log", "--oneline", `-${limit}`, `${from}..${to}`],
|
|
3627
|
+
(out) => out.split("\n").filter((l) => l.trim().length > 0)
|
|
3628
|
+
);
|
|
3629
|
+
}
|
|
3630
|
+
async function gitHeadHash(cwd) {
|
|
3631
|
+
return new Promise((resolve10) => {
|
|
3632
|
+
execFile("git", ["rev-parse", "HEAD"], { cwd, timeout: 3e3 }, (err, stdout) => {
|
|
3633
|
+
if (err) {
|
|
3634
|
+
const message = err.stderr?.trim() || err.message || "unknown git error";
|
|
3635
|
+
resolve10({ ok: false, reason: "git_error", message });
|
|
3636
|
+
return;
|
|
3637
|
+
}
|
|
3638
|
+
resolve10({ ok: true, data: stdout.trim() });
|
|
3639
|
+
});
|
|
3640
|
+
});
|
|
3641
|
+
}
|
|
3642
|
+
async function gitCommitDistance(cwd, fromSha, toSha) {
|
|
3643
|
+
if (!SAFE_REF.test(fromSha) || !SAFE_REF.test(toSha)) {
|
|
3644
|
+
return { ok: false, reason: "git_error", message: "invalid ref format" };
|
|
3645
|
+
}
|
|
3646
|
+
return git(cwd, ["rev-list", "--count", `${fromSha}..${toSha}`], (out) => {
|
|
3647
|
+
const n = parseInt(out.trim(), 10);
|
|
3648
|
+
if (Number.isNaN(n)) throw new Error(`unexpected rev-list output: ${out}`);
|
|
3649
|
+
return n;
|
|
3650
|
+
});
|
|
3651
|
+
}
|
|
3652
|
+
function parseDiffNumstat(out) {
|
|
3653
|
+
const lines = out.split("\n").filter((l) => l.length > 0);
|
|
3654
|
+
let insertions = 0;
|
|
3655
|
+
let deletions = 0;
|
|
3656
|
+
let filesChanged = 0;
|
|
3657
|
+
for (const line of lines) {
|
|
3658
|
+
const parts = line.split(" ");
|
|
3659
|
+
if (parts.length < 3) continue;
|
|
3660
|
+
const added = parseInt(parts[0], 10);
|
|
3661
|
+
const removed = parseInt(parts[1], 10);
|
|
3662
|
+
if (!Number.isNaN(added)) insertions += added;
|
|
3663
|
+
if (!Number.isNaN(removed)) deletions += removed;
|
|
3664
|
+
filesChanged++;
|
|
3665
|
+
}
|
|
3666
|
+
return { filesChanged, insertions, deletions, totalLines: insertions + deletions };
|
|
3667
|
+
}
|
|
3668
|
+
var GIT_TIMEOUT, SAFE_REF;
|
|
3669
|
+
var init_git_inspector = __esm({
|
|
3670
|
+
"src/autonomous/git-inspector.ts"() {
|
|
3671
|
+
"use strict";
|
|
3672
|
+
init_esm_shims();
|
|
3673
|
+
GIT_TIMEOUT = 1e4;
|
|
3674
|
+
SAFE_REF = /^[0-9a-f]{4,40}$/i;
|
|
3675
|
+
}
|
|
3676
|
+
});
|
|
3677
|
+
|
|
3463
3678
|
// src/core/snapshot.ts
|
|
3464
3679
|
var snapshot_exports = {};
|
|
3465
3680
|
__export(snapshot_exports, {
|
|
@@ -3499,6 +3714,10 @@ async function saveSnapshot(root, loadResult) {
|
|
|
3499
3714
|
}))
|
|
3500
3715
|
} : {}
|
|
3501
3716
|
};
|
|
3717
|
+
const headResult = await gitHeadHash(absRoot);
|
|
3718
|
+
if (headResult.ok) {
|
|
3719
|
+
snapshot.gitHead = headResult.data;
|
|
3720
|
+
}
|
|
3502
3721
|
const json = JSON.stringify(snapshot, null, 2) + "\n";
|
|
3503
3722
|
const targetPath = join11(snapshotsDir, filename);
|
|
3504
3723
|
const wrapDir = join11(absRoot, ".story");
|
|
@@ -3694,7 +3913,7 @@ function diffStates(snapshotState, currentState) {
|
|
|
3694
3913
|
handovers: { added: handoversAdded, removed: handoversRemoved }
|
|
3695
3914
|
};
|
|
3696
3915
|
}
|
|
3697
|
-
function buildRecap(currentState, snapshotInfo) {
|
|
3916
|
+
async function buildRecap(currentState, snapshotInfo, root) {
|
|
3698
3917
|
const next = nextTicket(currentState);
|
|
3699
3918
|
const nextTicketAction = next.kind === "found" ? { id: next.ticket.id, title: next.ticket.title, phase: next.ticket.phase } : null;
|
|
3700
3919
|
const highSeverityIssues = currentState.issues.filter(
|
|
@@ -3724,6 +3943,30 @@ function buildRecap(currentState, snapshotInfo) {
|
|
|
3724
3943
|
});
|
|
3725
3944
|
const changes = diffStates(snapshotState, currentState);
|
|
3726
3945
|
const recentlyClearedBlockers = changes.blockers.cleared;
|
|
3946
|
+
let staleness;
|
|
3947
|
+
if (snapshot.gitHead) {
|
|
3948
|
+
const currentHeadResult = await gitHeadHash(root);
|
|
3949
|
+
if (currentHeadResult.ok) {
|
|
3950
|
+
const snapshotSha = snapshot.gitHead;
|
|
3951
|
+
const currentSha = currentHeadResult.data;
|
|
3952
|
+
if (snapshotSha !== currentSha) {
|
|
3953
|
+
const ancestorResult = await gitIsAncestor(root, snapshotSha, currentSha);
|
|
3954
|
+
if (ancestorResult.ok && ancestorResult.data) {
|
|
3955
|
+
const distResult = await gitCommitDistance(root, snapshotSha, currentSha);
|
|
3956
|
+
if (distResult.ok) {
|
|
3957
|
+
staleness = {
|
|
3958
|
+
status: "behind",
|
|
3959
|
+
snapshotSha,
|
|
3960
|
+
currentSha,
|
|
3961
|
+
commitsBehind: distResult.data
|
|
3962
|
+
};
|
|
3963
|
+
}
|
|
3964
|
+
} else if (ancestorResult.ok && !ancestorResult.data) {
|
|
3965
|
+
staleness = { status: "diverged", snapshotSha, currentSha };
|
|
3966
|
+
}
|
|
3967
|
+
}
|
|
3968
|
+
}
|
|
3969
|
+
}
|
|
3727
3970
|
return {
|
|
3728
3971
|
snapshot: { filename, createdAt: snapshot.createdAt },
|
|
3729
3972
|
changes,
|
|
@@ -3732,7 +3975,8 @@ function buildRecap(currentState, snapshotInfo) {
|
|
|
3732
3975
|
highSeverityIssues,
|
|
3733
3976
|
recentlyClearedBlockers
|
|
3734
3977
|
},
|
|
3735
|
-
partial: (snapshot.warnings ?? []).length > 0
|
|
3978
|
+
partial: (snapshot.warnings ?? []).length > 0,
|
|
3979
|
+
...staleness ? { staleness } : {}
|
|
3736
3980
|
};
|
|
3737
3981
|
}
|
|
3738
3982
|
function formatSnapshotFilename(date) {
|
|
@@ -3779,6 +4023,7 @@ var init_snapshot = __esm({
|
|
|
3779
4023
|
init_project_state();
|
|
3780
4024
|
init_queries();
|
|
3781
4025
|
init_project_loader();
|
|
4026
|
+
init_git_inspector();
|
|
3782
4027
|
LoadWarningSchema = z9.object({
|
|
3783
4028
|
type: z9.string(),
|
|
3784
4029
|
file: z9.string(),
|
|
@@ -3795,7 +4040,8 @@ var init_snapshot = __esm({
|
|
|
3795
4040
|
notes: z9.array(NoteSchema).optional().default([]),
|
|
3796
4041
|
lessons: z9.array(LessonSchema).optional().default([]),
|
|
3797
4042
|
handoverFilenames: z9.array(z9.string()).optional().default([]),
|
|
3798
|
-
warnings: z9.array(LoadWarningSchema).optional()
|
|
4043
|
+
warnings: z9.array(LoadWarningSchema).optional(),
|
|
4044
|
+
gitHead: z9.string().optional()
|
|
3799
4045
|
});
|
|
3800
4046
|
MAX_SNAPSHOTS = 20;
|
|
3801
4047
|
}
|
|
@@ -4172,9 +4418,9 @@ var init_session = __esm({
|
|
|
4172
4418
|
|
|
4173
4419
|
// src/mcp/index.ts
|
|
4174
4420
|
init_esm_shims();
|
|
4175
|
-
import { realpathSync as realpathSync3, existsSync as
|
|
4176
|
-
import { resolve as resolve9, join as
|
|
4177
|
-
import { z as
|
|
4421
|
+
import { realpathSync as realpathSync3, existsSync as existsSync14 } from "fs";
|
|
4422
|
+
import { resolve as resolve9, join as join26, isAbsolute } from "path";
|
|
4423
|
+
import { z as z12 } from "zod";
|
|
4178
4424
|
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
4179
4425
|
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
4180
4426
|
|
|
@@ -4223,7 +4469,8 @@ function checkRoot(candidate) {
|
|
|
4223
4469
|
init_esm_shims();
|
|
4224
4470
|
init_session_types();
|
|
4225
4471
|
import { z as z10 } from "zod";
|
|
4226
|
-
import {
|
|
4472
|
+
import { readFileSync as readFileSync12, writeFileSync as writeFileSync7, mkdirSync as mkdirSync5 } from "fs";
|
|
4473
|
+
import { join as join23 } from "path";
|
|
4227
4474
|
|
|
4228
4475
|
// src/autonomous/review-lenses/mcp-handlers.ts
|
|
4229
4476
|
init_esm_shims();
|
|
@@ -5593,6 +5840,9 @@ function validateFindings(raw, lensName) {
|
|
|
5593
5840
|
continue;
|
|
5594
5841
|
}
|
|
5595
5842
|
const normalized = normalizeFields(item);
|
|
5843
|
+
if (typeof normalized.lens !== "string" && typeof lensName === "string") {
|
|
5844
|
+
normalized.lens = lensName;
|
|
5845
|
+
}
|
|
5596
5846
|
const reason = checkFinding(normalized, lensName);
|
|
5597
5847
|
if (reason) {
|
|
5598
5848
|
invalid.push({ raw: item, reason });
|
|
@@ -6318,6 +6568,58 @@ function redactArtifactSecrets(artifact, redactedLines) {
|
|
|
6318
6568
|
return lines.map((line, i) => linesToRedact.has(i) ? "[REDACTED -- potential secret]" : line).join("\n");
|
|
6319
6569
|
}
|
|
6320
6570
|
|
|
6571
|
+
// src/autonomous/review-lenses/diff-scope.ts
|
|
6572
|
+
init_esm_shims();
|
|
6573
|
+
function parseDiffScope(diff) {
|
|
6574
|
+
const changedFiles = /* @__PURE__ */ new Set();
|
|
6575
|
+
const addedLines = /* @__PURE__ */ new Map();
|
|
6576
|
+
if (!diff) return { changedFiles, addedLines };
|
|
6577
|
+
const lines = diff.split("\n");
|
|
6578
|
+
let currentFile = null;
|
|
6579
|
+
let currentLineNum = 0;
|
|
6580
|
+
for (const line of lines) {
|
|
6581
|
+
if (line.startsWith("+++ ")) {
|
|
6582
|
+
if (line.startsWith("+++ /dev/null")) {
|
|
6583
|
+
currentFile = null;
|
|
6584
|
+
continue;
|
|
6585
|
+
}
|
|
6586
|
+
const rawPath = line.startsWith("+++ b/") ? line.slice(6) : line.slice(4);
|
|
6587
|
+
currentFile = normalizePath(rawPath);
|
|
6588
|
+
changedFiles.add(currentFile);
|
|
6589
|
+
if (!addedLines.has(currentFile)) {
|
|
6590
|
+
addedLines.set(currentFile, /* @__PURE__ */ new Set());
|
|
6591
|
+
}
|
|
6592
|
+
currentLineNum = 0;
|
|
6593
|
+
continue;
|
|
6594
|
+
}
|
|
6595
|
+
const hunkMatch = line.match(/^@@ -\d+(?:,\d+)? \+(\d+)/);
|
|
6596
|
+
if (hunkMatch) {
|
|
6597
|
+
currentLineNum = parseInt(hunkMatch[1], 10) - 1;
|
|
6598
|
+
continue;
|
|
6599
|
+
}
|
|
6600
|
+
if (!currentFile) continue;
|
|
6601
|
+
if (line.startsWith("-")) continue;
|
|
6602
|
+
currentLineNum++;
|
|
6603
|
+
if (line.startsWith("+")) {
|
|
6604
|
+
addedLines.get(currentFile).add(currentLineNum);
|
|
6605
|
+
}
|
|
6606
|
+
}
|
|
6607
|
+
return { changedFiles, addedLines };
|
|
6608
|
+
}
|
|
6609
|
+
function normalizePath(p) {
|
|
6610
|
+
return p.startsWith("./") ? p.slice(2) : p;
|
|
6611
|
+
}
|
|
6612
|
+
function classifyOrigin(finding, scope, stage) {
|
|
6613
|
+
if (stage === "PLAN_REVIEW") return "introduced";
|
|
6614
|
+
if (!finding.file) return "introduced";
|
|
6615
|
+
const file = normalizePath(finding.file);
|
|
6616
|
+
if (!scope.changedFiles.has(file)) return "pre-existing";
|
|
6617
|
+
if (finding.line == null) return "introduced";
|
|
6618
|
+
const fileLines = scope.addedLines.get(file);
|
|
6619
|
+
if (!fileLines || !fileLines.has(finding.line)) return "pre-existing";
|
|
6620
|
+
return "introduced";
|
|
6621
|
+
}
|
|
6622
|
+
|
|
6321
6623
|
// src/autonomous/review-lenses/mcp-handlers.ts
|
|
6322
6624
|
var MAX_PROMPT_SIZE = 1e4;
|
|
6323
6625
|
function handlePrepare(input) {
|
|
@@ -6415,6 +6717,7 @@ function handleSynthesize(input) {
|
|
|
6415
6717
|
}
|
|
6416
6718
|
}
|
|
6417
6719
|
const stage = input.stage ?? "CODE_REVIEW";
|
|
6720
|
+
const diffScope = input.diff && input.changedFiles && stage === "CODE_REVIEW" ? parseDiffScope(input.diff) : null;
|
|
6418
6721
|
const lensesCompleted = [];
|
|
6419
6722
|
const lensesFailed = [];
|
|
6420
6723
|
const lensesInsufficientContext = [];
|
|
@@ -6447,7 +6750,8 @@ function handleSynthesize(input) {
|
|
|
6447
6750
|
const enriched = {
|
|
6448
6751
|
...f,
|
|
6449
6752
|
issueKey: generateIssueKey(f),
|
|
6450
|
-
blocking: computeBlocking(f, stage, policy)
|
|
6753
|
+
blocking: computeBlocking(f, stage, policy),
|
|
6754
|
+
origin: diffScope ? classifyOrigin(f, diffScope, stage) : void 0
|
|
6451
6755
|
};
|
|
6452
6756
|
allFindings.push(enriched);
|
|
6453
6757
|
}
|
|
@@ -6462,6 +6766,9 @@ function handleSynthesize(input) {
|
|
|
6462
6766
|
lensesFailed.push(lens);
|
|
6463
6767
|
}
|
|
6464
6768
|
}
|
|
6769
|
+
const preExistingFindings = allFindings.filter(
|
|
6770
|
+
(f) => f.origin === "pre-existing" && f.severity !== "suggestion"
|
|
6771
|
+
);
|
|
6465
6772
|
const lensMetadata = buildLensMetadata(lensesCompleted, lensesFailed, lensesInsufficientContext);
|
|
6466
6773
|
const mergerPrompt = buildMergerPrompt(allFindings, lensMetadata, stage);
|
|
6467
6774
|
return {
|
|
@@ -6471,7 +6778,9 @@ function handleSynthesize(input) {
|
|
|
6471
6778
|
lensesFailed,
|
|
6472
6779
|
lensesInsufficientContext,
|
|
6473
6780
|
droppedFindings: droppedTotal,
|
|
6474
|
-
droppedDetails: dropReasons.slice(0, 5)
|
|
6781
|
+
droppedDetails: dropReasons.slice(0, 5),
|
|
6782
|
+
preExistingFindings,
|
|
6783
|
+
preExistingCount: preExistingFindings.length
|
|
6475
6784
|
};
|
|
6476
6785
|
}
|
|
6477
6786
|
function handleJudge(input) {
|
|
@@ -6496,8 +6805,8 @@ function handleJudge(input) {
|
|
|
6496
6805
|
[...input.lensesSkipped]
|
|
6497
6806
|
);
|
|
6498
6807
|
if (input.convergenceHistory && input.convergenceHistory.length > 0) {
|
|
6499
|
-
const
|
|
6500
|
-
const historyTable = input.convergenceHistory.map((h) => `| R${h.round} | ${
|
|
6808
|
+
const sanitize2 = (s) => s.replace(/[|\n\r#>`*_~\[\]]/g, " ").slice(0, 50);
|
|
6809
|
+
const historyTable = input.convergenceHistory.map((h) => `| R${h.round} | ${sanitize2(h.verdict)} | ${h.blocking} | ${h.important} | ${sanitize2(h.newCode)} |`).join("\n");
|
|
6501
6810
|
judgePrompt += `
|
|
6502
6811
|
|
|
6503
6812
|
## Convergence History
|
|
@@ -6846,7 +7155,7 @@ init_output_formatter();
|
|
|
6846
7155
|
init_snapshot();
|
|
6847
7156
|
async function handleRecap(ctx) {
|
|
6848
7157
|
const snapshotInfo = await loadLatestSnapshot(ctx.root);
|
|
6849
|
-
const recap = buildRecap(ctx.state, snapshotInfo);
|
|
7158
|
+
const recap = await buildRecap(ctx.state, snapshotInfo, ctx.root);
|
|
6850
7159
|
return { output: formatRecap(recap, ctx.state, ctx.format) };
|
|
6851
7160
|
}
|
|
6852
7161
|
|
|
@@ -7814,8 +8123,8 @@ init_handover();
|
|
|
7814
8123
|
init_esm_shims();
|
|
7815
8124
|
init_session_types();
|
|
7816
8125
|
init_session();
|
|
7817
|
-
import { readFileSync as readFileSync10, writeFileSync as
|
|
7818
|
-
import { join as
|
|
8126
|
+
import { readFileSync as readFileSync10, writeFileSync as writeFileSync6, readdirSync as readdirSync4 } from "fs";
|
|
8127
|
+
import { join as join20 } from "path";
|
|
7819
8128
|
|
|
7820
8129
|
// src/autonomous/state-machine.ts
|
|
7821
8130
|
init_esm_shims();
|
|
@@ -7835,15 +8144,17 @@ var TRANSITIONS = {
|
|
|
7835
8144
|
// advance → IMPLEMENT, retry stays, exhaustion → PLAN, no-op → COMPLETE (ISS-069)
|
|
7836
8145
|
TEST: ["CODE_REVIEW", "IMPLEMENT", "TEST"],
|
|
7837
8146
|
// pass → CODE_REVIEW, fail → IMPLEMENT, retry
|
|
7838
|
-
CODE_REVIEW: ["VERIFY", "FINALIZE", "IMPLEMENT", "PLAN", "CODE_REVIEW", "SESSION_END"],
|
|
7839
|
-
// approve → VERIFY/FINALIZE, reject → IMPLEMENT/PLAN, stay for next round; SESSION_END for tiered exit
|
|
7840
|
-
VERIFY: ["FINALIZE", "IMPLEMENT", "VERIFY"],
|
|
8147
|
+
CODE_REVIEW: ["VERIFY", "BUILD", "FINALIZE", "IMPLEMENT", "PLAN", "CODE_REVIEW", "SESSION_END", "ISSUE_FIX"],
|
|
8148
|
+
// approve → VERIFY/BUILD/FINALIZE, reject → IMPLEMENT/PLAN, stay for next round; SESSION_END for tiered exit; T-208: ISSUE_FIX for issue-fix reviews
|
|
8149
|
+
VERIFY: ["BUILD", "FINALIZE", "IMPLEMENT", "VERIFY"],
|
|
8150
|
+
// pass → BUILD/FINALIZE, fail → IMPLEMENT, retry
|
|
8151
|
+
BUILD: ["FINALIZE", "IMPLEMENT", "BUILD"],
|
|
7841
8152
|
// pass → FINALIZE, fail → IMPLEMENT, retry
|
|
7842
8153
|
FINALIZE: ["COMPLETE", "PICK_TICKET"],
|
|
7843
8154
|
// ISS-084: issues now route through COMPLETE too; PICK_TICKET kept for in-flight session compat
|
|
7844
8155
|
COMPLETE: ["PICK_TICKET", "HANDOVER", "ISSUE_SWEEP", "SESSION_END"],
|
|
7845
|
-
ISSUE_FIX: ["FINALIZE", "PICK_TICKET", "ISSUE_FIX"],
|
|
7846
|
-
// T-153: fix done → FINALIZE, cancel → PICK_TICKET, retry self
|
|
8156
|
+
ISSUE_FIX: ["FINALIZE", "PICK_TICKET", "ISSUE_FIX", "CODE_REVIEW"],
|
|
8157
|
+
// T-153: fix done → FINALIZE, cancel → PICK_TICKET, retry self; T-208: optional code review
|
|
7847
8158
|
LESSON_CAPTURE: ["ISSUE_SWEEP", "HANDOVER", "LESSON_CAPTURE"],
|
|
7848
8159
|
// advance → ISSUE_SWEEP, retry self, done → HANDOVER
|
|
7849
8160
|
ISSUE_SWEEP: ["ISSUE_SWEEP", "HANDOVER", "PICK_TICKET"],
|
|
@@ -7933,189 +8244,55 @@ function requiredRounds(risk) {
|
|
|
7933
8244
|
return 3;
|
|
7934
8245
|
}
|
|
7935
8246
|
}
|
|
7936
|
-
|
|
7937
|
-
|
|
7938
|
-
if (
|
|
7939
|
-
|
|
8247
|
+
var CODEX_CACHE_TTL_MS = 10 * 60 * 1e3;
|
|
8248
|
+
function isCodexUnavailable(codexUnavailableSince) {
|
|
8249
|
+
if (!codexUnavailableSince) return false;
|
|
8250
|
+
const since = new Date(codexUnavailableSince).getTime();
|
|
8251
|
+
if (Number.isNaN(since)) return false;
|
|
8252
|
+
return Date.now() - since < CODEX_CACHE_TTL_MS;
|
|
8253
|
+
}
|
|
8254
|
+
function nextReviewer(previousRounds, backends, codexUnavailable, codexUnavailableSince) {
|
|
8255
|
+
const unavailable = codexUnavailableSince ? isCodexUnavailable(codexUnavailableSince) : !!codexUnavailable;
|
|
8256
|
+
const effective = unavailable ? backends.filter((b) => b !== "codex") : backends;
|
|
8257
|
+
if (effective.length === 0) return "agent";
|
|
8258
|
+
if (effective.length === 1) return effective[0];
|
|
8259
|
+
if (previousRounds.length === 0) return effective[0];
|
|
7940
8260
|
const lastReviewer = previousRounds[previousRounds.length - 1].reviewer;
|
|
7941
|
-
const lastIndex =
|
|
7942
|
-
if (lastIndex === -1) return
|
|
7943
|
-
return
|
|
8261
|
+
const lastIndex = effective.indexOf(lastReviewer);
|
|
8262
|
+
if (lastIndex === -1) return effective[0];
|
|
8263
|
+
return effective[(lastIndex + 1) % effective.length];
|
|
7944
8264
|
}
|
|
7945
8265
|
|
|
7946
|
-
// src/autonomous/
|
|
8266
|
+
// src/autonomous/guide.ts
|
|
8267
|
+
init_git_inspector();
|
|
8268
|
+
|
|
8269
|
+
// src/autonomous/recipes/loader.ts
|
|
7947
8270
|
init_esm_shims();
|
|
7948
|
-
import {
|
|
7949
|
-
|
|
7950
|
-
|
|
7951
|
-
|
|
7952
|
-
|
|
7953
|
-
|
|
7954
|
-
|
|
7955
|
-
|
|
7956
|
-
|
|
7957
|
-
|
|
7958
|
-
|
|
7959
|
-
|
|
7960
|
-
|
|
7961
|
-
|
|
7962
|
-
|
|
7963
|
-
|
|
7964
|
-
|
|
7965
|
-
|
|
7966
|
-
|
|
7967
|
-
|
|
7968
|
-
|
|
7969
|
-
|
|
7970
|
-
|
|
7971
|
-
);
|
|
7972
|
-
|
|
7973
|
-
async function gitHead(cwd) {
|
|
7974
|
-
const hashResult = await git(cwd, ["rev-parse", "HEAD"], (out) => out.trim());
|
|
7975
|
-
if (!hashResult.ok) return hashResult;
|
|
7976
|
-
const branchResult = await gitBranch(cwd);
|
|
7977
|
-
return {
|
|
7978
|
-
ok: true,
|
|
7979
|
-
data: {
|
|
7980
|
-
hash: hashResult.data,
|
|
7981
|
-
branch: branchResult.ok ? branchResult.data : null
|
|
7982
|
-
}
|
|
7983
|
-
};
|
|
7984
|
-
}
|
|
7985
|
-
async function gitBranch(cwd) {
|
|
7986
|
-
return git(cwd, ["symbolic-ref", "--short", "HEAD"], (out) => out.trim());
|
|
7987
|
-
}
|
|
7988
|
-
async function gitMergeBase(cwd, base) {
|
|
7989
|
-
return git(cwd, ["merge-base", "HEAD", base], (out) => out.trim());
|
|
7990
|
-
}
|
|
7991
|
-
async function gitDiffStat(cwd, base) {
|
|
7992
|
-
return git(cwd, ["diff", "--numstat", base], parseDiffNumstat);
|
|
7993
|
-
}
|
|
7994
|
-
async function gitDiffNames(cwd, base) {
|
|
7995
|
-
return git(
|
|
7996
|
-
cwd,
|
|
7997
|
-
["diff", "--name-only", base],
|
|
7998
|
-
(out) => out.split("\n").filter((l) => l.length > 0)
|
|
7999
|
-
);
|
|
8000
|
-
}
|
|
8001
|
-
async function gitBlobHash(cwd, file) {
|
|
8002
|
-
return git(cwd, ["hash-object", file], (out) => out.trim());
|
|
8003
|
-
}
|
|
8004
|
-
async function gitDiffCachedNames(cwd) {
|
|
8005
|
-
return git(
|
|
8006
|
-
cwd,
|
|
8007
|
-
["diff", "--cached", "--name-only"],
|
|
8008
|
-
(out) => out.split("\n").filter((l) => l.length > 0)
|
|
8009
|
-
);
|
|
8010
|
-
}
|
|
8011
|
-
async function gitStash(cwd, message) {
|
|
8012
|
-
const pushResult = await git(cwd, ["stash", "push", "-m", message], () => void 0);
|
|
8013
|
-
if (!pushResult.ok) return { ok: false, reason: pushResult.reason, message: pushResult.message };
|
|
8014
|
-
const hashResult = await git(cwd, ["rev-parse", "stash@{0}"], (out) => out.trim());
|
|
8015
|
-
if (!hashResult.ok) {
|
|
8016
|
-
const listResult = await git(
|
|
8017
|
-
cwd,
|
|
8018
|
-
["stash", "list", "--format=%gd %s"],
|
|
8019
|
-
(out) => out.split("\n").filter((l) => l.includes(message))
|
|
8020
|
-
);
|
|
8021
|
-
if (listResult.ok && listResult.data.length > 0) {
|
|
8022
|
-
const ref = listResult.data[0].split(" ")[0];
|
|
8023
|
-
const refHash = await git(cwd, ["rev-parse", ref], (out) => out.trim());
|
|
8024
|
-
if (refHash.ok) return { ok: true, data: refHash.data };
|
|
8025
|
-
}
|
|
8026
|
-
return { ok: false, reason: "stash_hash_failed", message: "Stash created but could not be identified. Run `git stash list` to find and pop it manually." };
|
|
8027
|
-
}
|
|
8028
|
-
return { ok: true, data: hashResult.data };
|
|
8029
|
-
}
|
|
8030
|
-
async function gitStashPop(cwd, commitHash) {
|
|
8031
|
-
if (!commitHash) {
|
|
8032
|
-
return git(cwd, ["stash", "pop"], () => void 0);
|
|
8033
|
-
}
|
|
8034
|
-
const listResult = await git(
|
|
8035
|
-
cwd,
|
|
8036
|
-
["stash", "list", "--format=%gd %H"],
|
|
8037
|
-
(out) => out.split("\n").filter((l) => l.length > 0).map((l) => {
|
|
8038
|
-
const [ref, hash] = l.split(" ", 2);
|
|
8039
|
-
return { ref, hash };
|
|
8040
|
-
})
|
|
8041
|
-
);
|
|
8042
|
-
if (!listResult.ok) {
|
|
8043
|
-
return { ok: false, reason: "stash_list_failed", message: `Cannot list stash entries to find ${commitHash}. Run \`git stash list\` and pop manually.` };
|
|
8044
|
-
}
|
|
8045
|
-
const match = listResult.data.find((e) => e.hash === commitHash);
|
|
8046
|
-
if (!match) {
|
|
8047
|
-
return { ok: false, reason: "stash_not_found", message: `No stash entry with commit hash ${commitHash}` };
|
|
8048
|
-
}
|
|
8049
|
-
return git(cwd, ["stash", "pop", match.ref], () => void 0);
|
|
8050
|
-
}
|
|
8051
|
-
async function gitDiffTreeNames(cwd, commitHash) {
|
|
8052
|
-
return git(
|
|
8053
|
-
cwd,
|
|
8054
|
-
["diff-tree", "--name-only", "--no-commit-id", "-r", commitHash],
|
|
8055
|
-
(out) => out.split("\n").filter((l) => l.trim().length > 0)
|
|
8056
|
-
);
|
|
8057
|
-
}
|
|
8058
|
-
var SAFE_REF = /^[0-9a-f]{4,40}$/i;
|
|
8059
|
-
async function gitLogRange(cwd, from, to, limit = 20) {
|
|
8060
|
-
if (from && !SAFE_REF.test(from)) {
|
|
8061
|
-
return { ok: false, reason: "invalid_ref", message: `Invalid git ref: ${from}` };
|
|
8062
|
-
}
|
|
8063
|
-
if (to && !SAFE_REF.test(to)) {
|
|
8064
|
-
return { ok: false, reason: "invalid_ref", message: `Invalid git ref: ${to}` };
|
|
8065
|
-
}
|
|
8066
|
-
if (!from || !to) {
|
|
8067
|
-
return { ok: true, data: [] };
|
|
8068
|
-
}
|
|
8069
|
-
return git(
|
|
8070
|
-
cwd,
|
|
8071
|
-
["log", "--oneline", `-${limit}`, `${from}..${to}`],
|
|
8072
|
-
(out) => out.split("\n").filter((l) => l.trim().length > 0)
|
|
8073
|
-
);
|
|
8074
|
-
}
|
|
8075
|
-
function parseDiffNumstat(out) {
|
|
8076
|
-
const lines = out.split("\n").filter((l) => l.length > 0);
|
|
8077
|
-
let insertions = 0;
|
|
8078
|
-
let deletions = 0;
|
|
8079
|
-
let filesChanged = 0;
|
|
8080
|
-
for (const line of lines) {
|
|
8081
|
-
const parts = line.split(" ");
|
|
8082
|
-
if (parts.length < 3) continue;
|
|
8083
|
-
const added = parseInt(parts[0], 10);
|
|
8084
|
-
const removed = parseInt(parts[1], 10);
|
|
8085
|
-
if (!Number.isNaN(added)) insertions += added;
|
|
8086
|
-
if (!Number.isNaN(removed)) deletions += removed;
|
|
8087
|
-
filesChanged++;
|
|
8088
|
-
}
|
|
8089
|
-
return { filesChanged, insertions, deletions, totalLines: insertions + deletions };
|
|
8090
|
-
}
|
|
8091
|
-
|
|
8092
|
-
// src/autonomous/recipes/loader.ts
|
|
8093
|
-
init_esm_shims();
|
|
8094
|
-
import { readFileSync as readFileSync7 } from "fs";
|
|
8095
|
-
import { join as join14, dirname as dirname3 } from "path";
|
|
8096
|
-
import { fileURLToPath as fileURLToPath2 } from "url";
|
|
8097
|
-
var DEFAULT_PIPELINE = [
|
|
8098
|
-
"PICK_TICKET",
|
|
8099
|
-
"PLAN",
|
|
8100
|
-
"PLAN_REVIEW",
|
|
8101
|
-
"IMPLEMENT",
|
|
8102
|
-
"CODE_REVIEW",
|
|
8103
|
-
"FINALIZE",
|
|
8104
|
-
"COMPLETE"
|
|
8105
|
-
];
|
|
8106
|
-
var DEFAULT_DEFAULTS = {
|
|
8107
|
-
maxTicketsPerSession: 3,
|
|
8108
|
-
compactThreshold: "high",
|
|
8109
|
-
reviewBackends: ["codex", "agent"]
|
|
8110
|
-
};
|
|
8111
|
-
function loadRecipe(recipeName) {
|
|
8112
|
-
if (!/^[A-Za-z0-9_-]+$/.test(recipeName)) {
|
|
8113
|
-
throw new Error(`Invalid recipe name: ${recipeName}`);
|
|
8114
|
-
}
|
|
8115
|
-
const recipesDir = join14(dirname3(fileURLToPath2(import.meta.url)), "..", "recipes");
|
|
8116
|
-
const path2 = join14(recipesDir, `${recipeName}.json`);
|
|
8117
|
-
const raw = readFileSync7(path2, "utf-8");
|
|
8118
|
-
return JSON.parse(raw);
|
|
8271
|
+
import { readFileSync as readFileSync7 } from "fs";
|
|
8272
|
+
import { join as join14, dirname as dirname3 } from "path";
|
|
8273
|
+
import { fileURLToPath as fileURLToPath2 } from "url";
|
|
8274
|
+
var DEFAULT_PIPELINE = [
|
|
8275
|
+
"PICK_TICKET",
|
|
8276
|
+
"PLAN",
|
|
8277
|
+
"PLAN_REVIEW",
|
|
8278
|
+
"IMPLEMENT",
|
|
8279
|
+
"CODE_REVIEW",
|
|
8280
|
+
"FINALIZE",
|
|
8281
|
+
"COMPLETE"
|
|
8282
|
+
];
|
|
8283
|
+
var DEFAULT_DEFAULTS = {
|
|
8284
|
+
maxTicketsPerSession: 3,
|
|
8285
|
+
compactThreshold: "high",
|
|
8286
|
+
reviewBackends: ["codex", "agent"]
|
|
8287
|
+
};
|
|
8288
|
+
function loadRecipe(recipeName) {
|
|
8289
|
+
if (!/^[A-Za-z0-9_-]+$/.test(recipeName)) {
|
|
8290
|
+
throw new Error(`Invalid recipe name: ${recipeName}`);
|
|
8291
|
+
}
|
|
8292
|
+
const recipesDir = join14(dirname3(fileURLToPath2(import.meta.url)), "..", "recipes");
|
|
8293
|
+
const path2 = join14(recipesDir, `${recipeName}.json`);
|
|
8294
|
+
const raw = readFileSync7(path2, "utf-8");
|
|
8295
|
+
return JSON.parse(raw);
|
|
8119
8296
|
}
|
|
8120
8297
|
function resolveRecipe(recipeName, projectOverrides) {
|
|
8121
8298
|
let raw;
|
|
@@ -8169,6 +8346,13 @@ function resolveRecipe(recipeName, projectOverrides) {
|
|
|
8169
8346
|
pipeline.splice(implementIdx + 1, 0, "TEST");
|
|
8170
8347
|
}
|
|
8171
8348
|
}
|
|
8349
|
+
if (stages2.ISSUE_FIX?.enableCodeReview) {
|
|
8350
|
+
if (pipeline.includes("VERIFY") || pipeline.includes("BUILD")) {
|
|
8351
|
+
throw new Error(
|
|
8352
|
+
"ISSUE_FIX.enableCodeReview is incompatible with VERIFY/BUILD in the pipeline (issue fixes use goto transitions, not pipeline walker)"
|
|
8353
|
+
);
|
|
8354
|
+
}
|
|
8355
|
+
}
|
|
8172
8356
|
const postComplete = raw.postComplete ? [...raw.postComplete] : [];
|
|
8173
8357
|
const recipeDefaults = raw.defaults ?? {};
|
|
8174
8358
|
const defaults = {
|
|
@@ -8466,7 +8650,15 @@ function buildTargetedStuckHandover(candidatesText, sessionId) {
|
|
|
8466
8650
|
var PickTicketStage = class {
|
|
8467
8651
|
id = "PICK_TICKET";
|
|
8468
8652
|
async enter(ctx) {
|
|
8469
|
-
|
|
8653
|
+
let projectState;
|
|
8654
|
+
try {
|
|
8655
|
+
({ state: projectState } = await ctx.loadProject());
|
|
8656
|
+
} catch (err) {
|
|
8657
|
+
return {
|
|
8658
|
+
action: "retry",
|
|
8659
|
+
instruction: `Failed to load project state: ${err instanceof Error ? err.message : String(err)}. Check .story/ files for corruption, then call autonomous_guide with action "report" again.`
|
|
8660
|
+
};
|
|
8661
|
+
}
|
|
8470
8662
|
if (isTargetedMode(ctx.state)) {
|
|
8471
8663
|
const remaining = getRemainingTargets(ctx.state);
|
|
8472
8664
|
if (remaining.length === 0) {
|
|
@@ -8566,7 +8758,12 @@ var PickTicketStage = class {
|
|
|
8566
8758
|
}
|
|
8567
8759
|
const targetReject = this.enforceTargetList(ctx, ticketId);
|
|
8568
8760
|
if (targetReject) return targetReject;
|
|
8569
|
-
|
|
8761
|
+
let projectState;
|
|
8762
|
+
try {
|
|
8763
|
+
({ state: projectState } = await ctx.loadProject());
|
|
8764
|
+
} catch (err) {
|
|
8765
|
+
return { action: "retry", instruction: `Failed to load project state: ${err instanceof Error ? err.message : String(err)}. Check .story/ files for corruption.` };
|
|
8766
|
+
}
|
|
8570
8767
|
const ticket = projectState.ticketByID(ticketId);
|
|
8571
8768
|
if (!ticket) {
|
|
8572
8769
|
return { action: "retry", instruction: `Ticket ${ticketId} not found. Pick a valid ticket.` };
|
|
@@ -8588,7 +8785,8 @@ var PickTicketStage = class {
|
|
|
8588
8785
|
ctx.updateDraft({
|
|
8589
8786
|
ticket: { id: ticket.id, title: ticket.title, claimed: true },
|
|
8590
8787
|
reviews: { plan: [], code: [] },
|
|
8591
|
-
finalizeCheckpoint: null
|
|
8788
|
+
finalizeCheckpoint: null,
|
|
8789
|
+
ticketStartedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
8592
8790
|
});
|
|
8593
8791
|
return {
|
|
8594
8792
|
action: "advance",
|
|
@@ -8619,7 +8817,12 @@ ${ticket.description}` : "",
|
|
|
8619
8817
|
async handleIssuePick(ctx, issueId) {
|
|
8620
8818
|
const targetReject = this.enforceTargetList(ctx, issueId);
|
|
8621
8819
|
if (targetReject) return targetReject;
|
|
8622
|
-
|
|
8820
|
+
let projectState;
|
|
8821
|
+
try {
|
|
8822
|
+
({ state: projectState } = await ctx.loadProject());
|
|
8823
|
+
} catch (err) {
|
|
8824
|
+
return { action: "retry", instruction: `Failed to load project state: ${err instanceof Error ? err.message : String(err)}. Check .story/ files for corruption.` };
|
|
8825
|
+
}
|
|
8623
8826
|
const issue = projectState.issues.find((i) => i.id === issueId);
|
|
8624
8827
|
if (!issue) {
|
|
8625
8828
|
return { action: "retry", instruction: `Issue ${issueId} not found. Pick a valid issue or ticket.` };
|
|
@@ -8628,11 +8831,16 @@ ${ticket.description}` : "",
|
|
|
8628
8831
|
if (issue.status !== "open" && !(targeted && issue.status === "inprogress")) {
|
|
8629
8832
|
return { action: "retry", instruction: `Issue ${issueId} is ${issue.status}. Pick an open issue.` };
|
|
8630
8833
|
}
|
|
8834
|
+
const transitionId = `issue-pick-${issueId}-${Date.now()}`;
|
|
8835
|
+
ctx.writeState({
|
|
8836
|
+
pendingProjectMutation: { type: "issue_update", target: issueId, field: "status", value: "inprogress", expectedCurrent: issue.status, transitionId }
|
|
8837
|
+
});
|
|
8631
8838
|
try {
|
|
8632
8839
|
const { handleIssueUpdate: handleIssueUpdate2 } = await Promise.resolve().then(() => (init_issue2(), issue_exports));
|
|
8633
|
-
await handleIssueUpdate2(
|
|
8840
|
+
await handleIssueUpdate2(issueId, { status: "inprogress" }, "json", ctx.root);
|
|
8634
8841
|
} catch {
|
|
8635
8842
|
}
|
|
8843
|
+
ctx.writeState({ pendingProjectMutation: null });
|
|
8636
8844
|
ctx.updateDraft({
|
|
8637
8845
|
currentIssue: { id: issue.id, title: issue.title, severity: issue.severity },
|
|
8638
8846
|
ticket: void 0,
|
|
@@ -8773,7 +8981,7 @@ var PlanReviewStage = class {
|
|
|
8773
8981
|
const backends = ctx.state.config.reviewBackends;
|
|
8774
8982
|
const existingReviews = ctx.state.reviews.plan;
|
|
8775
8983
|
const roundNum = existingReviews.length + 1;
|
|
8776
|
-
const reviewer = nextReviewer(existingReviews, backends);
|
|
8984
|
+
const reviewer = nextReviewer(existingReviews, backends, ctx.state.codexUnavailable, ctx.state.codexUnavailableSince);
|
|
8777
8985
|
const risk = ctx.state.ticket?.risk ?? "low";
|
|
8778
8986
|
const minRounds = requiredRounds(risk);
|
|
8779
8987
|
if (reviewer === "lenses") {
|
|
@@ -8784,10 +8992,11 @@ var PlanReviewStage = class {
|
|
|
8784
8992
|
"This round uses the **multi-lens review orchestrator** for plan review. It fans out to specialized review agents (Clean Code, Security, Error Handling, and more) in parallel to evaluate the plan from multiple perspectives.",
|
|
8785
8993
|
"",
|
|
8786
8994
|
"1. Read the plan file",
|
|
8787
|
-
"2. Call `
|
|
8788
|
-
"3. Spawn all lens subagents in parallel",
|
|
8789
|
-
"4. Collect results and
|
|
8790
|
-
"5.
|
|
8995
|
+
"2. Call `claudestory_review_lenses_prepare` with the plan text as diff, stage: PLAN_REVIEW, and ticketDescription",
|
|
8996
|
+
"3. Spawn all lens subagents in parallel (each prompt is returned by the prepare tool)",
|
|
8997
|
+
"4. Collect results and call `claudestory_review_lenses_synthesize` with the lens results",
|
|
8998
|
+
"5. Run the merger agent with the returned mergerPrompt, then call `claudestory_review_lenses_judge`",
|
|
8999
|
+
"6. Run the judge agent and report the final SynthesisResult verdict and findings",
|
|
8791
9000
|
"",
|
|
8792
9001
|
"When done, call `claudestory_autonomous_guide` with:",
|
|
8793
9002
|
"```json",
|
|
@@ -8814,7 +9023,11 @@ var PlanReviewStage = class {
|
|
|
8814
9023
|
`{ "sessionId": "${ctx.state.sessionId}", "action": "report", "report": { "completedAction": "plan_review_round", "verdict": "<approve|revise|request_changes|reject>", "findings": [...] } }`,
|
|
8815
9024
|
"```"
|
|
8816
9025
|
].join("\n"),
|
|
8817
|
-
reminders: [
|
|
9026
|
+
reminders: [
|
|
9027
|
+
"Report the exact verdict and findings from the reviewer.",
|
|
9028
|
+
"IMPORTANT: After the review, file ANY pre-existing issues discovered using claudestory_issue_create with severity and impact. Do NOT skip this step.",
|
|
9029
|
+
...reviewer === "codex" ? ["If codex is unavailable (usage limit, error, etc.), fall back to agent review and include 'codex unavailable' in your report notes."] : []
|
|
9030
|
+
],
|
|
8818
9031
|
transitionedFrom: ctx.state.previousState ?? void 0
|
|
8819
9032
|
};
|
|
8820
9033
|
}
|
|
@@ -8827,7 +9040,8 @@ var PlanReviewStage = class {
|
|
|
8827
9040
|
const roundNum = planReviews.length + 1;
|
|
8828
9041
|
const findings = report.findings ?? [];
|
|
8829
9042
|
const backends = ctx.state.config.reviewBackends;
|
|
8830
|
-
const
|
|
9043
|
+
const computedReviewer = nextReviewer(planReviews, backends, ctx.state.codexUnavailable, ctx.state.codexUnavailableSince);
|
|
9044
|
+
const reviewerBackend = report.reviewer ?? (computedReviewer === "codex" && report.notes && /codex\b.*\b(unavail|limit|failed|down|error|usage)/i.test(report.notes) ? "agent" : null) ?? computedReviewer;
|
|
8831
9045
|
planReviews.push({
|
|
8832
9046
|
round: roundNum,
|
|
8833
9047
|
reviewer: reviewerBackend,
|
|
@@ -8839,6 +9053,9 @@ var PlanReviewStage = class {
|
|
|
8839
9053
|
codexSessionId: report.reviewerSessionId,
|
|
8840
9054
|
timestamp: (/* @__PURE__ */ new Date()).toISOString()
|
|
8841
9055
|
});
|
|
9056
|
+
if (report.notes && /codex\b.*\b(unavail|limit|failed|down|error|usage)/i.test(report.notes)) {
|
|
9057
|
+
ctx.writeState({ codexUnavailable: true, codexUnavailableSince: (/* @__PURE__ */ new Date()).toISOString() });
|
|
9058
|
+
}
|
|
8842
9059
|
const risk = ctx.state.ticket?.risk ?? "low";
|
|
8843
9060
|
const minRounds = requiredRounds(risk);
|
|
8844
9061
|
const hasCriticalOrMajor = findings.some(
|
|
@@ -8926,7 +9143,7 @@ var PlanReviewStage = class {
|
|
|
8926
9143
|
}
|
|
8927
9144
|
return { action: "advance" };
|
|
8928
9145
|
}
|
|
8929
|
-
const nextReviewerName = nextReviewer(planReviews, backends);
|
|
9146
|
+
const nextReviewerName = nextReviewer(planReviews, backends, ctx.state.codexUnavailable, ctx.state.codexUnavailableSince);
|
|
8930
9147
|
return {
|
|
8931
9148
|
action: "retry",
|
|
8932
9149
|
instruction: [
|
|
@@ -8943,6 +9160,7 @@ var PlanReviewStage = class {
|
|
|
8943
9160
|
|
|
8944
9161
|
// src/autonomous/stages/implement.ts
|
|
8945
9162
|
init_esm_shims();
|
|
9163
|
+
init_git_inspector();
|
|
8946
9164
|
var ImplementStage = class {
|
|
8947
9165
|
id = "IMPLEMENT";
|
|
8948
9166
|
async enter(ctx) {
|
|
@@ -9048,7 +9266,7 @@ var WriteTestsStage = class {
|
|
|
9048
9266
|
const exitMatch = notes.match(EXIT_CODE_REGEX);
|
|
9049
9267
|
const exitCode = exitMatch ? parseInt(exitMatch[1], 10) : -1;
|
|
9050
9268
|
const failMatch = notes.match(FAIL_COUNT_REGEX);
|
|
9051
|
-
const currentFailCount = failMatch ? parseInt(failMatch[1], 10) : -1;
|
|
9269
|
+
const currentFailCount = failMatch ? parseInt(failMatch[1], 10) : exitCode === 0 ? 0 : -1;
|
|
9052
9270
|
const baseline = ctx.state.testBaseline;
|
|
9053
9271
|
const baselineFailCount = baseline?.failCount ?? -1;
|
|
9054
9272
|
const nextRetry = retryCount + 1;
|
|
@@ -9224,26 +9442,29 @@ var CodeReviewStage = class {
|
|
|
9224
9442
|
const backends = ctx.state.config.reviewBackends;
|
|
9225
9443
|
const codeReviews = ctx.state.reviews.code;
|
|
9226
9444
|
const roundNum = codeReviews.length + 1;
|
|
9227
|
-
const reviewer = nextReviewer(codeReviews, backends);
|
|
9445
|
+
const reviewer = nextReviewer(codeReviews, backends, ctx.state.codexUnavailable, ctx.state.codexUnavailableSince);
|
|
9228
9446
|
const risk = ctx.state.ticket?.realizedRisk ?? ctx.state.ticket?.risk ?? "low";
|
|
9229
9447
|
const rounds = requiredRounds(risk);
|
|
9230
9448
|
const mergeBase = ctx.state.git.mergeBase;
|
|
9449
|
+
const isIssueFix = !!ctx.state.currentIssue;
|
|
9450
|
+
const issueHeader = isIssueFix ? `Issue Fix Code Review (${ctx.state.currentIssue.id})` : "Code Review";
|
|
9231
9451
|
const diffCommand = mergeBase ? `\`git diff ${mergeBase}\`` : `\`git diff HEAD\` AND \`git ls-files --others --exclude-standard\``;
|
|
9232
9452
|
const diffReminder = mergeBase ? `Run: git diff ${mergeBase} \u2014 pass FULL output to reviewer.` : "Run: git diff HEAD + git ls-files --others --exclude-standard \u2014 pass FULL output to reviewer.";
|
|
9233
9453
|
if (reviewer === "lenses") {
|
|
9234
9454
|
return {
|
|
9235
9455
|
instruction: [
|
|
9236
|
-
`# Multi-Lens
|
|
9456
|
+
`# Multi-Lens ${issueHeader} \u2014 Round ${roundNum} of ${rounds} minimum`,
|
|
9237
9457
|
"",
|
|
9238
9458
|
`Capture the diff with: ${diffCommand}`,
|
|
9239
9459
|
"",
|
|
9240
9460
|
"This round uses the **multi-lens review orchestrator**. It fans out to specialized review agents (Clean Code, Security, Error Handling, and more) in parallel, then synthesizes findings into a single verdict.",
|
|
9241
9461
|
"",
|
|
9242
|
-
"1. Capture the full diff",
|
|
9243
|
-
"2. Call `
|
|
9244
|
-
"3. Spawn all lens subagents in parallel (each prompt is
|
|
9245
|
-
"4. Collect results and
|
|
9246
|
-
"5.
|
|
9462
|
+
"1. Capture the full diff and changed file list (`git diff --name-only`)",
|
|
9463
|
+
"2. Call `claudestory_review_lenses_prepare` with the diff, changedFiles, stage: CODE_REVIEW, and ticketDescription",
|
|
9464
|
+
"3. Spawn all lens subagents in parallel (each prompt is returned by the prepare tool)",
|
|
9465
|
+
"4. Collect results and call `claudestory_review_lenses_synthesize` with the lens results, plus the diff and changedFiles from step 1 and the sessionId (enables automatic origin classification and issue filing for pre-existing findings)",
|
|
9466
|
+
"5. Run the merger agent with the returned mergerPrompt, then call `claudestory_review_lenses_judge`",
|
|
9467
|
+
"6. Run the judge agent and report the final SynthesisResult verdict and findings",
|
|
9247
9468
|
"",
|
|
9248
9469
|
"When done, report verdict and findings."
|
|
9249
9470
|
].join("\n"),
|
|
@@ -9251,14 +9472,14 @@ var CodeReviewStage = class {
|
|
|
9251
9472
|
diffReminder,
|
|
9252
9473
|
"Do NOT compress or summarize the diff.",
|
|
9253
9474
|
"Lens subagents run in parallel with read-only tools (Read, Grep, Glob).",
|
|
9254
|
-
"
|
|
9475
|
+
"Pre-existing issues in surrounding code are automatically classified and filed by the synthesize tool when you pass diff, changedFiles, and sessionId. Check filedIssues in the synthesize response."
|
|
9255
9476
|
],
|
|
9256
9477
|
transitionedFrom: ctx.state.previousState ?? void 0
|
|
9257
9478
|
};
|
|
9258
9479
|
}
|
|
9259
9480
|
return {
|
|
9260
9481
|
instruction: [
|
|
9261
|
-
`#
|
|
9482
|
+
`# ${issueHeader} \u2014 Round ${roundNum} of ${rounds} minimum`,
|
|
9262
9483
|
"",
|
|
9263
9484
|
`Capture the diff with: ${diffCommand}`,
|
|
9264
9485
|
"",
|
|
@@ -9270,7 +9491,8 @@ var CodeReviewStage = class {
|
|
|
9270
9491
|
reminders: [
|
|
9271
9492
|
diffReminder,
|
|
9272
9493
|
"Do NOT compress or summarize the diff.",
|
|
9273
|
-
"If the reviewer flags pre-existing issues unrelated to your changes, file them as issues using claudestory_issue_create with severity and impact. Do not fix them in this ticket."
|
|
9494
|
+
"If the reviewer flags pre-existing issues unrelated to your changes, file them as issues using claudestory_issue_create with severity and impact. Do not fix them in this ticket.",
|
|
9495
|
+
...reviewer === "codex" ? ["If codex is unavailable (usage limit, error, etc.), fall back to agent review and include 'codex unavailable' in your report notes."] : []
|
|
9274
9496
|
],
|
|
9275
9497
|
transitionedFrom: ctx.state.previousState ?? void 0
|
|
9276
9498
|
};
|
|
@@ -9284,7 +9506,8 @@ var CodeReviewStage = class {
|
|
|
9284
9506
|
const roundNum = codeReviews.length + 1;
|
|
9285
9507
|
const findings = report.findings ?? [];
|
|
9286
9508
|
const backends = ctx.state.config.reviewBackends;
|
|
9287
|
-
const
|
|
9509
|
+
const computedReviewer = nextReviewer(codeReviews, backends, ctx.state.codexUnavailable, ctx.state.codexUnavailableSince);
|
|
9510
|
+
const reviewerBackend = report.reviewer ?? (computedReviewer === "codex" && report.notes && /codex\b.*\b(unavail|limit|failed|down|error|usage)/i.test(report.notes) ? "agent" : null) ?? computedReviewer;
|
|
9288
9511
|
codeReviews.push({
|
|
9289
9512
|
round: roundNum,
|
|
9290
9513
|
reviewer: reviewerBackend,
|
|
@@ -9296,6 +9519,9 @@ var CodeReviewStage = class {
|
|
|
9296
9519
|
codexSessionId: report.reviewerSessionId,
|
|
9297
9520
|
timestamp: (/* @__PURE__ */ new Date()).toISOString()
|
|
9298
9521
|
});
|
|
9522
|
+
if (report.notes && /codex\b.*\b(unavail|limit|failed|down|error|usage)/i.test(report.notes)) {
|
|
9523
|
+
ctx.writeState({ codexUnavailable: true, codexUnavailableSince: (/* @__PURE__ */ new Date()).toISOString() });
|
|
9524
|
+
}
|
|
9299
9525
|
const risk = ctx.state.ticket?.realizedRisk ?? ctx.state.ticket?.risk ?? "low";
|
|
9300
9526
|
const minRounds = requiredRounds(risk);
|
|
9301
9527
|
const hasCriticalOrMajor = findings.some(
|
|
@@ -9320,6 +9546,7 @@ var CodeReviewStage = class {
|
|
|
9320
9546
|
} else {
|
|
9321
9547
|
nextAction = "CODE_REVIEW";
|
|
9322
9548
|
}
|
|
9549
|
+
const isIssueFix = !!ctx.state.currentIssue;
|
|
9323
9550
|
if (nextAction === "PLAN") {
|
|
9324
9551
|
clearCache(ctx.dir);
|
|
9325
9552
|
ctx.writeState({
|
|
@@ -9331,9 +9558,12 @@ var CodeReviewStage = class {
|
|
|
9331
9558
|
round: roundNum,
|
|
9332
9559
|
verdict,
|
|
9333
9560
|
findingCount: findings.length,
|
|
9334
|
-
redirectedTo: "PLAN"
|
|
9561
|
+
redirectedTo: isIssueFix ? "ISSUE_FIX" : "PLAN"
|
|
9335
9562
|
});
|
|
9336
9563
|
await ctx.fileDeferredFindings(findings, "code");
|
|
9564
|
+
if (isIssueFix) {
|
|
9565
|
+
return { action: "goto", target: "ISSUE_FIX" };
|
|
9566
|
+
}
|
|
9337
9567
|
return { action: "back", target: "PLAN", reason: "plan_redirect" };
|
|
9338
9568
|
}
|
|
9339
9569
|
const stateUpdate = {
|
|
@@ -9356,6 +9586,9 @@ var CodeReviewStage = class {
|
|
|
9356
9586
|
});
|
|
9357
9587
|
await ctx.fileDeferredFindings(findings, "code");
|
|
9358
9588
|
if (nextAction === "IMPLEMENT") {
|
|
9589
|
+
if (isIssueFix) {
|
|
9590
|
+
return { action: "goto", target: "ISSUE_FIX" };
|
|
9591
|
+
}
|
|
9359
9592
|
return { action: "back", target: "IMPLEMENT", reason: "request_changes" };
|
|
9360
9593
|
}
|
|
9361
9594
|
if (nextAction === "FINALIZE") {
|
|
@@ -9382,7 +9615,7 @@ var CodeReviewStage = class {
|
|
|
9382
9615
|
}
|
|
9383
9616
|
return { action: "advance" };
|
|
9384
9617
|
}
|
|
9385
|
-
const nextReviewerName = nextReviewer(codeReviews, backends);
|
|
9618
|
+
const nextReviewerName = nextReviewer(codeReviews, backends, ctx.state.codexUnavailable, ctx.state.codexUnavailableSince);
|
|
9386
9619
|
const mergeBase = ctx.state.git.mergeBase;
|
|
9387
9620
|
return {
|
|
9388
9621
|
action: "retry",
|
|
@@ -9481,6 +9714,7 @@ var BuildStage = class {
|
|
|
9481
9714
|
|
|
9482
9715
|
// src/autonomous/stages/verify.ts
|
|
9483
9716
|
init_esm_shims();
|
|
9717
|
+
init_git_inspector();
|
|
9484
9718
|
var MAX_VERIFY_RETRIES = 3;
|
|
9485
9719
|
var APP_ROUTER_RE = /^(?:src\/)?app\/((?:.*\/)?api\/.*?)\/route\.[jt]sx?$/;
|
|
9486
9720
|
var PAGES_ROUTER_RE = /^(?:src\/)?pages\/(api\/.*?)\.[jt]sx?$/;
|
|
@@ -9663,19 +9897,53 @@ function detectEndpoints(changedFiles) {
|
|
|
9663
9897
|
|
|
9664
9898
|
// src/autonomous/stages/finalize.ts
|
|
9665
9899
|
init_esm_shims();
|
|
9900
|
+
init_git_inspector();
|
|
9666
9901
|
var FinalizeStage = class {
|
|
9667
9902
|
id = "FINALIZE";
|
|
9668
9903
|
async enter(ctx) {
|
|
9904
|
+
if (ctx.state.finalizeCheckpoint === "committed") {
|
|
9905
|
+
return { action: "advance" };
|
|
9906
|
+
}
|
|
9907
|
+
const previousHead = ctx.state.git.expectedHead ?? ctx.state.git.initHead;
|
|
9908
|
+
if (previousHead) {
|
|
9909
|
+
const headResult = await gitHead(ctx.root);
|
|
9910
|
+
if (headResult.ok && headResult.data.hash !== previousHead) {
|
|
9911
|
+
const treeResult = await gitDiffTreeNames(ctx.root, headResult.data.hash);
|
|
9912
|
+
const ticketId = ctx.state.ticket?.id;
|
|
9913
|
+
if (ticketId) {
|
|
9914
|
+
const ticketPath = `.story/tickets/${ticketId}.json`;
|
|
9915
|
+
if (treeResult.ok && !treeResult.data.includes(ticketPath)) {
|
|
9916
|
+
} else {
|
|
9917
|
+
ctx.writeState({ finalizeCheckpoint: "precommit_passed" });
|
|
9918
|
+
return this.handleCommit(ctx, { completedAction: "commit_done", commitHash: headResult.data.hash });
|
|
9919
|
+
}
|
|
9920
|
+
}
|
|
9921
|
+
const issueId = ctx.state.currentIssue?.id;
|
|
9922
|
+
if (issueId) {
|
|
9923
|
+
const issuePath = `.story/issues/${issueId}.json`;
|
|
9924
|
+
if (treeResult.ok && !treeResult.data.includes(issuePath)) {
|
|
9925
|
+
} else {
|
|
9926
|
+
ctx.writeState({ finalizeCheckpoint: "precommit_passed" });
|
|
9927
|
+
return this.handleCommit(ctx, { completedAction: "commit_done", commitHash: headResult.data.hash });
|
|
9928
|
+
}
|
|
9929
|
+
}
|
|
9930
|
+
if (!ticketId && !issueId) {
|
|
9931
|
+
ctx.writeState({ finalizeCheckpoint: "precommit_passed" });
|
|
9932
|
+
return this.handleCommit(ctx, { completedAction: "commit_done", commitHash: headResult.data.hash });
|
|
9933
|
+
}
|
|
9934
|
+
}
|
|
9935
|
+
}
|
|
9669
9936
|
return {
|
|
9670
9937
|
instruction: [
|
|
9671
9938
|
"# Finalize",
|
|
9672
9939
|
"",
|
|
9673
9940
|
"Code review passed. Time to commit.",
|
|
9674
9941
|
"",
|
|
9675
|
-
|
|
9676
|
-
ctx.state.
|
|
9677
|
-
|
|
9678
|
-
|
|
9942
|
+
"1. Run `git reset` to clear the staging area (ensures no stale files from prior operations)",
|
|
9943
|
+
ctx.state.ticket ? `2. Update ticket ${ctx.state.ticket.id} status to "complete" in .story/` : "",
|
|
9944
|
+
ctx.state.currentIssue ? `2. Ensure .story/issues/${ctx.state.currentIssue.id}.json is updated with status: "resolved"` : "",
|
|
9945
|
+
"3. Stage only the files you modified for this fix (code + .story/ changes). Do NOT use `git add -A` or `git add .`",
|
|
9946
|
+
'4. Call me with completedAction: "files_staged"'
|
|
9679
9947
|
].filter(Boolean).join("\n"),
|
|
9680
9948
|
reminders: [
|
|
9681
9949
|
ctx.state.currentIssue ? "Stage both code changes and .story/ issue update in the same commit. Only stage files related to this fix." : "Stage both code changes and .story/ ticket update in the same commit. Only stage files related to this ticket."
|
|
@@ -9696,6 +9964,9 @@ var FinalizeStage = class {
|
|
|
9696
9964
|
return this.handlePrecommit(ctx);
|
|
9697
9965
|
}
|
|
9698
9966
|
if (action === "commit_done") {
|
|
9967
|
+
if (!checkpoint) {
|
|
9968
|
+
ctx.writeState({ finalizeCheckpoint: "precommit_passed" });
|
|
9969
|
+
}
|
|
9699
9970
|
return this.handleCommit(ctx, report);
|
|
9700
9971
|
}
|
|
9701
9972
|
return {
|
|
@@ -9709,12 +9980,12 @@ var FinalizeStage = class {
|
|
|
9709
9980
|
return {
|
|
9710
9981
|
action: "retry",
|
|
9711
9982
|
instruction: [
|
|
9712
|
-
"Files staged. Now
|
|
9983
|
+
"Files staged. Now commit.",
|
|
9713
9984
|
"",
|
|
9714
|
-
|
|
9715
|
-
|
|
9716
|
-
|
|
9717
|
-
|
|
9985
|
+
ctx.state.ticket ? `Commit with message: "feat: <description> (${ctx.state.ticket.id})"` : "Commit with a descriptive message.",
|
|
9986
|
+
"",
|
|
9987
|
+
'Call me with completedAction: "commit_done" and include the commitHash.'
|
|
9988
|
+
].join("\n")
|
|
9718
9989
|
};
|
|
9719
9990
|
}
|
|
9720
9991
|
const stagedResult = await gitDiffCachedNames(ctx.root);
|
|
@@ -9749,15 +10020,14 @@ var FinalizeStage = class {
|
|
|
9749
10020
|
return { action: "retry", instruction: 'No files are staged. Stage your changes and call me again with completedAction: "files_staged".' };
|
|
9750
10021
|
}
|
|
9751
10022
|
const baselineUntracked = ctx.state.git.baseline?.untrackedPaths ?? [];
|
|
9752
|
-
let overlapOverridden = false;
|
|
9753
10023
|
if (baselineUntracked.length > 0) {
|
|
9754
10024
|
const sessionTicketPath = ctx.state.ticket?.id ? `.story/tickets/${ctx.state.ticket.id}.json` : null;
|
|
10025
|
+
const sessionIssuePath = ctx.state.currentIssue?.id ? `.story/issues/${ctx.state.currentIssue.id}.json` : null;
|
|
9755
10026
|
const overlap = stagedResult.data.filter(
|
|
9756
|
-
(f) => baselineUntracked.includes(f) && f !== sessionTicketPath
|
|
10027
|
+
(f) => baselineUntracked.includes(f) && f !== sessionTicketPath && f !== sessionIssuePath
|
|
9757
10028
|
);
|
|
9758
10029
|
if (overlap.length > 0) {
|
|
9759
10030
|
if (report.overrideOverlap) {
|
|
9760
|
-
overlapOverridden = true;
|
|
9761
10031
|
} else {
|
|
9762
10032
|
return {
|
|
9763
10033
|
action: "retry",
|
|
@@ -9787,17 +10057,17 @@ var FinalizeStage = class {
|
|
|
9787
10057
|
}
|
|
9788
10058
|
}
|
|
9789
10059
|
ctx.writeState({
|
|
9790
|
-
finalizeCheckpoint:
|
|
10060
|
+
finalizeCheckpoint: "precommit_passed"
|
|
9791
10061
|
});
|
|
9792
10062
|
return {
|
|
9793
10063
|
action: "retry",
|
|
9794
10064
|
instruction: [
|
|
9795
|
-
"Files staged. Now
|
|
10065
|
+
"Files staged. Now commit.",
|
|
9796
10066
|
"",
|
|
9797
|
-
|
|
9798
|
-
|
|
9799
|
-
|
|
9800
|
-
|
|
10067
|
+
ctx.state.ticket ? `Commit with message: "feat: <description> (${ctx.state.ticket.id})"` : "Commit with a descriptive message.",
|
|
10068
|
+
"",
|
|
10069
|
+
'Call me with completedAction: "commit_done" and include the commitHash.'
|
|
10070
|
+
].join("\n")
|
|
9801
10071
|
};
|
|
9802
10072
|
}
|
|
9803
10073
|
async handlePrecommit(ctx) {
|
|
@@ -9814,8 +10084,9 @@ var FinalizeStage = class {
|
|
|
9814
10084
|
const baselineUntracked = ctx.state.git.baseline?.untrackedPaths ?? [];
|
|
9815
10085
|
if (baselineUntracked.length > 0) {
|
|
9816
10086
|
const sessionTicketPath = ctx.state.ticket?.id ? `.story/tickets/${ctx.state.ticket.id}.json` : null;
|
|
10087
|
+
const sessionIssuePath = ctx.state.currentIssue?.id ? `.story/issues/${ctx.state.currentIssue.id}.json` : null;
|
|
9817
10088
|
const overlap = stagedResult.data.filter(
|
|
9818
|
-
(f) => baselineUntracked.includes(f) && f !== sessionTicketPath
|
|
10089
|
+
(f) => baselineUntracked.includes(f) && f !== sessionTicketPath && f !== sessionIssuePath
|
|
9819
10090
|
);
|
|
9820
10091
|
if (overlap.length > 0) {
|
|
9821
10092
|
ctx.writeState({ finalizeCheckpoint: null });
|
|
@@ -9886,6 +10157,7 @@ var FinalizeStage = class {
|
|
|
9886
10157
|
finalizeCheckpoint: "committed",
|
|
9887
10158
|
resolvedIssues: [...ctx.state.resolvedIssues ?? [], currentIssue.id],
|
|
9888
10159
|
currentIssue: null,
|
|
10160
|
+
ticketStartedAt: null,
|
|
9889
10161
|
git: {
|
|
9890
10162
|
...ctx.state.git,
|
|
9891
10163
|
mergeBase: normalizedHash,
|
|
@@ -9895,11 +10167,20 @@ var FinalizeStage = class {
|
|
|
9895
10167
|
ctx.appendEvent("commit", { commitHash: normalizedHash, issueId: currentIssue.id });
|
|
9896
10168
|
return { action: "goto", target: "COMPLETE" };
|
|
9897
10169
|
}
|
|
9898
|
-
const completedTicket = ctx.state.ticket ? {
|
|
10170
|
+
const completedTicket = ctx.state.ticket ? {
|
|
10171
|
+
id: ctx.state.ticket.id,
|
|
10172
|
+
title: ctx.state.ticket.title,
|
|
10173
|
+
commitHash: normalizedHash,
|
|
10174
|
+
risk: ctx.state.ticket.risk,
|
|
10175
|
+
realizedRisk: ctx.state.ticket.realizedRisk,
|
|
10176
|
+
startedAt: ctx.state.ticketStartedAt ?? void 0,
|
|
10177
|
+
completedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
10178
|
+
} : void 0;
|
|
9899
10179
|
ctx.writeState({
|
|
9900
10180
|
finalizeCheckpoint: "committed",
|
|
9901
10181
|
completedTickets: completedTicket ? [...ctx.state.completedTickets, completedTicket] : ctx.state.completedTickets,
|
|
9902
10182
|
ticket: void 0,
|
|
10183
|
+
ticketStartedAt: null,
|
|
9903
10184
|
git: {
|
|
9904
10185
|
...ctx.state.git,
|
|
9905
10186
|
mergeBase: normalizedHash,
|
|
@@ -9933,7 +10214,7 @@ var CompleteStage = class {
|
|
|
9933
10214
|
target: "HANDOVER",
|
|
9934
10215
|
result: {
|
|
9935
10216
|
instruction: [
|
|
9936
|
-
`# Ticket Complete
|
|
10217
|
+
`# Ticket Complete -- ${mode} mode session ending`,
|
|
9937
10218
|
"",
|
|
9938
10219
|
`Ticket **${ctx.state.ticket?.id}** completed. Write a brief session handover.`,
|
|
9939
10220
|
"",
|
|
@@ -9944,36 +10225,23 @@ var CompleteStage = class {
|
|
|
9944
10225
|
}
|
|
9945
10226
|
};
|
|
9946
10227
|
}
|
|
9947
|
-
|
|
9948
|
-
|
|
9949
|
-
|
|
9950
|
-
|
|
9951
|
-
|
|
9952
|
-
|
|
9953
|
-
|
|
9954
|
-
|
|
9955
|
-
|
|
9956
|
-
|
|
9957
|
-
|
|
9958
|
-
|
|
9959
|
-
|
|
9960
|
-
|
|
9961
|
-
].join("\n");
|
|
9962
|
-
await handleHandoverCreate3(content, "checkpoint", "md", ctx.root);
|
|
9963
|
-
} catch {
|
|
9964
|
-
}
|
|
9965
|
-
try {
|
|
9966
|
-
const { loadProject: loadProject2 } = await Promise.resolve().then(() => (init_project_loader(), project_loader_exports));
|
|
9967
|
-
const { saveSnapshot: saveSnapshot2 } = await Promise.resolve().then(() => (init_snapshot(), snapshot_exports));
|
|
9968
|
-
const loadResult = await loadProject2(ctx.root);
|
|
9969
|
-
await saveSnapshot2(ctx.root, loadResult);
|
|
9970
|
-
} catch {
|
|
9971
|
-
}
|
|
9972
|
-
ctx.appendEvent("checkpoint", { ticketsDone, issuesDone, totalWorkDone, interval: handoverInterval });
|
|
10228
|
+
await this.tryCheckpoint(ctx, totalWorkDone, ticketsDone, issuesDone);
|
|
10229
|
+
let projectState;
|
|
10230
|
+
try {
|
|
10231
|
+
({ state: projectState } = await ctx.loadProject());
|
|
10232
|
+
} catch (err) {
|
|
10233
|
+
return {
|
|
10234
|
+
action: "goto",
|
|
10235
|
+
target: "HANDOVER",
|
|
10236
|
+
result: {
|
|
10237
|
+
instruction: `Failed to load project state: ${err instanceof Error ? err.message : String(err)}. Ending session -- write a handover noting the error.`,
|
|
10238
|
+
reminders: [],
|
|
10239
|
+
transitionedFrom: "COMPLETE"
|
|
10240
|
+
}
|
|
10241
|
+
};
|
|
9973
10242
|
}
|
|
9974
|
-
const { state: projectState } = await ctx.loadProject();
|
|
9975
|
-
let nextTarget;
|
|
9976
10243
|
const targetedRemaining = isTargetedMode(ctx.state) ? getRemainingTargets(ctx.state) : null;
|
|
10244
|
+
let nextTarget;
|
|
9977
10245
|
if (targetedRemaining !== null) {
|
|
9978
10246
|
nextTarget = targetedRemaining.length === 0 ? "HANDOVER" : "PICK_TICKET";
|
|
9979
10247
|
} else if (maxTickets > 0 && totalWorkDone >= maxTickets) {
|
|
@@ -9988,66 +10256,118 @@ var CompleteStage = class {
|
|
|
9988
10256
|
}
|
|
9989
10257
|
}
|
|
9990
10258
|
if (nextTarget === "HANDOVER") {
|
|
9991
|
-
|
|
9992
|
-
|
|
9993
|
-
|
|
9994
|
-
|
|
9995
|
-
|
|
10259
|
+
return this.buildHandoverResult(ctx, targetedRemaining, ticketsDone, issuesDone);
|
|
10260
|
+
}
|
|
10261
|
+
if (targetedRemaining !== null) {
|
|
10262
|
+
return this.buildTargetedPickResult(ctx, targetedRemaining, projectState);
|
|
10263
|
+
}
|
|
10264
|
+
return this.buildStandardPickResult(ctx, projectState, ticketsDone, maxTickets);
|
|
10265
|
+
}
|
|
10266
|
+
async report(ctx, _report) {
|
|
10267
|
+
return this.enter(ctx);
|
|
10268
|
+
}
|
|
10269
|
+
// ---------------------------------------------------------------------------
|
|
10270
|
+
// Checkpoint -- mid-session handover + snapshot (best-effort)
|
|
10271
|
+
// ---------------------------------------------------------------------------
|
|
10272
|
+
async tryCheckpoint(ctx, totalWorkDone, ticketsDone, issuesDone) {
|
|
10273
|
+
const handoverInterval = ctx.state.config.handoverInterval ?? 5;
|
|
10274
|
+
if (handoverInterval <= 0 || totalWorkDone <= 0 || totalWorkDone % handoverInterval !== 0) return;
|
|
10275
|
+
try {
|
|
10276
|
+
const { handleHandoverCreate: handleHandoverCreate3 } = await Promise.resolve().then(() => (init_handover(), handover_exports));
|
|
10277
|
+
const completedIds = ctx.state.completedTickets.map((t) => t.id).join(", ");
|
|
10278
|
+
const resolvedIds = (ctx.state.resolvedIssues ?? []).join(", ");
|
|
10279
|
+
const content = [
|
|
10280
|
+
`# Checkpoint -- ${totalWorkDone} items completed`,
|
|
10281
|
+
"",
|
|
10282
|
+
`**Session:** ${ctx.state.sessionId}`,
|
|
10283
|
+
...completedIds ? [`**Tickets:** ${completedIds}`] : [],
|
|
10284
|
+
...resolvedIds ? [`**Issues resolved:** ${resolvedIds}`] : [],
|
|
10285
|
+
"",
|
|
10286
|
+
"This is an automatic mid-session checkpoint. The session is still active."
|
|
10287
|
+
].join("\n");
|
|
10288
|
+
await handleHandoverCreate3(content, "checkpoint", "md", ctx.root);
|
|
10289
|
+
} catch {
|
|
10290
|
+
}
|
|
10291
|
+
try {
|
|
10292
|
+
const { loadProject: loadProject2 } = await Promise.resolve().then(() => (init_project_loader(), project_loader_exports));
|
|
10293
|
+
const { saveSnapshot: saveSnapshot2 } = await Promise.resolve().then(() => (init_snapshot(), snapshot_exports));
|
|
10294
|
+
const loadResult = await loadProject2(ctx.root);
|
|
10295
|
+
await saveSnapshot2(ctx.root, loadResult);
|
|
10296
|
+
} catch {
|
|
10297
|
+
}
|
|
10298
|
+
ctx.appendEvent("checkpoint", { ticketsDone, issuesDone, totalWorkDone, interval: handoverInterval });
|
|
10299
|
+
}
|
|
10300
|
+
// ---------------------------------------------------------------------------
|
|
10301
|
+
// HANDOVER instruction -- session ending
|
|
10302
|
+
// ---------------------------------------------------------------------------
|
|
10303
|
+
buildHandoverResult(ctx, targetedRemaining, ticketsDone, issuesDone) {
|
|
10304
|
+
const postComplete = ctx.state.resolvedPostComplete ?? ctx.recipe.postComplete;
|
|
10305
|
+
const postResult = findFirstPostComplete(postComplete, ctx);
|
|
10306
|
+
if (postResult.kind === "found") {
|
|
10307
|
+
ctx.writeState({ pipelinePhase: "postComplete" });
|
|
10308
|
+
return { action: "goto", target: postResult.stage.id };
|
|
10309
|
+
}
|
|
10310
|
+
const handoverHeader = targetedRemaining !== null ? `# Targeted Session Complete -- All ${ctx.state.targetWork.length} target(s) done` : `# Session Complete -- ${ticketsDone} ticket(s) and ${issuesDone} issue(s) done`;
|
|
10311
|
+
return {
|
|
10312
|
+
action: "goto",
|
|
10313
|
+
target: "HANDOVER",
|
|
10314
|
+
result: {
|
|
10315
|
+
instruction: [
|
|
10316
|
+
handoverHeader,
|
|
10317
|
+
"",
|
|
10318
|
+
"Write a session handover summarizing what was accomplished, decisions made, and what's next.",
|
|
10319
|
+
"",
|
|
10320
|
+
'Call me with completedAction: "handover_written" and include the content in handoverContent.'
|
|
10321
|
+
].join("\n"),
|
|
10322
|
+
reminders: [],
|
|
10323
|
+
transitionedFrom: "COMPLETE",
|
|
10324
|
+
contextAdvice: "ok"
|
|
9996
10325
|
}
|
|
9997
|
-
|
|
10326
|
+
};
|
|
10327
|
+
}
|
|
10328
|
+
// ---------------------------------------------------------------------------
|
|
10329
|
+
// Targeted PICK_TICKET instruction
|
|
10330
|
+
// ---------------------------------------------------------------------------
|
|
10331
|
+
buildTargetedPickResult(ctx, targetedRemaining, projectState) {
|
|
10332
|
+
const { text: candidatesText, firstReady } = buildTargetedCandidatesText(targetedRemaining, projectState);
|
|
10333
|
+
if (!firstReady) {
|
|
9998
10334
|
return {
|
|
9999
10335
|
action: "goto",
|
|
10000
10336
|
target: "HANDOVER",
|
|
10001
10337
|
result: {
|
|
10002
|
-
instruction:
|
|
10003
|
-
handoverHeader,
|
|
10004
|
-
"",
|
|
10005
|
-
"Write a session handover summarizing what was accomplished, decisions made, and what's next.",
|
|
10006
|
-
"",
|
|
10007
|
-
'Call me with completedAction: "handover_written" and include the content in handoverContent.'
|
|
10008
|
-
].join("\n"),
|
|
10338
|
+
instruction: buildTargetedStuckHandover(candidatesText, ctx.state.sessionId),
|
|
10009
10339
|
reminders: [],
|
|
10010
|
-
transitionedFrom: "COMPLETE"
|
|
10011
|
-
contextAdvice: "ok"
|
|
10340
|
+
transitionedFrom: "COMPLETE"
|
|
10012
10341
|
}
|
|
10013
10342
|
};
|
|
10014
10343
|
}
|
|
10015
|
-
|
|
10016
|
-
|
|
10017
|
-
|
|
10018
|
-
|
|
10019
|
-
|
|
10020
|
-
|
|
10021
|
-
|
|
10022
|
-
|
|
10023
|
-
|
|
10024
|
-
|
|
10025
|
-
|
|
10026
|
-
|
|
10344
|
+
const precomputed = { text: candidatesText, firstReady };
|
|
10345
|
+
const targetedInstruction = buildTargetedPickInstruction(targetedRemaining, projectState, ctx.state.sessionId, precomputed);
|
|
10346
|
+
return {
|
|
10347
|
+
action: "goto",
|
|
10348
|
+
target: "PICK_TICKET",
|
|
10349
|
+
result: {
|
|
10350
|
+
instruction: [
|
|
10351
|
+
`# Item Complete -- Continuing (${ctx.state.targetWork.length - targetedRemaining.length}/${ctx.state.targetWork.length} targets done)`,
|
|
10352
|
+
"",
|
|
10353
|
+
"Do NOT stop. Do NOT ask the user. Continue immediately with the next target.",
|
|
10354
|
+
"",
|
|
10355
|
+
targetedInstruction
|
|
10356
|
+
].join("\n"),
|
|
10357
|
+
reminders: [
|
|
10358
|
+
"Do NOT stop or summarize. Call autonomous_guide IMMEDIATELY to pick the next target.",
|
|
10359
|
+
"Do NOT ask the user for confirmation.",
|
|
10360
|
+
"You are in targeted auto mode -- pick ONLY from the listed items."
|
|
10361
|
+
],
|
|
10362
|
+
transitionedFrom: "COMPLETE",
|
|
10363
|
+
contextAdvice: "ok"
|
|
10027
10364
|
}
|
|
10028
|
-
|
|
10029
|
-
|
|
10030
|
-
|
|
10031
|
-
|
|
10032
|
-
|
|
10033
|
-
|
|
10034
|
-
instruction: [
|
|
10035
|
-
`# Item Complete -- Continuing (${ctx.state.targetWork.length - targetedRemaining.length}/${ctx.state.targetWork.length} targets done)`,
|
|
10036
|
-
"",
|
|
10037
|
-
"Do NOT stop. Do NOT ask the user. Continue immediately with the next target.",
|
|
10038
|
-
"",
|
|
10039
|
-
targetedInstruction
|
|
10040
|
-
].join("\n"),
|
|
10041
|
-
reminders: [
|
|
10042
|
-
"Do NOT stop or summarize. Call autonomous_guide IMMEDIATELY to pick the next target.",
|
|
10043
|
-
"Do NOT ask the user for confirmation.",
|
|
10044
|
-
"You are in targeted auto mode -- pick ONLY from the listed items."
|
|
10045
|
-
],
|
|
10046
|
-
transitionedFrom: "COMPLETE",
|
|
10047
|
-
contextAdvice: "ok"
|
|
10048
|
-
}
|
|
10049
|
-
};
|
|
10050
|
-
}
|
|
10365
|
+
};
|
|
10366
|
+
}
|
|
10367
|
+
// ---------------------------------------------------------------------------
|
|
10368
|
+
// Standard auto PICK_TICKET instruction
|
|
10369
|
+
// ---------------------------------------------------------------------------
|
|
10370
|
+
buildStandardPickResult(ctx, projectState, ticketsDone, maxTickets) {
|
|
10051
10371
|
const candidates = nextTickets(projectState, 5);
|
|
10052
10372
|
let candidatesText = "";
|
|
10053
10373
|
if (candidates.kind === "found") {
|
|
@@ -10061,7 +10381,7 @@ var CompleteStage = class {
|
|
|
10061
10381
|
target: "PICK_TICKET",
|
|
10062
10382
|
result: {
|
|
10063
10383
|
instruction: [
|
|
10064
|
-
`# Ticket Complete
|
|
10384
|
+
`# Ticket Complete -- Continuing (${ticketsDone}/${maxTickets})`,
|
|
10065
10385
|
"",
|
|
10066
10386
|
"Do NOT stop. Do NOT ask the user. Continue immediately with the next ticket.",
|
|
10067
10387
|
"",
|
|
@@ -10075,16 +10395,13 @@ var CompleteStage = class {
|
|
|
10075
10395
|
reminders: [
|
|
10076
10396
|
"Do NOT stop or summarize. Call autonomous_guide IMMEDIATELY to pick the next ticket.",
|
|
10077
10397
|
"Do NOT ask the user for confirmation.",
|
|
10078
|
-
"You are in autonomous mode
|
|
10398
|
+
"You are in autonomous mode -- continue working until all tickets are done or the session limit is reached."
|
|
10079
10399
|
],
|
|
10080
10400
|
transitionedFrom: "COMPLETE",
|
|
10081
10401
|
contextAdvice: "ok"
|
|
10082
10402
|
}
|
|
10083
10403
|
};
|
|
10084
10404
|
}
|
|
10085
|
-
async report(ctx, _report) {
|
|
10086
|
-
return this.enter(ctx);
|
|
10087
|
-
}
|
|
10088
10405
|
};
|
|
10089
10406
|
|
|
10090
10407
|
// src/autonomous/stages/lesson-capture.ts
|
|
@@ -10200,7 +10517,32 @@ var IssueFixStage = class {
|
|
|
10200
10517
|
if (!issue) {
|
|
10201
10518
|
return { action: "goto", target: "PICK_TICKET" };
|
|
10202
10519
|
}
|
|
10203
|
-
|
|
10520
|
+
let projectState;
|
|
10521
|
+
try {
|
|
10522
|
+
({ state: projectState } = await ctx.loadProject());
|
|
10523
|
+
} catch {
|
|
10524
|
+
return {
|
|
10525
|
+
instruction: [
|
|
10526
|
+
"# Fix Issue",
|
|
10527
|
+
"",
|
|
10528
|
+
`**${issue.id}**: ${issue.title} (severity: ${issue.severity})`,
|
|
10529
|
+
"",
|
|
10530
|
+
"(Warning: could not load full issue details from .story/ -- using session state.)",
|
|
10531
|
+
"",
|
|
10532
|
+
'Fix this issue, then update its status to "resolved" in `.story/issues/`.',
|
|
10533
|
+
"Add a resolution description explaining the fix.",
|
|
10534
|
+
"",
|
|
10535
|
+
"When done, call `claudestory_autonomous_guide` with:",
|
|
10536
|
+
"```json",
|
|
10537
|
+
`{ "sessionId": "${ctx.state.sessionId}", "action": "report", "report": { "completedAction": "issue_fixed" } }`,
|
|
10538
|
+
"```"
|
|
10539
|
+
].join("\n"),
|
|
10540
|
+
reminders: [
|
|
10541
|
+
'Update the issue JSON: set status to "resolved", add resolution text, set resolvedDate.',
|
|
10542
|
+
"Do NOT ask the user for confirmation."
|
|
10543
|
+
]
|
|
10544
|
+
};
|
|
10545
|
+
}
|
|
10204
10546
|
const fullIssue = projectState.issues.find((i) => i.id === issue.id);
|
|
10205
10547
|
const details = fullIssue ? [
|
|
10206
10548
|
`**${fullIssue.id}**: ${fullIssue.title}`,
|
|
@@ -10235,7 +10577,12 @@ var IssueFixStage = class {
|
|
|
10235
10577
|
if (!issue) {
|
|
10236
10578
|
return { action: "goto", target: "PICK_TICKET" };
|
|
10237
10579
|
}
|
|
10238
|
-
|
|
10580
|
+
let projectState;
|
|
10581
|
+
try {
|
|
10582
|
+
({ state: projectState } = await ctx.loadProject());
|
|
10583
|
+
} catch (err) {
|
|
10584
|
+
return { action: "retry", instruction: `Failed to load project state: ${err instanceof Error ? err.message : String(err)}. Check .story/ files for corruption, then report again.` };
|
|
10585
|
+
}
|
|
10239
10586
|
const current = projectState.issues.find((i) => i.id === issue.id);
|
|
10240
10587
|
if (!current || current.status !== "resolved") {
|
|
10241
10588
|
return {
|
|
@@ -10244,6 +10591,10 @@ var IssueFixStage = class {
|
|
|
10244
10591
|
reminders: ["Set status to 'resolved', add resolution text, set resolvedDate."]
|
|
10245
10592
|
};
|
|
10246
10593
|
}
|
|
10594
|
+
const enableCodeReview = !!ctx.recipe.stages.ISSUE_FIX?.enableCodeReview;
|
|
10595
|
+
if (enableCodeReview) {
|
|
10596
|
+
return { action: "goto", target: "CODE_REVIEW" };
|
|
10597
|
+
}
|
|
10247
10598
|
return {
|
|
10248
10599
|
action: "goto",
|
|
10249
10600
|
target: "FINALIZE",
|
|
@@ -10253,9 +10604,10 @@ var IssueFixStage = class {
|
|
|
10253
10604
|
"",
|
|
10254
10605
|
`Issue ${issue.id} resolved. Time to commit.`,
|
|
10255
10606
|
"",
|
|
10256
|
-
|
|
10257
|
-
|
|
10258
|
-
|
|
10607
|
+
"1. Run `git reset` to clear the staging area (ensures no stale files from prior operations)",
|
|
10608
|
+
`2. Ensure .story/issues/${issue.id}.json is updated with status: "resolved"`,
|
|
10609
|
+
"3. Stage only the files you modified for this fix (code + .story/ changes). Do NOT use `git add -A` or `git add .`",
|
|
10610
|
+
'4. Call me with completedAction: "files_staged"'
|
|
10259
10611
|
].join("\n"),
|
|
10260
10612
|
reminders: ["Stage both code changes and .story/ issue update in the same commit. Only stage files related to this fix."],
|
|
10261
10613
|
transitionedFrom: "ISSUE_FIX"
|
|
@@ -10273,7 +10625,12 @@ var IssueSweepStage = class {
|
|
|
10273
10625
|
return !issueConfig?.enabled;
|
|
10274
10626
|
}
|
|
10275
10627
|
async enter(ctx) {
|
|
10276
|
-
|
|
10628
|
+
let projectState;
|
|
10629
|
+
try {
|
|
10630
|
+
({ state: projectState } = await ctx.loadProject());
|
|
10631
|
+
} catch {
|
|
10632
|
+
return { action: "goto", target: "HANDOVER" };
|
|
10633
|
+
}
|
|
10277
10634
|
const allIssues = projectState.issues.filter((i) => i.status === "open");
|
|
10278
10635
|
if (allIssues.length === 0) {
|
|
10279
10636
|
return { action: "goto", target: "HANDOVER" };
|
|
@@ -10325,7 +10682,12 @@ var IssueSweepStage = class {
|
|
|
10325
10682
|
}
|
|
10326
10683
|
const current = sweep.current;
|
|
10327
10684
|
if (current) {
|
|
10328
|
-
|
|
10685
|
+
let verifyState;
|
|
10686
|
+
try {
|
|
10687
|
+
({ state: verifyState } = await ctx.loadProject());
|
|
10688
|
+
} catch (err) {
|
|
10689
|
+
return { action: "retry", instruction: `Failed to load project state: ${err instanceof Error ? err.message : String(err)}. Check .story/ files, then report again.` };
|
|
10690
|
+
}
|
|
10329
10691
|
const currentIssue = verifyState.issues.find((i) => i.id === current);
|
|
10330
10692
|
if (currentIssue && currentIssue.status === "open") {
|
|
10331
10693
|
return {
|
|
@@ -10344,7 +10706,16 @@ var IssueSweepStage = class {
|
|
|
10344
10706
|
ctx.appendEvent("issue_sweep_complete", { resolved: resolved.length });
|
|
10345
10707
|
return { action: "goto", target: "HANDOVER" };
|
|
10346
10708
|
}
|
|
10347
|
-
|
|
10709
|
+
let projectState;
|
|
10710
|
+
try {
|
|
10711
|
+
({ state: projectState } = await ctx.loadProject());
|
|
10712
|
+
} catch {
|
|
10713
|
+
return {
|
|
10714
|
+
action: "retry",
|
|
10715
|
+
instruction: `Issue ${next} is next. Fix it and report again. (Could not load full details from .story/.)`,
|
|
10716
|
+
reminders: ["Set status to 'resolved' and add a resolution description."]
|
|
10717
|
+
};
|
|
10718
|
+
}
|
|
10348
10719
|
const nextIssue = projectState.issues.find((i) => i.id === next);
|
|
10349
10720
|
return {
|
|
10350
10721
|
action: "retry",
|
|
@@ -10368,8 +10739,292 @@ Impact: ${nextIssue.impact}` : ""}` : `Fix issue ${next}.`,
|
|
|
10368
10739
|
// src/autonomous/stages/handover.ts
|
|
10369
10740
|
init_esm_shims();
|
|
10370
10741
|
init_handover();
|
|
10371
|
-
|
|
10742
|
+
init_git_inspector();
|
|
10743
|
+
import { writeFileSync as writeFileSync5 } from "fs";
|
|
10744
|
+
import { join as join18 } from "path";
|
|
10745
|
+
|
|
10746
|
+
// src/autonomous/resume-marker.ts
|
|
10747
|
+
init_esm_shims();
|
|
10748
|
+
import { mkdirSync as mkdirSync4, writeFileSync as writeFileSync4, unlinkSync as unlinkSync3, existsSync as existsSync11 } from "fs";
|
|
10372
10749
|
import { join as join17 } from "path";
|
|
10750
|
+
var MARKER_FILENAME = "autonomous-resume.md";
|
|
10751
|
+
function sanitize(input, maxLen = 120) {
|
|
10752
|
+
return input.replace(/[\r\n]+/g, " ").replace(/\s+/g, " ").trim().slice(0, maxLen);
|
|
10753
|
+
}
|
|
10754
|
+
function writeResumeMarker(root, sessionId, state) {
|
|
10755
|
+
try {
|
|
10756
|
+
const rulesDir = join17(root, ".claude", "rules");
|
|
10757
|
+
mkdirSync4(rulesDir, { recursive: true });
|
|
10758
|
+
const ticketInfo = state.ticket ? `Working on: ${sanitize(state.ticket.id, 20)} (${sanitize(state.ticket.title)})` : "Between tickets";
|
|
10759
|
+
const progress = `Progress: ${state.completedTickets.length} tickets completed, ${(state.resolvedIssues ?? []).length} issues resolved`;
|
|
10760
|
+
const lines = [
|
|
10761
|
+
"CRITICAL: An autonomous coding session is active and waiting to resume.",
|
|
10762
|
+
"",
|
|
10763
|
+
`Session: ${sessionId}`,
|
|
10764
|
+
ticketInfo,
|
|
10765
|
+
progress
|
|
10766
|
+
];
|
|
10767
|
+
if (state.preCompactState) {
|
|
10768
|
+
lines.push(`State before compaction: ${state.preCompactState}`);
|
|
10769
|
+
}
|
|
10770
|
+
lines.push(
|
|
10771
|
+
"",
|
|
10772
|
+
"You MUST call `claudestory_autonomous_guide` with:",
|
|
10773
|
+
`{"sessionId": "${sessionId}", "action": "resume"}`,
|
|
10774
|
+
"",
|
|
10775
|
+
"Do NOT do any other work until you have called this.",
|
|
10776
|
+
"Do NOT manually create tickets, issues, or handovers.",
|
|
10777
|
+
"The guide manages your workflow."
|
|
10778
|
+
);
|
|
10779
|
+
const content = lines.join("\n") + "\n";
|
|
10780
|
+
writeFileSync4(join17(rulesDir, MARKER_FILENAME), content, "utf-8");
|
|
10781
|
+
} catch {
|
|
10782
|
+
}
|
|
10783
|
+
}
|
|
10784
|
+
function removeResumeMarker(root) {
|
|
10785
|
+
try {
|
|
10786
|
+
const markerPath = join17(root, ".claude", "rules", MARKER_FILENAME);
|
|
10787
|
+
if (existsSync11(markerPath)) unlinkSync3(markerPath);
|
|
10788
|
+
} catch {
|
|
10789
|
+
}
|
|
10790
|
+
}
|
|
10791
|
+
|
|
10792
|
+
// src/core/session-report-formatter.ts
|
|
10793
|
+
init_esm_shims();
|
|
10794
|
+
function formatSessionReport(data, format) {
|
|
10795
|
+
const { state, events, planContent, gitLog } = data;
|
|
10796
|
+
if (format === "json") {
|
|
10797
|
+
return JSON.stringify({
|
|
10798
|
+
ok: true,
|
|
10799
|
+
data: {
|
|
10800
|
+
summary: buildSummaryData(state),
|
|
10801
|
+
ticketProgression: state.completedTickets,
|
|
10802
|
+
reviewStats: state.reviews,
|
|
10803
|
+
events: events.events.slice(-50),
|
|
10804
|
+
malformedEventCount: events.malformedCount,
|
|
10805
|
+
contextPressure: state.contextPressure,
|
|
10806
|
+
git: {
|
|
10807
|
+
branch: state.git.branch,
|
|
10808
|
+
initHead: state.git.initHead,
|
|
10809
|
+
commits: gitLog
|
|
10810
|
+
},
|
|
10811
|
+
problems: buildProblems(state, events)
|
|
10812
|
+
}
|
|
10813
|
+
}, null, 2);
|
|
10814
|
+
}
|
|
10815
|
+
const sections = [];
|
|
10816
|
+
sections.push(buildSummarySection(state));
|
|
10817
|
+
sections.push(buildTicketSection(state));
|
|
10818
|
+
sections.push(buildReviewSection(state));
|
|
10819
|
+
sections.push(buildEventSection(events));
|
|
10820
|
+
sections.push(buildPressureSection(state));
|
|
10821
|
+
sections.push(buildGitSection(state, gitLog));
|
|
10822
|
+
sections.push(buildProblemsSection(state, events));
|
|
10823
|
+
return sections.join("\n\n---\n\n");
|
|
10824
|
+
}
|
|
10825
|
+
function buildSummaryData(state) {
|
|
10826
|
+
return {
|
|
10827
|
+
sessionId: state.sessionId,
|
|
10828
|
+
mode: state.mode ?? "auto",
|
|
10829
|
+
recipe: state.recipe,
|
|
10830
|
+
status: state.status,
|
|
10831
|
+
terminationReason: state.terminationReason,
|
|
10832
|
+
startedAt: state.startedAt,
|
|
10833
|
+
lastGuideCall: state.lastGuideCall,
|
|
10834
|
+
guideCallCount: state.guideCallCount,
|
|
10835
|
+
ticketsCompleted: state.completedTickets.length
|
|
10836
|
+
};
|
|
10837
|
+
}
|
|
10838
|
+
function buildSummarySection(state) {
|
|
10839
|
+
const duration = state.startedAt && state.lastGuideCall ? formatDuration(state.startedAt, state.lastGuideCall) : "unknown";
|
|
10840
|
+
return [
|
|
10841
|
+
"## Session Summary",
|
|
10842
|
+
"",
|
|
10843
|
+
`- **ID:** ${state.sessionId}`,
|
|
10844
|
+
`- **Mode:** ${state.mode ?? "auto"}`,
|
|
10845
|
+
`- **Recipe:** ${state.recipe}`,
|
|
10846
|
+
`- **Status:** ${state.status}${state.terminationReason ? ` (${state.terminationReason})` : ""}`,
|
|
10847
|
+
`- **Duration:** ${duration}`,
|
|
10848
|
+
`- **Guide calls:** ${state.guideCallCount}`,
|
|
10849
|
+
`- **Tickets completed:** ${state.completedTickets.length}`
|
|
10850
|
+
].join("\n");
|
|
10851
|
+
}
|
|
10852
|
+
function buildTicketSection(state) {
|
|
10853
|
+
if (state.completedTickets.length === 0) {
|
|
10854
|
+
const current = state.ticket;
|
|
10855
|
+
if (current) {
|
|
10856
|
+
return [
|
|
10857
|
+
"## Ticket Progression",
|
|
10858
|
+
"",
|
|
10859
|
+
`In progress: **${current.id}** \u2014 ${current.title} (risk: ${current.risk ?? "unknown"})`
|
|
10860
|
+
].join("\n");
|
|
10861
|
+
}
|
|
10862
|
+
return "## Ticket Progression\n\nNo tickets completed.";
|
|
10863
|
+
}
|
|
10864
|
+
const lines = ["## Ticket Progression", ""];
|
|
10865
|
+
for (const t of state.completedTickets) {
|
|
10866
|
+
const risk = t.realizedRisk ? `${t.risk ?? "?"} \u2192 ${t.realizedRisk}` : t.risk ?? "unknown";
|
|
10867
|
+
const duration = t.startedAt && t.completedAt ? formatDuration(t.startedAt, t.completedAt) : null;
|
|
10868
|
+
const durationPart = duration ? ` | duration: ${duration}` : "";
|
|
10869
|
+
lines.push(`- **${t.id}:** ${t.title} | risk: ${risk}${durationPart} | commit: \`${t.commitHash ?? "?"}\``);
|
|
10870
|
+
}
|
|
10871
|
+
return lines.join("\n");
|
|
10872
|
+
}
|
|
10873
|
+
function buildReviewSection(state) {
|
|
10874
|
+
const plan = state.reviews.plan;
|
|
10875
|
+
const code = state.reviews.code;
|
|
10876
|
+
if (plan.length === 0 && code.length === 0) {
|
|
10877
|
+
return "## Review Stats\n\nNo reviews recorded.";
|
|
10878
|
+
}
|
|
10879
|
+
const lines = ["## Review Stats", ""];
|
|
10880
|
+
if (plan.length > 0) {
|
|
10881
|
+
lines.push(`**Plan reviews:** ${plan.length} round(s)`);
|
|
10882
|
+
for (const r of plan) {
|
|
10883
|
+
lines.push(` - Round ${r.round}: ${r.verdict} (${r.findingCount} findings, ${r.criticalCount} critical, ${r.majorCount} major) \u2014 ${r.reviewer}`);
|
|
10884
|
+
}
|
|
10885
|
+
}
|
|
10886
|
+
if (code.length > 0) {
|
|
10887
|
+
lines.push(`**Code reviews:** ${code.length} round(s)`);
|
|
10888
|
+
for (const r of code) {
|
|
10889
|
+
lines.push(` - Round ${r.round}: ${r.verdict} (${r.findingCount} findings, ${r.criticalCount} critical, ${r.majorCount} major) \u2014 ${r.reviewer}`);
|
|
10890
|
+
}
|
|
10891
|
+
}
|
|
10892
|
+
const totalFindings = [...plan, ...code].reduce((sum, r) => sum + r.findingCount, 0);
|
|
10893
|
+
lines.push("", `**Total findings:** ${totalFindings}`);
|
|
10894
|
+
return lines.join("\n");
|
|
10895
|
+
}
|
|
10896
|
+
function buildEventSection(events) {
|
|
10897
|
+
if (events.events.length === 0 && events.malformedCount === 0) {
|
|
10898
|
+
return "## Event Timeline\n\nNot available.";
|
|
10899
|
+
}
|
|
10900
|
+
const capped = events.events.slice(-50);
|
|
10901
|
+
const omitted = events.events.length - capped.length;
|
|
10902
|
+
const lines = ["## Event Timeline", ""];
|
|
10903
|
+
if (omitted > 0) {
|
|
10904
|
+
lines.push(`*${omitted} earlier events omitted*`, "");
|
|
10905
|
+
}
|
|
10906
|
+
for (const e of capped) {
|
|
10907
|
+
const ts = e.timestamp ? e.timestamp.slice(11, 19) : "??:??:??";
|
|
10908
|
+
const detail = e.data ? Object.entries(e.data).map(([k, v]) => `${k}=${JSON.stringify(v)}`).join(" ") : "";
|
|
10909
|
+
lines.push(`- \`${ts}\` [${e.type}] ${detail}`.trimEnd());
|
|
10910
|
+
}
|
|
10911
|
+
if (events.malformedCount > 0) {
|
|
10912
|
+
lines.push("", `*${events.malformedCount} malformed event line(s) skipped*`);
|
|
10913
|
+
}
|
|
10914
|
+
return lines.join("\n");
|
|
10915
|
+
}
|
|
10916
|
+
function buildPressureSection(state) {
|
|
10917
|
+
const p = state.contextPressure;
|
|
10918
|
+
return [
|
|
10919
|
+
"## Context Pressure",
|
|
10920
|
+
"",
|
|
10921
|
+
`- **Level:** ${p.level}`,
|
|
10922
|
+
`- **Guide calls:** ${p.guideCallCount}`,
|
|
10923
|
+
`- **Tickets completed:** ${p.ticketsCompleted}`,
|
|
10924
|
+
`- **Compactions:** ${p.compactionCount}`,
|
|
10925
|
+
`- **Events log:** ${p.eventsLogBytes} bytes`
|
|
10926
|
+
].join("\n");
|
|
10927
|
+
}
|
|
10928
|
+
function buildGitSection(state, gitLog) {
|
|
10929
|
+
const lines = [
|
|
10930
|
+
"## Git Summary",
|
|
10931
|
+
"",
|
|
10932
|
+
`- **Branch:** ${state.git.branch ?? "unknown"}`,
|
|
10933
|
+
`- **Init HEAD:** \`${state.git.initHead ?? "?"}\``,
|
|
10934
|
+
`- **Expected HEAD:** \`${state.git.expectedHead ?? "?"}\``
|
|
10935
|
+
];
|
|
10936
|
+
if (gitLog && gitLog.length > 0) {
|
|
10937
|
+
lines.push("", "**Commits:**");
|
|
10938
|
+
for (const c of gitLog) {
|
|
10939
|
+
lines.push(`- ${c}`);
|
|
10940
|
+
}
|
|
10941
|
+
} else {
|
|
10942
|
+
lines.push("", "Commits: Not available.");
|
|
10943
|
+
}
|
|
10944
|
+
return lines.join("\n");
|
|
10945
|
+
}
|
|
10946
|
+
function buildProblems(state, events) {
|
|
10947
|
+
const problems = [];
|
|
10948
|
+
if (state.terminationReason && state.terminationReason !== "normal") {
|
|
10949
|
+
problems.push(`Abnormal termination: ${state.terminationReason}`);
|
|
10950
|
+
}
|
|
10951
|
+
if (events.malformedCount > 0) {
|
|
10952
|
+
problems.push(`${events.malformedCount} malformed event line(s) in events.log`);
|
|
10953
|
+
}
|
|
10954
|
+
for (const e of events.events) {
|
|
10955
|
+
if (e.type.includes("error") || e.type.includes("exhaustion")) {
|
|
10956
|
+
problems.push(`[${e.type}] ${e.timestamp ?? ""} ${JSON.stringify(e.data)}`);
|
|
10957
|
+
} else if (e.data?.result === "exhaustion") {
|
|
10958
|
+
problems.push(`[${e.type}] exhaustion at ${e.timestamp ?? ""}`);
|
|
10959
|
+
}
|
|
10960
|
+
}
|
|
10961
|
+
if (state.deferralsUnfiled) {
|
|
10962
|
+
problems.push("Session has unfiled deferrals");
|
|
10963
|
+
}
|
|
10964
|
+
return problems;
|
|
10965
|
+
}
|
|
10966
|
+
function buildProblemsSection(state, events) {
|
|
10967
|
+
const problems = buildProblems(state, events);
|
|
10968
|
+
if (problems.length === 0) {
|
|
10969
|
+
return "## Problems\n\nNone detected.";
|
|
10970
|
+
}
|
|
10971
|
+
return ["## Problems", "", ...problems.map((p) => `- ${p}`)].join("\n");
|
|
10972
|
+
}
|
|
10973
|
+
function formatCompactReport(data) {
|
|
10974
|
+
const { state, remainingWork } = data;
|
|
10975
|
+
const endTime = data.endedAt ?? state.lastGuideCall ?? (/* @__PURE__ */ new Date()).toISOString();
|
|
10976
|
+
const duration = state.startedAt ? formatDuration(state.startedAt, endTime) : "unknown";
|
|
10977
|
+
const ticketCount = state.completedTickets.length;
|
|
10978
|
+
const issueCount = (state.resolvedIssues ?? []).length;
|
|
10979
|
+
const reviewRounds = state.reviews.plan.length + state.reviews.code.length;
|
|
10980
|
+
const totalFindings = [...state.reviews.plan, ...state.reviews.code].reduce((s, r) => s + r.findingCount, 0);
|
|
10981
|
+
const compactions = state.contextPressure?.compactionCount ?? 0;
|
|
10982
|
+
const lines = [
|
|
10983
|
+
"## Session Report",
|
|
10984
|
+
"",
|
|
10985
|
+
`**Duration:** ${duration} | **Tickets:** ${ticketCount} | **Issues:** ${issueCount} | **Reviews:** ${reviewRounds} rounds (${totalFindings} findings) | **Compactions:** ${compactions}`
|
|
10986
|
+
];
|
|
10987
|
+
if (ticketCount > 0) {
|
|
10988
|
+
lines.push("", "### Completed", "| Ticket | Title | Duration |", "|--------|-------|----------|");
|
|
10989
|
+
for (const t of state.completedTickets) {
|
|
10990
|
+
const ticketDuration = t.startedAt && t.completedAt ? formatDuration(t.startedAt, t.completedAt) : "--";
|
|
10991
|
+
const safeTitle = (t.title ?? "").replace(/\|/g, "\\|");
|
|
10992
|
+
lines.push(`| ${t.id} | ${safeTitle} | ${ticketDuration} |`);
|
|
10993
|
+
}
|
|
10994
|
+
const timings = state.completedTickets.filter((t) => t.startedAt && t.completedAt).map((t) => new Date(t.completedAt).getTime() - new Date(t.startedAt).getTime());
|
|
10995
|
+
if (timings.length > 0) {
|
|
10996
|
+
const avgMs = timings.reduce((a, b) => a + b, 0) / timings.length;
|
|
10997
|
+
const avgMins = Math.round(avgMs / 6e4);
|
|
10998
|
+
lines.push("", `**Avg time per ticket:** ${avgMins}m`);
|
|
10999
|
+
}
|
|
11000
|
+
}
|
|
11001
|
+
if (remainingWork && (remainingWork.tickets.length > 0 || remainingWork.issues.length > 0)) {
|
|
11002
|
+
lines.push("", "### What's Left");
|
|
11003
|
+
for (const t of remainingWork.tickets) {
|
|
11004
|
+
lines.push(`- ${t.id}: ${t.title} (unblocked)`);
|
|
11005
|
+
}
|
|
11006
|
+
for (const i of remainingWork.issues) {
|
|
11007
|
+
lines.push(`- ${i.id}: ${i.title} (${i.severity})`);
|
|
11008
|
+
}
|
|
11009
|
+
}
|
|
11010
|
+
return lines.join("\n");
|
|
11011
|
+
}
|
|
11012
|
+
function formatDuration(start, end) {
|
|
11013
|
+
try {
|
|
11014
|
+
const ms = new Date(end).getTime() - new Date(start).getTime();
|
|
11015
|
+
if (isNaN(ms) || ms < 0) return "unknown";
|
|
11016
|
+
const mins = Math.floor(ms / 6e4);
|
|
11017
|
+
if (mins < 60) return `${mins}m`;
|
|
11018
|
+
const hours = Math.floor(mins / 60);
|
|
11019
|
+
return `${hours}h ${mins % 60}m`;
|
|
11020
|
+
} catch {
|
|
11021
|
+
return "unknown";
|
|
11022
|
+
}
|
|
11023
|
+
}
|
|
11024
|
+
|
|
11025
|
+
// src/autonomous/stages/handover.ts
|
|
11026
|
+
init_project_loader();
|
|
11027
|
+
init_queries();
|
|
10373
11028
|
var HandoverStage = class {
|
|
10374
11029
|
id = "HANDOVER";
|
|
10375
11030
|
async enter(ctx) {
|
|
@@ -10400,8 +11055,8 @@ var HandoverStage = class {
|
|
|
10400
11055
|
} catch {
|
|
10401
11056
|
handoverFailed = true;
|
|
10402
11057
|
try {
|
|
10403
|
-
const fallbackPath =
|
|
10404
|
-
|
|
11058
|
+
const fallbackPath = join18(ctx.dir, "handover-fallback.md");
|
|
11059
|
+
writeFileSync5(fallbackPath, content, "utf-8");
|
|
10405
11060
|
} catch {
|
|
10406
11061
|
}
|
|
10407
11062
|
}
|
|
@@ -10427,6 +11082,19 @@ var HandoverStage = class {
|
|
|
10427
11082
|
issuesResolved: (ctx.state.resolvedIssues ?? []).length,
|
|
10428
11083
|
handoverFailed
|
|
10429
11084
|
});
|
|
11085
|
+
removeResumeMarker(ctx.root);
|
|
11086
|
+
let reportSection = "";
|
|
11087
|
+
try {
|
|
11088
|
+
const { state: projectState } = await loadProject(ctx.root);
|
|
11089
|
+
const nextResult = nextTickets(projectState, 5);
|
|
11090
|
+
const openIssues = projectState.issues.filter((i) => i.status === "open" || i.status === "inprogress").slice(0, 5);
|
|
11091
|
+
const remainingWork = {
|
|
11092
|
+
tickets: nextResult.kind === "found" ? nextResult.candidates.map((c) => ({ id: c.ticket.id, title: c.ticket.title })) : [],
|
|
11093
|
+
issues: openIssues.map((i) => ({ id: i.id, title: i.title, severity: i.severity }))
|
|
11094
|
+
};
|
|
11095
|
+
reportSection = "\n\n" + formatCompactReport({ state: ctx.state, endedAt: (/* @__PURE__ */ new Date()).toISOString(), remainingWork });
|
|
11096
|
+
} catch {
|
|
11097
|
+
}
|
|
10430
11098
|
const ticketsDone = ctx.state.completedTickets.length;
|
|
10431
11099
|
const issuesDone = (ctx.state.resolvedIssues ?? []).length;
|
|
10432
11100
|
const resolvedList = (ctx.state.resolvedIssues ?? []).map((id) => `- ${id} (resolved)`).join("\n");
|
|
@@ -10440,7 +11108,7 @@ var HandoverStage = class {
|
|
|
10440
11108
|
"",
|
|
10441
11109
|
ctx.state.completedTickets.map((t) => `- ${t.id}${t.title ? `: ${t.title}` : ""} (${t.commitHash ?? "no commit"})`).join("\n"),
|
|
10442
11110
|
...resolvedList ? [resolvedList] : []
|
|
10443
|
-
].join("\n"),
|
|
11111
|
+
].join("\n") + reportSection,
|
|
10444
11112
|
reminders: [],
|
|
10445
11113
|
transitionedFrom: "HANDOVER"
|
|
10446
11114
|
}
|
|
@@ -10474,7 +11142,7 @@ init_queries();
|
|
|
10474
11142
|
// src/autonomous/version-check.ts
|
|
10475
11143
|
init_esm_shims();
|
|
10476
11144
|
import { readFileSync as readFileSync9 } from "fs";
|
|
10477
|
-
import { join as
|
|
11145
|
+
import { join as join19, dirname as dirname4 } from "path";
|
|
10478
11146
|
import { fileURLToPath as fileURLToPath3 } from "url";
|
|
10479
11147
|
function checkVersionMismatch(runningVersion, installedVersion) {
|
|
10480
11148
|
if (!installedVersion) return null;
|
|
@@ -10486,8 +11154,8 @@ function getInstalledVersion() {
|
|
|
10486
11154
|
try {
|
|
10487
11155
|
const thisFile = fileURLToPath3(import.meta.url);
|
|
10488
11156
|
const candidates = [
|
|
10489
|
-
|
|
10490
|
-
|
|
11157
|
+
join19(dirname4(thisFile), "..", "..", "package.json"),
|
|
11158
|
+
join19(dirname4(thisFile), "..", "package.json")
|
|
10491
11159
|
];
|
|
10492
11160
|
for (const candidate of candidates) {
|
|
10493
11161
|
try {
|
|
@@ -10503,7 +11171,7 @@ function getInstalledVersion() {
|
|
|
10503
11171
|
}
|
|
10504
11172
|
}
|
|
10505
11173
|
function getRunningVersion() {
|
|
10506
|
-
return "0.1.
|
|
11174
|
+
return "0.1.63";
|
|
10507
11175
|
}
|
|
10508
11176
|
|
|
10509
11177
|
// src/autonomous/guide.ts
|
|
@@ -10522,24 +11190,25 @@ var RECOVERY_MAPPING = {
|
|
|
10522
11190
|
CODE_REVIEW: { state: "PLAN", resetPlan: true, resetCode: true },
|
|
10523
11191
|
FINALIZE: { state: "IMPLEMENT", resetPlan: false, resetCode: true },
|
|
10524
11192
|
LESSON_CAPTURE: { state: "PICK_TICKET", resetPlan: false, resetCode: false },
|
|
10525
|
-
ISSUE_FIX: { state: "
|
|
11193
|
+
ISSUE_FIX: { state: "ISSUE_FIX", resetPlan: false, resetCode: false },
|
|
11194
|
+
// T-208: self-recover to avoid dangling currentIssue
|
|
10526
11195
|
ISSUE_SWEEP: { state: "PICK_TICKET", resetPlan: false, resetCode: false }
|
|
10527
11196
|
};
|
|
10528
11197
|
function buildGuideRecommendOptions(root) {
|
|
10529
11198
|
const opts = {};
|
|
10530
11199
|
try {
|
|
10531
|
-
const handoversDir =
|
|
11200
|
+
const handoversDir = join20(root, ".story", "handovers");
|
|
10532
11201
|
const files = readdirSync4(handoversDir, "utf-8").filter((f) => f.endsWith(".md")).sort();
|
|
10533
11202
|
if (files.length > 0) {
|
|
10534
|
-
opts.latestHandoverContent = readFileSync10(
|
|
11203
|
+
opts.latestHandoverContent = readFileSync10(join20(handoversDir, files[files.length - 1]), "utf-8");
|
|
10535
11204
|
}
|
|
10536
11205
|
} catch {
|
|
10537
11206
|
}
|
|
10538
11207
|
try {
|
|
10539
|
-
const snapshotsDir =
|
|
11208
|
+
const snapshotsDir = join20(root, ".story", "snapshots");
|
|
10540
11209
|
const snapFiles = readdirSync4(snapshotsDir, "utf-8").filter((f) => f.endsWith(".json")).sort();
|
|
10541
11210
|
if (snapFiles.length > 0) {
|
|
10542
|
-
const raw = readFileSync10(
|
|
11211
|
+
const raw = readFileSync10(join20(snapshotsDir, snapFiles[snapFiles.length - 1]), "utf-8");
|
|
10543
11212
|
const snap = JSON.parse(raw);
|
|
10544
11213
|
if (snap.issues) {
|
|
10545
11214
|
opts.previousOpenIssueCount = snap.issues.filter((i) => i.status !== "resolved").length;
|
|
@@ -10617,6 +11286,33 @@ async function recoverPendingMutation(dir, state, root) {
|
|
|
10617
11286
|
const mutation = state.pendingProjectMutation;
|
|
10618
11287
|
if (!mutation || typeof mutation !== "object") return state;
|
|
10619
11288
|
const m = mutation;
|
|
11289
|
+
if (m.type === "issue_update") {
|
|
11290
|
+
const targetId2 = m.target;
|
|
11291
|
+
const targetValue2 = m.value;
|
|
11292
|
+
const expectedCurrent2 = m.expectedCurrent;
|
|
11293
|
+
try {
|
|
11294
|
+
const { loadProject: loadProject2 } = await Promise.resolve().then(() => (init_project_loader(), project_loader_exports));
|
|
11295
|
+
const { state: projectState } = await loadProject2(root);
|
|
11296
|
+
const issue = projectState.issues.find((i) => i.id === targetId2);
|
|
11297
|
+
if (issue) {
|
|
11298
|
+
if (issue.status === targetValue2) {
|
|
11299
|
+
} else if (expectedCurrent2 && issue.status === expectedCurrent2) {
|
|
11300
|
+
const { handleIssueUpdate: handleIssueUpdate2 } = await Promise.resolve().then(() => (init_issue2(), issue_exports));
|
|
11301
|
+
await handleIssueUpdate2(targetId2, { status: targetValue2 }, "json", root);
|
|
11302
|
+
} else {
|
|
11303
|
+
appendEvent(dir, {
|
|
11304
|
+
rev: state.revision,
|
|
11305
|
+
type: "mutation_conflict",
|
|
11306
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
11307
|
+
data: { targetId: targetId2, expected: expectedCurrent2, actual: issue.status, transitionId: m.transitionId }
|
|
11308
|
+
});
|
|
11309
|
+
}
|
|
11310
|
+
}
|
|
11311
|
+
} catch {
|
|
11312
|
+
}
|
|
11313
|
+
const cleared2 = { ...state, pendingProjectMutation: null };
|
|
11314
|
+
return writeSessionSync(dir, cleared2);
|
|
11315
|
+
}
|
|
10620
11316
|
if (m.type !== "ticket_update") return state;
|
|
10621
11317
|
const targetId = m.target;
|
|
10622
11318
|
const targetValue = m.value;
|
|
@@ -10892,6 +11588,7 @@ async function handleStart(root, args) {
|
|
|
10892
11588
|
reviewBackends: sessionConfig.reviewBackends,
|
|
10893
11589
|
stages: sessionConfig.stageOverrides
|
|
10894
11590
|
});
|
|
11591
|
+
removeResumeMarker(root);
|
|
10895
11592
|
const session = createSession(root, recipe, wsId, sessionConfig);
|
|
10896
11593
|
const dir = sessionDir(root, session.sessionId);
|
|
10897
11594
|
try {
|
|
@@ -11014,7 +11711,7 @@ Staged: ${stagedResult.data.join(", ")}`
|
|
|
11014
11711
|
const passMatch = combined.match(/(\d+)\s*pass/i);
|
|
11015
11712
|
const failMatch = combined.match(/(\d+)\s*fail/i);
|
|
11016
11713
|
const passCount = passMatch ? parseInt(passMatch[1], 10) : -1;
|
|
11017
|
-
const failCount = failMatch ? parseInt(failMatch[1], 10) : -1;
|
|
11714
|
+
const failCount = failMatch ? parseInt(failMatch[1], 10) : exitCode === 0 && passCount > 0 ? 0 : -1;
|
|
11018
11715
|
const output = combined.slice(-500);
|
|
11019
11716
|
updated = { ...updated, testBaseline: { exitCode, passCount, failCount, summary: output } };
|
|
11020
11717
|
if (writeTestsEnabled && failCount < 0) {
|
|
@@ -11054,7 +11751,7 @@ Staged: ${stagedResult.data.join(", ")}`
|
|
|
11054
11751
|
}
|
|
11055
11752
|
}
|
|
11056
11753
|
const { state: projectState, warnings } = await loadProject(root);
|
|
11057
|
-
const handoversDir =
|
|
11754
|
+
const handoversDir = join20(root, ".story", "handovers");
|
|
11058
11755
|
const ctx = { state: projectState, warnings, root, handoversDir, format: "md" };
|
|
11059
11756
|
let handoverText = "";
|
|
11060
11757
|
try {
|
|
@@ -11065,13 +11762,13 @@ Staged: ${stagedResult.data.join(", ")}`
|
|
|
11065
11762
|
let recapText = "";
|
|
11066
11763
|
try {
|
|
11067
11764
|
const snapshotInfo = await loadLatestSnapshot(root);
|
|
11068
|
-
const recap = buildRecap(projectState, snapshotInfo);
|
|
11765
|
+
const recap = await buildRecap(projectState, snapshotInfo, root);
|
|
11069
11766
|
if (recap.changes) {
|
|
11070
11767
|
recapText = "Changes since last snapshot available.";
|
|
11071
11768
|
}
|
|
11072
11769
|
} catch {
|
|
11073
11770
|
}
|
|
11074
|
-
const rulesText = readFileSafe2(
|
|
11771
|
+
const rulesText = readFileSafe2(join20(root, "RULES.md"));
|
|
11075
11772
|
const lessonDigest = buildLessonDigest(projectState.lessons);
|
|
11076
11773
|
const digestParts = [
|
|
11077
11774
|
handoverText ? `## Recent Handovers
|
|
@@ -11087,7 +11784,7 @@ ${rulesText}` : "",
|
|
|
11087
11784
|
].filter(Boolean);
|
|
11088
11785
|
const digest = digestParts.join("\n\n---\n\n");
|
|
11089
11786
|
try {
|
|
11090
|
-
|
|
11787
|
+
writeFileSync6(join20(dir, "context-digest.md"), digest, "utf-8");
|
|
11091
11788
|
} catch {
|
|
11092
11789
|
}
|
|
11093
11790
|
if (mode !== "auto" && args.ticketId) {
|
|
@@ -11485,8 +12182,18 @@ async function handleResume(root, args) {
|
|
|
11485
12182
|
`Cannot validate git state for session ${args.sessionId}. Check git status and try "resume" again, or run "claudestory session stop ${args.sessionId}" to end the session.`
|
|
11486
12183
|
));
|
|
11487
12184
|
}
|
|
12185
|
+
let ownCommitDrift = false;
|
|
11488
12186
|
if (expectedHead && headResult.data.hash !== expectedHead) {
|
|
11489
|
-
const
|
|
12187
|
+
const ancestorCheck = await gitIsAncestor(root, expectedHead, headResult.data.hash);
|
|
12188
|
+
if (ancestorCheck.ok && ancestorCheck.data) {
|
|
12189
|
+
ownCommitDrift = true;
|
|
12190
|
+
}
|
|
12191
|
+
}
|
|
12192
|
+
if (expectedHead && headResult.data.hash !== expectedHead && !ownCommitDrift) {
|
|
12193
|
+
let mapping = RECOVERY_MAPPING[resumeState] ?? { state: "PICK_TICKET", resetPlan: false, resetCode: false };
|
|
12194
|
+
if (info.state.currentIssue && resumeState === "CODE_REVIEW") {
|
|
12195
|
+
mapping = { state: "ISSUE_FIX", resetPlan: false, resetCode: true };
|
|
12196
|
+
}
|
|
11490
12197
|
const recoveryReviews = {
|
|
11491
12198
|
plan: mapping.resetPlan ? [] : info.state.reviews.plan,
|
|
11492
12199
|
code: mapping.resetCode ? [] : info.state.reviews.code
|
|
@@ -11514,6 +12221,19 @@ async function handleResume(root, args) {
|
|
|
11514
12221
|
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
11515
12222
|
data: { drift: true, previousState: resumeState, recoveryState: mapping.state, expectedHead, actualHead: headResult.data.hash, ticketId: info.state.ticket?.id }
|
|
11516
12223
|
});
|
|
12224
|
+
appendEvent(info.dir, {
|
|
12225
|
+
rev: driftWritten.revision,
|
|
12226
|
+
type: "resumed",
|
|
12227
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
12228
|
+
data: {
|
|
12229
|
+
preCompactState: resumeState,
|
|
12230
|
+
compactionCount: driftWritten.contextPressure?.compactionCount ?? 0,
|
|
12231
|
+
ticketId: info.state.ticket?.id ?? null,
|
|
12232
|
+
headMatch: false,
|
|
12233
|
+
recoveryState: mapping.state
|
|
12234
|
+
}
|
|
12235
|
+
});
|
|
12236
|
+
removeResumeMarker(root);
|
|
11517
12237
|
const driftPreamble = `**HEAD changed during compaction** (expected ${expectedHead.slice(0, 8)}, got ${headResult.data.hash.slice(0, 8)}). Review state invalidated.
|
|
11518
12238
|
|
|
11519
12239
|
`;
|
|
@@ -11586,6 +12306,29 @@ async function handleResume(root, args) {
|
|
|
11586
12306
|
reminders: ["Re-implement and verify before re-submitting for code review."]
|
|
11587
12307
|
});
|
|
11588
12308
|
}
|
|
12309
|
+
if (mapping.state === "ISSUE_FIX") {
|
|
12310
|
+
const issueFixStage = getStage("ISSUE_FIX");
|
|
12311
|
+
if (issueFixStage) {
|
|
12312
|
+
const recipe = resolveRecipeFromState(driftWritten);
|
|
12313
|
+
const ctx = new StageContext(root, info.dir, driftWritten, recipe);
|
|
12314
|
+
const enterResult = await issueFixStage.enter(ctx);
|
|
12315
|
+
if (isStageAdvance(enterResult)) {
|
|
12316
|
+
return processAdvance(ctx, issueFixStage, enterResult);
|
|
12317
|
+
}
|
|
12318
|
+
return guideResult(ctx.state, "ISSUE_FIX", {
|
|
12319
|
+
instruction: [
|
|
12320
|
+
"# Resumed After Compact \u2014 HEAD Mismatch",
|
|
12321
|
+
"",
|
|
12322
|
+
`${driftPreamble}Recovered to **ISSUE_FIX**. Re-fix the issue and mark resolved.`,
|
|
12323
|
+
"",
|
|
12324
|
+
"---",
|
|
12325
|
+
"",
|
|
12326
|
+
enterResult.instruction
|
|
12327
|
+
].join("\n"),
|
|
12328
|
+
reminders: enterResult.reminders ?? []
|
|
12329
|
+
});
|
|
12330
|
+
}
|
|
12331
|
+
}
|
|
11589
12332
|
return guideResult(driftWritten, mapping.state, {
|
|
11590
12333
|
instruction: `# Resumed After Compact \u2014 HEAD Mismatch
|
|
11591
12334
|
|
|
@@ -11607,8 +12350,23 @@ ${driftPreamble}Recovered to state: **${mapping.state}**. Continue from here.`,
|
|
|
11607
12350
|
compactPreparedAt: null,
|
|
11608
12351
|
resumeBlocked: false,
|
|
11609
12352
|
guideCallCount: 0,
|
|
11610
|
-
contextPressure: { ...resumePressure, level: evaluatePressure({ ...info.state, guideCallCount: 0, contextPressure: resumePressure }) }
|
|
12353
|
+
contextPressure: { ...resumePressure, level: evaluatePressure({ ...info.state, guideCallCount: 0, contextPressure: resumePressure }) },
|
|
12354
|
+
// T-184: Update expectedHead on own-commit drift (mergeBase stays at branch-off point)
|
|
12355
|
+
...ownCommitDrift ? { git: { ...info.state.git, expectedHead: headResult.data.hash } } : {}
|
|
12356
|
+
});
|
|
12357
|
+
appendEvent(info.dir, {
|
|
12358
|
+
rev: written.revision,
|
|
12359
|
+
type: "resumed",
|
|
12360
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
12361
|
+
data: {
|
|
12362
|
+
preCompactState: resumeState,
|
|
12363
|
+
compactionCount: written.contextPressure?.compactionCount ?? 0,
|
|
12364
|
+
ticketId: info.state.ticket?.id ?? null,
|
|
12365
|
+
headMatch: !ownCommitDrift,
|
|
12366
|
+
ownCommit: ownCommitDrift || void 0
|
|
12367
|
+
}
|
|
11611
12368
|
});
|
|
12369
|
+
removeResumeMarker(root);
|
|
11612
12370
|
if (resumeState === "PICK_TICKET") {
|
|
11613
12371
|
if (isTargetedMode(written)) {
|
|
11614
12372
|
const dispatched = await dispatchTargetedResume(root, written, info.dir, [
|
|
@@ -11722,6 +12480,12 @@ async function handlePreCompact(root, args) {
|
|
|
11722
12480
|
await saveSnapshot2(root, loadResult);
|
|
11723
12481
|
} catch {
|
|
11724
12482
|
}
|
|
12483
|
+
writeResumeMarker(root, result.sessionId, {
|
|
12484
|
+
ticket: info.state.ticket,
|
|
12485
|
+
completedTickets: info.state.completedTickets,
|
|
12486
|
+
resolvedIssues: info.state.resolvedIssues,
|
|
12487
|
+
preCompactState: result.preCompactState
|
|
12488
|
+
});
|
|
11725
12489
|
const reread = findSessionById(root, args.sessionId);
|
|
11726
12490
|
const written = reread?.state ?? info.state;
|
|
11727
12491
|
return guideResult(written, "COMPACT", {
|
|
@@ -11818,276 +12582,96 @@ async function handleCancel(root, args) {
|
|
|
11818
12582
|
appendEvent(cancelInfo.dir, {
|
|
11819
12583
|
rev: written.revision,
|
|
11820
12584
|
type: "cancelled",
|
|
11821
|
-
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
11822
|
-
data: {
|
|
11823
|
-
previousState: cancelInfo.state.state,
|
|
11824
|
-
ticketId: ticketId ?? null,
|
|
11825
|
-
ticketReleased,
|
|
11826
|
-
ticketConflict,
|
|
11827
|
-
stashPopFailed
|
|
11828
|
-
}
|
|
11829
|
-
});
|
|
11830
|
-
const stashNote = stashPopFailed ? " Auto-stash pop failed \u2014 run `git stash pop` manually." : "";
|
|
11831
|
-
return {
|
|
11832
|
-
content: [{ type: "text", text: `Session ${args.sessionId} cancelled. ${written.completedTickets.length} ticket(s) and ${(written.resolvedIssues ?? []).length} issue(s) were completed.${stashNote}` }]
|
|
11833
|
-
};
|
|
11834
|
-
}
|
|
11835
|
-
function guideResult(state, currentState, opts) {
|
|
11836
|
-
const summary = {
|
|
11837
|
-
ticket: state.ticket ? `${state.ticket.id}: ${state.ticket.title}` : "none",
|
|
11838
|
-
risk: state.ticket?.risk ?? "unknown",
|
|
11839
|
-
completed: [...state.completedTickets.map((t) => t.id), ...state.resolvedIssues ?? []],
|
|
11840
|
-
currentStep: currentState,
|
|
11841
|
-
contextPressure: state.contextPressure?.level ?? "low",
|
|
11842
|
-
branch: state.git?.branch ?? null
|
|
11843
|
-
};
|
|
11844
|
-
const allReminders = [...opts.reminders ?? []];
|
|
11845
|
-
if ((state.mode === "auto" || !state.mode) && currentState !== "SESSION_END") {
|
|
11846
|
-
allReminders.push(
|
|
11847
|
-
"NEVER cancel this session due to context size. Compaction is automatic \u2014 Claude Story preserves all session state across compactions via hooks."
|
|
11848
|
-
);
|
|
11849
|
-
}
|
|
11850
|
-
const output = {
|
|
11851
|
-
sessionId: state.sessionId,
|
|
11852
|
-
state: currentState,
|
|
11853
|
-
transitionedFrom: opts.transitionedFrom,
|
|
11854
|
-
instruction: opts.instruction,
|
|
11855
|
-
reminders: allReminders,
|
|
11856
|
-
contextAdvice: "ok",
|
|
11857
|
-
sessionSummary: summary
|
|
11858
|
-
};
|
|
11859
|
-
const parts = [
|
|
11860
|
-
output.instruction,
|
|
11861
|
-
"",
|
|
11862
|
-
"---",
|
|
11863
|
-
`**Session:** ${output.sessionId}`,
|
|
11864
|
-
`**State:** ${output.state}${output.transitionedFrom ? ` (from ${output.transitionedFrom})` : ""}`,
|
|
11865
|
-
`**Ticket:** ${summary.ticket}`,
|
|
11866
|
-
`**Risk:** ${summary.risk}`,
|
|
11867
|
-
`**Completed:** ${summary.completed.length > 0 ? summary.completed.join(", ") : "none"}`,
|
|
11868
|
-
`**Tickets done:** ${summary.completed.length}`,
|
|
11869
|
-
summary.branch ? `**Branch:** ${summary.branch}` : "",
|
|
11870
|
-
output.reminders.length > 0 ? `
|
|
11871
|
-
**Reminders:**
|
|
11872
|
-
${output.reminders.map((r) => `- ${r}`).join("\n")}` : ""
|
|
11873
|
-
].filter(Boolean);
|
|
11874
|
-
return { content: [{ type: "text", text: parts.join("\n") }] };
|
|
11875
|
-
}
|
|
11876
|
-
function guideError(err) {
|
|
11877
|
-
const message = err instanceof Error ? err.message : String(err);
|
|
11878
|
-
return {
|
|
11879
|
-
content: [{ type: "text", text: `[autonomous_guide error] ${message}` }],
|
|
11880
|
-
isError: true
|
|
11881
|
-
};
|
|
11882
|
-
}
|
|
11883
|
-
function readFileSafe2(path2) {
|
|
11884
|
-
try {
|
|
11885
|
-
return readFileSync10(path2, "utf-8");
|
|
11886
|
-
} catch {
|
|
11887
|
-
return "";
|
|
11888
|
-
}
|
|
11889
|
-
}
|
|
11890
|
-
|
|
11891
|
-
// src/cli/commands/session-report.ts
|
|
11892
|
-
init_esm_shims();
|
|
11893
|
-
init_session();
|
|
11894
|
-
init_session_types();
|
|
11895
|
-
import { readFileSync as readFileSync11, existsSync as existsSync12 } from "fs";
|
|
11896
|
-
import { join as join20 } from "path";
|
|
11897
|
-
|
|
11898
|
-
// src/core/session-report-formatter.ts
|
|
11899
|
-
init_esm_shims();
|
|
11900
|
-
function formatSessionReport(data, format) {
|
|
11901
|
-
const { state, events, planContent, gitLog } = data;
|
|
11902
|
-
if (format === "json") {
|
|
11903
|
-
return JSON.stringify({
|
|
11904
|
-
ok: true,
|
|
11905
|
-
data: {
|
|
11906
|
-
summary: buildSummaryData(state),
|
|
11907
|
-
ticketProgression: state.completedTickets,
|
|
11908
|
-
reviewStats: state.reviews,
|
|
11909
|
-
events: events.events.slice(-50),
|
|
11910
|
-
malformedEventCount: events.malformedCount,
|
|
11911
|
-
contextPressure: state.contextPressure,
|
|
11912
|
-
git: {
|
|
11913
|
-
branch: state.git.branch,
|
|
11914
|
-
initHead: state.git.initHead,
|
|
11915
|
-
commits: gitLog
|
|
11916
|
-
},
|
|
11917
|
-
problems: buildProblems(state, events)
|
|
11918
|
-
}
|
|
11919
|
-
}, null, 2);
|
|
11920
|
-
}
|
|
11921
|
-
const sections = [];
|
|
11922
|
-
sections.push(buildSummarySection(state));
|
|
11923
|
-
sections.push(buildTicketSection(state));
|
|
11924
|
-
sections.push(buildReviewSection(state));
|
|
11925
|
-
sections.push(buildEventSection(events));
|
|
11926
|
-
sections.push(buildPressureSection(state));
|
|
11927
|
-
sections.push(buildGitSection(state, gitLog));
|
|
11928
|
-
sections.push(buildProblemsSection(state, events));
|
|
11929
|
-
return sections.join("\n\n---\n\n");
|
|
11930
|
-
}
|
|
11931
|
-
function buildSummaryData(state) {
|
|
11932
|
-
return {
|
|
11933
|
-
sessionId: state.sessionId,
|
|
11934
|
-
mode: state.mode ?? "auto",
|
|
11935
|
-
recipe: state.recipe,
|
|
11936
|
-
status: state.status,
|
|
11937
|
-
terminationReason: state.terminationReason,
|
|
11938
|
-
startedAt: state.startedAt,
|
|
11939
|
-
lastGuideCall: state.lastGuideCall,
|
|
11940
|
-
guideCallCount: state.guideCallCount,
|
|
11941
|
-
ticketsCompleted: state.completedTickets.length
|
|
11942
|
-
};
|
|
11943
|
-
}
|
|
11944
|
-
function buildSummarySection(state) {
|
|
11945
|
-
const duration = state.startedAt && state.lastGuideCall ? formatDuration(state.startedAt, state.lastGuideCall) : "unknown";
|
|
11946
|
-
return [
|
|
11947
|
-
"## Session Summary",
|
|
11948
|
-
"",
|
|
11949
|
-
`- **ID:** ${state.sessionId}`,
|
|
11950
|
-
`- **Mode:** ${state.mode ?? "auto"}`,
|
|
11951
|
-
`- **Recipe:** ${state.recipe}`,
|
|
11952
|
-
`- **Status:** ${state.status}${state.terminationReason ? ` (${state.terminationReason})` : ""}`,
|
|
11953
|
-
`- **Duration:** ${duration}`,
|
|
11954
|
-
`- **Guide calls:** ${state.guideCallCount}`,
|
|
11955
|
-
`- **Tickets completed:** ${state.completedTickets.length}`
|
|
11956
|
-
].join("\n");
|
|
11957
|
-
}
|
|
11958
|
-
function buildTicketSection(state) {
|
|
11959
|
-
if (state.completedTickets.length === 0) {
|
|
11960
|
-
const current = state.ticket;
|
|
11961
|
-
if (current) {
|
|
11962
|
-
return [
|
|
11963
|
-
"## Ticket Progression",
|
|
11964
|
-
"",
|
|
11965
|
-
`In progress: **${current.id}** \u2014 ${current.title} (risk: ${current.risk ?? "unknown"})`
|
|
11966
|
-
].join("\n");
|
|
11967
|
-
}
|
|
11968
|
-
return "## Ticket Progression\n\nNo tickets completed.";
|
|
11969
|
-
}
|
|
11970
|
-
const lines = ["## Ticket Progression", ""];
|
|
11971
|
-
for (const t of state.completedTickets) {
|
|
11972
|
-
const risk = t.realizedRisk ? `${t.risk ?? "?"} \u2192 ${t.realizedRisk}` : t.risk ?? "unknown";
|
|
11973
|
-
lines.push(`- **${t.id}:** ${t.title} | risk: ${risk} | commit: \`${t.commitHash ?? "?"}\``);
|
|
11974
|
-
}
|
|
11975
|
-
return lines.join("\n");
|
|
11976
|
-
}
|
|
11977
|
-
function buildReviewSection(state) {
|
|
11978
|
-
const plan = state.reviews.plan;
|
|
11979
|
-
const code = state.reviews.code;
|
|
11980
|
-
if (plan.length === 0 && code.length === 0) {
|
|
11981
|
-
return "## Review Stats\n\nNo reviews recorded.";
|
|
11982
|
-
}
|
|
11983
|
-
const lines = ["## Review Stats", ""];
|
|
11984
|
-
if (plan.length > 0) {
|
|
11985
|
-
lines.push(`**Plan reviews:** ${plan.length} round(s)`);
|
|
11986
|
-
for (const r of plan) {
|
|
11987
|
-
lines.push(` - Round ${r.round}: ${r.verdict} (${r.findingCount} findings, ${r.criticalCount} critical, ${r.majorCount} major) \u2014 ${r.reviewer}`);
|
|
11988
|
-
}
|
|
11989
|
-
}
|
|
11990
|
-
if (code.length > 0) {
|
|
11991
|
-
lines.push(`**Code reviews:** ${code.length} round(s)`);
|
|
11992
|
-
for (const r of code) {
|
|
11993
|
-
lines.push(` - Round ${r.round}: ${r.verdict} (${r.findingCount} findings, ${r.criticalCount} critical, ${r.majorCount} major) \u2014 ${r.reviewer}`);
|
|
11994
|
-
}
|
|
11995
|
-
}
|
|
11996
|
-
const totalFindings = [...plan, ...code].reduce((sum, r) => sum + r.findingCount, 0);
|
|
11997
|
-
lines.push("", `**Total findings:** ${totalFindings}`);
|
|
11998
|
-
return lines.join("\n");
|
|
11999
|
-
}
|
|
12000
|
-
function buildEventSection(events) {
|
|
12001
|
-
if (events.events.length === 0 && events.malformedCount === 0) {
|
|
12002
|
-
return "## Event Timeline\n\nNot available.";
|
|
12003
|
-
}
|
|
12004
|
-
const capped = events.events.slice(-50);
|
|
12005
|
-
const omitted = events.events.length - capped.length;
|
|
12006
|
-
const lines = ["## Event Timeline", ""];
|
|
12007
|
-
if (omitted > 0) {
|
|
12008
|
-
lines.push(`*${omitted} earlier events omitted*`, "");
|
|
12009
|
-
}
|
|
12010
|
-
for (const e of capped) {
|
|
12011
|
-
const ts = e.timestamp ? e.timestamp.slice(11, 19) : "??:??:??";
|
|
12012
|
-
const detail = e.data ? Object.entries(e.data).map(([k, v]) => `${k}=${JSON.stringify(v)}`).join(" ") : "";
|
|
12013
|
-
lines.push(`- \`${ts}\` [${e.type}] ${detail}`.trimEnd());
|
|
12014
|
-
}
|
|
12015
|
-
if (events.malformedCount > 0) {
|
|
12016
|
-
lines.push("", `*${events.malformedCount} malformed event line(s) skipped*`);
|
|
12017
|
-
}
|
|
12018
|
-
return lines.join("\n");
|
|
12019
|
-
}
|
|
12020
|
-
function buildPressureSection(state) {
|
|
12021
|
-
const p = state.contextPressure;
|
|
12022
|
-
return [
|
|
12023
|
-
"## Context Pressure",
|
|
12024
|
-
"",
|
|
12025
|
-
`- **Level:** ${p.level}`,
|
|
12026
|
-
`- **Guide calls:** ${p.guideCallCount}`,
|
|
12027
|
-
`- **Tickets completed:** ${p.ticketsCompleted}`,
|
|
12028
|
-
`- **Compactions:** ${p.compactionCount}`,
|
|
12029
|
-
`- **Events log:** ${p.eventsLogBytes} bytes`
|
|
12030
|
-
].join("\n");
|
|
12031
|
-
}
|
|
12032
|
-
function buildGitSection(state, gitLog) {
|
|
12033
|
-
const lines = [
|
|
12034
|
-
"## Git Summary",
|
|
12035
|
-
"",
|
|
12036
|
-
`- **Branch:** ${state.git.branch ?? "unknown"}`,
|
|
12037
|
-
`- **Init HEAD:** \`${state.git.initHead ?? "?"}\``,
|
|
12038
|
-
`- **Expected HEAD:** \`${state.git.expectedHead ?? "?"}\``
|
|
12039
|
-
];
|
|
12040
|
-
if (gitLog && gitLog.length > 0) {
|
|
12041
|
-
lines.push("", "**Commits:**");
|
|
12042
|
-
for (const c of gitLog) {
|
|
12043
|
-
lines.push(`- ${c}`);
|
|
12585
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
12586
|
+
data: {
|
|
12587
|
+
previousState: cancelInfo.state.state,
|
|
12588
|
+
ticketId: ticketId ?? null,
|
|
12589
|
+
ticketReleased,
|
|
12590
|
+
ticketConflict,
|
|
12591
|
+
stashPopFailed
|
|
12044
12592
|
}
|
|
12045
|
-
}
|
|
12046
|
-
|
|
12593
|
+
});
|
|
12594
|
+
removeResumeMarker(root);
|
|
12595
|
+
let reportSection = "";
|
|
12596
|
+
try {
|
|
12597
|
+
const { state: projectState } = await loadProject(root);
|
|
12598
|
+
const nextResult = nextTickets(projectState, 5);
|
|
12599
|
+
const openIssues = projectState.issues.filter((i) => i.status === "open" || i.status === "inprogress").slice(0, 5);
|
|
12600
|
+
const remainingWork = {
|
|
12601
|
+
tickets: nextResult.kind === "found" ? nextResult.candidates.map((c) => ({ id: c.ticket.id, title: c.ticket.title })) : [],
|
|
12602
|
+
issues: openIssues.map((i) => ({ id: i.id, title: i.title, severity: i.severity }))
|
|
12603
|
+
};
|
|
12604
|
+
reportSection = "\n\n" + formatCompactReport({ state: written, endedAt: (/* @__PURE__ */ new Date()).toISOString(), remainingWork });
|
|
12605
|
+
} catch {
|
|
12047
12606
|
}
|
|
12048
|
-
|
|
12607
|
+
const stashNote = stashPopFailed ? " Auto-stash pop failed \u2014 run `git stash pop` manually." : "";
|
|
12608
|
+
return {
|
|
12609
|
+
content: [{ type: "text", text: `Session ${args.sessionId} cancelled. ${written.completedTickets.length} ticket(s) and ${(written.resolvedIssues ?? []).length} issue(s) were completed.${stashNote}${reportSection}` }]
|
|
12610
|
+
};
|
|
12049
12611
|
}
|
|
12050
|
-
function
|
|
12051
|
-
const
|
|
12052
|
-
|
|
12053
|
-
|
|
12054
|
-
|
|
12055
|
-
|
|
12056
|
-
|
|
12057
|
-
|
|
12058
|
-
|
|
12059
|
-
|
|
12060
|
-
|
|
12061
|
-
|
|
12062
|
-
|
|
12063
|
-
|
|
12064
|
-
}
|
|
12065
|
-
if (state.deferralsUnfiled) {
|
|
12066
|
-
problems.push("Session has unfiled deferrals");
|
|
12612
|
+
function guideResult(state, currentState, opts) {
|
|
12613
|
+
const summary = {
|
|
12614
|
+
ticket: state.ticket ? `${state.ticket.id}: ${state.ticket.title}` : "none",
|
|
12615
|
+
risk: state.ticket?.risk ?? "unknown",
|
|
12616
|
+
completed: [...state.completedTickets.map((t) => t.id), ...state.resolvedIssues ?? []],
|
|
12617
|
+
currentStep: currentState,
|
|
12618
|
+
contextPressure: state.contextPressure?.level ?? "low",
|
|
12619
|
+
branch: state.git?.branch ?? null
|
|
12620
|
+
};
|
|
12621
|
+
const allReminders = [...opts.reminders ?? []];
|
|
12622
|
+
if ((state.mode === "auto" || !state.mode) && currentState !== "SESSION_END") {
|
|
12623
|
+
allReminders.push(
|
|
12624
|
+
"NEVER cancel this session due to context size. Compaction is automatic \u2014 Claude Story preserves all session state across compactions via hooks."
|
|
12625
|
+
);
|
|
12067
12626
|
}
|
|
12068
|
-
|
|
12627
|
+
const output = {
|
|
12628
|
+
sessionId: state.sessionId,
|
|
12629
|
+
state: currentState,
|
|
12630
|
+
transitionedFrom: opts.transitionedFrom,
|
|
12631
|
+
instruction: opts.instruction,
|
|
12632
|
+
reminders: allReminders,
|
|
12633
|
+
contextAdvice: "ok",
|
|
12634
|
+
sessionSummary: summary
|
|
12635
|
+
};
|
|
12636
|
+
const parts = [
|
|
12637
|
+
output.instruction,
|
|
12638
|
+
"",
|
|
12639
|
+
"---",
|
|
12640
|
+
`**Session:** ${output.sessionId}`,
|
|
12641
|
+
`**State:** ${output.state}${output.transitionedFrom ? ` (from ${output.transitionedFrom})` : ""}`,
|
|
12642
|
+
`**Ticket:** ${summary.ticket}`,
|
|
12643
|
+
`**Risk:** ${summary.risk}`,
|
|
12644
|
+
`**Completed:** ${summary.completed.length > 0 ? summary.completed.join(", ") : "none"}`,
|
|
12645
|
+
`**Tickets done:** ${summary.completed.length}`,
|
|
12646
|
+
summary.branch ? `**Branch:** ${summary.branch}` : "",
|
|
12647
|
+
output.reminders.length > 0 ? `
|
|
12648
|
+
**Reminders:**
|
|
12649
|
+
${output.reminders.map((r) => `- ${r}`).join("\n")}` : ""
|
|
12650
|
+
].filter(Boolean);
|
|
12651
|
+
return { content: [{ type: "text", text: parts.join("\n") }] };
|
|
12069
12652
|
}
|
|
12070
|
-
function
|
|
12071
|
-
const
|
|
12072
|
-
|
|
12073
|
-
|
|
12074
|
-
|
|
12075
|
-
|
|
12653
|
+
function guideError(err) {
|
|
12654
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
12655
|
+
return {
|
|
12656
|
+
content: [{ type: "text", text: `[autonomous_guide error] ${message}` }],
|
|
12657
|
+
isError: true
|
|
12658
|
+
};
|
|
12076
12659
|
}
|
|
12077
|
-
function
|
|
12660
|
+
function readFileSafe2(path2) {
|
|
12078
12661
|
try {
|
|
12079
|
-
|
|
12080
|
-
if (isNaN(ms) || ms < 0) return "unknown";
|
|
12081
|
-
const mins = Math.floor(ms / 6e4);
|
|
12082
|
-
if (mins < 60) return `${mins}m`;
|
|
12083
|
-
const hours = Math.floor(mins / 60);
|
|
12084
|
-
return `${hours}h ${mins % 60}m`;
|
|
12662
|
+
return readFileSync10(path2, "utf-8");
|
|
12085
12663
|
} catch {
|
|
12086
|
-
return "
|
|
12664
|
+
return "";
|
|
12087
12665
|
}
|
|
12088
12666
|
}
|
|
12089
12667
|
|
|
12090
12668
|
// src/cli/commands/session-report.ts
|
|
12669
|
+
init_esm_shims();
|
|
12670
|
+
init_session();
|
|
12671
|
+
init_session_types();
|
|
12672
|
+
init_git_inspector();
|
|
12673
|
+
import { readFileSync as readFileSync11, existsSync as existsSync13 } from "fs";
|
|
12674
|
+
import { join as join21 } from "path";
|
|
12091
12675
|
var UUID_REGEX = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
|
|
12092
12676
|
async function handleSessionReport(sessionId, root, format = "md") {
|
|
12093
12677
|
if (!UUID_REGEX.test(sessionId)) {
|
|
@@ -12099,7 +12683,7 @@ async function handleSessionReport(sessionId, root, format = "md") {
|
|
|
12099
12683
|
};
|
|
12100
12684
|
}
|
|
12101
12685
|
const dir = sessionDir(root, sessionId);
|
|
12102
|
-
if (!
|
|
12686
|
+
if (!existsSync13(dir)) {
|
|
12103
12687
|
return {
|
|
12104
12688
|
output: `Error: Session ${sessionId} not found.`,
|
|
12105
12689
|
exitCode: 1,
|
|
@@ -12107,8 +12691,8 @@ async function handleSessionReport(sessionId, root, format = "md") {
|
|
|
12107
12691
|
isError: true
|
|
12108
12692
|
};
|
|
12109
12693
|
}
|
|
12110
|
-
const statePath2 =
|
|
12111
|
-
if (!
|
|
12694
|
+
const statePath2 = join21(dir, "state.json");
|
|
12695
|
+
if (!existsSync13(statePath2)) {
|
|
12112
12696
|
return {
|
|
12113
12697
|
output: `Error: Session ${sessionId} corrupt \u2014 state.json missing.`,
|
|
12114
12698
|
exitCode: 1,
|
|
@@ -12146,7 +12730,7 @@ async function handleSessionReport(sessionId, root, format = "md") {
|
|
|
12146
12730
|
const events = readEvents(dir);
|
|
12147
12731
|
let planContent = null;
|
|
12148
12732
|
try {
|
|
12149
|
-
planContent = readFileSync11(
|
|
12733
|
+
planContent = readFileSync11(join21(dir, "plan.md"), "utf-8");
|
|
12150
12734
|
} catch {
|
|
12151
12735
|
}
|
|
12152
12736
|
let gitLog = null;
|
|
@@ -12171,7 +12755,7 @@ init_issue();
|
|
|
12171
12755
|
init_roadmap();
|
|
12172
12756
|
init_output_formatter();
|
|
12173
12757
|
init_helpers();
|
|
12174
|
-
import { join as
|
|
12758
|
+
import { join as join22, resolve as resolve7 } from "path";
|
|
12175
12759
|
var PHASE_ID_REGEX = /^[a-z0-9]+(-[a-z0-9]+)*$/;
|
|
12176
12760
|
var PHASE_ID_MAX_LENGTH = 40;
|
|
12177
12761
|
function validatePhaseId(id) {
|
|
@@ -12280,7 +12864,7 @@ function formatMcpError(code, message) {
|
|
|
12280
12864
|
async function runMcpReadTool(pinnedRoot, handler) {
|
|
12281
12865
|
try {
|
|
12282
12866
|
const { state, warnings } = await loadProject(pinnedRoot);
|
|
12283
|
-
const handoversDir =
|
|
12867
|
+
const handoversDir = join23(pinnedRoot, ".story", "handovers");
|
|
12284
12868
|
const ctx = { state, warnings, root: pinnedRoot, handoversDir, format: "md" };
|
|
12285
12869
|
const result = await handler(ctx);
|
|
12286
12870
|
if (result.errorCode && INFRASTRUCTURE_ERROR_CODES.includes(result.errorCode)) {
|
|
@@ -12828,6 +13412,7 @@ function registerAllTools(server, pinnedRoot) {
|
|
|
12828
13412
|
disposition: z10.string()
|
|
12829
13413
|
})).optional().describe("Review findings"),
|
|
12830
13414
|
reviewerSessionId: z10.string().optional().describe("Codex session ID"),
|
|
13415
|
+
reviewer: z10.string().optional().describe("Actual reviewer backend used (e.g. 'agent' when codex was unavailable)"),
|
|
12831
13416
|
notes: z10.string().optional().describe("Free-text notes")
|
|
12832
13417
|
}).optional().describe("Report data (required for report action)")
|
|
12833
13418
|
}
|
|
@@ -12852,7 +13437,7 @@ function registerAllTools(server, pinnedRoot) {
|
|
|
12852
13437
|
}
|
|
12853
13438
|
});
|
|
12854
13439
|
server.registerTool("claudestory_review_lenses_synthesize", {
|
|
12855
|
-
description: "Synthesize lens results after parallel review. Validates findings, applies blocking policy, generates merger prompt. Call after collecting all lens subagent results.",
|
|
13440
|
+
description: "Synthesize lens results after parallel review. Validates findings, applies blocking policy, classifies origin (introduced vs pre-existing), auto-files pre-existing issues, generates merger prompt. Call after collecting all lens subagent results.",
|
|
12856
13441
|
inputSchema: {
|
|
12857
13442
|
stage: z10.enum(["CODE_REVIEW", "PLAN_REVIEW"]).optional().describe("Review stage (defaults to CODE_REVIEW)"),
|
|
12858
13443
|
lensResults: z10.array(z10.object({
|
|
@@ -12863,9 +13448,13 @@ function registerAllTools(server, pinnedRoot) {
|
|
|
12863
13448
|
activeLenses: z10.array(z10.string()).describe("Active lens names from prepare step"),
|
|
12864
13449
|
skippedLenses: z10.array(z10.string()).describe("Skipped lens names from prepare step"),
|
|
12865
13450
|
reviewRound: z10.number().int().min(1).optional().describe("Current review round"),
|
|
12866
|
-
reviewId: z10.string().optional().describe("Review ID from prepare step")
|
|
13451
|
+
reviewId: z10.string().optional().describe("Review ID from prepare step"),
|
|
13452
|
+
// T-192: Origin classification inputs
|
|
13453
|
+
diff: z10.string().optional().describe("The diff being reviewed (for origin classification of findings into introduced vs pre-existing)"),
|
|
13454
|
+
changedFiles: z10.array(z10.string()).optional().describe("Changed file paths from prepare step (for origin classification)"),
|
|
13455
|
+
sessionId: z10.string().uuid().optional().describe("Active session ID (for dedup of auto-filed pre-existing issues across review rounds)")
|
|
12867
13456
|
}
|
|
12868
|
-
}, (args) => {
|
|
13457
|
+
}, async (args) => {
|
|
12869
13458
|
try {
|
|
12870
13459
|
const result = handleSynthesize({
|
|
12871
13460
|
stage: args.stage,
|
|
@@ -12876,9 +13465,55 @@ function registerAllTools(server, pinnedRoot) {
|
|
|
12876
13465
|
reviewRound: args.reviewRound ?? 1,
|
|
12877
13466
|
reviewId: args.reviewId ?? "unknown"
|
|
12878
13467
|
},
|
|
12879
|
-
projectRoot: pinnedRoot
|
|
13468
|
+
projectRoot: pinnedRoot,
|
|
13469
|
+
diff: args.diff,
|
|
13470
|
+
changedFiles: args.changedFiles
|
|
12880
13471
|
});
|
|
12881
|
-
|
|
13472
|
+
const filedIssues = [];
|
|
13473
|
+
if (result.preExistingFindings.length > 0) {
|
|
13474
|
+
const sessionDir2 = args.sessionId ? join23(pinnedRoot, ".story", "sessions", args.sessionId) : null;
|
|
13475
|
+
const alreadyFiled = sessionDir2 ? readFiledPreexisting(sessionDir2) : /* @__PURE__ */ new Set();
|
|
13476
|
+
const sizeBeforeLoop = alreadyFiled.size;
|
|
13477
|
+
for (const f of result.preExistingFindings) {
|
|
13478
|
+
const dedupKey = f.issueKey ?? `${f.file ?? ""}:${f.line ?? 0}:${f.category}`;
|
|
13479
|
+
if (alreadyFiled.has(dedupKey)) continue;
|
|
13480
|
+
try {
|
|
13481
|
+
const { handleIssueCreate: handleIssueCreate2 } = await Promise.resolve().then(() => (init_issue2(), issue_exports));
|
|
13482
|
+
const severityMap = { critical: "critical", major: "high", minor: "medium" };
|
|
13483
|
+
const severity = severityMap[f.severity] ?? "medium";
|
|
13484
|
+
const issueResult = await handleIssueCreate2(
|
|
13485
|
+
{
|
|
13486
|
+
title: `[pre-existing] [${f.category}] ${f.description.slice(0, 60)}`,
|
|
13487
|
+
severity,
|
|
13488
|
+
impact: f.description,
|
|
13489
|
+
components: ["review-lenses"],
|
|
13490
|
+
relatedTickets: [],
|
|
13491
|
+
location: f.file && f.line != null ? [`${f.file}:${f.line}`] : []
|
|
13492
|
+
},
|
|
13493
|
+
"json",
|
|
13494
|
+
pinnedRoot
|
|
13495
|
+
);
|
|
13496
|
+
let issueId;
|
|
13497
|
+
try {
|
|
13498
|
+
const parsed = JSON.parse(issueResult.output ?? "");
|
|
13499
|
+
issueId = parsed?.data?.id;
|
|
13500
|
+
} catch {
|
|
13501
|
+
const match = issueResult.output?.match(/ISS-\d+/);
|
|
13502
|
+
issueId = match?.[0];
|
|
13503
|
+
}
|
|
13504
|
+
if (issueId) {
|
|
13505
|
+
filedIssues.push({ issueKey: dedupKey, issueId });
|
|
13506
|
+
alreadyFiled.add(dedupKey);
|
|
13507
|
+
}
|
|
13508
|
+
} catch {
|
|
13509
|
+
}
|
|
13510
|
+
}
|
|
13511
|
+
if (sessionDir2 && alreadyFiled.size > sizeBeforeLoop) {
|
|
13512
|
+
writeFiledPreexisting(sessionDir2, alreadyFiled);
|
|
13513
|
+
}
|
|
13514
|
+
}
|
|
13515
|
+
const output = { ...result, filedIssues };
|
|
13516
|
+
return { content: [{ type: "text", text: JSON.stringify(output, null, 2) }] };
|
|
12882
13517
|
} catch (err) {
|
|
12883
13518
|
const msg = err instanceof Error ? err.message.replace(/\/[^\s]+/g, "<path>") : "unknown error";
|
|
12884
13519
|
return { content: [{ type: "text", text: `Error synthesizing lens results: ${msg}` }], isError: true };
|
|
@@ -12919,16 +13554,33 @@ function registerAllTools(server, pinnedRoot) {
|
|
|
12919
13554
|
}
|
|
12920
13555
|
});
|
|
12921
13556
|
}
|
|
13557
|
+
var FILED_PREEXISTING_FILE = "filed-preexisting.json";
|
|
13558
|
+
function readFiledPreexisting(sessionDir2) {
|
|
13559
|
+
try {
|
|
13560
|
+
const raw = readFileSync12(join23(sessionDir2, FILED_PREEXISTING_FILE), "utf-8");
|
|
13561
|
+
const arr = JSON.parse(raw);
|
|
13562
|
+
return new Set(Array.isArray(arr) ? arr : []);
|
|
13563
|
+
} catch {
|
|
13564
|
+
return /* @__PURE__ */ new Set();
|
|
13565
|
+
}
|
|
13566
|
+
}
|
|
13567
|
+
function writeFiledPreexisting(sessionDir2, keys) {
|
|
13568
|
+
try {
|
|
13569
|
+
mkdirSync5(sessionDir2, { recursive: true });
|
|
13570
|
+
writeFileSync7(join23(sessionDir2, FILED_PREEXISTING_FILE), JSON.stringify([...keys], null, 2));
|
|
13571
|
+
} catch {
|
|
13572
|
+
}
|
|
13573
|
+
}
|
|
12922
13574
|
|
|
12923
13575
|
// src/core/init.ts
|
|
12924
13576
|
init_esm_shims();
|
|
12925
13577
|
init_project_loader();
|
|
12926
13578
|
init_errors();
|
|
12927
13579
|
import { mkdir as mkdir4, stat as stat2, readFile as readFile4, writeFile as writeFile2 } from "fs/promises";
|
|
12928
|
-
import { join as
|
|
13580
|
+
import { join as join24, resolve as resolve8 } from "path";
|
|
12929
13581
|
async function initProject(root, options) {
|
|
12930
13582
|
const absRoot = resolve8(root);
|
|
12931
|
-
const wrapDir =
|
|
13583
|
+
const wrapDir = join24(absRoot, ".story");
|
|
12932
13584
|
let exists = false;
|
|
12933
13585
|
try {
|
|
12934
13586
|
const s = await stat2(wrapDir);
|
|
@@ -12948,11 +13600,11 @@ async function initProject(root, options) {
|
|
|
12948
13600
|
".story/ already exists. Use --force to overwrite config and roadmap."
|
|
12949
13601
|
);
|
|
12950
13602
|
}
|
|
12951
|
-
await mkdir4(
|
|
12952
|
-
await mkdir4(
|
|
12953
|
-
await mkdir4(
|
|
12954
|
-
await mkdir4(
|
|
12955
|
-
await mkdir4(
|
|
13603
|
+
await mkdir4(join24(wrapDir, "tickets"), { recursive: true });
|
|
13604
|
+
await mkdir4(join24(wrapDir, "issues"), { recursive: true });
|
|
13605
|
+
await mkdir4(join24(wrapDir, "handovers"), { recursive: true });
|
|
13606
|
+
await mkdir4(join24(wrapDir, "notes"), { recursive: true });
|
|
13607
|
+
await mkdir4(join24(wrapDir, "lessons"), { recursive: true });
|
|
12956
13608
|
const created = [
|
|
12957
13609
|
".story/config.json",
|
|
12958
13610
|
".story/roadmap.json",
|
|
@@ -12992,7 +13644,7 @@ async function initProject(root, options) {
|
|
|
12992
13644
|
};
|
|
12993
13645
|
await writeConfig(config, absRoot);
|
|
12994
13646
|
await writeRoadmap(roadmap, absRoot);
|
|
12995
|
-
const gitignorePath =
|
|
13647
|
+
const gitignorePath = join24(wrapDir, ".gitignore");
|
|
12996
13648
|
await ensureGitignoreEntries(gitignorePath, STORY_GITIGNORE_ENTRIES);
|
|
12997
13649
|
const warnings = [];
|
|
12998
13650
|
if (options.force && exists) {
|
|
@@ -13028,10 +13680,393 @@ async function ensureGitignoreEntries(gitignorePath, entries) {
|
|
|
13028
13680
|
await writeFile2(gitignorePath, content, "utf-8");
|
|
13029
13681
|
}
|
|
13030
13682
|
|
|
13683
|
+
// src/channel/inbox-watcher.ts
|
|
13684
|
+
init_esm_shims();
|
|
13685
|
+
import { watch } from "fs";
|
|
13686
|
+
import { readdir as readdir4, readFile as readFile5, unlink as unlink3, rename as rename2, mkdir as mkdir5 } from "fs/promises";
|
|
13687
|
+
import { join as join25 } from "path";
|
|
13688
|
+
|
|
13689
|
+
// src/channel/events.ts
|
|
13690
|
+
init_esm_shims();
|
|
13691
|
+
import { z as z11 } from "zod";
|
|
13692
|
+
var TicketRequestedPayload = z11.object({
|
|
13693
|
+
ticketId: z11.string().regex(/^T-\d+[a-z]?$/)
|
|
13694
|
+
});
|
|
13695
|
+
var PauseSessionPayload = z11.object({});
|
|
13696
|
+
var ResumeSessionPayload = z11.object({});
|
|
13697
|
+
var CancelSessionPayload = z11.object({
|
|
13698
|
+
reason: z11.string().max(1e3).optional()
|
|
13699
|
+
});
|
|
13700
|
+
var PriorityChangedPayload = z11.object({
|
|
13701
|
+
ticketId: z11.string().regex(/^T-\d+[a-z]?$/),
|
|
13702
|
+
newOrder: z11.number().int()
|
|
13703
|
+
});
|
|
13704
|
+
var PermissionResponsePayload = z11.object({
|
|
13705
|
+
requestId: z11.string().regex(/^[a-zA-Z0-9]{5}$/),
|
|
13706
|
+
behavior: z11.enum(["approve", "deny"])
|
|
13707
|
+
});
|
|
13708
|
+
var ChannelEventSchema = z11.discriminatedUnion("event", [
|
|
13709
|
+
z11.object({
|
|
13710
|
+
event: z11.literal("ticket_requested"),
|
|
13711
|
+
timestamp: z11.string(),
|
|
13712
|
+
payload: TicketRequestedPayload
|
|
13713
|
+
}),
|
|
13714
|
+
z11.object({
|
|
13715
|
+
event: z11.literal("pause_session"),
|
|
13716
|
+
timestamp: z11.string(),
|
|
13717
|
+
payload: PauseSessionPayload
|
|
13718
|
+
}),
|
|
13719
|
+
z11.object({
|
|
13720
|
+
event: z11.literal("resume_session"),
|
|
13721
|
+
timestamp: z11.string(),
|
|
13722
|
+
payload: ResumeSessionPayload
|
|
13723
|
+
}),
|
|
13724
|
+
z11.object({
|
|
13725
|
+
event: z11.literal("cancel_session"),
|
|
13726
|
+
timestamp: z11.string(),
|
|
13727
|
+
payload: CancelSessionPayload
|
|
13728
|
+
}),
|
|
13729
|
+
z11.object({
|
|
13730
|
+
event: z11.literal("priority_changed"),
|
|
13731
|
+
timestamp: z11.string(),
|
|
13732
|
+
payload: PriorityChangedPayload
|
|
13733
|
+
}),
|
|
13734
|
+
z11.object({
|
|
13735
|
+
event: z11.literal("permission_response"),
|
|
13736
|
+
timestamp: z11.string(),
|
|
13737
|
+
payload: PermissionResponsePayload
|
|
13738
|
+
})
|
|
13739
|
+
]);
|
|
13740
|
+
var VALID_FILENAME = /^[\d]{4}-[\d]{2}-[\d]{2}T[\w.:-]+-[\w]+\.json$/;
|
|
13741
|
+
function isValidInboxFilename(filename) {
|
|
13742
|
+
if (filename.includes("/") || filename.includes("\\") || filename.includes("..")) {
|
|
13743
|
+
return false;
|
|
13744
|
+
}
|
|
13745
|
+
return VALID_FILENAME.test(filename);
|
|
13746
|
+
}
|
|
13747
|
+
function formatChannelContent(event) {
|
|
13748
|
+
switch (event.event) {
|
|
13749
|
+
case "ticket_requested":
|
|
13750
|
+
return `User requested ticket ${event.payload.ticketId} be started.`;
|
|
13751
|
+
case "pause_session":
|
|
13752
|
+
return "User requested the autonomous session be paused.";
|
|
13753
|
+
case "resume_session":
|
|
13754
|
+
return "User requested the autonomous session be resumed.";
|
|
13755
|
+
case "cancel_session": {
|
|
13756
|
+
const reason = event.payload.reason ? ` Reason: ${event.payload.reason}` : "";
|
|
13757
|
+
return `User requested the autonomous session be cancelled.${reason}`;
|
|
13758
|
+
}
|
|
13759
|
+
case "priority_changed":
|
|
13760
|
+
return `User changed priority of ticket ${event.payload.ticketId} to order ${event.payload.newOrder}.`;
|
|
13761
|
+
case "permission_response":
|
|
13762
|
+
return `Permission response for request ${event.payload.requestId}: ${event.payload.behavior}.`;
|
|
13763
|
+
}
|
|
13764
|
+
}
|
|
13765
|
+
function formatChannelMeta(event) {
|
|
13766
|
+
const meta = { event: event.event };
|
|
13767
|
+
switch (event.event) {
|
|
13768
|
+
case "ticket_requested":
|
|
13769
|
+
meta.ticketId = event.payload.ticketId;
|
|
13770
|
+
break;
|
|
13771
|
+
case "cancel_session":
|
|
13772
|
+
if (event.payload.reason) meta.reason = event.payload.reason;
|
|
13773
|
+
break;
|
|
13774
|
+
case "priority_changed":
|
|
13775
|
+
meta.ticketId = event.payload.ticketId;
|
|
13776
|
+
meta.newOrder = String(event.payload.newOrder);
|
|
13777
|
+
break;
|
|
13778
|
+
case "permission_response":
|
|
13779
|
+
meta.requestId = event.payload.requestId;
|
|
13780
|
+
meta.behavior = event.payload.behavior;
|
|
13781
|
+
break;
|
|
13782
|
+
}
|
|
13783
|
+
return meta;
|
|
13784
|
+
}
|
|
13785
|
+
|
|
13786
|
+
// src/channel/inbox-watcher.ts
|
|
13787
|
+
var INBOX_DIR = "channel-inbox";
|
|
13788
|
+
var FAILED_DIR = ".failed";
|
|
13789
|
+
var MAX_INBOX_DEPTH = 50;
|
|
13790
|
+
var MAX_FAILED_FILES = 20;
|
|
13791
|
+
var DEBOUNCE_MS = 100;
|
|
13792
|
+
var MAX_PERMISSION_RETRIES = 15;
|
|
13793
|
+
var MAX_EVENT_RETRIES = 30;
|
|
13794
|
+
var EVENT_EXPIRY_MS = 6e4;
|
|
13795
|
+
var watcher = null;
|
|
13796
|
+
var permissionRetryCount = /* @__PURE__ */ new Map();
|
|
13797
|
+
var eventRetryCount = /* @__PURE__ */ new Map();
|
|
13798
|
+
var debounceTimer = null;
|
|
13799
|
+
async function startInboxWatcher(root, server) {
|
|
13800
|
+
const inboxPath = join25(root, ".story", INBOX_DIR);
|
|
13801
|
+
if (watcher) {
|
|
13802
|
+
watcher.close();
|
|
13803
|
+
watcher = null;
|
|
13804
|
+
permissionRetryCount.clear();
|
|
13805
|
+
}
|
|
13806
|
+
await mkdir5(inboxPath, { recursive: true });
|
|
13807
|
+
await recoverStaleProcessingFiles(inboxPath);
|
|
13808
|
+
await processInbox(inboxPath, server);
|
|
13809
|
+
try {
|
|
13810
|
+
watcher = watch(inboxPath, (eventType) => {
|
|
13811
|
+
if (eventType === "rename") {
|
|
13812
|
+
scheduleDebouncedProcess(inboxPath, server);
|
|
13813
|
+
}
|
|
13814
|
+
});
|
|
13815
|
+
watcher.on("error", (err) => {
|
|
13816
|
+
process.stderr.write(`claudestory: channel inbox watcher error: ${err.message}
|
|
13817
|
+
`);
|
|
13818
|
+
startPollingFallback(inboxPath, server);
|
|
13819
|
+
});
|
|
13820
|
+
process.stderr.write(`claudestory: channel inbox watcher started at ${inboxPath}
|
|
13821
|
+
`);
|
|
13822
|
+
} catch (err) {
|
|
13823
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
13824
|
+
process.stderr.write(`claudestory: failed to start inbox watcher, using polling fallback: ${msg}
|
|
13825
|
+
`);
|
|
13826
|
+
startPollingFallback(inboxPath, server);
|
|
13827
|
+
}
|
|
13828
|
+
}
|
|
13829
|
+
function stopInboxWatcher() {
|
|
13830
|
+
if (debounceTimer) {
|
|
13831
|
+
clearTimeout(debounceTimer);
|
|
13832
|
+
debounceTimer = null;
|
|
13833
|
+
}
|
|
13834
|
+
if (watcher) {
|
|
13835
|
+
watcher.close();
|
|
13836
|
+
watcher = null;
|
|
13837
|
+
}
|
|
13838
|
+
if (pollInterval) {
|
|
13839
|
+
clearInterval(pollInterval);
|
|
13840
|
+
pollInterval = null;
|
|
13841
|
+
}
|
|
13842
|
+
permissionRetryCount.clear();
|
|
13843
|
+
}
|
|
13844
|
+
function scheduleDebouncedProcess(inboxPath, server) {
|
|
13845
|
+
if (debounceTimer) clearTimeout(debounceTimer);
|
|
13846
|
+
debounceTimer = setTimeout(() => {
|
|
13847
|
+
debounceTimer = null;
|
|
13848
|
+
processInbox(inboxPath, server).catch((err) => {
|
|
13849
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
13850
|
+
process.stderr.write(`claudestory: inbox processing error: ${msg}
|
|
13851
|
+
`);
|
|
13852
|
+
});
|
|
13853
|
+
}, DEBOUNCE_MS);
|
|
13854
|
+
}
|
|
13855
|
+
var pollInterval = null;
|
|
13856
|
+
function startPollingFallback(inboxPath, server) {
|
|
13857
|
+
if (pollInterval) return;
|
|
13858
|
+
pollInterval = setInterval(() => {
|
|
13859
|
+
processInbox(inboxPath, server).catch((err) => {
|
|
13860
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
13861
|
+
process.stderr.write(`claudestory: poll processing error: ${msg}
|
|
13862
|
+
`);
|
|
13863
|
+
});
|
|
13864
|
+
}, 2e3);
|
|
13865
|
+
}
|
|
13866
|
+
async function recoverStaleProcessingFiles(inboxPath) {
|
|
13867
|
+
let entries;
|
|
13868
|
+
try {
|
|
13869
|
+
entries = await readdir4(inboxPath);
|
|
13870
|
+
} catch {
|
|
13871
|
+
return;
|
|
13872
|
+
}
|
|
13873
|
+
for (const f of entries) {
|
|
13874
|
+
if (f.endsWith(".processing")) {
|
|
13875
|
+
const originalName = f.slice(0, -".processing".length);
|
|
13876
|
+
try {
|
|
13877
|
+
await rename2(join25(inboxPath, f), join25(inboxPath, originalName));
|
|
13878
|
+
process.stderr.write(`claudestory: recovered stale processing file: ${f}
|
|
13879
|
+
`);
|
|
13880
|
+
} catch {
|
|
13881
|
+
}
|
|
13882
|
+
}
|
|
13883
|
+
}
|
|
13884
|
+
}
|
|
13885
|
+
async function processInbox(inboxPath, server) {
|
|
13886
|
+
while (true) {
|
|
13887
|
+
let entries;
|
|
13888
|
+
try {
|
|
13889
|
+
entries = await readdir4(inboxPath);
|
|
13890
|
+
} catch {
|
|
13891
|
+
return;
|
|
13892
|
+
}
|
|
13893
|
+
const eventFiles = entries.filter((f) => f.endsWith(".json") && !f.startsWith(".")).sort();
|
|
13894
|
+
if (eventFiles.length === 0) break;
|
|
13895
|
+
const batch = eventFiles.slice(0, MAX_INBOX_DEPTH);
|
|
13896
|
+
if (eventFiles.length > MAX_INBOX_DEPTH) {
|
|
13897
|
+
process.stderr.write(
|
|
13898
|
+
`claudestory: channel inbox has ${eventFiles.length} files, processing batch of ${MAX_INBOX_DEPTH}
|
|
13899
|
+
`
|
|
13900
|
+
);
|
|
13901
|
+
}
|
|
13902
|
+
for (const filename of batch) {
|
|
13903
|
+
await processEventFile(inboxPath, filename, server);
|
|
13904
|
+
}
|
|
13905
|
+
if (eventFiles.length <= MAX_INBOX_DEPTH) break;
|
|
13906
|
+
}
|
|
13907
|
+
await trimFailedDirectory(inboxPath);
|
|
13908
|
+
}
|
|
13909
|
+
async function processEventFile(inboxPath, filename, server) {
|
|
13910
|
+
if (!isValidInboxFilename(filename)) {
|
|
13911
|
+
process.stderr.write(`claudestory: rejecting invalid inbox filename: ${filename}
|
|
13912
|
+
`);
|
|
13913
|
+
await moveToFailed(inboxPath, filename);
|
|
13914
|
+
return;
|
|
13915
|
+
}
|
|
13916
|
+
const filePath = join25(inboxPath, filename);
|
|
13917
|
+
const processingPath = join25(inboxPath, `${filename}.processing`);
|
|
13918
|
+
try {
|
|
13919
|
+
await rename2(filePath, processingPath);
|
|
13920
|
+
} catch {
|
|
13921
|
+
return;
|
|
13922
|
+
}
|
|
13923
|
+
let raw;
|
|
13924
|
+
try {
|
|
13925
|
+
raw = await readFile5(processingPath, "utf-8");
|
|
13926
|
+
} catch {
|
|
13927
|
+
await moveToFailed(inboxPath, `${filename}.processing`, filename);
|
|
13928
|
+
return;
|
|
13929
|
+
}
|
|
13930
|
+
const processingFilename = `${filename}.processing`;
|
|
13931
|
+
let parsed;
|
|
13932
|
+
try {
|
|
13933
|
+
parsed = JSON.parse(raw);
|
|
13934
|
+
} catch {
|
|
13935
|
+
process.stderr.write(`claudestory: invalid JSON in channel event ${filename}
|
|
13936
|
+
`);
|
|
13937
|
+
await moveToFailed(inboxPath, processingFilename, filename);
|
|
13938
|
+
return;
|
|
13939
|
+
}
|
|
13940
|
+
const result = ChannelEventSchema.safeParse(parsed);
|
|
13941
|
+
if (!result.success) {
|
|
13942
|
+
process.stderr.write(`claudestory: invalid channel event schema in ${filename}: ${result.error.message}
|
|
13943
|
+
`);
|
|
13944
|
+
await moveToFailed(inboxPath, processingFilename, filename);
|
|
13945
|
+
return;
|
|
13946
|
+
}
|
|
13947
|
+
const event = result.data;
|
|
13948
|
+
try {
|
|
13949
|
+
if (event.event === "permission_response") {
|
|
13950
|
+
await server.server.sendNotification({
|
|
13951
|
+
method: "notifications/claude/channel/permission",
|
|
13952
|
+
params: {
|
|
13953
|
+
requestId: event.payload.requestId,
|
|
13954
|
+
behavior: event.payload.behavior
|
|
13955
|
+
}
|
|
13956
|
+
});
|
|
13957
|
+
} else {
|
|
13958
|
+
const content = formatChannelContent(event);
|
|
13959
|
+
const meta = formatChannelMeta(event);
|
|
13960
|
+
await server.server.sendNotification({
|
|
13961
|
+
method: "notifications/claude/channel",
|
|
13962
|
+
params: { content, meta }
|
|
13963
|
+
});
|
|
13964
|
+
}
|
|
13965
|
+
process.stderr.write(`claudestory: sent channel event ${event.event}
|
|
13966
|
+
`);
|
|
13967
|
+
permissionRetryCount.delete(filename);
|
|
13968
|
+
eventRetryCount.delete(filename);
|
|
13969
|
+
} catch (err) {
|
|
13970
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
13971
|
+
if (event.event === "permission_response") {
|
|
13972
|
+
const retries2 = (permissionRetryCount.get(filename) ?? 0) + 1;
|
|
13973
|
+
permissionRetryCount.set(filename, retries2);
|
|
13974
|
+
if (retries2 >= MAX_PERMISSION_RETRIES) {
|
|
13975
|
+
process.stderr.write(`claudestory: permission notification failed after ${retries2} retries, quarantining: ${msg}
|
|
13976
|
+
`);
|
|
13977
|
+
permissionRetryCount.delete(filename);
|
|
13978
|
+
await moveToFailed(inboxPath, processingFilename, filename);
|
|
13979
|
+
return;
|
|
13980
|
+
}
|
|
13981
|
+
try {
|
|
13982
|
+
await rename2(processingPath, filePath);
|
|
13983
|
+
} catch (renameErr) {
|
|
13984
|
+
const renameMsg = renameErr instanceof Error ? renameErr.message : String(renameErr);
|
|
13985
|
+
process.stderr.write(`claudestory: rename-back failed for ${filename}, quarantining: ${renameMsg}
|
|
13986
|
+
`);
|
|
13987
|
+
permissionRetryCount.delete(filename);
|
|
13988
|
+
await moveToFailed(inboxPath, processingFilename, filename);
|
|
13989
|
+
return;
|
|
13990
|
+
}
|
|
13991
|
+
process.stderr.write(`claudestory: permission notification failed (attempt ${retries2}/${MAX_PERMISSION_RETRIES}), keeping for retry: ${msg}
|
|
13992
|
+
`);
|
|
13993
|
+
return;
|
|
13994
|
+
}
|
|
13995
|
+
const eventAge = Date.now() - new Date(event.timestamp).getTime();
|
|
13996
|
+
if (eventAge > EVENT_EXPIRY_MS) {
|
|
13997
|
+
process.stderr.write(`claudestory: channel event ${event.event} expired after ${Math.round(eventAge / 1e3)}s, quarantining: ${msg}
|
|
13998
|
+
`);
|
|
13999
|
+
eventRetryCount.delete(filename);
|
|
14000
|
+
await moveToFailed(inboxPath, processingFilename, filename);
|
|
14001
|
+
return;
|
|
14002
|
+
}
|
|
14003
|
+
const retries = (eventRetryCount.get(filename) ?? 0) + 1;
|
|
14004
|
+
eventRetryCount.set(filename, retries);
|
|
14005
|
+
if (retries >= MAX_EVENT_RETRIES) {
|
|
14006
|
+
process.stderr.write(`claudestory: channel event ${event.event} failed after ${retries} retries, quarantining: ${msg}
|
|
14007
|
+
`);
|
|
14008
|
+
eventRetryCount.delete(filename);
|
|
14009
|
+
await moveToFailed(inboxPath, processingFilename, filename);
|
|
14010
|
+
return;
|
|
14011
|
+
}
|
|
14012
|
+
try {
|
|
14013
|
+
await rename2(processingPath, filePath);
|
|
14014
|
+
} catch (renameErr) {
|
|
14015
|
+
const renameMsg = renameErr instanceof Error ? renameErr.message : String(renameErr);
|
|
14016
|
+
process.stderr.write(`claudestory: rename-back failed for ${filename}, quarantining: ${renameMsg}
|
|
14017
|
+
`);
|
|
14018
|
+
eventRetryCount.delete(filename);
|
|
14019
|
+
await moveToFailed(inboxPath, processingFilename, filename);
|
|
14020
|
+
return;
|
|
14021
|
+
}
|
|
14022
|
+
process.stderr.write(`claudestory: channel event ${event.event} failed (attempt ${retries}/${MAX_EVENT_RETRIES}), keeping for retry: ${msg}
|
|
14023
|
+
`);
|
|
14024
|
+
return;
|
|
14025
|
+
}
|
|
14026
|
+
try {
|
|
14027
|
+
await unlink3(processingPath);
|
|
14028
|
+
} catch {
|
|
14029
|
+
}
|
|
14030
|
+
}
|
|
14031
|
+
async function moveToFailed(inboxPath, sourceFilename, destFilename) {
|
|
14032
|
+
const failedDir = join25(inboxPath, FAILED_DIR);
|
|
14033
|
+
const targetName = destFilename ?? sourceFilename;
|
|
14034
|
+
try {
|
|
14035
|
+
await mkdir5(failedDir, { recursive: true });
|
|
14036
|
+
await rename2(join25(inboxPath, sourceFilename), join25(failedDir, targetName));
|
|
14037
|
+
} catch (err) {
|
|
14038
|
+
try {
|
|
14039
|
+
await unlink3(join25(inboxPath, sourceFilename));
|
|
14040
|
+
} catch {
|
|
14041
|
+
}
|
|
14042
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
14043
|
+
process.stderr.write(`claudestory: failed to move ${sourceFilename} to .failed/: ${msg}
|
|
14044
|
+
`);
|
|
14045
|
+
}
|
|
14046
|
+
}
|
|
14047
|
+
async function trimFailedDirectory(inboxPath) {
|
|
14048
|
+
const failedDir = join25(inboxPath, FAILED_DIR);
|
|
14049
|
+
let files;
|
|
14050
|
+
try {
|
|
14051
|
+
files = await readdir4(failedDir);
|
|
14052
|
+
} catch {
|
|
14053
|
+
return;
|
|
14054
|
+
}
|
|
14055
|
+
const sorted = files.filter((f) => f.endsWith(".json")).sort();
|
|
14056
|
+
if (sorted.length <= MAX_FAILED_FILES) return;
|
|
14057
|
+
const toDelete = sorted.slice(0, sorted.length - MAX_FAILED_FILES);
|
|
14058
|
+
for (const f of toDelete) {
|
|
14059
|
+
try {
|
|
14060
|
+
await unlink3(join25(failedDir, f));
|
|
14061
|
+
} catch {
|
|
14062
|
+
}
|
|
14063
|
+
}
|
|
14064
|
+
}
|
|
14065
|
+
|
|
13031
14066
|
// src/mcp/index.ts
|
|
13032
14067
|
var ENV_VAR2 = "CLAUDESTORY_PROJECT_ROOT";
|
|
13033
14068
|
var CONFIG_PATH2 = ".story/config.json";
|
|
13034
|
-
var version = "0.1.
|
|
14069
|
+
var version = "0.1.63";
|
|
13035
14070
|
function tryDiscoverRoot() {
|
|
13036
14071
|
const envRoot = process.env[ENV_VAR2];
|
|
13037
14072
|
if (envRoot) {
|
|
@@ -13043,7 +14078,7 @@ function tryDiscoverRoot() {
|
|
|
13043
14078
|
const resolved = resolve9(envRoot);
|
|
13044
14079
|
try {
|
|
13045
14080
|
const canonical = realpathSync3(resolved);
|
|
13046
|
-
if (
|
|
14081
|
+
if (existsSync14(join26(canonical, CONFIG_PATH2))) {
|
|
13047
14082
|
return canonical;
|
|
13048
14083
|
}
|
|
13049
14084
|
process.stderr.write(`Warning: No .story/config.json at ${canonical}
|
|
@@ -13071,9 +14106,9 @@ function registerDegradedTools(server) {
|
|
|
13071
14106
|
const degradedInit = server.registerTool("claudestory_init", {
|
|
13072
14107
|
description: "Initialize a new .story/ project in the current directory",
|
|
13073
14108
|
inputSchema: {
|
|
13074
|
-
name:
|
|
13075
|
-
type:
|
|
13076
|
-
language:
|
|
14109
|
+
name: z12.string().describe("Project name"),
|
|
14110
|
+
type: z12.string().optional().describe("Project type (e.g. npm, macapp, cargo, generic)"),
|
|
14111
|
+
language: z12.string().optional().describe("Primary language (e.g. typescript, swift, rust)")
|
|
13077
14112
|
}
|
|
13078
14113
|
}, async (args) => {
|
|
13079
14114
|
let result;
|
|
@@ -13103,6 +14138,12 @@ function registerDegradedTools(server) {
|
|
|
13103
14138
|
return { content: [{ type: "text", text: `Initialized .story/ project "${args.name}" at ${result.root}
|
|
13104
14139
|
|
|
13105
14140
|
Warning: tool registration failed. Restart the MCP server for full tool access.` }] };
|
|
14141
|
+
}
|
|
14142
|
+
try {
|
|
14143
|
+
await startInboxWatcher(result.root, server);
|
|
14144
|
+
} catch (watchErr) {
|
|
14145
|
+
process.stderr.write(`claudestory: inbox watcher failed after init: ${watchErr instanceof Error ? watchErr.message : String(watchErr)}
|
|
14146
|
+
`);
|
|
13106
14147
|
}
|
|
13107
14148
|
process.stderr.write(`claudestory: initialized at ${result.root}
|
|
13108
14149
|
`);
|
|
@@ -13122,17 +14163,32 @@ async function main() {
|
|
|
13122
14163
|
const server = new McpServer(
|
|
13123
14164
|
{ name: "claudestory", version },
|
|
13124
14165
|
{
|
|
13125
|
-
instructions: root ? "Start with claudestory_status for a project overview, then claudestory_ticket_next for the highest-priority work, then claudestory_handover_latest for session context." : "No .story/ project found. Use claudestory_init to initialize a new project, or navigate to a directory with .story/."
|
|
14166
|
+
instructions: root ? "Start with claudestory_status for a project overview, then claudestory_ticket_next for the highest-priority work, then claudestory_handover_latest for session context." : "No .story/ project found. Use claudestory_init to initialize a new project, or navigate to a directory with .story/.",
|
|
14167
|
+
capabilities: {
|
|
14168
|
+
experimental: {
|
|
14169
|
+
"claude/channel": {},
|
|
14170
|
+
"claude/channel/permission": {}
|
|
14171
|
+
}
|
|
14172
|
+
}
|
|
13126
14173
|
}
|
|
13127
14174
|
);
|
|
13128
14175
|
if (root) {
|
|
13129
14176
|
registerAllTools(server, root);
|
|
14177
|
+
await startInboxWatcher(root, server);
|
|
13130
14178
|
process.stderr.write(`claudestory MCP server running (root: ${root})
|
|
13131
14179
|
`);
|
|
13132
14180
|
} else {
|
|
13133
14181
|
registerDegradedTools(server);
|
|
13134
14182
|
process.stderr.write("claudestory MCP server running (no project \u2014 claudestory_init available)\n");
|
|
13135
14183
|
}
|
|
14184
|
+
process.on("SIGINT", () => {
|
|
14185
|
+
stopInboxWatcher();
|
|
14186
|
+
process.exit(0);
|
|
14187
|
+
});
|
|
14188
|
+
process.on("SIGTERM", () => {
|
|
14189
|
+
stopInboxWatcher();
|
|
14190
|
+
process.exit(0);
|
|
14191
|
+
});
|
|
13136
14192
|
const transport = new StdioServerTransport();
|
|
13137
14193
|
await server.connect(transport);
|
|
13138
14194
|
}
|