@pmelab/gtd 65.0.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 +3 -2
- package/claude/hooks/drive.ts +10 -1
- package/claude/hooks/git.ts +2 -2
- package/claude/hooks/pr.ts +80 -20
- package/claude/hooks/register.tsx +19 -4
- package/dist/chunks/{flows-DGs0W3HI.mjs → flows-CmQplvh1.mjs} +134 -40
- package/dist/chunks/{steering-DEh_MS40.mjs → steering-CMrePUiY.mjs} +115 -17
- package/dist/chunks/{testkit-CgrLmpdJ.mjs → testkit-BJhAAVGt.mjs} +179 -34
- package/dist/chunks/{wire-BNS8ciNu.mjs → wire-xTbzFc26.mjs} +7 -3
- package/dist/flows.mjs +1 -1
- package/dist/gtd.bundle.mjs +430 -100
- package/dist/gtd.core.mjs +147 -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 +1 -1
- package/src/cli/Program.ts +18 -2
- package/src/edge/Edge.ts +93 -11
- package/src/flows/helpers.ts +36 -18
- package/src/flows/index.test.ts +3 -0
- package/src/flows/index.ts +5 -0
- package/src/flows/runtime.ts +123 -16
- 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/SyncIo.ts +2 -0
- package/src/platform/Workspace.test.ts +37 -0
- package/src/platform/Workspace.ts +36 -0
- package/src/replay/Replay.ts +33 -1
- package/src/replay/Trailers.ts +3 -0
- package/src/replay/Tree.ts +5 -0
- 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/RepoSnapshot.ts +4 -0
- package/src/step/planStep.ts +6 -6
- package/src/twins/EmittedScriptRecognizer.ts +1 -1
- package/src/twins/InMemRepo.ts +24 -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 +4 -2
- 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/workflow/Workflow.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
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import { A as RUN_FAILED_PATH, N as renderFormat, O as ACCESS_REFUSAL, a as renderScriptJson, d as renderLandPlain, f as DEMAND_BRIEFING, g as statusOf, h as renderBeatJson, i as judgeJson, k as PIN_PATH, l as noopText, m as beatDocument, n as resolveSession, o as scriptFields, p as renderBeatPlain, r as selectPath, s as landFields, t as globMatches, u as renderLandJson, v as beatKindOf, y as demandOf } from "./wire-
|
|
3
|
-
import { F as headingSectionBodies, I as formatFinding, a as sections, c as openThreads, d as formatFromSpec, i as sectionBodies, l as steeringFormatFor, o as threads, r as openQuestions, s as BUILT_IN_MODE_NAMES, t as parseCodeThreads, u as unansweredQuestions } from "./steering-
|
|
1
|
+
import { A as revertScript, E as moveScript, G as installContext, M as writeScript$1, O as removeScript, Q as protocolMismatch, T as checkScript, Y as markedOptions, i as configure, j as showScript, k as restoreScript, n as configProblems, o as isFactoryWorkflow, p as numeric, s as answered, t as flows_exports, w as applyScript } from "./flows-CmQplvh1.mjs";
|
|
2
|
+
import { A as RUN_FAILED_PATH, N as renderFormat, O as ACCESS_REFUSAL, a as renderScriptJson, d as renderLandPlain, f as DEMAND_BRIEFING, g as statusOf, h as renderBeatJson, i as judgeJson, k as PIN_PATH, l as noopText, m as beatDocument, n as resolveSession, o as scriptFields, p as renderBeatPlain, r as selectPath, s as landFields, t as globMatches, u as renderLandJson, v as beatKindOf, y as demandOf } from "./wire-xTbzFc26.mjs";
|
|
3
|
+
import { F as headingSectionBodies, I as formatFinding, a as sections, c as openThreads, d as formatFromSpec, i as sectionBodies, l as steeringFormatFor, o as threads, r as openQuestions, s as BUILT_IN_MODE_NAMES, t as parseCodeThreads, u as unansweredQuestions } from "./steering-CMrePUiY.mjs";
|
|
4
4
|
import { createRequire, isBuiltin } from "node:module";
|
|
5
5
|
import * as Crypto from "node:crypto";
|
|
6
6
|
import { createHash } from "node:crypto";
|
|
@@ -20354,7 +20354,8 @@ const orAbsent = (read) => {
|
|
|
20354
20354
|
};
|
|
20355
20355
|
const infoOf = (stats) => ({
|
|
20356
20356
|
type: stats.isSymbolicLink() ? "symlink" : stats.isDirectory() ? "directory" : stats.isFile() ? "file" : "other",
|
|
20357
|
-
uid: process.platform === "win32" ? void 0 : stats.uid
|
|
20357
|
+
uid: process.platform === "win32" ? void 0 : stats.uid,
|
|
20358
|
+
...process.platform === "win32" ? {} : { executable: (stats.mode & 64) !== 0 }
|
|
20358
20359
|
});
|
|
20359
20360
|
const nodeSyncIo = {
|
|
20360
20361
|
readFile: (path) => orAbsent(() => readFileSync(path)),
|
|
@@ -20824,6 +20825,15 @@ const applyRawDiff = (tree, entries) => {
|
|
|
20824
20825
|
tree.set(entry.path, entry.newId);
|
|
20825
20826
|
}
|
|
20826
20827
|
};
|
|
20828
|
+
/** `git ls-tree -r -z --full-tree <ref>`'s output, parsed into `path → mode`. */
|
|
20829
|
+
const parseLsTreeModes = (out) => {
|
|
20830
|
+
const modes = /* @__PURE__ */ new Map();
|
|
20831
|
+
for (const record of out.split("\0")) {
|
|
20832
|
+
const tab = record.indexOf(" ");
|
|
20833
|
+
if (tab !== -1) modes.set(record.slice(tab + 1), record.slice(0, tab).split(" ")[0]);
|
|
20834
|
+
}
|
|
20835
|
+
return modes;
|
|
20836
|
+
};
|
|
20827
20837
|
/** `git ls-tree -r -z --full-tree <ref>`'s output, parsed into `path → blob id`. */
|
|
20828
20838
|
const parseLsTree = (out) => {
|
|
20829
20839
|
const entries = /* @__PURE__ */ new Map();
|
|
@@ -21056,6 +21066,34 @@ const makeWorkspaceOps = ({ root, pid }, git, commonDir, paths, io) => {
|
|
|
21056
21066
|
}
|
|
21057
21067
|
return new Map([...entries].sort(([a], [b]) => a < b ? -1 : a > b ? 1 : 0));
|
|
21058
21068
|
};
|
|
21069
|
+
const modeSync = (ref) => {
|
|
21070
|
+
const modes = parseLsTreeModes(gitSync([
|
|
21071
|
+
"ls-tree",
|
|
21072
|
+
"-r",
|
|
21073
|
+
"-z",
|
|
21074
|
+
"--full-tree",
|
|
21075
|
+
ref ?? "HEAD"
|
|
21076
|
+
]));
|
|
21077
|
+
if (ref !== void 0) return modes;
|
|
21078
|
+
const status = gitSync([
|
|
21079
|
+
"status",
|
|
21080
|
+
"--porcelain=v1",
|
|
21081
|
+
"-z",
|
|
21082
|
+
"--untracked-files=all",
|
|
21083
|
+
"--no-renames",
|
|
21084
|
+
"--ignore-submodules=none"
|
|
21085
|
+
]);
|
|
21086
|
+
for (const record of status.split("\0")) {
|
|
21087
|
+
if (record.length < 4) continue;
|
|
21088
|
+
const path = record.slice(3).replace(/\/$/, "");
|
|
21089
|
+
const stat = io.lstat(paths.join(root, path));
|
|
21090
|
+
if (stat === void 0) modes.delete(path);
|
|
21091
|
+
else if (stat.type === "symlink") modes.set(path, "120000");
|
|
21092
|
+
else if (stat.type === "directory") modes.set(path, "160000");
|
|
21093
|
+
else modes.set(path, stat.executable === true ? "100755" : "100644");
|
|
21094
|
+
}
|
|
21095
|
+
return modes;
|
|
21096
|
+
};
|
|
21059
21097
|
/** A submodule's (or embedded repository's) checked-out commit — what `git add` records for it. */
|
|
21060
21098
|
const gitlinkOf = (path) => {
|
|
21061
21099
|
try {
|
|
@@ -21073,6 +21111,7 @@ const makeWorkspaceOps = ({ root, pid }, git, commonDir, paths, io) => {
|
|
|
21073
21111
|
return {
|
|
21074
21112
|
treeSync,
|
|
21075
21113
|
worktreeSync,
|
|
21114
|
+
modeSync,
|
|
21076
21115
|
episodeTrees,
|
|
21077
21116
|
readSync,
|
|
21078
21117
|
read: (path) => try_({
|
|
@@ -22730,6 +22769,7 @@ while :; do
|
|
|
22730
22769
|
model="$(gtd next --json=model)"
|
|
22731
22770
|
system="$(gtd next --json=system)"
|
|
22732
22771
|
validate="$(gtd next --json=validate)"
|
|
22772
|
+
fresh="$(gtd next --json=fresh)"
|
|
22733
22773
|
log="$(gtd next --json=log)"
|
|
22734
22774
|
# \`model\` is a hint, not a model name — gtd-workflows' default ships
|
|
22735
22775
|
# \`smart\` and \`base\`, which \`claude --model\` rejects. Resolve them
|
|
@@ -22772,8 +22812,14 @@ while :; do
|
|
|
22772
22812
|
then first=--resume second=--session-id
|
|
22773
22813
|
else first=--session-id second=--resume
|
|
22774
22814
|
fi
|
|
22815
|
+
# A validator retry resumes with the correction alone; a session that
|
|
22816
|
+
# cannot resume starts over with \`fresh\`, the original prompt plus it.
|
|
22817
|
+
second_prompt="$log.prompt"
|
|
22818
|
+
if [ "$second" = --session-id ] && [ -n "\${fresh:-}" ]
|
|
22819
|
+
then printf '%s\\n' "$fresh" >"$log.fresh" && second_prompt="$log.fresh"
|
|
22820
|
+
fi
|
|
22775
22821
|
turn "$first" "$session_id" <"$log.prompt" ||
|
|
22776
|
-
{ [ "$ran" = false ] && turn "$second" "$session_id" <"$
|
|
22822
|
+
{ [ "$ran" = false ] && turn "$second" "$session_id" <"$second_prompt"; } || {
|
|
22777
22823
|
echo "agent turn failed — see $log" >&2
|
|
22778
22824
|
exit 1
|
|
22779
22825
|
}
|
|
@@ -22787,7 +22833,7 @@ while :; do
|
|
|
22787
22833
|
exit 1
|
|
22788
22834
|
}
|
|
22789
22835
|
done
|
|
22790
|
-
rm -f "$log.prompt" ;;
|
|
22836
|
+
rm -f "$log.prompt" "$log.fresh" ;;
|
|
22791
22837
|
esac
|
|
22792
22838
|
|
|
22793
22839
|
# Read every value the landing needs BEFORE piping the script to \`sh\`:
|
|
@@ -23379,6 +23425,9 @@ const treeFromRecord = (files) => ({
|
|
|
23379
23425
|
});
|
|
23380
23426
|
const diffTrees$1 = (before, after) => {
|
|
23381
23427
|
const same = (path) => {
|
|
23428
|
+
if (before.mode !== void 0 && after.mode !== void 0) {
|
|
23429
|
+
if (before.mode(path) !== after.mode(path)) return false;
|
|
23430
|
+
}
|
|
23382
23431
|
const a = before.id?.(path);
|
|
23383
23432
|
const b = after.id?.(path);
|
|
23384
23433
|
return a !== void 0 && b !== void 0 ? a === b : before.read(path) === after.read(path);
|
|
@@ -23618,7 +23667,8 @@ const formatCommitMessage = (spec) => {
|
|
|
23618
23667
|
...trailer("Anchor", spec.anchor),
|
|
23619
23668
|
...trailer("Branch", spec.branch),
|
|
23620
23669
|
...trailer("Version", spec.version),
|
|
23621
|
-
`Gtd-Format: 4
|
|
23670
|
+
`Gtd-Format: 4`,
|
|
23671
|
+
...trailer("Shipped", spec.shipped)
|
|
23622
23672
|
];
|
|
23623
23673
|
return `${formatSubject(spec.actor, spec.to, spec.from)}\n\n${lines.join("\n")}`;
|
|
23624
23674
|
};
|
|
@@ -23753,7 +23803,11 @@ const KNOWN_OPTIONS = {
|
|
|
23753
23803
|
"model",
|
|
23754
23804
|
"system",
|
|
23755
23805
|
"allowEmpty",
|
|
23756
|
-
"keep"
|
|
23806
|
+
"keep",
|
|
23807
|
+
"validate",
|
|
23808
|
+
"cap",
|
|
23809
|
+
"exhausted",
|
|
23810
|
+
"correction"
|
|
23757
23811
|
]),
|
|
23758
23812
|
human: /* @__PURE__ */ new Set([
|
|
23759
23813
|
...STEERING_OPTIONS,
|
|
@@ -23765,6 +23819,18 @@ const KNOWN_OPTIONS = {
|
|
|
23765
23819
|
ship: /* @__PURE__ */ new Set(["label", "base"]),
|
|
23766
23820
|
judge: /* @__PURE__ */ new Set([...STEERING_OPTIONS, "message"])
|
|
23767
23821
|
};
|
|
23822
|
+
const validatorOptionFault = (name, options) => {
|
|
23823
|
+
const { validate, cap, exhausted, correction } = options;
|
|
23824
|
+
const prefix = `gtd: step "${name}": `;
|
|
23825
|
+
const notFunction = Object.entries({
|
|
23826
|
+
validate,
|
|
23827
|
+
exhausted,
|
|
23828
|
+
correction
|
|
23829
|
+
}).find(([, value]) => value !== void 0 && typeof value !== "function");
|
|
23830
|
+
if (notFunction !== void 0) return `${prefix}${notFunction[0]} must be a function in agent() options`;
|
|
23831
|
+
if (cap !== void 0 && !(Number.isInteger(cap) && cap > 0)) return `${prefix}cap must be a positive integer in agent() options`;
|
|
23832
|
+
if (validate !== void 0 && options.keep === true) return `${prefix}validate cannot be combined with keep: true — a kept turn is set aside, so there is no landed result to validate`;
|
|
23833
|
+
};
|
|
23768
23834
|
const restsOnFail = (step) => step.request.kind === "run" && step.request.options.onFail === "rest";
|
|
23769
23835
|
const onFailFault = (step) => step.request.kind === "run" && step.request.options.onFail !== void 0 && step.request.options.onFail !== "rest" ? `gtd: step "${step.name}": onFail must be "rest" in run() options` : void 0;
|
|
23770
23836
|
const unknownOptions = (step) => {
|
|
@@ -24093,6 +24159,8 @@ const replay = async (raw) => {
|
|
|
24093
24159
|
if (scoped(request.name).startsWith("gtd:")) return `gtd: step "${scoped(request.name)}": the "${ENGINE_PREFIX}" prefix is reserved for gtd's own beats — rename the step`;
|
|
24094
24160
|
if (request.kind !== "agent") return bodyFault(request);
|
|
24095
24161
|
const name = scoped(request.name);
|
|
24162
|
+
const optionFault = validatorOptionFault(name, request.options);
|
|
24163
|
+
if (optionFault !== void 0) return optionFault;
|
|
24096
24164
|
if (request.options.keep === true) {
|
|
24097
24165
|
if (request.options.allowEmpty === true) return `gtd: step "${name}": keep: true cannot be combined with allowEmpty — a turn that changes nothing has nothing to keep`;
|
|
24098
24166
|
const refFault = name.includes("/") ? "it contains \"/\"" : refNameFault(name);
|
|
@@ -25871,11 +25939,10 @@ const validateScriptFor = (resolved, file) => gen(function* () {
|
|
|
25871
25939
|
});
|
|
25872
25940
|
// src/step/planStep.ts
|
|
25873
25941
|
/**
|
|
25874
|
-
*
|
|
25875
|
-
*
|
|
25876
|
-
* up — a tick is read-progress, never sign-off.
|
|
25942
|
+
* Clear ticks at the human review gate, a tick is read-progress. Keep them when
|
|
25943
|
+
* the next step is an agent turn: the agent reads them.
|
|
25877
25944
|
*/
|
|
25878
|
-
const uncheckStep = (snapshot) => snapshot.stepDef.actor === "human" && snapshot.stepDef.mode === "review" && snapshot.file !== void 0 ? [{
|
|
25945
|
+
const uncheckStep = (snapshot, keepTicks) => !keepTicks && snapshot.stepDef.actor === "human" && snapshot.stepDef.mode === "review" && snapshot.file !== void 0 ? [{
|
|
25879
25946
|
kind: "uncheck",
|
|
25880
25947
|
file: snapshot.file
|
|
25881
25948
|
}] : [];
|
|
@@ -25955,7 +26022,7 @@ const planStep = (snapshot, opts = {}) => {
|
|
|
25955
26022
|
...opts.judge !== void 0 && opts.judge.length > 0 ? { judge: opts.judge } : {}
|
|
25956
26023
|
});
|
|
25957
26024
|
const subject = message.split("\n")[0];
|
|
25958
|
-
const historyRef = landing.keepHistory ?? landing.ship?.historyRef;
|
|
26025
|
+
const historyRef = landing.keepHistory ?? landing.ship?.historyRef ?? landing.recordedHistory;
|
|
25959
26026
|
const outcome = landing.to === snapshot.state ? {
|
|
25960
26027
|
kind: "outcome",
|
|
25961
26028
|
outcome: {
|
|
@@ -25980,7 +26047,7 @@ const planStep = (snapshot, opts = {}) => {
|
|
|
25980
26047
|
kind: "switchBranch",
|
|
25981
26048
|
branch
|
|
25982
26049
|
})),
|
|
25983
|
-
...uncheckStep(snapshot),
|
|
26050
|
+
...uncheckStep(snapshot, landing.keepTicks === true),
|
|
25984
26051
|
{
|
|
25985
26052
|
kind: "gitWrite",
|
|
25986
26053
|
write: gitWriteOf(landing, message)
|
|
@@ -26534,6 +26601,7 @@ const promptDef = (common, request, skills, access) => ({
|
|
|
26534
26601
|
...optional("system", request.options.system),
|
|
26535
26602
|
...optional("allowEmpty", request.options.allowEmpty),
|
|
26536
26603
|
...optional("keep", request.options.keep),
|
|
26604
|
+
...optional("original", request.original),
|
|
26537
26605
|
...optional("skills", skills),
|
|
26538
26606
|
...optional("access", access)
|
|
26539
26607
|
});
|
|
@@ -26878,6 +26946,7 @@ const renderRest = (rest) => succeed$1({
|
|
|
26878
26946
|
...rest.shipBlocked !== void 0 ? { blocked: rest.shipBlocked } : {},
|
|
26879
26947
|
...rest.memory !== void 0 ? { memory: rest.memory } : {},
|
|
26880
26948
|
memoryResumed: rest.memoryResumed,
|
|
26949
|
+
...rest.stepDef.original !== void 0 ? { original: rest.stepDef.original } : {},
|
|
26881
26950
|
...rest.pin !== void 0 ? { pin: rest.pin } : {}
|
|
26882
26951
|
});
|
|
26883
26952
|
/**
|
|
@@ -26924,7 +26993,7 @@ const cleanLanding = (rest) => {
|
|
|
26924
26993
|
settled: false
|
|
26925
26994
|
};
|
|
26926
26995
|
};
|
|
26927
|
-
/** At the human review gate the pending tree lands with every tick cleared
|
|
26996
|
+
/** At the human review gate the pending tree lands with every tick cleared; an agent-bound landing keeps them only if the unrewritten tree replays to the same step. */
|
|
26928
26997
|
const reviewGateRewrite = (def) => {
|
|
26929
26998
|
const reviewFormat = steeringFormatFor("review");
|
|
26930
26999
|
if (!isHumanReviewGate(def) || def.file === void 0 || reviewFormat === void 0) return;
|
|
@@ -26933,6 +27002,15 @@ const reviewGateRewrite = (def) => {
|
|
|
26933
27002
|
apply: (content) => reviewFormat.clearTicks(content)
|
|
26934
27003
|
};
|
|
26935
27004
|
};
|
|
27005
|
+
/** Whether the pending review file differs from HEAD's once both have their ticks cleared. */
|
|
27006
|
+
const reviewEditedBeyondTicks = (rest) => {
|
|
27007
|
+
const rewrite = reviewGateRewrite(rest.stepDef);
|
|
27008
|
+
if (rewrite === void 0) return false;
|
|
27009
|
+
const pending = rest.setup.workspace.readSync(rewrite.path);
|
|
27010
|
+
const committed = rest.setup.workspace.readCommittedSync(rewrite.path);
|
|
27011
|
+
if (pending === void 0 || committed === void 0) return true;
|
|
27012
|
+
return rewrite.apply(pending) !== rewrite.apply(committed);
|
|
27013
|
+
};
|
|
26936
27014
|
const multilineRefusal = (vars) => {
|
|
26937
27015
|
const multiline = multilineSetting(vars);
|
|
26938
27016
|
return multiline === void 0 ? void 0 : {
|
|
@@ -27076,7 +27154,17 @@ const decideLanding = (rest, verdicts, branch, approver, shipped) => gen(functio
|
|
|
27076
27154
|
}));
|
|
27077
27155
|
const to = yield* landingTarget(outcome, rest);
|
|
27078
27156
|
if (typeof to !== "string") return to;
|
|
27079
|
-
|
|
27157
|
+
const landed = landingTo(rest, to, branch, approver, outcomeMarks(outcome));
|
|
27158
|
+
if (landed.kind !== "commit" || !isHumanReviewGate(rest.stepDef) || outcome.kind !== "rest" || outcome.rest.request.kind !== "agent" || !reviewEditedBeyondTicks(rest)) return landed;
|
|
27159
|
+
const ticked = yield* promise(() => replayFor(rest.setup, {
|
|
27160
|
+
tree: pendingTree(rest.setup.workspace, rest.setup.pendingWorktree(), void 0),
|
|
27161
|
+
...verdicts !== void 0 ? { verdicts } : {},
|
|
27162
|
+
...shipped !== void 0 ? { shipped } : {}
|
|
27163
|
+
}));
|
|
27164
|
+
return ticked.kind === "rest" && ticked.rest.name === outcome.rest.name ? {
|
|
27165
|
+
...landed,
|
|
27166
|
+
keepTicks: true
|
|
27167
|
+
} : landed;
|
|
27080
27168
|
});
|
|
27081
27169
|
/** The user's branch: `<b>` for a process on `gtd/<b>` while `<b>` still exists; any other branch as is. */
|
|
27082
27170
|
const userBranch = (git) => gen(function* () {
|
|
@@ -27119,7 +27207,7 @@ const withHistory = (landing) => gen(function* () {
|
|
|
27119
27207
|
...out,
|
|
27120
27208
|
leaveBranch: back
|
|
27121
27209
|
};
|
|
27122
|
-
if (landing.keepHistory === void 0 && landing.ship === void 0) return out;
|
|
27210
|
+
if (landing.keepHistory === void 0 && landing.ship === void 0 && landing.recordedHistory === void 0) return out;
|
|
27123
27211
|
const remote = (yield* (yield* ConfigService).load).historyRemote;
|
|
27124
27212
|
return remote === void 0 ? out : {
|
|
27125
27213
|
...out,
|
|
@@ -27142,6 +27230,14 @@ const refusedSnapshot = (rest, message) => snapshotOf(rest, {
|
|
|
27142
27230
|
});
|
|
27143
27231
|
const SHIPPED_STAND_IN = "0".repeat(40);
|
|
27144
27232
|
const SHIP_EXCLUDED = ".gtd/";
|
|
27233
|
+
/** `tree` with file modes read from `ref` (the working tree when `undefined`), for the ship decision only: reading them costs a git call. */
|
|
27234
|
+
const withModes = (workspace, tree, ref) => {
|
|
27235
|
+
let modes;
|
|
27236
|
+
return {
|
|
27237
|
+
...tree,
|
|
27238
|
+
mode: (path) => (modes ??= workspace.modeSync(ref)).get(path)
|
|
27239
|
+
};
|
|
27240
|
+
};
|
|
27145
27241
|
const shippedPaths = (before, after) => {
|
|
27146
27242
|
const diff = diffTrees$1(before, after);
|
|
27147
27243
|
return [
|
|
@@ -27174,14 +27270,15 @@ const shipTarget = (git) => gen(function* () {
|
|
|
27174
27270
|
* revert. A path the working tree changed since HEAD counts as brought in:
|
|
27175
27271
|
* whether it carries `<b>`'s change is the person's call, not a merge gtd redoes.
|
|
27176
27272
|
*/
|
|
27177
|
-
const revertedPaths = (git, workspace, parent, tree, changed) => gen(function* () {
|
|
27273
|
+
const revertedPaths = (git, workspace, parent, tree, changed, treeRef) => gen(function* () {
|
|
27178
27274
|
const head = yield* git.resolveRef("HEAD");
|
|
27179
27275
|
const shipped = parseCommitMessage(yield* git.lastCommitMessage(parent)).tip;
|
|
27180
27276
|
if (shipped !== void 0 && (yield* git.isAncestor(shipped, head))) return [];
|
|
27181
27277
|
const fork = yield* git.mergeBase(parent, head);
|
|
27182
|
-
const foreign = isNone(fork) ? [...changed] : shippedPaths(commitTree(workspace, fork.value), commitTree(workspace, parent));
|
|
27183
|
-
const committed = commitTree(workspace, head);
|
|
27184
|
-
|
|
27278
|
+
const foreign = isNone(fork) ? [...changed] : shippedPaths(withModes(workspace, commitTree(workspace, fork.value), fork.value), withModes(workspace, commitTree(workspace, parent), parent));
|
|
27279
|
+
const committed = withModes(workspace, commitTree(workspace, head), head);
|
|
27280
|
+
const moded = withModes(workspace, tree, treeRef);
|
|
27281
|
+
return foreign.filter((path) => changed.has(path) && moded.read(path) === committed.read(path) && moded.mode(path) === committed.mode(path));
|
|
27185
27282
|
});
|
|
27186
27283
|
/**
|
|
27187
27284
|
* Whether landing a ship step with `tree` would ship, and onto what. The squash
|
|
@@ -27189,13 +27286,18 @@ const revertedPaths = (git, workspace, parent, tree, changed) => gen(function* (
|
|
|
27189
27286
|
* branch carries a change `tree` would revert: one made since the last ship
|
|
27190
27287
|
* (whose `Gtd-Tip` HEAD descends from), or since it forked from HEAD.
|
|
27191
27288
|
*/
|
|
27192
|
-
const decideShip = (workspace, tree) => gen(function* () {
|
|
27289
|
+
const decideShip = (workspace, tree, treeRef) => gen(function* () {
|
|
27193
27290
|
const git = yield* GitService;
|
|
27194
27291
|
const target = yield* shipTarget(git);
|
|
27195
27292
|
if (target.kind !== "ship") return target;
|
|
27196
27293
|
const { branch, parent } = target;
|
|
27197
|
-
const changed = new Set(shippedPaths(commitTree(workspace, parent), tree));
|
|
27198
|
-
const
|
|
27294
|
+
const changed = new Set(shippedPaths(withModes(workspace, commitTree(workspace, parent), parent), withModes(workspace, tree, treeRef)));
|
|
27295
|
+
const pending = parseCommitMessage(yield* git.lastCommitMessage(parent)).tip;
|
|
27296
|
+
if (changed.size === 0 && pending !== void 0 && pending === (yield* git.resolveRef("HEAD"))) return {
|
|
27297
|
+
kind: "recorded",
|
|
27298
|
+
hash: parent
|
|
27299
|
+
};
|
|
27300
|
+
const reverted = yield* revertedPaths(git, workspace, parent, tree, changed, treeRef);
|
|
27199
27301
|
if (reverted.length > 0) return {
|
|
27200
27302
|
kind: "blocked",
|
|
27201
27303
|
message: `gtd: ${branch} changed outside this process, and shipping would revert ${reverted.join(", ")} — bring those changes into the working tree (\`git merge --squash --no-commit ${branch}\`, or by hand) and land`
|
|
@@ -27208,16 +27310,27 @@ const decideShip = (workspace, tree) => gen(function* () {
|
|
|
27208
27310
|
const shipBlockedAt = (setup, pin, step, changes) => gen(function* () {
|
|
27209
27311
|
if (pin !== void 0 || step.request.kind !== "ship" || changes.length > 0) return void 0;
|
|
27210
27312
|
const head = yield* (yield* GitService).resolveRef("HEAD");
|
|
27211
|
-
const decision = yield* decideShip(setup.workspace, commitTree(setup.workspace, head));
|
|
27313
|
+
const decision = yield* decideShip(setup.workspace, commitTree(setup.workspace, head), head);
|
|
27212
27314
|
return decision.kind === "blocked" ? decision.message : void 0;
|
|
27213
27315
|
});
|
|
27214
27316
|
const shipSnapshot = (rest, message, verdicts, branch, approver) => gen(function* () {
|
|
27215
27317
|
const tree = pendingTree(rest.setup.workspace, rest.setup.pendingWorktree(), void 0);
|
|
27216
|
-
const decision = yield* decideShip(rest.setup.workspace, tree);
|
|
27318
|
+
const decision = yield* decideShip(rest.setup.workspace, tree, void 0);
|
|
27217
27319
|
if (decision.kind === "blocked") return refusedSnapshot(rest, decision.message);
|
|
27218
27320
|
const shipping = decision.kind === "ship";
|
|
27219
|
-
const decided = yield* decideLanding(rest, verdicts, branch, approver, shipping ? SHIPPED_STAND_IN : void 0);
|
|
27321
|
+
const decided = yield* decideLanding(rest, verdicts, branch, approver, decision.kind === "recorded" ? decision.hash : shipping ? SHIPPED_STAND_IN : void 0);
|
|
27220
27322
|
if (decided.kind !== "commit") return snapshotOf(rest, decided);
|
|
27323
|
+
if (decision.kind === "recorded") {
|
|
27324
|
+
const recorded = {
|
|
27325
|
+
...decided,
|
|
27326
|
+
spec: {
|
|
27327
|
+
...decided.spec,
|
|
27328
|
+
shipped: decision.hash
|
|
27329
|
+
},
|
|
27330
|
+
recordedHistory: processHistoryRef(rest.run.startHash)
|
|
27331
|
+
};
|
|
27332
|
+
return snapshotOf(rest, yield* withBranchAndPush(recorded));
|
|
27333
|
+
}
|
|
27221
27334
|
const landing = shipping ? {
|
|
27222
27335
|
...decided,
|
|
27223
27336
|
ship: {
|
|
@@ -28498,7 +28611,8 @@ const runRestoreCommand = (json, out) => gen(function* () {
|
|
|
28498
28611
|
if (isNone(retained)) return yield* fail$4(/* @__PURE__ */ new Error("gtd restore: no retained history to restore."));
|
|
28499
28612
|
const tip = retained.value;
|
|
28500
28613
|
const headHash = yield* git.resolveRef("HEAD");
|
|
28501
|
-
const
|
|
28614
|
+
const shippedTip = parseCommitMessage(yield* git.lastCommitMessage(headHash)).tip;
|
|
28615
|
+
const check = shippedTip !== void 0 && (yield* git.isAncestor(shippedTip, tip)) ? { ok: true } : yield* restorability(git, headHash, tip);
|
|
28502
28616
|
if (!check.ok) return yield* fail$4(/* @__PURE__ */ new Error(`gtd restore: ${check.reason} — HEAD ${headHash.slice(0, 7)} is ahead of the retained tip ${tip.slice(0, 7)}.`));
|
|
28503
28617
|
const after = yield* restAt(tip);
|
|
28504
28618
|
const resumeOn = yield* resumeBranch(git, tip);
|
|
@@ -28734,6 +28848,18 @@ const computeStatusChanges = (changes) => changes.map((change) => ({
|
|
|
28734
28848
|
}));
|
|
28735
28849
|
/** The step the pending change would land the process at. */
|
|
28736
28850
|
const computeNextMatch = (rest) => map(rest.changes.length === 0 ? succeed$1(void 0) : previewLanding(rest), (target) => target === void 0 ? null : { target });
|
|
28851
|
+
/** A validator retry's content is the correction; a session that cannot resume needs the original prompt too, and one that can hands a driver that loses it `fresh`. */
|
|
28852
|
+
const retryContent = (kind, rendered, resumes) => {
|
|
28853
|
+
if (kind !== "prompt" || rendered.original === void 0) return { rendered };
|
|
28854
|
+
const joined = `${rendered.original}\n\n${rendered.content}`;
|
|
28855
|
+
return resumes ? {
|
|
28856
|
+
rendered,
|
|
28857
|
+
fresh: joined
|
|
28858
|
+
} : { rendered: {
|
|
28859
|
+
...rendered,
|
|
28860
|
+
content: joined
|
|
28861
|
+
} };
|
|
28862
|
+
};
|
|
28737
28863
|
/** Everything one beat needs beyond the resolved rest itself, gathered once so plain/`--json` can never describe different rests for the same beat — built as `wire`'s `Demand` (what to do) and `BeatStatus` (what no driver branches on), then flattened by `beatDocument` into the one wire-shaped object plain/`--json` both render from. */
|
|
28738
28864
|
const gatherBeatDocument = (rest, rendered) => gen(function* () {
|
|
28739
28865
|
const kind = restBeatKind(rest);
|
|
@@ -28741,9 +28867,11 @@ const gatherBeatDocument = (rest, rendered) => gen(function* () {
|
|
|
28741
28867
|
const resolvedValidate = kind === "prompt" ? yield* resolveValidateScript(rest).pipe(catch_(() => succeed$1(void 0))) : void 0;
|
|
28742
28868
|
const validate = resolvedValidate !== void 0 && resolvedValidate.script.length > 0 ? resolvedValidate.script : void 0;
|
|
28743
28869
|
const log = yield* loopLogPath;
|
|
28870
|
+
const retry = retryContent(kind, rendered, session?.resume === true);
|
|
28744
28871
|
const demand = demandOf({
|
|
28745
|
-
rendered,
|
|
28872
|
+
rendered: retry.rendered,
|
|
28746
28873
|
kind,
|
|
28874
|
+
...retry.fresh !== void 0 ? { fresh: retry.fresh } : {},
|
|
28747
28875
|
...session !== void 0 ? { session: {
|
|
28748
28876
|
id: session.sessionId,
|
|
28749
28877
|
resume: session.resume
|
|
@@ -29411,7 +29539,7 @@ The fixed answerer reads --answers and leaves out the questions it does not cove
|
|
|
29411
29539
|
arity: { name: "file" },
|
|
29412
29540
|
group: "Driver plumbing",
|
|
29413
29541
|
summary: "Untick a review file's boxes",
|
|
29414
|
-
details: `Reset every ticked review box (- [x]) in <file> to - [ ], writing the file only when it changes; a missing file writes nothing. Reads no workflow and runs from any directory — the landing script at a review gate calls it
|
|
29542
|
+
details: `Reset every ticked review box (- [x]) in <file> to - [ ], writing the file only when it changes; a missing file writes nothing. Reads no workflow and runs from any directory — the landing script at a review gate calls it unless the next step is an agent turn, which keeps the ticks.`
|
|
29415
29543
|
},
|
|
29416
29544
|
{
|
|
29417
29545
|
token: "lsp",
|
|
@@ -29961,6 +30089,9 @@ function makeHash(message, parent, tree) {
|
|
|
29961
30089
|
const treeStr = [...tree.entries()].sort(([a], [b]) => a.localeCompare(b)).map(([k, v]) => `${k}:${v}`).join("\n");
|
|
29962
30090
|
return sha1(`${message}\n${parent ?? "null"}\n${treeStr}`);
|
|
29963
30091
|
}
|
|
30092
|
+
/** An executable file's content carries this prefix inside the twin, so every tree copy keeps the mode for free; only the read edges strip it. */
|
|
30093
|
+
const EXEC = "\0exec\0";
|
|
30094
|
+
const plain = (content) => content.startsWith(EXEC) ? content.slice(6) : content;
|
|
29964
30095
|
var InMemRepo = class InMemRepo {
|
|
29965
30096
|
testDouble = TEST_DOUBLE_SENTINEL;
|
|
29966
30097
|
commits = /* @__PURE__ */ new Map();
|
|
@@ -30219,7 +30350,8 @@ var InMemRepo = class InMemRepo {
|
|
|
30219
30350
|
if (!hash) return null;
|
|
30220
30351
|
const c = this.getCommit(hash);
|
|
30221
30352
|
if (!c) return null;
|
|
30222
|
-
|
|
30353
|
+
const content = c.files.get(path);
|
|
30354
|
+
return content === void 0 ? null : plain(content);
|
|
30223
30355
|
}
|
|
30224
30356
|
/**
|
|
30225
30357
|
* `base` (default HEAD) mirrors the port's own optional base. Tree-vs-worktree
|
|
@@ -30243,7 +30375,18 @@ var InMemRepo = class InMemRepo {
|
|
|
30243
30375
|
}
|
|
30244
30376
|
/** The worktree content of `path`, or `undefined` when absent. A LIVE read — never a snapshot. */
|
|
30245
30377
|
readFile(path) {
|
|
30246
|
-
|
|
30378
|
+
const content = this.worktree.get(path);
|
|
30379
|
+
return content === void 0 ? void 0 : plain(content);
|
|
30380
|
+
}
|
|
30381
|
+
/** `chmod +x` / `-x` on a working-tree file. */
|
|
30382
|
+
setExecutable(path, on) {
|
|
30383
|
+
const content = this.readFile(path);
|
|
30384
|
+
if (content !== void 0) this.worktree.set(path, on ? EXEC + content : content);
|
|
30385
|
+
}
|
|
30386
|
+
/** Every path's file mode at `ref`, or in the working tree when `ref` is `undefined`. */
|
|
30387
|
+
modesAt(ref) {
|
|
30388
|
+
const tree = ref === void 0 ? this.worktree : this.treeAt(ref);
|
|
30389
|
+
return new Map([...tree].map(([path, c]) => [path, c.startsWith(EXEC) ? "100755" : "100644"]));
|
|
30247
30390
|
}
|
|
30248
30391
|
/** True when `path` is exactly a worktree file, or a directory prefix of one (any worktree key starts with `path/`). */
|
|
30249
30392
|
hasPath(path) {
|
|
@@ -30451,7 +30594,8 @@ var InMemRepo = class InMemRepo {
|
|
|
30451
30594
|
this.worktree = newWorktree;
|
|
30452
30595
|
}
|
|
30453
30596
|
writeFile(path, content) {
|
|
30454
|
-
this.worktree.
|
|
30597
|
+
const was = this.worktree.get(path);
|
|
30598
|
+
this.worktree.set(path, was?.startsWith(EXEC) ? EXEC + content : content);
|
|
30455
30599
|
}
|
|
30456
30600
|
deleteFile(path) {
|
|
30457
30601
|
this.worktree.delete(path);
|
|
@@ -30679,6 +30823,7 @@ const makeInMemoryWorkspaceOps = (repo, root) => {
|
|
|
30679
30823
|
},
|
|
30680
30824
|
treeSync: treeOf,
|
|
30681
30825
|
episodeTrees: (base, commits) => succeed$1(new Map([...base !== void 0 ? [base] : [], ...commits].map((h) => [h, treeOf(h)]))),
|
|
30826
|
+
modeSync: (ref) => repo.modesAt(ref),
|
|
30682
30827
|
worktreeSync: () => new Map(repo.pathsUnder("").filter((path) => !isGitDirKey(path)).map((path) => [path, repo.readFile(path)])),
|
|
30683
30828
|
atPath: (path) => readAt(toKey(path)),
|
|
30684
30829
|
writeAtPath: (path, content) => try_({
|
|
@@ -31605,7 +31750,7 @@ const recognizeGtdCheck = (repo, block) => {
|
|
|
31605
31750
|
};
|
|
31606
31751
|
};
|
|
31607
31752
|
const GTD_UNCHECK_RE = /^gtd uncheck (.+)$/;
|
|
31608
|
-
/** `gtd uncheck <file>` — the review-gate reset prepended ahead of the human's commit,
|
|
31753
|
+
/** `gtd uncheck <file>` — the review-gate reset prepended ahead of the human's commit, absent when the next step is an agent turn. Re-runs the REAL `clearTicks`, writing back only on an actual change. */
|
|
31609
31754
|
const recognizeGtdUncheck = (repo, block) => {
|
|
31610
31755
|
const match = GTD_UNCHECK_RE.exec(block);
|
|
31611
31756
|
if (!match) return void 0;
|
|
@@ -84,7 +84,7 @@ const stallDiagnosis = (state, actor, failure, blocked) => blocked !== void 0 ?
|
|
|
84
84
|
* any other kind.
|
|
85
85
|
*/
|
|
86
86
|
const demandOf = (input) => {
|
|
87
|
-
const { rendered, kind, session, validate } = input;
|
|
87
|
+
const { rendered, kind, session, validate, fresh = null } = input;
|
|
88
88
|
const content = kind === "stalled" ? stallDiagnosis(rendered.state, rendered.actor, rendered.failure, rendered.blocked) : rendered.content;
|
|
89
89
|
const skills = rendered.skills !== void 0 && rendered.skills.length > 0 ? rendered.skills : void 0;
|
|
90
90
|
const access = rendered.access ?? {
|
|
@@ -96,6 +96,7 @@ const demandOf = (input) => {
|
|
|
96
96
|
content,
|
|
97
97
|
session,
|
|
98
98
|
validate,
|
|
99
|
+
fresh,
|
|
99
100
|
skills,
|
|
100
101
|
access
|
|
101
102
|
};
|
|
@@ -158,6 +159,7 @@ const dispatchFieldsOf = (demand) => {
|
|
|
158
159
|
const none = {
|
|
159
160
|
session: void 0,
|
|
160
161
|
validate: void 0,
|
|
162
|
+
fresh: null,
|
|
161
163
|
skills: void 0,
|
|
162
164
|
access: void 0,
|
|
163
165
|
pin: void 0
|
|
@@ -167,6 +169,7 @@ const dispatchFieldsOf = (demand) => {
|
|
|
167
169
|
...none,
|
|
168
170
|
session: demand.session,
|
|
169
171
|
validate: demand.validate,
|
|
172
|
+
fresh: demand.fresh,
|
|
170
173
|
skills: demand.skills,
|
|
171
174
|
access: demand.access
|
|
172
175
|
};
|
|
@@ -181,13 +184,13 @@ const dispatchFieldsOf = (demand) => {
|
|
|
181
184
|
}
|
|
182
185
|
};
|
|
183
186
|
/**
|
|
184
|
-
* Flatten a `Demand` plus its `BeatStatus` into the single
|
|
187
|
+
* Flatten a `Demand` plus its `BeatStatus` into the single 27-key document
|
|
185
188
|
* `gtd next --json` emits — the ONLY place the two are joined. `cost`/
|
|
186
189
|
* `costByModel` are omitted together, exactly when no cost was recorded
|
|
187
190
|
* (`cost <= 0`).
|
|
188
191
|
*/
|
|
189
192
|
const beatDocument = (demand, status) => {
|
|
190
|
-
const { session, validate, skills, access, pin } = dispatchFieldsOf(demand);
|
|
193
|
+
const { session, validate, fresh, skills, access, pin } = dispatchFieldsOf(demand);
|
|
191
194
|
const hasCost = status.cost > 0;
|
|
192
195
|
return {
|
|
193
196
|
schema: 1,
|
|
@@ -199,6 +202,7 @@ const beatDocument = (demand, status) => {
|
|
|
199
202
|
model: status.model,
|
|
200
203
|
system: status.system,
|
|
201
204
|
validate,
|
|
205
|
+
fresh,
|
|
202
206
|
pin,
|
|
203
207
|
log: status.log,
|
|
204
208
|
state: status.state,
|
package/dist/flows.mjs
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
export {
|
|
1
|
+
export { N as FLOWS_PROTOCOL, P as FlowsProtocolError, F as accessFor, I as agent, s as answered, w as applyScript, L as changes, R as changesSince, c as check, T as checkScript, l as codeChanges, z as codeThreads, r as defineConfig, a as defineWorkflow, u as displayPath, B as envSettings, V as envSettingsOf, H as glob, d as hasThreadFor, U as head, W as human, K as judge, f as judgeDiffShape, q as kept, J as mark, X as matches, E as moveScript, p as numeric, Z as openQuestions, m as progressErrors, D as quote, $ as read, et as refuse, O as removeScript, h as repliesErrors, g as requireAnswers, _ as requireProgress, v as requireReplies, y as requireRevert, b as requireThreadsClosed, tt as restart, k as restoreScript, x as revertErrors, A as revertScript, nt as run, rt as scope, S as seamGate, it as sectionBodies, at as sections, ot as settings, st as settingsOf, ct as ship, j as showScript, lt as skillsFor, ut as start, dt as threads, ft as workflow, M as writeScript, C as wrote } from "./chunks/flows-CmQplvh1.mjs";
|