@pmelab/gtd 27.7.0 → 27.8.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 +23 -13
- package/claude/hooks/assist.ts +35 -2
- package/claude/hooks/delegate.ts +22 -0
- package/claude/hooks/handoff.ts +13 -4
- package/claude/hooks/jobs.ts +67 -0
- package/claude/hooks/paste.ts +17 -0
- package/claude/hooks/register.tsx +126 -65
- package/claude/hooks/ship.ts +7 -2
- package/claude/hooks/tools.ts +32 -0
- package/claude/hooks/wording.ts +11 -0
- package/dist/chunks/{reserved-Df8GeSHw.mjs → reserved-UdSuHDUJ.mjs} +2 -2
- package/dist/chunks/{tail-DmIfEAI4.mjs → tail-fj60rQ3y.mjs} +1 -1
- package/dist/flows.mjs +16 -1
- package/dist/gtd.bundle.mjs +75 -2
- package/dist/testkit.mjs +60 -2
- package/dist/workflows/cleanup.mjs +2 -2
- package/dist/workflows/feature.mjs +3 -3
- package/dist/workflows/fix.mjs +2 -2
- package/dist/workflows/review.mjs +2 -2
- package/dist/workflows/rollout.mjs +1 -1
- package/dist/workflows.mjs +3 -3
- package/package.json +1 -1
- package/src/edge/Edge.test.ts +28 -0
- package/src/edge/Edge.ts +13 -0
- package/src/flows/runtime.test.ts +27 -0
- package/src/flows/runtime.ts +18 -0
- package/src/replay/Replay.test.ts +137 -0
- package/src/replay/Replay.ts +39 -1
- package/src/replay/Slices.test.ts +73 -0
- package/src/replay/Slices.ts +45 -0
- package/src/replay/index.ts +1 -0
- package/src/workflows/feature/review.test.ts +9 -3
- package/src/workflows/feature/review.ts +3 -3
- package/src/workflows/shared/steps.ts +1 -2
- package/src/workflows/shared/tail.test.ts +18 -9
package/claude/hooks/wording.ts
CHANGED
|
@@ -2,6 +2,9 @@
|
|
|
2
2
|
|
|
3
3
|
import type { Stop } from "../types"
|
|
4
4
|
import type { Beat } from "./drive"
|
|
5
|
+
import type { Shipped } from "./ship"
|
|
6
|
+
import { JOB_NAME } from "./tools"
|
|
7
|
+
import type { JobName } from "./tools"
|
|
5
8
|
|
|
6
9
|
const step = (s: { label?: string; state?: string }) => s.label ?? s.state ?? "the next step"
|
|
7
10
|
|
|
@@ -53,3 +56,11 @@ export function pushText(stop: Stop, repo: string, url?: string) {
|
|
|
53
56
|
if (stop.kind !== "gate") return at
|
|
54
57
|
return url ? `${at}. Open: ${url}` : at
|
|
55
58
|
}
|
|
59
|
+
|
|
60
|
+
const capitalised = (s: string) => s[0]!.toUpperCase() + s.slice(1)
|
|
61
|
+
|
|
62
|
+
export const jobHeadline = (job: JobName, outcome: Shipped) =>
|
|
63
|
+
`${outcome.ok ? "✔" : "✘"} ${capitalised(JOB_NAME[job])} ${outcome.ok ? "finished" : "failed"}`
|
|
64
|
+
|
|
65
|
+
export const jobPushText = (job: JobName, outcome: Shipped, repo: string) =>
|
|
66
|
+
`${repo}: ${JOB_NAME[job]} ${outcome.ok ? "finished" : "failed"}. ${outcome.text}`.trim()
|
|
@@ -1234,13 +1234,13 @@ const fixRisks = (notes) => agentWithSkills("review.fix.risks.fixing", buildRevi
|
|
|
1234
1234
|
system: reviewerSystem(),
|
|
1235
1235
|
allowEmpty: true
|
|
1236
1236
|
});
|
|
1237
|
-
const awaitReview = (base, message = buildReviewAwaitReviewMessage(base)
|
|
1237
|
+
const awaitReview = (base, message = buildReviewAwaitReviewMessage(base)) => human("review.await-review", {
|
|
1238
1238
|
message,
|
|
1239
1239
|
label: "Awaiting your review",
|
|
1240
1240
|
file: REVIEW,
|
|
1241
1241
|
mode: "review",
|
|
1242
1242
|
base,
|
|
1243
|
-
|
|
1243
|
+
acceptClean: true
|
|
1244
1244
|
});
|
|
1245
1245
|
/** The round at commit `round` left no `.gtd/REVIEW.md`. */
|
|
1246
1246
|
const reviewMissing = (round) => human("review.review-missing", {
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { read, refuse, removeScript, run, scope, showScript, vars } from "../flows.mjs";
|
|
2
2
|
import { fixFindings, lensesOf, qualityLap as qualityLap$1, reviewToHandOff } from "../workflow-kit.mjs";
|
|
3
|
-
import { C as fixRisks, D as reviewing, E as reviewQuality, G as handOffAwaitReviewMessage, S as fixQuality, T as reviewMissing, f as guarded, g as QUALITY, p as healthy, u as escalation, v as REVIEW, x as fixCheck, y as awaitReview } from "./reserved-
|
|
3
|
+
import { C as fixRisks, D as reviewing, E as reviewQuality, G as handOffAwaitReviewMessage, S as fixQuality, T as reviewMissing, f as guarded, g as QUALITY, p as healthy, u as escalation, v as REVIEW, x as fixCheck, y as awaitReview } from "./reserved-UdSuHDUJ.mjs";
|
|
4
4
|
//#region src/workflows/shared/tail.ts
|
|
5
5
|
/** The reviewer's own `.gtd/REVIEW.md`, kept beside the human's notes so `fix` can tell them apart. */
|
|
6
6
|
const REVIEW_ORIG = ".gtd/REVIEW.orig.md";
|
package/dist/flows.mjs
CHANGED
|
@@ -86,6 +86,20 @@ const scope = async (scope, fn) => {
|
|
|
86
86
|
context.popScope();
|
|
87
87
|
}
|
|
88
88
|
};
|
|
89
|
+
/**
|
|
90
|
+
* Mark the span `fn` covers as one named slice of the process. Lands no
|
|
91
|
+
* commit, prefixes no step name, opens no memory scope.
|
|
92
|
+
*/
|
|
93
|
+
const slice = async (name, fn) => {
|
|
94
|
+
const context = ctx();
|
|
95
|
+
if (typeof context.enterSlice !== "function") return fn();
|
|
96
|
+
context.enterSlice(name);
|
|
97
|
+
try {
|
|
98
|
+
return await fn();
|
|
99
|
+
} finally {
|
|
100
|
+
context.leaveSlice?.();
|
|
101
|
+
}
|
|
102
|
+
};
|
|
89
103
|
const read = (path) => ctx().read(path);
|
|
90
104
|
/** Every path in the tree matching `pattern` (`*` stays within a segment, `**` crosses them). */
|
|
91
105
|
const glob = (pattern) => ctx().glob(pattern);
|
|
@@ -652,10 +666,11 @@ var flows_exports = /* @__PURE__ */ __exportAll({
|
|
|
652
666
|
sections: () => sections,
|
|
653
667
|
showScript: () => showScript,
|
|
654
668
|
skillsFor: () => skillsFor,
|
|
669
|
+
slice: () => slice,
|
|
655
670
|
start: () => start,
|
|
656
671
|
threads: () => threads,
|
|
657
672
|
vars: () => vars,
|
|
658
673
|
wrote: () => wrote
|
|
659
674
|
});
|
|
660
675
|
//#endregion
|
|
661
|
-
export { FLOWS_PROTOCOL, FlowsProtocolError, accessFor, agent, answered, changes, changesSince, check, checkScript, codeChanges, codeThreads, configProblems, configure, defineConfig, defineWorkflow, displayPath, env, glob, hasThreadFor, head, human, installContext, isFactoryWorkflow, judge, judgeDiffShape, kept, mark, matches, moveScript, numeric, openQuestions, protocolMismatch, quote, read, refuse, removeScript, requireAnswers, requireProgress, requireReplies, requireRevert, requireThreadsClosed, restart, restoreScript, revertScript, run, scope, seamGate, sectionBodies, sections, showScript, skillsFor, start, flows_exports as t, threads, vars, wrote };
|
|
676
|
+
export { FLOWS_PROTOCOL, FlowsProtocolError, accessFor, agent, answered, changes, changesSince, check, checkScript, codeChanges, codeThreads, configProblems, configure, defineConfig, defineWorkflow, displayPath, env, glob, hasThreadFor, head, human, installContext, isFactoryWorkflow, judge, judgeDiffShape, kept, mark, matches, moveScript, numeric, openQuestions, protocolMismatch, quote, read, refuse, removeScript, requireAnswers, requireProgress, requireReplies, requireRevert, requireThreadsClosed, restart, restoreScript, revertScript, run, scope, seamGate, sectionBodies, sections, showScript, skillsFor, slice, start, flows_exports as t, threads, vars, wrote };
|
package/dist/gtd.bundle.mjs
CHANGED
|
@@ -21724,6 +21724,20 @@ const scope = async (scope, fn) => {
|
|
|
21724
21724
|
context.popScope();
|
|
21725
21725
|
}
|
|
21726
21726
|
};
|
|
21727
|
+
/**
|
|
21728
|
+
* Mark the span `fn` covers as one named slice of the process. Lands no
|
|
21729
|
+
* commit, prefixes no step name, opens no memory scope.
|
|
21730
|
+
*/
|
|
21731
|
+
const slice = async (name, fn) => {
|
|
21732
|
+
const context = ctx();
|
|
21733
|
+
if (typeof context.enterSlice !== "function") return fn();
|
|
21734
|
+
context.enterSlice(name);
|
|
21735
|
+
try {
|
|
21736
|
+
return await fn();
|
|
21737
|
+
} finally {
|
|
21738
|
+
context.leaveSlice?.();
|
|
21739
|
+
}
|
|
21740
|
+
};
|
|
21727
21741
|
const read = (path) => ctx().read(path);
|
|
21728
21742
|
/** Every path in the tree matching `pattern` (`*` stays within a segment, `**` crosses them). */
|
|
21729
21743
|
const glob = (pattern) => ctx().glob(pattern);
|
|
@@ -22290,6 +22304,7 @@ var flows_exports = /* @__PURE__ */ __exportAll({
|
|
|
22290
22304
|
sections: () => sections,
|
|
22291
22305
|
showScript: () => showScript,
|
|
22292
22306
|
skillsFor: () => skillsFor,
|
|
22307
|
+
slice: () => slice,
|
|
22293
22308
|
start: () => start,
|
|
22294
22309
|
threads: () => threads,
|
|
22295
22310
|
vars: () => vars,
|
|
@@ -37670,6 +37685,32 @@ const refNameFault = (name) => {
|
|
|
37670
37685
|
}
|
|
37671
37686
|
};
|
|
37672
37687
|
//#endregion
|
|
37688
|
+
//#region src/replay/Slices.ts
|
|
37689
|
+
const IMPLICIT_SLICE = "main";
|
|
37690
|
+
/** Same shape as mark()'s name rule; kept local so @pmelab/gtd/flows publishes no regex. */
|
|
37691
|
+
const SLICE_NAME = /^[a-z][a-z0-9-]*$/;
|
|
37692
|
+
/**
|
|
37693
|
+
* Folds boundaries into ranges: each kept span's base is the previous kept
|
|
37694
|
+
* span's `leftAt`, so a gap before a span belongs to it, and the last span
|
|
37695
|
+
* runs to the episode tip. A closed span that landed nothing is dropped.
|
|
37696
|
+
*/
|
|
37697
|
+
const sliceRanges = (spans, episode) => {
|
|
37698
|
+
if (spans.some((s) => s.enteredAt === "" || s.leftAt === "")) throw new Error("sliceRanges: a pending turn's \"\" hash is not a commit");
|
|
37699
|
+
const kept = spans.filter((s) => s.leftAt === void 0 || s.enteredAt !== s.leftAt);
|
|
37700
|
+
if (kept.length === 0) return [{
|
|
37701
|
+
name: IMPLICIT_SLICE,
|
|
37702
|
+
base: episode.base,
|
|
37703
|
+
tip: episode.tip,
|
|
37704
|
+
open: true
|
|
37705
|
+
}];
|
|
37706
|
+
return kept.map((s, i) => ({
|
|
37707
|
+
name: s.name,
|
|
37708
|
+
base: i === 0 ? episode.base : kept[i - 1].leftAt ?? episode.base,
|
|
37709
|
+
tip: i === kept.length - 1 ? episode.tip : s.leftAt ?? episode.tip,
|
|
37710
|
+
open: i === kept.length - 1
|
|
37711
|
+
}));
|
|
37712
|
+
};
|
|
37713
|
+
//#endregion
|
|
37673
37714
|
//#region src/replay/Tree.ts
|
|
37674
37715
|
const treeFromRecord = (files) => ({
|
|
37675
37716
|
paths: () => Object.keys(files).sort(),
|
|
@@ -38017,6 +38058,7 @@ const replay = async (input) => {
|
|
|
38017
38058
|
const keptTips = /* @__PURE__ */ new Map();
|
|
38018
38059
|
let landed;
|
|
38019
38060
|
const marks = [];
|
|
38061
|
+
const slices = [];
|
|
38020
38062
|
let outcome;
|
|
38021
38063
|
let signal;
|
|
38022
38064
|
const settled = new Promise((resolve) => {
|
|
@@ -38223,7 +38265,8 @@ const replay = async (input) => {
|
|
|
38223
38265
|
rest: step,
|
|
38224
38266
|
trace,
|
|
38225
38267
|
landed,
|
|
38226
|
-
marks
|
|
38268
|
+
marks,
|
|
38269
|
+
slices: slices.map((s) => ({ ...s }))
|
|
38227
38270
|
});
|
|
38228
38271
|
return landPending(step, input.pending);
|
|
38229
38272
|
};
|
|
@@ -38287,6 +38330,31 @@ const replay = async (input) => {
|
|
|
38287
38330
|
scopes.pop();
|
|
38288
38331
|
scopeCallIds.pop();
|
|
38289
38332
|
},
|
|
38333
|
+
enterSlice: (name) => {
|
|
38334
|
+
if (outcome !== void 0) return;
|
|
38335
|
+
const fail = (message) => {
|
|
38336
|
+
finish({
|
|
38337
|
+
kind: "failed",
|
|
38338
|
+
message
|
|
38339
|
+
});
|
|
38340
|
+
throw new Stop();
|
|
38341
|
+
};
|
|
38342
|
+
if (typeof name !== "string" || !SLICE_NAME.test(name)) fail(`gtd: slice "${name}": name must match ${SLICE_NAME}`);
|
|
38343
|
+
if (name === "main") fail(`gtd: slice "${IMPLICIT_SLICE}" is reserved for the implicit slice`);
|
|
38344
|
+
const open = slices.find((s) => s.leftAt === void 0);
|
|
38345
|
+
if (open !== void 0) fail(`gtd: slice "${name}" is entered inside slice "${open.name}" — slices do not nest`);
|
|
38346
|
+
if (slices.some((s) => s.name === name)) fail(`gtd: slice "${name}" is entered twice in this episode — a loop must sit inside its slice`);
|
|
38347
|
+
slices.push({
|
|
38348
|
+
name,
|
|
38349
|
+
enteredAt: position.hash,
|
|
38350
|
+
leftAt: void 0
|
|
38351
|
+
});
|
|
38352
|
+
},
|
|
38353
|
+
leaveSlice: () => {
|
|
38354
|
+
if (outcome !== void 0) return;
|
|
38355
|
+
const open = slices.find((s) => s.leftAt === void 0);
|
|
38356
|
+
if (open !== void 0) open.leftAt = position.hash;
|
|
38357
|
+
},
|
|
38290
38358
|
read: (path) => position.tree.read(path),
|
|
38291
38359
|
glob: (pattern) => position.tree.paths().filter((path) => globMatches(path, pattern)),
|
|
38292
38360
|
changes: () => changesBetween(previousPosition.tree, position.tree),
|
|
@@ -39976,6 +40044,10 @@ const pendingChanges = (git, withIndex) => gen(function* () {
|
|
|
39976
40044
|
const seen = new Set(worktree.map((e) => e.path));
|
|
39977
40045
|
return [...worktree, ...(yield* git.stagedPaths()).filter((e) => !seen.has(e.path))];
|
|
39978
40046
|
});
|
|
40047
|
+
const sliceRangesOf = (slices, run) => sliceRanges(slices, {
|
|
40048
|
+
base: run.startParentHash,
|
|
40049
|
+
tip: run.trace.at(-1)?.hash ?? run.startParentHash
|
|
40050
|
+
});
|
|
39979
40051
|
/** Resolve the rest at `ref`, or at HEAD when `ref` is `undefined`. */
|
|
39980
40052
|
const restAt = (ref, startOverrides = {}) => gen(function* () {
|
|
39981
40053
|
const git = yield* GitService;
|
|
@@ -40018,7 +40090,8 @@ const restAt = (ref, startOverrides = {}) => gen(function* () {
|
|
|
40018
40090
|
total: run.costEntries.reduce((sum, entry) => sum + entry.cost, 0),
|
|
40019
40091
|
byModel: costByModel(run.costEntries)
|
|
40020
40092
|
},
|
|
40021
|
-
setup
|
|
40093
|
+
setup,
|
|
40094
|
+
slices: sliceRangesOf(outcome.slices, run)
|
|
40022
40095
|
};
|
|
40023
40096
|
});
|
|
40024
40097
|
const currentRest = restAt(void 0);
|
package/dist/testkit.mjs
CHANGED
|
@@ -6817,6 +6817,32 @@ const refNameFault = (name) => {
|
|
|
6817
6817
|
}
|
|
6818
6818
|
};
|
|
6819
6819
|
//#endregion
|
|
6820
|
+
//#region src/replay/Slices.ts
|
|
6821
|
+
const IMPLICIT_SLICE = "main";
|
|
6822
|
+
/** Same shape as mark()'s name rule; kept local so @pmelab/gtd/flows publishes no regex. */
|
|
6823
|
+
const SLICE_NAME = /^[a-z][a-z0-9-]*$/;
|
|
6824
|
+
/**
|
|
6825
|
+
* Folds boundaries into ranges: each kept span's base is the previous kept
|
|
6826
|
+
* span's `leftAt`, so a gap before a span belongs to it, and the last span
|
|
6827
|
+
* runs to the episode tip. A closed span that landed nothing is dropped.
|
|
6828
|
+
*/
|
|
6829
|
+
const sliceRanges = (spans, episode) => {
|
|
6830
|
+
if (spans.some((s) => s.enteredAt === "" || s.leftAt === "")) throw new Error("sliceRanges: a pending turn's \"\" hash is not a commit");
|
|
6831
|
+
const kept = spans.filter((s) => s.leftAt === void 0 || s.enteredAt !== s.leftAt);
|
|
6832
|
+
if (kept.length === 0) return [{
|
|
6833
|
+
name: IMPLICIT_SLICE,
|
|
6834
|
+
base: episode.base,
|
|
6835
|
+
tip: episode.tip,
|
|
6836
|
+
open: true
|
|
6837
|
+
}];
|
|
6838
|
+
return kept.map((s, i) => ({
|
|
6839
|
+
name: s.name,
|
|
6840
|
+
base: i === 0 ? episode.base : kept[i - 1].leftAt ?? episode.base,
|
|
6841
|
+
tip: i === kept.length - 1 ? episode.tip : s.leftAt ?? episode.tip,
|
|
6842
|
+
open: i === kept.length - 1
|
|
6843
|
+
}));
|
|
6844
|
+
};
|
|
6845
|
+
//#endregion
|
|
6820
6846
|
//#region src/replay/Tree.ts
|
|
6821
6847
|
const treeFromRecord = (files) => ({
|
|
6822
6848
|
paths: () => Object.keys(files).sort(),
|
|
@@ -7164,6 +7190,7 @@ const replay = async (input) => {
|
|
|
7164
7190
|
const keptTips = /* @__PURE__ */ new Map();
|
|
7165
7191
|
let landed;
|
|
7166
7192
|
const marks = [];
|
|
7193
|
+
const slices = [];
|
|
7167
7194
|
let outcome;
|
|
7168
7195
|
let signal;
|
|
7169
7196
|
const settled = new Promise((resolve) => {
|
|
@@ -7370,7 +7397,8 @@ const replay = async (input) => {
|
|
|
7370
7397
|
rest: step,
|
|
7371
7398
|
trace,
|
|
7372
7399
|
landed,
|
|
7373
|
-
marks
|
|
7400
|
+
marks,
|
|
7401
|
+
slices: slices.map((s) => ({ ...s }))
|
|
7374
7402
|
});
|
|
7375
7403
|
return landPending(step, input.pending);
|
|
7376
7404
|
};
|
|
@@ -7434,6 +7462,31 @@ const replay = async (input) => {
|
|
|
7434
7462
|
scopes.pop();
|
|
7435
7463
|
scopeCallIds.pop();
|
|
7436
7464
|
},
|
|
7465
|
+
enterSlice: (name) => {
|
|
7466
|
+
if (outcome !== void 0) return;
|
|
7467
|
+
const fail = (message) => {
|
|
7468
|
+
finish({
|
|
7469
|
+
kind: "failed",
|
|
7470
|
+
message
|
|
7471
|
+
});
|
|
7472
|
+
throw new Stop();
|
|
7473
|
+
};
|
|
7474
|
+
if (typeof name !== "string" || !SLICE_NAME.test(name)) fail(`gtd: slice "${name}": name must match ${SLICE_NAME}`);
|
|
7475
|
+
if (name === "main") fail(`gtd: slice "${IMPLICIT_SLICE}" is reserved for the implicit slice`);
|
|
7476
|
+
const open = slices.find((s) => s.leftAt === void 0);
|
|
7477
|
+
if (open !== void 0) fail(`gtd: slice "${name}" is entered inside slice "${open.name}" — slices do not nest`);
|
|
7478
|
+
if (slices.some((s) => s.name === name)) fail(`gtd: slice "${name}" is entered twice in this episode — a loop must sit inside its slice`);
|
|
7479
|
+
slices.push({
|
|
7480
|
+
name,
|
|
7481
|
+
enteredAt: position.hash,
|
|
7482
|
+
leftAt: void 0
|
|
7483
|
+
});
|
|
7484
|
+
},
|
|
7485
|
+
leaveSlice: () => {
|
|
7486
|
+
if (outcome !== void 0) return;
|
|
7487
|
+
const open = slices.find((s) => s.leftAt === void 0);
|
|
7488
|
+
if (open !== void 0) open.leftAt = position.hash;
|
|
7489
|
+
},
|
|
7437
7490
|
read: (path) => position.tree.read(path),
|
|
7438
7491
|
glob: (pattern) => position.tree.paths().filter((path) => globMatches(path, pattern)),
|
|
7439
7492
|
changes: () => changesBetween(previousPosition.tree, position.tree),
|
|
@@ -9112,6 +9165,10 @@ const pendingChanges = (git, withIndex) => gen(function* () {
|
|
|
9112
9165
|
const seen = new Set(worktree.map((e) => e.path));
|
|
9113
9166
|
return [...worktree, ...(yield* git.stagedPaths()).filter((e) => !seen.has(e.path))];
|
|
9114
9167
|
});
|
|
9168
|
+
const sliceRangesOf = (slices, run) => sliceRanges(slices, {
|
|
9169
|
+
base: run.startParentHash,
|
|
9170
|
+
tip: run.trace.at(-1)?.hash ?? run.startParentHash
|
|
9171
|
+
});
|
|
9115
9172
|
/** Resolve the rest at `ref`, or at HEAD when `ref` is `undefined`. */
|
|
9116
9173
|
const restAt = (ref, startOverrides = {}) => gen(function* () {
|
|
9117
9174
|
const git = yield* GitService;
|
|
@@ -9154,7 +9211,8 @@ const restAt = (ref, startOverrides = {}) => gen(function* () {
|
|
|
9154
9211
|
total: run.costEntries.reduce((sum, entry) => sum + entry.cost, 0),
|
|
9155
9212
|
byModel: costByModel(run.costEntries)
|
|
9156
9213
|
},
|
|
9157
|
-
setup
|
|
9214
|
+
setup,
|
|
9215
|
+
slices: sliceRangesOf(outcome.slices, run)
|
|
9158
9216
|
};
|
|
9159
9217
|
});
|
|
9160
9218
|
const currentRest = restAt(void 0);
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { agent, changes, glob, read, refuse, scope, start, vars } from "../flows.mjs";
|
|
2
|
-
import { a as flagShipped, b as coder, c as envDefaults, i as skills, n as summary, r as access, s as defaults, t as steering } from "../chunks/reserved-
|
|
3
|
-
import { i as refusePendingHandOff, r as handOffTail } from "../chunks/tail-
|
|
2
|
+
import { a as flagShipped, b as coder, c as envDefaults, i as skills, n as summary, r as access, s as defaults, t as steering } from "../chunks/reserved-UdSuHDUJ.mjs";
|
|
3
|
+
import { i as refusePendingHandOff, r as handOffTail } from "../chunks/tail-fj60rQ3y.mjs";
|
|
4
4
|
import { i as requireFlag, r as registryEntry, t as flagReferences } from "../chunks/registry-CP3YuV9j.mjs";
|
|
5
5
|
//#region src/workflows/cleanup/cleanup.ts
|
|
6
6
|
const removingPrompt = (name, registry) => `Clean up the release flag "${name}", which is default-on: delete its entry in ${registry}, and remove every \`GTD_FLAG_${name.toUpperCase()}\` check from the shipped sources. Keep the flag-on path; delete code only the off path reached, including tests that pin the off path.`;
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { answered, changes, changesSince, glob, hasThreadFor, head, human, judge, judgeDiffShape, kept, mark, numeric, quote, read, refuse, removeScript, requireAnswers, requireProgress, requireReplies, requireRevert, requireThreadsClosed, restoreScript, revertScript, run, scope, seamGate, start, vars } from "../flows.mjs";
|
|
2
2
|
import { review as review$1 } from "../workflow-kit.mjs";
|
|
3
|
-
import { $ as refactorFixPrompt, A as architectureAuthorPrompt, B as designGateAnswerMessage, C as fixRisks, D as reviewing, F as buildReviewCollectingPrompt, H as designTriagePrompt, I as buildReviewFixNitsPrompt, J as implementFixPrompt, K as idleMessage, L as buildReviewTriageMessage, M as architectureGateAnswerMessage, N as buildReviewAnswerQuestionsPrompt, O as agentWithSkills, P as buildReviewAwaitAfterDeferMessage, Q as refactorBuildingPrompt, R as builderSystem, T as reviewMissing, V as designSystem, X as packagesItemBuildingPrompt, Y as naivePassPrompt, Z as packagesItemFixSuitePrompt, _ as REQUIREMENTS, at as seamByHandMessage, b as coder, c as envDefaults, ct as unwindFailedMessage, d as gate, et as reviewEditNotesCapture, h as FEEDBACK, i as skills, it as reviewerSystem, j as architectureDecomposePrompt, k as architectSystem, lt as unwindFailure, m as ARCHITECTURE, n as summary, o as shipped, ot as seamSteerMessage, p as healthy, q as implementBuildingPrompt, r as access, rt as reviewNotesCapture, s as defaults, st as startGateBlockedMessage, t as steering, tt as reviewEditsCapture, v as REVIEW, w as planner, x as fixCheck, y as awaitReview, z as deferNotesScript } from "../chunks/reserved-
|
|
4
|
-
import { a as tail, i as refusePendingHandOff, n as closeReview } from "../chunks/tail-
|
|
3
|
+
import { $ as refactorFixPrompt, A as architectureAuthorPrompt, B as designGateAnswerMessage, C as fixRisks, D as reviewing, F as buildReviewCollectingPrompt, H as designTriagePrompt, I as buildReviewFixNitsPrompt, J as implementFixPrompt, K as idleMessage, L as buildReviewTriageMessage, M as architectureGateAnswerMessage, N as buildReviewAnswerQuestionsPrompt, O as agentWithSkills, P as buildReviewAwaitAfterDeferMessage, Q as refactorBuildingPrompt, R as builderSystem, T as reviewMissing, V as designSystem, X as packagesItemBuildingPrompt, Y as naivePassPrompt, Z as packagesItemFixSuitePrompt, _ as REQUIREMENTS, at as seamByHandMessage, b as coder, c as envDefaults, ct as unwindFailedMessage, d as gate, et as reviewEditNotesCapture, h as FEEDBACK, i as skills, it as reviewerSystem, j as architectureDecomposePrompt, k as architectSystem, lt as unwindFailure, m as ARCHITECTURE, n as summary, o as shipped, ot as seamSteerMessage, p as healthy, q as implementBuildingPrompt, r as access, rt as reviewNotesCapture, s as defaults, st as startGateBlockedMessage, t as steering, tt as reviewEditsCapture, v as REVIEW, w as planner, x as fixCheck, y as awaitReview, z as deferNotesScript } from "../chunks/reserved-UdSuHDUJ.mjs";
|
|
4
|
+
import { a as tail, i as refusePendingHandOff, n as closeReview } from "../chunks/tail-fj60rQ3y.mjs";
|
|
5
5
|
//#region src/workflows/feature/tier.ts
|
|
6
6
|
/** GTD_FLAG_NAIVE_PASS, GTD_FLAG_RISK_TIER: both pinned at process start, so a mid-process flip never moves a process. */
|
|
7
7
|
const riskTierOn = () => vars.flag_naive_pass === "1" && vars.flag_risk_tier === "1";
|
|
@@ -355,7 +355,7 @@ const review = (base, escalations = { rounds: 0 }) => {
|
|
|
355
355
|
awaitReview: (b) => {
|
|
356
356
|
const notes = deferred;
|
|
357
357
|
deferred = [];
|
|
358
|
-
return notes.length > 0 ? awaitReview(b, buildReviewAwaitAfterDeferMessage(b, notes)
|
|
358
|
+
return notes.length > 0 ? awaitReview(b, buildReviewAwaitAfterDeferMessage(b, notes)) : awaitReview(b);
|
|
359
359
|
},
|
|
360
360
|
reviewMissing,
|
|
361
361
|
collecting,
|
package/dist/workflows/fix.mjs
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { read, refuse, removeScript, run, scope, start } from "../flows.mjs";
|
|
2
2
|
import { d as reviewNotes, f as reviewRisks } from "../chunks/steering-DVILanVf.mjs";
|
|
3
|
-
import { O as agentWithSkills, U as finisherSystem, W as fixReviewNotesPrompt, b as coder, c as envDefaults, i as skills, l as baseline, n as summary, o as shipped, r as access, s as defaults, t as steering, v as REVIEW } from "../chunks/reserved-
|
|
4
|
-
import { r as handOffTail, t as REVIEW_ORIG } from "../chunks/tail-
|
|
3
|
+
import { O as agentWithSkills, U as finisherSystem, W as fixReviewNotesPrompt, b as coder, c as envDefaults, i as skills, l as baseline, n as summary, o as shipped, r as access, s as defaults, t as steering, v as REVIEW } from "../chunks/reserved-UdSuHDUJ.mjs";
|
|
4
|
+
import { r as handOffTail, t as REVIEW_ORIG } from "../chunks/tail-fj60rQ3y.mjs";
|
|
5
5
|
//#region src/workflows/fix/fix.ts
|
|
6
6
|
/**
|
|
7
7
|
* Fix every note a human left in a handed-off `.gtd/REVIEW.md` and every
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { start } from "../flows.mjs";
|
|
2
|
-
import { c as envDefaults, d as gate, i as skills, n as summary, nt as reviewGateBlockedMessage, o as shipped, r as access, s as defaults, t as steering } from "../chunks/reserved-
|
|
3
|
-
import { i as refusePendingHandOff, r as handOffTail } from "../chunks/tail-
|
|
2
|
+
import { c as envDefaults, d as gate, i as skills, n as summary, nt as reviewGateBlockedMessage, o as shipped, r as access, s as defaults, t as steering } from "../chunks/reserved-UdSuHDUJ.mjs";
|
|
3
|
+
import { i as refusePendingHandOff, r as handOffTail } from "../chunks/tail-fj60rQ3y.mjs";
|
|
4
4
|
//#region src/workflows/review/review.ts
|
|
5
5
|
/**
|
|
6
6
|
* Pure review of everything since `reviewBase`: a green gate, the quality
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { agent, changesSince, head, read, refuse, scope, vars } from "../flows.mjs";
|
|
2
|
-
import { a as flagShipped, b as coder, c as envDefaults, d as gate, i as skills, n as summary, r as access, s as defaults, t as steering } from "../chunks/reserved-
|
|
2
|
+
import { a as flagShipped, b as coder, c as envDefaults, d as gate, i as skills, n as summary, r as access, s as defaults, t as steering } from "../chunks/reserved-UdSuHDUJ.mjs";
|
|
3
3
|
import { i as requireFlag, n as flipProblems } from "../chunks/registry-CP3YuV9j.mjs";
|
|
4
4
|
//#region src/workflows/rollout/rollout.ts
|
|
5
5
|
const flippingPrompt = (name, registry) => `Roll out the release flag "${name}": in ${registry}, set \`defaultOn: true\` on its entry. Change nothing else — no other entry, no other field, no other file, no comments.`;
|
package/dist/workflows.mjs
CHANGED
|
@@ -17995,13 +17995,13 @@ const fixRisks = (notes) => agentWithSkills("review.fix.risks.fixing", buildRevi
|
|
|
17995
17995
|
system: reviewerSystem(),
|
|
17996
17996
|
allowEmpty: true
|
|
17997
17997
|
});
|
|
17998
|
-
const awaitReview = (base, message = buildReviewAwaitReviewMessage(base)
|
|
17998
|
+
const awaitReview = (base, message = buildReviewAwaitReviewMessage(base)) => human("review.await-review", {
|
|
17999
17999
|
message,
|
|
18000
18000
|
label: "Awaiting your review",
|
|
18001
18001
|
file: REVIEW,
|
|
18002
18002
|
mode: "review",
|
|
18003
18003
|
base,
|
|
18004
|
-
|
|
18004
|
+
acceptClean: true
|
|
18005
18005
|
});
|
|
18006
18006
|
/** The round at commit `round` left no `.gtd/REVIEW.md`. */
|
|
18007
18007
|
const reviewMissing = (round) => human("review.review-missing", {
|
|
@@ -37911,7 +37911,7 @@ const review$1 = (base, escalations = { rounds: 0 }) => {
|
|
|
37911
37911
|
awaitReview: (b) => {
|
|
37912
37912
|
const notes = deferred;
|
|
37913
37913
|
deferred = [];
|
|
37914
|
-
return notes.length > 0 ? awaitReview(b, buildReviewAwaitAfterDeferMessage(b, notes)
|
|
37914
|
+
return notes.length > 0 ? awaitReview(b, buildReviewAwaitAfterDeferMessage(b, notes)) : awaitReview(b);
|
|
37915
37915
|
},
|
|
37916
37916
|
reviewMissing,
|
|
37917
37917
|
collecting,
|
package/package.json
CHANGED
package/src/edge/Edge.test.ts
CHANGED
|
@@ -596,6 +596,34 @@ export const summary = (it) => it.marks.map((m) => m.name + "=" + m.value).join(
|
|
|
596
596
|
})
|
|
597
597
|
})
|
|
598
598
|
|
|
599
|
+
describe("Rest.slices", () => {
|
|
600
|
+
const SLICED = `import { agent, human, slice } from "@pmelab/gtd/flows"
|
|
601
|
+
|
|
602
|
+
export default async () => {
|
|
603
|
+
await human("idle", { message: "idle-message" })
|
|
604
|
+
await slice("build", async () => {
|
|
605
|
+
await agent("building", "build-prompt")
|
|
606
|
+
})
|
|
607
|
+
await slice("verify", async () => {
|
|
608
|
+
await agent("checking", "check-prompt")
|
|
609
|
+
})
|
|
610
|
+
}
|
|
611
|
+
`
|
|
612
|
+
|
|
613
|
+
it("folds the replay's spans over the run's base and its last commit", async () => {
|
|
614
|
+
const repo = repoWith(SLICED)
|
|
615
|
+
const boundary = headOf(repo)
|
|
616
|
+
await land(repo, { "a.txt": "a\n" })
|
|
617
|
+
const checking = await land(repo, { "b.txt": "b\n" })
|
|
618
|
+
const rest = await provide(currentRest, repo)
|
|
619
|
+
expect(rest.state).toBe("checking")
|
|
620
|
+
expect(rest.slices).toEqual([
|
|
621
|
+
{ name: "build", base: boundary, tip: checking, open: false },
|
|
622
|
+
{ name: "verify", base: checking, tip: checking, open: true },
|
|
623
|
+
])
|
|
624
|
+
})
|
|
625
|
+
})
|
|
626
|
+
|
|
599
627
|
describe("restAt", () => {
|
|
600
628
|
it("restAt(ref) resolves where the process rested at that commit", async () => {
|
|
601
629
|
const repo = repoWith(LINEAR)
|
package/src/edge/Edge.ts
CHANGED
|
@@ -37,11 +37,13 @@ import {
|
|
|
37
37
|
isRecordableApproval,
|
|
38
38
|
parseCommitMessage,
|
|
39
39
|
replay,
|
|
40
|
+
sliceRanges,
|
|
40
41
|
treeFromRecord,
|
|
41
42
|
type EpisodeCommit,
|
|
42
43
|
type JudgeVerdict,
|
|
43
44
|
type ReachedStep,
|
|
44
45
|
type ReplayOutcome,
|
|
46
|
+
type SliceRange,
|
|
45
47
|
type TreeView,
|
|
46
48
|
} from "../replay/index.js"
|
|
47
49
|
import type { Mark, ScopeAccess } from "../flows/index.js"
|
|
@@ -895,6 +897,7 @@ interface Rest extends ResolvedRest {
|
|
|
895
897
|
/** Total recorded cost of the process, and its per-model breakdown. */
|
|
896
898
|
readonly cost: { readonly total: number; readonly byModel: readonly ModelCost[] }
|
|
897
899
|
readonly setup: ReplaySetup
|
|
900
|
+
readonly slices: readonly SliceRange[]
|
|
898
901
|
}
|
|
899
902
|
|
|
900
903
|
/** Per-model token totals, highest-cost first (ties broken by model name). */
|
|
@@ -987,6 +990,15 @@ const pendingChanges = (git: GitOperations, withIndex: boolean) =>
|
|
|
987
990
|
return [...worktree, ...(yield* git.stagedPaths()).filter((e) => !seen.has(e.path))]
|
|
988
991
|
})
|
|
989
992
|
|
|
993
|
+
const sliceRangesOf = (
|
|
994
|
+
slices: Parameters<typeof sliceRanges>[0],
|
|
995
|
+
run: { readonly startParentHash: string; readonly trace: readonly { readonly hash: string }[] },
|
|
996
|
+
) =>
|
|
997
|
+
sliceRanges(slices, {
|
|
998
|
+
base: run.startParentHash,
|
|
999
|
+
tip: run.trace.at(-1)?.hash ?? run.startParentHash,
|
|
1000
|
+
})
|
|
1001
|
+
|
|
990
1002
|
/** Resolve the rest at `ref`, or at HEAD when `ref` is `undefined`. */
|
|
991
1003
|
export const restAt = (
|
|
992
1004
|
ref: string | undefined,
|
|
@@ -1060,6 +1072,7 @@ export const restAt = (
|
|
|
1060
1072
|
byModel: costByModel(run.costEntries),
|
|
1061
1073
|
},
|
|
1062
1074
|
setup,
|
|
1075
|
+
slices: sliceRangesOf(outcome.slices, run),
|
|
1063
1076
|
}
|
|
1064
1077
|
})
|
|
1065
1078
|
|
|
@@ -7,6 +7,7 @@ import {
|
|
|
7
7
|
kept,
|
|
8
8
|
mark,
|
|
9
9
|
protocolMismatch,
|
|
10
|
+
slice,
|
|
10
11
|
type FlowContext,
|
|
11
12
|
} from "./runtime.js"
|
|
12
13
|
|
|
@@ -127,3 +128,29 @@ describe("kept()", () => {
|
|
|
127
128
|
expect(FLOWS_PROTOCOL).toBe(1)
|
|
128
129
|
})
|
|
129
130
|
})
|
|
131
|
+
|
|
132
|
+
describe("slice()", () => {
|
|
133
|
+
const withSlices = () => {
|
|
134
|
+
const calls: string[] = []
|
|
135
|
+
installContext({
|
|
136
|
+
enterSlice: (n: string) => calls.push(`enter ${n}`),
|
|
137
|
+
leaveSlice: () => calls.push("leave"),
|
|
138
|
+
} as unknown as FlowContext)
|
|
139
|
+
return calls
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
it("brackets fn with enter and leave and resolves to its value", async () => {
|
|
143
|
+
const calls = withSlices()
|
|
144
|
+
const value = await slice("refactor", async () => {
|
|
145
|
+
calls.push("fn")
|
|
146
|
+
return 42
|
|
147
|
+
})
|
|
148
|
+
expect(value).toBe(42)
|
|
149
|
+
expect(calls).toEqual(["enter refactor", "fn", "leave"])
|
|
150
|
+
})
|
|
151
|
+
|
|
152
|
+
it("runs fn unsliced on a context without enterSlice", async () => {
|
|
153
|
+
installContext(context)
|
|
154
|
+
expect(await slice("refactor", async () => "ran")).toBe("ran")
|
|
155
|
+
})
|
|
156
|
+
})
|
package/src/flows/runtime.ts
CHANGED
|
@@ -150,6 +150,9 @@ export interface FlowContext {
|
|
|
150
150
|
readonly refuse: (message: string) => never
|
|
151
151
|
readonly pushScope: (scope: ScopeOptions) => void
|
|
152
152
|
readonly popScope: () => void
|
|
153
|
+
/** Absent on an engine older than `slice`; FLOWS_PROTOCOL stays 1. */
|
|
154
|
+
readonly enterSlice?: (name: string) => void
|
|
155
|
+
readonly leaveSlice?: () => void
|
|
153
156
|
readonly read: (path: string) => string | undefined
|
|
154
157
|
readonly glob: (pattern: string) => readonly string[]
|
|
155
158
|
/** The last step's changes, content read on demand. */
|
|
@@ -284,6 +287,21 @@ export const scope = async <T>(scope: string | ScopeOptions, fn: () => Promise<T
|
|
|
284
287
|
}
|
|
285
288
|
}
|
|
286
289
|
|
|
290
|
+
/**
|
|
291
|
+
* Mark the span `fn` covers as one named slice of the process. Lands no
|
|
292
|
+
* commit, prefixes no step name, opens no memory scope.
|
|
293
|
+
*/
|
|
294
|
+
export const slice = async <T>(name: string, fn: () => Promise<T>): Promise<T> => {
|
|
295
|
+
const context = ctx()
|
|
296
|
+
if (typeof context.enterSlice !== "function") return fn()
|
|
297
|
+
context.enterSlice(name)
|
|
298
|
+
try {
|
|
299
|
+
return await fn()
|
|
300
|
+
} finally {
|
|
301
|
+
context.leaveSlice?.()
|
|
302
|
+
}
|
|
303
|
+
}
|
|
304
|
+
|
|
287
305
|
// ── Reads of the commit replay stands on ────────────────────────────────────
|
|
288
306
|
|
|
289
307
|
export const read = (path: string): string | undefined => ctx().read(path)
|