@pmelab/gtd 64.3.0 → 66.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude-plugin/plugin.json +1 -1
- package/README.md +35 -25
- package/claude/hooks/assist.ts +9 -35
- package/claude/hooks/doors.ts +4 -4
- package/claude/hooks/drive.ts +10 -1
- package/claude/hooks/git.ts +56 -0
- package/claude/hooks/handoff.ts +11 -11
- package/claude/hooks/jobs.ts +8 -8
- package/claude/hooks/pr.ts +86 -26
- package/claude/hooks/register.tsx +35 -82
- package/claude/hooks/tools.ts +7 -20
- package/claude/hooks/wording.ts +3 -3
- package/dist/chunks/{flows-dnIE_qbe.mjs → flows-CmQplvh1.mjs} +146 -35
- package/dist/chunks/{steering-DEh_MS40.mjs → steering-CMrePUiY.mjs} +115 -17
- package/dist/chunks/{testkit-BUX-47TD.mjs → testkit-BJhAAVGt.mjs} +905 -99
- package/dist/chunks/{wire-CaPJ9Tme.mjs → wire-xTbzFc26.mjs} +12 -6
- package/dist/flows.mjs +1 -1
- package/dist/gtd.bundle.mjs +965 -163
- package/dist/gtd.core.mjs +150 -134
- package/dist/steering.mjs +1 -1
- package/dist/testkit/gherkin.mjs +1 -1
- package/dist/testkit.mjs +2 -2
- package/dist/web/main.css +1 -1
- package/dist/web/main.js +185 -18
- package/dist/wire.mjs +1 -1
- package/dist/workflow-kit.mjs +9 -3
- package/package.json +1 -1
- package/src/cli/Cli.ts +5 -5
- package/src/cli/History.test.ts +10 -8
- package/src/cli/Metrics.test.ts +2 -1
- package/src/cli/Program.test.ts +4 -2
- package/src/cli/Program.ts +105 -7
- package/src/edge/Edge.test.ts +37 -16
- package/src/edge/Edge.ts +380 -33
- package/src/emit/GitScript.ts +116 -15
- package/src/emit/index.ts +7 -0
- package/src/flows/config.ts +7 -1
- package/src/flows/helpers.ts +36 -18
- package/src/flows/index.test.ts +4 -0
- package/src/flows/index.ts +7 -0
- package/src/flows/runtime.ts +153 -15
- package/src/install/Install.ts +9 -2
- package/src/platform/Git.test.ts +7 -11
- package/src/platform/NodeSync.ts +2 -0
- package/src/platform/RetainedHistory.ts +3 -0
- package/src/platform/SyncIo.ts +2 -0
- package/src/platform/Workspace.test.ts +37 -0
- package/src/platform/Workspace.ts +36 -0
- package/src/platform/index.ts +1 -0
- package/src/replay/Replay.test.ts +9 -0
- package/src/replay/Replay.ts +92 -16
- package/src/replay/Trailers.ts +17 -0
- package/src/replay/Tree.ts +5 -0
- package/src/replay/index.ts +1 -1
- package/src/steering/FORMATS.md +3 -1
- package/src/steering/SteeringFormat.ts +13 -0
- package/src/steering/fixtures/review/sectioned.md +1 -1
- package/src/steering/fixtures/review/sectioned.read.json +0 -1
- package/src/steering/index.ts +1 -0
- package/src/steering/review.test.ts +181 -5
- package/src/steering/review.ts +160 -18
- package/src/step/LandStep.ts +12 -0
- package/src/step/RepoSnapshot.ts +15 -0
- package/src/step/index.ts +1 -0
- package/src/step/planStart.test.ts +4 -3
- package/src/step/planStart.ts +7 -0
- package/src/step/planStep.ts +22 -17
- package/src/step/startBranch.ts +54 -0
- package/src/twins/EmittedScriptRecognizer.ts +118 -1
- package/src/twins/EmittedScripts.test.ts +147 -1
- package/src/twins/InMemRepo.ts +140 -3
- package/src/twins/Layers.ts +1 -0
- package/src/twins/SyncIo.test.ts +19 -4
- package/src/twins/SyncIo.ts +13 -1
- package/src/wire/BeatDocument.test.ts +15 -0
- package/src/wire/BeatDocument.ts +8 -2
- package/src/wire/Demand.ts +26 -12
- package/src/wire/constants.ts +3 -0
- package/src/wire/golden/1/beat-capture.json +1 -0
- package/src/wire/golden/1/beat-message.json +1 -0
- package/src/wire/golden/1/beat-prompt.json +1 -0
- package/src/wire/golden/1/beat-script.json +1 -0
- package/src/wire/golden/1/beat-stalled.json +1 -0
- package/src/wire/types.ts +4 -0
- package/src/workflow/Workflow.ts +3 -0
- package/src/workflow/compile.ts +2 -0
- package/src/workflow/load.ts +2 -0
- package/src/workflow-kit/index.test.ts +1 -0
- package/src/workflow-kit/index.ts +1 -0
- package/src/workflow-kit/scenarios.ts +8 -0
- package/claude/hooks/ship.ts +0 -730
|
@@ -15461,6 +15461,7 @@ const buildHunkFile = (content, item, paragraph, token, sourceLine) => {
|
|
|
15461
15461
|
...parsed.line !== void 0 ? { line: parsed.line } : {},
|
|
15462
15462
|
...parsed.rangeEnd !== void 0 ? { rangeEnd: parsed.rangeEnd } : {},
|
|
15463
15463
|
checked: item.checked === true,
|
|
15464
|
+
...item.checked === null ? { plain: true } : {},
|
|
15464
15465
|
sourceLine,
|
|
15465
15466
|
endLine: hunkOwnEndLine(item, sourceLine),
|
|
15466
15467
|
...note.length > 0 ? {
|
|
@@ -15567,6 +15568,35 @@ const answerListOf = (content, body) => {
|
|
|
15567
15568
|
})
|
|
15568
15569
|
};
|
|
15569
15570
|
};
|
|
15571
|
+
const COMMENT_LABEL_RE = /^(Impact|Fix):[ \t]*/;
|
|
15572
|
+
/** Splits a drafted comment at its `Impact:` / `Fix:` lines (outside fences) into labelled block sections; no labels is one What. */
|
|
15573
|
+
const commentSections = (comment) => {
|
|
15574
|
+
const parts = [{
|
|
15575
|
+
label: "What",
|
|
15576
|
+
lines: []
|
|
15577
|
+
}];
|
|
15578
|
+
let fence;
|
|
15579
|
+
for (const line of comment.split("\n")) {
|
|
15580
|
+
const marker = /^\s*(`{3,}|~{3,})/.exec(line)?.[1];
|
|
15581
|
+
if (marker !== void 0) {
|
|
15582
|
+
if (fence === void 0) fence = marker;
|
|
15583
|
+
else if (marker.startsWith(fence)) fence = void 0;
|
|
15584
|
+
}
|
|
15585
|
+
const label = fence === void 0 && marker === void 0 ? COMMENT_LABEL_RE.exec(line) : null;
|
|
15586
|
+
if (label) parts.push({
|
|
15587
|
+
label: label[1],
|
|
15588
|
+
lines: [line.slice(label[0].length)]
|
|
15589
|
+
});
|
|
15590
|
+
else parts.at(-1).lines.push(line);
|
|
15591
|
+
}
|
|
15592
|
+
return parts.map(({ label, lines }) => ({
|
|
15593
|
+
label,
|
|
15594
|
+
text: lines.join("\n").trim()
|
|
15595
|
+
})).filter(({ text }) => text.length > 0).map(({ label, text }) => ({
|
|
15596
|
+
label,
|
|
15597
|
+
body: blockNodesOfRun(text, parseMarkdown(text).children)
|
|
15598
|
+
}));
|
|
15599
|
+
};
|
|
15570
15600
|
/** A leading severity line is the chunk's tier, never its prose. */
|
|
15571
15601
|
const splitSeverity = (content, run) => {
|
|
15572
15602
|
const first = run[0];
|
|
@@ -15580,28 +15610,52 @@ const splitSeverity = (content, run) => {
|
|
|
15580
15610
|
leadingRun: run.slice(1)
|
|
15581
15611
|
};
|
|
15582
15612
|
};
|
|
15613
|
+
/** Every `listItem` under `node`, task or plain, at any depth. */
|
|
15614
|
+
const allListItems = (node) => {
|
|
15615
|
+
const items = [];
|
|
15616
|
+
const walk = (n) => {
|
|
15617
|
+
if (n.type === "listItem") items.push(n);
|
|
15618
|
+
n.children?.forEach((c) => walk(c));
|
|
15619
|
+
};
|
|
15620
|
+
walk(node);
|
|
15621
|
+
return items;
|
|
15622
|
+
};
|
|
15583
15623
|
/** Splits one chunk's body nodes into its file pointers (hunk pointers are task items, collected recursively at ANY nesting depth via `taskItems` — a nested hunk is the same kind of hunk as a top-level one) and description prose. The description is the chunk's own leading run and never a node that contains a hunk pointer: it stops at the first node whose `taskItems` include a real pointer (at any depth — a blockquote wrapping a pointer list stops it just as a top-level list would). Every node kind in what's left before that point contributes — a `footnoteDefinition` is the one exception, excluded by `blockNodesOfRun` itself (it's a note, never document content). */
|
|
15584
|
-
const parseChunkBody = (content, lines, title, body, answerable) => {
|
|
15624
|
+
const parseChunkBody = (content, lines, title, body, answerable, finding) => {
|
|
15585
15625
|
const answer = answerable ? answerListOf(content, body) : void 0;
|
|
15586
15626
|
const isAnswer = (n) => answer !== void 0 && (n === answer.list || n === body[answer.lead]);
|
|
15587
|
-
const
|
|
15627
|
+
const pointerItems = (n) => finding ? allListItems(n) : taskItems(n);
|
|
15628
|
+
const firstPointerIndex = body.findIndex((n) => !isAnswer(n) && pointerItems(n).some((item) => hasPointerToken(content, item)));
|
|
15588
15629
|
const run = (firstPointerIndex === -1 ? body : body.slice(0, firstPointerIndex)).filter((n) => !isAnswer(n));
|
|
15589
|
-
const
|
|
15630
|
+
const draftIndex = answer !== void 0 ? body.findIndex((n, i) => !isAnswer(n) && n.type === "blockquote" && body[i - 1]?.type === "paragraph" && /^draft pr comment:?$/i.test(sourceText(content, body[i - 1]).trim())) : -1;
|
|
15631
|
+
const comment = draftIndex === -1 ? void 0 : content.slice(body[draftIndex].position.start.offset, body[draftIndex].position.end.offset).split("\n").map((l) => l.replace(/^\s*> ?/, "")).join("\n").trim();
|
|
15632
|
+
const draftNodes = new Set(draftIndex === -1 ? [] : [body[draftIndex], body[draftIndex - 1]]);
|
|
15633
|
+
const { severity, leadingRun: fullRun } = splitSeverity(content, run);
|
|
15634
|
+
const leadingRun = fullRun.filter((n) => !draftNodes.has(n));
|
|
15590
15635
|
const descriptionNodes = blockNodesOfRun(content, leadingRun);
|
|
15591
15636
|
const descriptionInline = collapseImages(blockRunInline(content, leadingRun));
|
|
15592
|
-
const
|
|
15637
|
+
const textOf = (nodes) => nodes.filter((n) => n.type !== "footnoteDefinition").map((n) => sourceText(content, n)).join(" ").trim();
|
|
15638
|
+
const description = textOf(fullRun);
|
|
15639
|
+
const detail = textOf(leadingRun);
|
|
15593
15640
|
const files = [];
|
|
15594
15641
|
const errors = [];
|
|
15595
|
-
for (const item of body.filter((n) => !isAnswer(n)).flatMap((n) =>
|
|
15642
|
+
for (const item of body.filter((n) => !isAnswer(n)).flatMap((n) => pointerItems(n))) {
|
|
15596
15643
|
const parsed = parseHunk(content, lines, title, item);
|
|
15597
15644
|
if (!parsed) continue;
|
|
15645
|
+
if (finding && item.checked !== null) errors.push({
|
|
15646
|
+
message: `Chunk "${title}" finding location ${pointerDisplay(parsed.file)} must be a plain list item — drop the checkbox`,
|
|
15647
|
+
line: parsed.file.sourceLine,
|
|
15648
|
+
range: lineRange(lines, parsed.file.sourceLine)
|
|
15649
|
+
});
|
|
15598
15650
|
files.push(parsed.file);
|
|
15599
15651
|
errors.push(...parsed.errors);
|
|
15600
15652
|
}
|
|
15601
15653
|
return {
|
|
15602
15654
|
description,
|
|
15655
|
+
detail,
|
|
15603
15656
|
descriptionNodes,
|
|
15604
15657
|
descriptionInline,
|
|
15658
|
+
...comment === void 0 ? {} : { comment },
|
|
15605
15659
|
files,
|
|
15606
15660
|
errors,
|
|
15607
15661
|
...answer === void 0 ? {} : {
|
|
@@ -15675,7 +15729,7 @@ const parseChangesets = (tree, content, lines) => {
|
|
|
15675
15729
|
const errors = [];
|
|
15676
15730
|
for (const { heading, headingLine, body, section } of splitChunks(tree, content).chunks) {
|
|
15677
15731
|
const title = headingText$1(content, heading);
|
|
15678
|
-
const { description, descriptionNodes, descriptionInline, files, answers, answerLine, severity, boxed, errors: bodyErrors } = parseChunkBody(content, lines, title, body, section !== "walkthrough");
|
|
15732
|
+
const { description, detail, descriptionNodes, descriptionInline, comment, files, answers, answerLine, severity, boxed, errors: bodyErrors } = parseChunkBody(content, lines, title, body, section !== "walkthrough", section === "findings");
|
|
15679
15733
|
errors.push(...bodyErrors);
|
|
15680
15734
|
if (files.length === 0 && boxed) errors.push({
|
|
15681
15735
|
message: `Chunk "${title}" has no file pointers`,
|
|
@@ -15685,8 +15739,10 @@ const parseChangesets = (tree, content, lines) => {
|
|
|
15685
15739
|
changesets.push({
|
|
15686
15740
|
title,
|
|
15687
15741
|
description,
|
|
15742
|
+
detail,
|
|
15688
15743
|
descriptionNodes,
|
|
15689
15744
|
descriptionInline,
|
|
15745
|
+
...comment === void 0 ? {} : { comment },
|
|
15690
15746
|
files,
|
|
15691
15747
|
headingLine,
|
|
15692
15748
|
...answers === void 0 ? {} : {
|
|
@@ -15832,7 +15888,7 @@ const decisionNodes = (lines, chunk, end) => {
|
|
|
15832
15888
|
selectionRange: lineRange(lines, chunk.answerLine)
|
|
15833
15889
|
}];
|
|
15834
15890
|
};
|
|
15835
|
-
const isOpenChunk = (chunk, markers) => chunk.files.some((file) => !file.checked) || markers > 0 || chunk.answers !== void 0 && decisionOf(chunk) === void 0;
|
|
15891
|
+
const isOpenChunk = (chunk, markers) => chunk.files.some((file) => !file.plain && !file.checked) || markers > 0 || chunk.answers !== void 0 && decisionOf(chunk) === void 0;
|
|
15836
15892
|
/**
|
|
15837
15893
|
* Document outline for `.gtd/REVIEW.md`: the chunks still left to review — an
|
|
15838
15894
|
* unchecked hunk, a footnote (so a human's comment never vanishes with the
|
|
@@ -15863,7 +15919,7 @@ const reviewOutline = (content) => {
|
|
|
15863
15919
|
];
|
|
15864
15920
|
if (!(sections.length > 0 && chunk.section === void 0) && !isOpenChunk(chunk, chunkMarkers.length)) return [];
|
|
15865
15921
|
const node = {
|
|
15866
|
-
name: chunk.files.length === 0 ? chunk.title : `${chunk.title} (${checkedCount}/${chunk.files.length})`,
|
|
15922
|
+
name: chunk.files.length === 0 || chunk.section === "findings" ? chunk.title : `${chunk.title} (${checkedCount}/${chunk.files.length})`,
|
|
15867
15923
|
range: spanRange(lines, start, end),
|
|
15868
15924
|
selectionRange: lineRange(lines, start),
|
|
15869
15925
|
...children.length > 0 ? { children } : {}
|
|
@@ -15926,6 +15982,43 @@ const footnoteBlockEnd = (content, tree, cursorLine) => {
|
|
|
15926
15982
|
const block = blockNodeAt(tree, cursorLine);
|
|
15927
15983
|
return block?.position ? toLspPosition(block.position.end).line : cursorLine;
|
|
15928
15984
|
};
|
|
15985
|
+
const FINDING_ACTIONS = [
|
|
15986
|
+
{
|
|
15987
|
+
title: "gtd: accept finding",
|
|
15988
|
+
index: 0
|
|
15989
|
+
},
|
|
15990
|
+
{
|
|
15991
|
+
title: "gtd: reject finding",
|
|
15992
|
+
index: 1
|
|
15993
|
+
},
|
|
15994
|
+
{
|
|
15995
|
+
title: "gtd: answer finding",
|
|
15996
|
+
index: 2
|
|
15997
|
+
}
|
|
15998
|
+
];
|
|
15999
|
+
/** Accept / reject / answer for one finding chunk; the one matching its single current tick is left out. */
|
|
16000
|
+
const findingActions = (content, chunk, chunkIndex) => {
|
|
16001
|
+
if (chunk.answers?.length !== 3) return [];
|
|
16002
|
+
const ticked = chunk.answers.flatMap((o, i) => o.checked ? [i] : []);
|
|
16003
|
+
const current = ticked.length === 1 ? ticked[0] : void 0;
|
|
16004
|
+
const items = answerItemsOf(content, chunkIndex);
|
|
16005
|
+
return FINDING_ACTIONS.filter((a) => a.index !== current).flatMap((a) => {
|
|
16006
|
+
const result = decisionEdits(content, chunkIndex, a.index, {});
|
|
16007
|
+
if (!result.ok) return [];
|
|
16008
|
+
const slot = items[2];
|
|
16009
|
+
const from = slot ? firstParagraphContentOffset(slot) : void 0;
|
|
16010
|
+
const to = slot?.children.find((c) => c.type === "paragraph")?.position?.end.offset;
|
|
16011
|
+
const select = a.index === 2 && from !== void 0 && to !== void 0 ? {
|
|
16012
|
+
start: toLspPositionFromOffset(content, from),
|
|
16013
|
+
end: toLspPositionFromOffset(content, to)
|
|
16014
|
+
} : void 0;
|
|
16015
|
+
return [{
|
|
16016
|
+
title: a.title,
|
|
16017
|
+
edits: result.edits,
|
|
16018
|
+
...select ? { select } : {}
|
|
16019
|
+
}];
|
|
16020
|
+
});
|
|
16021
|
+
};
|
|
15929
16022
|
/**
|
|
15930
16023
|
* Actions for `.gtd/REVIEW.md`: "check/uncheck this hunk" when `range` sits
|
|
15931
16024
|
* on a hunk line, "check/uncheck all hunks" when `range` sits anywhere in a
|
|
@@ -15943,7 +16036,7 @@ const reviewActions = (content, range) => {
|
|
|
15943
16036
|
const actions = [...threadReplyActions(content, range.start)];
|
|
15944
16037
|
changesets.forEach((chunk, i) => {
|
|
15945
16038
|
const hunk = chunk.files.find((file) => cursorLine >= file.sourceLine && cursorLine <= file.endLine);
|
|
15946
|
-
if (hunk) {
|
|
16039
|
+
if (hunk && !hunk.plain) {
|
|
15947
16040
|
const edit = toggleFilePointer(content, hunk.sourceLine);
|
|
15948
16041
|
if (edit) actions.push({
|
|
15949
16042
|
title: hunk.checked ? "gtd: uncheck this hunk" : "gtd: check this hunk",
|
|
@@ -15951,7 +16044,8 @@ const reviewActions = (content, range) => {
|
|
|
15951
16044
|
});
|
|
15952
16045
|
}
|
|
15953
16046
|
const chunkEnd = chunk.section === void 0 ? Math.max(chunk.headingLine, (changesets[i + 1]?.headingLine ?? lines.length) - 1) : endLines.get(chunk.headingLine) ?? chunk.headingLine;
|
|
15954
|
-
if (chunk.
|
|
16047
|
+
if (chunk.section === "findings" && cursorLine >= chunk.headingLine && cursorLine <= chunkEnd) actions.push(...findingActions(content, chunk, i));
|
|
16048
|
+
if (chunk.section !== "findings" && chunk.files.length > 0 && cursorLine >= chunk.headingLine && cursorLine <= chunkEnd) {
|
|
15955
16049
|
const edits = toggleChunkEdits(content, chunk.headingLine);
|
|
15956
16050
|
if (edits.length > 0) {
|
|
15957
16051
|
const checkedCount = chunk.files.filter((file) => file.checked).length;
|
|
@@ -16018,9 +16112,12 @@ const hunkLinkFor = (content, item) => {
|
|
|
16018
16112
|
const reviewDocumentLinks = (content) => {
|
|
16019
16113
|
const tree = parseMarkdown(content);
|
|
16020
16114
|
const links = [];
|
|
16021
|
-
for (const block of splitChunks(tree, content).chunks)
|
|
16022
|
-
const
|
|
16023
|
-
|
|
16115
|
+
for (const block of splitChunks(tree, content).chunks) {
|
|
16116
|
+
const items = block.body.flatMap((n) => block.section === "findings" ? allListItems(n) : taskItems(n));
|
|
16117
|
+
for (const item of items) {
|
|
16118
|
+
const link = hunkLinkFor(content, item);
|
|
16119
|
+
if (link) links.push(link);
|
|
16120
|
+
}
|
|
16024
16121
|
}
|
|
16025
16122
|
return links;
|
|
16026
16123
|
};
|
|
@@ -16032,9 +16129,10 @@ const reviewView = (content) => {
|
|
|
16032
16129
|
...shortHash ? { header: shortHash } : {},
|
|
16033
16130
|
nodes: changesets.map((chunk, chunkIndex) => ({
|
|
16034
16131
|
title: chunk.title,
|
|
16035
|
-
detail: chunk.
|
|
16132
|
+
detail: chunk.detail,
|
|
16036
16133
|
detailInline: chunk.descriptionInline,
|
|
16037
16134
|
body: chunk.descriptionNodes,
|
|
16135
|
+
...chunk.comment === void 0 ? {} : { comment: commentSections(chunk.comment) },
|
|
16038
16136
|
...chunk.severity === void 0 ? {} : { status: chunk.severity.label },
|
|
16039
16137
|
anchor: {
|
|
16040
16138
|
kind: "chunk",
|
|
@@ -16059,7 +16157,7 @@ const reviewView = (content) => {
|
|
|
16059
16157
|
path: file.path,
|
|
16060
16158
|
...file.line !== void 0 ? { line: file.line } : {},
|
|
16061
16159
|
...file.rangeEnd !== void 0 ? { endLine: file.rangeEnd } : {},
|
|
16062
|
-
checked: file.checked,
|
|
16160
|
+
...file.plain ? {} : { checked: file.checked },
|
|
16063
16161
|
...file.note !== void 0 ? {
|
|
16064
16162
|
detail: file.note,
|
|
16065
16163
|
detailInline: file.noteInline ?? []
|
|
@@ -16214,7 +16312,7 @@ const reviewApply = (content, anchor, opts) => {
|
|
|
16214
16312
|
const checked = opts.checked ?? true;
|
|
16215
16313
|
if (anchor.kind === "hunk") {
|
|
16216
16314
|
const file = changesets[anchor.chunkIndex]?.files[anchor.index];
|
|
16217
|
-
if (!file) return {
|
|
16315
|
+
if (!file || file.plain) return {
|
|
16218
16316
|
ok: false,
|
|
16219
16317
|
reason: "anchor-not-found"
|
|
16220
16318
|
};
|
|
@@ -16225,7 +16323,7 @@ const reviewApply = (content, anchor, opts) => {
|
|
|
16225
16323
|
}
|
|
16226
16324
|
if (anchor.kind === "chunk") {
|
|
16227
16325
|
const chunk = changesets[anchor.index];
|
|
16228
|
-
if (!chunk) return {
|
|
16326
|
+
if (!chunk || chunk.section === "findings") return {
|
|
16229
16327
|
ok: false,
|
|
16230
16328
|
reason: "anchor-not-found"
|
|
16231
16329
|
};
|