@pmelab/gtd 60.1.0 → 62.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 +16 -0
- package/dist/chunks/{testkit-BhTey1Do.mjs → testkit-XQeYCNMn.mjs} +1670 -1470
- package/dist/chunks/{wire-DZ8R1RU9.mjs → wire-CPNSm_9N.mjs} +27 -12
- package/dist/gtd.bundle.mjs +991 -627
- package/dist/gtd.core.mjs +117 -117
- package/dist/testkit/gherkin.mjs +1 -1
- package/dist/testkit.mjs +1 -1
- package/dist/wire.mjs +2 -2
- package/package.json +1 -1
- package/src/cli/Cli.ts +6 -1
- package/src/cli/History.test.ts +10 -1
- package/src/cli/Launch.test.ts +52 -0
- package/src/cli/Launch.ts +143 -0
- package/src/cli/Metrics.test.ts +10 -1
- package/src/cli/Program.test.ts +30 -20
- package/src/cli/Program.ts +4 -2
- package/src/cli/Strays.ts +3 -2
- package/src/cli/index.ts +1 -0
- package/src/edge/Edge.test.ts +79 -26
- package/src/edge/Edge.ts +178 -99
- package/src/emit/GitScript.ts +21 -3
- package/src/emit/index.ts +3 -0
- package/src/gherkin/messages.test.ts +1 -0
- package/src/gherkin/module-config.feature +1 -0
- package/src/gherkin/more-steps.feature +1 -0
- package/src/gherkin/testkit.feature +2 -1
- package/src/install/Install.ts +9 -2
- package/src/platform/PinFile.ts +61 -0
- package/src/platform/index.ts +1 -0
- package/src/replay/Format.test.ts +35 -8
- package/src/replay/Format.ts +7 -4
- package/src/replay/Replay.test.ts +54 -0
- package/src/replay/Replay.ts +61 -17
- package/src/replay/Trailers.test.ts +11 -48
- package/src/replay/Trailers.ts +36 -42
- package/src/replay/Tree.ts +11 -0
- package/src/replay/index.ts +3 -0
- package/src/step/LandStep.ts +1 -0
- package/src/step/RepoSnapshot.ts +2 -0
- package/src/step/planStart.test.ts +2 -3
- package/src/step/planStart.ts +1 -5
- package/src/step/planStep.test.ts +4 -4
- package/src/step/planStep.ts +10 -8
- package/src/testkit/TestRepo.test.ts +4 -1
- package/src/testkit/TestRepo.ts +3 -1
- package/src/twins/EmittedScriptRecognizer.ts +32 -1
- package/src/twins/GitTiers.ts +23 -0
- package/src/twins/InMemRepo.ts +14 -0
- package/src/twins/PinBeats.ts +33 -0
- package/src/twins/PinFile.test.ts +107 -0
- package/src/twins/PinFile.ts +12 -0
- package/src/twins/index.ts +2 -0
- package/src/wire/BeatDocument.ts +26 -5
- package/src/wire/Demand.ts +16 -6
- package/src/wire/constants.ts +3 -0
- package/src/wire/types.ts +4 -0
- package/src/workflow/fingerprint.ts +2 -2
- package/src/workflow/index.ts +2 -2
- package/src/workflow/load.ts +14 -1
- package/src/workflow/pin.ts +15 -7
package/dist/gtd.bundle.mjs
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import { createRequire as __createRequire } from 'module';
|
|
3
3
|
const require = __createRequire(import.meta.url);
|
|
4
|
-
import { createRequire, isBuiltin } from "node:module";
|
|
4
|
+
import { createRequire, isBuiltin, registerHooks } from "node:module";
|
|
5
5
|
import * as OS from "node:os";
|
|
6
6
|
import { homedir, networkInterfaces, tmpdir } from "node:os";
|
|
7
7
|
import * as NodeChildProcess from "node:child_process";
|
|
@@ -14,7 +14,7 @@ import * as NFS from "node:fs";
|
|
|
14
14
|
import { existsSync, lstatSync, mkdirSync, mkdtempSync, readFileSync, readlinkSync, realpathSync, renameSync, rmSync, statSync, unlinkSync, writeFileSync } from "node:fs";
|
|
15
15
|
import * as Path from "node:path";
|
|
16
16
|
import { basename, dirname, isAbsolute, join, relative, resolve, sep } from "node:path";
|
|
17
|
-
import { gzipSync } from "node:zlib";
|
|
17
|
+
import { gunzipSync, gzipSync } from "node:zlib";
|
|
18
18
|
import * as NodeUrl from "node:url";
|
|
19
19
|
import { fileURLToPath, pathToFileURL } from "node:url";
|
|
20
20
|
import { stripVTControlCharacters } from "node:util";
|
|
@@ -12054,6 +12054,57 @@ const restorability = (git, headHash, tipHash) => gen(function* () {
|
|
|
12054
12054
|
};
|
|
12055
12055
|
});
|
|
12056
12056
|
//#endregion
|
|
12057
|
+
//#region src/platform/PinFile.ts
|
|
12058
|
+
const livePinFile = (cwd, path) => () => {
|
|
12059
|
+
const file = join(cwd, path);
|
|
12060
|
+
if (existsSync(file)) try {
|
|
12061
|
+
return {
|
|
12062
|
+
path: file,
|
|
12063
|
+
bytes: readFileSync(file)
|
|
12064
|
+
};
|
|
12065
|
+
} catch {
|
|
12066
|
+
return {
|
|
12067
|
+
path: file,
|
|
12068
|
+
bytes: /* @__PURE__ */ new Uint8Array()
|
|
12069
|
+
};
|
|
12070
|
+
}
|
|
12071
|
+
try {
|
|
12072
|
+
return {
|
|
12073
|
+
path: file,
|
|
12074
|
+
bytes: execFileSync("git", ["show", `HEAD:${path}`], {
|
|
12075
|
+
cwd,
|
|
12076
|
+
stdio: [
|
|
12077
|
+
"ignore",
|
|
12078
|
+
"pipe",
|
|
12079
|
+
"ignore"
|
|
12080
|
+
]
|
|
12081
|
+
})
|
|
12082
|
+
};
|
|
12083
|
+
} catch {
|
|
12084
|
+
return;
|
|
12085
|
+
}
|
|
12086
|
+
};
|
|
12087
|
+
/**
|
|
12088
|
+
* Runs a pin's module as this process's main program. Node refuses an unknown
|
|
12089
|
+
* extension, so a load hook serves the unpacked source under the pin's own
|
|
12090
|
+
* URL — `import.meta.url` stays a file in the repository.
|
|
12091
|
+
*/
|
|
12092
|
+
const runPinModule = async (path, source) => {
|
|
12093
|
+
const url = pathToFileURL(path).href;
|
|
12094
|
+
registerHooks({
|
|
12095
|
+
resolve: (specifier, context, nextResolve) => specifier === url ? {
|
|
12096
|
+
url,
|
|
12097
|
+
shortCircuit: true
|
|
12098
|
+
} : nextResolve(specifier, context),
|
|
12099
|
+
load: (requested, context, nextLoad) => requested === url ? {
|
|
12100
|
+
format: "module",
|
|
12101
|
+
source,
|
|
12102
|
+
shortCircuit: true
|
|
12103
|
+
} : nextLoad(requested, context)
|
|
12104
|
+
});
|
|
12105
|
+
await import(url);
|
|
12106
|
+
};
|
|
12107
|
+
//#endregion
|
|
12057
12108
|
//#region src/flags/index.ts
|
|
12058
12109
|
const FLAGS$1 = [];
|
|
12059
12110
|
const BOOL = {
|
|
@@ -12326,7 +12377,13 @@ The \`kind\` field (\`--json=kind\`) selects what to do:
|
|
|
12326
12377
|
Land it immediately, no display needed.
|
|
12327
12378
|
- \`script\` — run \`--json=content\` with \`sh -c\`, appending its output to
|
|
12328
12379
|
\`--json=log\`, and ignore its exit code — the outcome lives in the tree,
|
|
12329
|
-
not in the exit code.
|
|
12380
|
+
not in the exit code. Two script beats carry a \`pin\` mark: the **pin
|
|
12381
|
+
beat** right after a process starts (\`pin: "add"\`, its script writes
|
|
12382
|
+
\`gtd bundle\`'s output to \`.gtd/process.gtdpin\`) and the **unpin beat**
|
|
12383
|
+
once the flow has ended (\`pin: "remove"\`, its script deletes that file).
|
|
12384
|
+
Both run unattended, like any script beat. A driver may write or delete the
|
|
12385
|
+
file its own way instead of running the script; either way it then lands,
|
|
12386
|
+
and the unpin's landing is the commit that closes the process.
|
|
12330
12387
|
- \`prompt\` — send plain \`gtd next\`'s own output to your agent CLI over
|
|
12331
12388
|
stdin (it's the bare content at this kind, so no separate \`--json=content\`
|
|
12332
12389
|
read is needed), using the accompanying \`session.id\`/\`session.resume\`/
|
|
@@ -12369,7 +12426,8 @@ skills, \`skills\` (an array of the skill names, already trimmed and split —
|
|
|
12369
12426
|
\`label\`, \`file\`, \`mode\`, \`edges\`, \`judge\` (the pending judgment's rendered
|
|
12370
12427
|
document). When a cost has been recorded (a prior \`gtd land --cost=<n>\`):
|
|
12371
12428
|
\`cost\`, \`costByModel\`. Always \`approvals\` (an array of
|
|
12372
|
-
\`{step, name, email, commit}\`, \`[]\` when none).
|
|
12429
|
+
\`{step, name, email, commit}\`, \`[]\` when none). Only at a pin or unpin
|
|
12430
|
+
beat: \`pin\` (\`"add"\` or \`"remove"\`).
|
|
12373
12431
|
|
|
12374
12432
|
\`gtd land\` fields, in fixed order: \`schema\`, \`script\` (the POSIX sh to run), \`settled\`
|
|
12375
12433
|
(true for a no-op at a \`script\` rest — stop immediately, nothing more to
|
|
@@ -28942,6 +29000,8 @@ const renderFormat = (fmt, ...args) => {
|
|
|
28942
29000
|
};
|
|
28943
29001
|
/** The first line of the landing refusal for a turn that wrote outside its `write` access — a stable marker drivers and tests grep for. */
|
|
28944
29002
|
const ACCESS_REFUSAL = "gtd land: this turn wrote outside its write access — revert these paths, then run `gtd land` again:";
|
|
29003
|
+
/** Where a process keeps its pin: the engine and workflow it runs on, as `gtd bundle` prints them. */
|
|
29004
|
+
const PIN_PATH = ".gtd/process.gtdpin";
|
|
28945
29005
|
//#endregion
|
|
28946
29006
|
//#region src/wire/Demand.ts
|
|
28947
29007
|
/**
|
|
@@ -28982,14 +29042,20 @@ const demandOf = (input) => {
|
|
|
28982
29042
|
read: null,
|
|
28983
29043
|
write: null
|
|
28984
29044
|
};
|
|
28985
|
-
|
|
29045
|
+
if (kind === "prompt") return {
|
|
28986
29046
|
kind,
|
|
28987
29047
|
content,
|
|
28988
29048
|
session,
|
|
28989
29049
|
validate,
|
|
28990
29050
|
skills,
|
|
28991
29051
|
access
|
|
28992
|
-
}
|
|
29052
|
+
};
|
|
29053
|
+
if (kind === "script" && rendered.pin !== void 0) return {
|
|
29054
|
+
kind,
|
|
29055
|
+
content,
|
|
29056
|
+
pin: rendered.pin
|
|
29057
|
+
};
|
|
29058
|
+
return {
|
|
28993
29059
|
kind,
|
|
28994
29060
|
content
|
|
28995
29061
|
};
|
|
@@ -29042,33 +29108,39 @@ const assertNeverDemand = (demand) => {
|
|
|
29042
29108
|
* falling through to "no session."
|
|
29043
29109
|
*/
|
|
29044
29110
|
const dispatchFieldsOf = (demand) => {
|
|
29111
|
+
const none = {
|
|
29112
|
+
session: void 0,
|
|
29113
|
+
validate: void 0,
|
|
29114
|
+
skills: void 0,
|
|
29115
|
+
access: void 0,
|
|
29116
|
+
pin: void 0
|
|
29117
|
+
};
|
|
29045
29118
|
switch (demand.kind) {
|
|
29046
29119
|
case "prompt": return {
|
|
29120
|
+
...none,
|
|
29047
29121
|
session: demand.session,
|
|
29048
29122
|
validate: demand.validate,
|
|
29049
29123
|
skills: demand.skills,
|
|
29050
29124
|
access: demand.access
|
|
29051
29125
|
};
|
|
29126
|
+
case "script": return {
|
|
29127
|
+
...none,
|
|
29128
|
+
pin: demand.pin
|
|
29129
|
+
};
|
|
29052
29130
|
case "capture":
|
|
29053
29131
|
case "message":
|
|
29054
|
-
case "
|
|
29055
|
-
case "stalled": return {
|
|
29056
|
-
session: void 0,
|
|
29057
|
-
validate: void 0,
|
|
29058
|
-
skills: void 0,
|
|
29059
|
-
access: void 0
|
|
29060
|
-
};
|
|
29132
|
+
case "stalled": return none;
|
|
29061
29133
|
default: return assertNeverDemand(demand);
|
|
29062
29134
|
}
|
|
29063
29135
|
};
|
|
29064
29136
|
/**
|
|
29065
|
-
* Flatten a `Demand` plus its `BeatStatus` into the single
|
|
29137
|
+
* Flatten a `Demand` plus its `BeatStatus` into the single 26-key document
|
|
29066
29138
|
* `gtd next --json` emits — the ONLY place the two are joined. `cost`/
|
|
29067
29139
|
* `costByModel` are omitted together, exactly when no cost was recorded
|
|
29068
29140
|
* (`cost <= 0`).
|
|
29069
29141
|
*/
|
|
29070
29142
|
const beatDocument = (demand, status) => {
|
|
29071
|
-
const { session, validate, skills, access } = dispatchFieldsOf(demand);
|
|
29143
|
+
const { session, validate, skills, access, pin } = dispatchFieldsOf(demand);
|
|
29072
29144
|
const hasCost = status.cost > 0;
|
|
29073
29145
|
return {
|
|
29074
29146
|
schema: 1,
|
|
@@ -29080,6 +29152,7 @@ const beatDocument = (demand, status) => {
|
|
|
29080
29152
|
model: status.model,
|
|
29081
29153
|
system: status.system,
|
|
29082
29154
|
validate,
|
|
29155
|
+
pin,
|
|
29083
29156
|
log: status.log,
|
|
29084
29157
|
state: status.state,
|
|
29085
29158
|
actor: status.actor,
|
|
@@ -29417,11 +29490,20 @@ const diffTrees = (before, after) => {
|
|
|
29417
29490
|
};
|
|
29418
29491
|
};
|
|
29419
29492
|
const isEmptyDiff = (diff) => diff.added.length === 0 && diff.modified.length === 0 && diff.deleted.length === 0;
|
|
29493
|
+
const withoutPath = (tree, hidden) => {
|
|
29494
|
+
let paths;
|
|
29495
|
+
return {
|
|
29496
|
+
paths: () => paths ??= tree.paths().filter((path) => path !== hidden),
|
|
29497
|
+
read: (path) => path === hidden ? void 0 : tree.read(path),
|
|
29498
|
+
...tree.id !== void 0 ? { id: (path) => path === hidden ? void 0 : tree.id(path) } : {}
|
|
29499
|
+
};
|
|
29500
|
+
};
|
|
29501
|
+
const OLDEST_READABLE_FORMAT = 1;
|
|
29420
29502
|
const short$1 = (hash) => hash.slice(0, 7);
|
|
29421
29503
|
const formatFault = (commits) => {
|
|
29422
|
-
const foreign = commits.find((c) => c.format
|
|
29504
|
+
const foreign = commits.find((c) => c.format < OLDEST_READABLE_FORMAT || c.format > 2);
|
|
29423
29505
|
if (foreign === void 0) return void 0;
|
|
29424
|
-
return `gtd: commit ${short$1(foreign.hash)} is written in history format ${foreign.format}, but this gtd reads
|
|
29506
|
+
return `gtd: commit ${short$1(foreign.hash)} is written in history format ${foreign.format}, but this gtd reads formats ${OLDEST_READABLE_FORMAT} to 2 — run a gtd release that reads format ${foreign.format} to continue this process`;
|
|
29425
29507
|
};
|
|
29426
29508
|
//#endregion
|
|
29427
29509
|
//#region src/replay/Trailers.ts
|
|
@@ -29458,6 +29540,11 @@ const parseStepId = (raw) => {
|
|
|
29458
29540
|
occurrence: Number(match[2])
|
|
29459
29541
|
};
|
|
29460
29542
|
};
|
|
29543
|
+
/** What a pin and an unpin beat rest at, and their commits name — neither is a step of the workflow. */
|
|
29544
|
+
const PIN_STATES = {
|
|
29545
|
+
add: "pin",
|
|
29546
|
+
remove: "unpin"
|
|
29547
|
+
};
|
|
29461
29548
|
const isVerdict = (value) => {
|
|
29462
29549
|
if (typeof value !== "object" || value === null) return false;
|
|
29463
29550
|
const v = value;
|
|
@@ -29477,13 +29564,7 @@ const parseJson = (raw) => {
|
|
|
29477
29564
|
const TRAILER_RE = /^(Gtd-[A-Za-z-]+):[ \t]*(.*?)[ \t]*$/gm;
|
|
29478
29565
|
const COST_RE = /^([0-9]+(?:\.[0-9]+)?)(?:[ \t]+(.+))?$/;
|
|
29479
29566
|
const VAR_RE = /^([^=\s]+)=(.*)$/;
|
|
29480
|
-
const CONFIG_RE = /^([0-9a-f]{16}) (.+)$/;
|
|
29481
29567
|
const CONTROL_CHARACTER$1 = /[\x00-\x1f\x7f-\x9f\u2028\u2029]/;
|
|
29482
|
-
const encodePath = (path) => path.trim() !== path || path.startsWith("\"") || CONTROL_CHARACTER$1.test(path) ? JSON.stringify(path) : path;
|
|
29483
|
-
const decodePath = (raw) => {
|
|
29484
|
-
const path = raw.startsWith("\"") ? parseJson(raw) : raw;
|
|
29485
|
-
return typeof path === "string" && path !== "" && !CONTROL_CHARACTER$1.test(path) ? path : void 0;
|
|
29486
|
-
};
|
|
29487
29568
|
const decodeApproval = (raw) => {
|
|
29488
29569
|
const parsed = parseJson(raw);
|
|
29489
29570
|
if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) return void 0;
|
|
@@ -29535,11 +29616,6 @@ const TRAILER_READERS = {
|
|
|
29535
29616
|
const kv = VAR_RE.exec(value);
|
|
29536
29617
|
if (kv !== null) into.vars[kv[1]] = kv[2];
|
|
29537
29618
|
},
|
|
29538
|
-
"Gtd-Config": (value, into) => {
|
|
29539
|
-
const entry = CONFIG_RE.exec(value);
|
|
29540
|
-
const path = entry === null ? void 0 : decodePath(entry[2]);
|
|
29541
|
-
if (path !== void 0) into.config[path] = entry[1];
|
|
29542
|
-
},
|
|
29543
29619
|
"Gtd-Review-Base": (value, into) => {
|
|
29544
29620
|
into.reviewBase ??= value.split(/\s/)[0];
|
|
29545
29621
|
},
|
|
@@ -29555,6 +29631,9 @@ const TRAILER_READERS = {
|
|
|
29555
29631
|
"Gtd-Approved": (value, into) => {
|
|
29556
29632
|
into.approved ??= decodeApproval(value);
|
|
29557
29633
|
},
|
|
29634
|
+
"Gtd-Pin": (value, into) => {
|
|
29635
|
+
if (value === "add" || value === "remove") into.pin ??= value;
|
|
29636
|
+
},
|
|
29558
29637
|
"Gtd-Format": (value, into) => {
|
|
29559
29638
|
if (FORMAT_RE.test(value)) into.format ??= Number(value);
|
|
29560
29639
|
},
|
|
@@ -29577,10 +29656,10 @@ const parseCommitMessage = (message) => {
|
|
|
29577
29656
|
version: void 0,
|
|
29578
29657
|
kept: void 0,
|
|
29579
29658
|
approved: void 0,
|
|
29659
|
+
pin: void 0,
|
|
29580
29660
|
judge: [],
|
|
29581
29661
|
marks: [],
|
|
29582
29662
|
vars: {},
|
|
29583
|
-
config: {},
|
|
29584
29663
|
cost: [],
|
|
29585
29664
|
format: void 0
|
|
29586
29665
|
};
|
|
@@ -29594,24 +29673,28 @@ const parseCommitMessage = (message) => {
|
|
|
29594
29673
|
};
|
|
29595
29674
|
const sortedEntries = (record = {}) => Object.entries(record).sort(([a], [b]) => a < b ? -1 : a > b ? 1 : 0);
|
|
29596
29675
|
/** Trailer order is fixed so the same spec always yields the same bytes. */
|
|
29676
|
+
const trailer = (name, value) => value === void 0 ? [] : [`Gtd-${name}: ${value}`];
|
|
29677
|
+
const costValue = (cost) => cost === void 0 ? void 0 : `${cost.cost}${cost.model !== void 0 ? ` ${cost.model}` : ""}`;
|
|
29597
29678
|
const formatCommitMessage = (spec) => {
|
|
29598
|
-
const lines = [
|
|
29599
|
-
|
|
29600
|
-
|
|
29601
|
-
|
|
29602
|
-
|
|
29603
|
-
|
|
29604
|
-
|
|
29605
|
-
|
|
29606
|
-
|
|
29607
|
-
|
|
29608
|
-
|
|
29609
|
-
|
|
29610
|
-
|
|
29679
|
+
const lines = [
|
|
29680
|
+
...trailer("Step", spec.step && formatStepId(spec.step)),
|
|
29681
|
+
...trailer("Workflow", spec.workflow),
|
|
29682
|
+
...trailer("Review-Base", spec.reviewBase),
|
|
29683
|
+
...sortedEntries(spec.vars).map(([name, value]) => `Gtd-Var: ${name}=${value}`),
|
|
29684
|
+
...trailer("Cost", costValue(spec.cost)),
|
|
29685
|
+
...(spec.judge ?? []).map((verdict) => `Gtd-Judge: ${JSON.stringify(verdict)}`),
|
|
29686
|
+
...(spec.marks ?? []).map((m) => `Gtd-Mark: ${m.name}=${m.value}`),
|
|
29687
|
+
...trailer("Approved", spec.approved && encodeApproval(spec.approved)),
|
|
29688
|
+
...trailer("Pin", spec.pin),
|
|
29689
|
+
...trailer("Branch", spec.branch),
|
|
29690
|
+
...trailer("Version", spec.version),
|
|
29691
|
+
`Gtd-Format: 2`
|
|
29692
|
+
];
|
|
29611
29693
|
return `${formatSubject(spec.actor, spec.to, spec.from)}\n\n${lines.join("\n")}`;
|
|
29612
29694
|
};
|
|
29613
29695
|
//#endregion
|
|
29614
29696
|
//#region src/replay/Replay.ts
|
|
29697
|
+
const UNPIN_STATE = PIN_STATES.remove;
|
|
29615
29698
|
const actorOfKind = (kind) => kind === "run" ? "check" : kind === "agent" ? "agent" : kind === "human" ? "human" : "judge";
|
|
29616
29699
|
const memoryScopeOf = (name) => {
|
|
29617
29700
|
const dot = name.lastIndexOf(".");
|
|
@@ -29745,7 +29828,33 @@ const modeWithoutFile = (step) => {
|
|
|
29745
29828
|
if (options.mode === void 0 || options.file !== void 0) return void 0;
|
|
29746
29829
|
return `gtd: step "${step.name}": mode requires file in ${step.kind}() options — name the steering file the mode checks`;
|
|
29747
29830
|
};
|
|
29748
|
-
const
|
|
29831
|
+
const withoutPin = (input) => {
|
|
29832
|
+
const hide = (tree) => withoutPath(tree, PIN_PATH);
|
|
29833
|
+
return {
|
|
29834
|
+
...input,
|
|
29835
|
+
episode: {
|
|
29836
|
+
base: {
|
|
29837
|
+
...input.episode.base,
|
|
29838
|
+
tree: hide(input.episode.base.tree)
|
|
29839
|
+
},
|
|
29840
|
+
commits: input.episode.commits.map((c) => ({
|
|
29841
|
+
...c,
|
|
29842
|
+
tree: hide(c.tree),
|
|
29843
|
+
...c.kept !== void 0 ? { kept: {
|
|
29844
|
+
...c.kept,
|
|
29845
|
+
tree: hide(c.kept.tree)
|
|
29846
|
+
} } : {}
|
|
29847
|
+
}))
|
|
29848
|
+
},
|
|
29849
|
+
...input.startTree !== void 0 ? { startTree: hide(input.startTree) } : {},
|
|
29850
|
+
...input.pending !== void 0 ? { pending: {
|
|
29851
|
+
...input.pending,
|
|
29852
|
+
tree: hide(input.pending.tree)
|
|
29853
|
+
} } : {}
|
|
29854
|
+
};
|
|
29855
|
+
};
|
|
29856
|
+
const replay = async (raw) => {
|
|
29857
|
+
const input = withoutPin(raw);
|
|
29749
29858
|
const commits = input.episode.commits.map((c) => ({
|
|
29750
29859
|
...c,
|
|
29751
29860
|
parsed: parseCommitMessage(c.message)
|
|
@@ -29754,6 +29863,9 @@ const replay = async (input) => {
|
|
|
29754
29863
|
let cursor = 0;
|
|
29755
29864
|
let pendingUsed = false;
|
|
29756
29865
|
let position = input.episode.base;
|
|
29866
|
+
const skipPins = () => {
|
|
29867
|
+
while (cursor < commits.length && commits[cursor].parsed.pin !== void 0) cursor++;
|
|
29868
|
+
};
|
|
29757
29869
|
let previousPosition = input.episode.base;
|
|
29758
29870
|
let expectedNext;
|
|
29759
29871
|
const occurrences = /* @__PURE__ */ new Map();
|
|
@@ -29935,19 +30047,24 @@ const replay = async (input) => {
|
|
|
29935
30047
|
if (unapproved !== void 0) return finish(unapproved);
|
|
29936
30048
|
cursor++;
|
|
29937
30049
|
advance(commit);
|
|
29938
|
-
|
|
29939
|
-
|
|
29940
|
-
hash: commit.kept.hash,
|
|
29941
|
-
before: commit.tree,
|
|
29942
|
-
after: commit.kept.tree
|
|
29943
|
-
});
|
|
30050
|
+
skipPins();
|
|
30051
|
+
noteKept(step, commit);
|
|
29944
30052
|
const to = commit.parsed.parsed?.to;
|
|
29945
|
-
if (to !== void 0) expectedNext = {
|
|
30053
|
+
if (to !== void 0 && to !== UNPIN_STATE) expectedNext = {
|
|
29946
30054
|
name: to,
|
|
29947
30055
|
hash: commit.hash
|
|
29948
30056
|
};
|
|
29949
30057
|
return replyFor(step.request, commit.parsed.judge);
|
|
29950
30058
|
};
|
|
30059
|
+
const noteKept = (step, commit) => {
|
|
30060
|
+
if (commit.parsed.kept === void 0) return;
|
|
30061
|
+
if (commit.kept === void 0) keptTips.delete(step.name);
|
|
30062
|
+
else keptTips.set(step.name, {
|
|
30063
|
+
hash: commit.kept.hash,
|
|
30064
|
+
before: commit.tree,
|
|
30065
|
+
after: commit.kept.tree
|
|
30066
|
+
});
|
|
30067
|
+
};
|
|
29951
30068
|
const landPending = (step, pending) => {
|
|
29952
30069
|
pendingUsed = true;
|
|
29953
30070
|
landed = step;
|
|
@@ -29964,7 +30081,10 @@ const replay = async (input) => {
|
|
|
29964
30081
|
return replyFor(step.request, pending.verdicts ?? []);
|
|
29965
30082
|
};
|
|
29966
30083
|
const answer = (step) => {
|
|
29967
|
-
while (cursor < commits.length && isAttemptAt(commits[cursor], step.name))
|
|
30084
|
+
while (cursor < commits.length && isAttemptAt(commits[cursor], step.name)) {
|
|
30085
|
+
cursor++;
|
|
30086
|
+
skipPins();
|
|
30087
|
+
}
|
|
29968
30088
|
const commit = commits[cursor];
|
|
29969
30089
|
if (commit !== void 0) return consumeCommit(step, commit);
|
|
29970
30090
|
if (input.pending === void 0 || pendingUsed) return finish({
|
|
@@ -29977,6 +30097,7 @@ const replay = async (input) => {
|
|
|
29977
30097
|
return landPending(step, input.pending);
|
|
29978
30098
|
};
|
|
29979
30099
|
const requestFault = (request) => {
|
|
30100
|
+
if (Object.values(PIN_STATES).includes(scoped(request.name))) return `gtd: step "${scoped(request.name)}": the name is reserved for the pin and unpin beats — rename the step`;
|
|
29980
30101
|
if (request.kind === "run" && typeof request.body !== "string") return `gtd: step "${scoped(request.name)}": a run() body is a shell script string — decide in flow code, then render the script (see checkScript and friends)`;
|
|
29981
30102
|
if (request.kind !== "agent") return void 0;
|
|
29982
30103
|
const name = scoped(request.name);
|
|
@@ -30003,15 +30124,18 @@ const replay = async (input) => {
|
|
|
30003
30124
|
const blocker = blockerAt(step);
|
|
30004
30125
|
return blocker === void 0 ? answer(step) : finish(blocker);
|
|
30005
30126
|
};
|
|
30006
|
-
const endOfFlow = (via) =>
|
|
30007
|
-
|
|
30008
|
-
|
|
30009
|
-
|
|
30010
|
-
|
|
30011
|
-
|
|
30012
|
-
|
|
30013
|
-
|
|
30014
|
-
|
|
30127
|
+
const endOfFlow = (via) => {
|
|
30128
|
+
skipPins();
|
|
30129
|
+
return cursor < commits.length ? {
|
|
30130
|
+
kind: "divergence",
|
|
30131
|
+
message: `gtd: the flow ended, but commit ${short(commits[cursor].hash)} continues the episode — ${DIVERGED}`
|
|
30132
|
+
} : {
|
|
30133
|
+
kind: "ended",
|
|
30134
|
+
via,
|
|
30135
|
+
trace,
|
|
30136
|
+
landed,
|
|
30137
|
+
marks
|
|
30138
|
+
};
|
|
30015
30139
|
};
|
|
30016
30140
|
const changesSince = (hash) => {
|
|
30017
30141
|
const tree = treeAt(hash);
|
|
@@ -30074,10 +30198,11 @@ const replay = async (input) => {
|
|
|
30074
30198
|
skillsFor: (localName) => resolveSkills(memoryScopeOf(scoped(localName))) ?? [],
|
|
30075
30199
|
accessFor: (localName, file) => foldedAccessFor(resolveAccess(memoryScopeOf(scoped(localName))), file),
|
|
30076
30200
|
head: () => {
|
|
30077
|
-
const rest = commits.slice(cursor);
|
|
30201
|
+
const rest = commits.slice(cursor).filter((c) => c.parsed.pin === void 0);
|
|
30078
30202
|
return rest.length > 0 && rest.every((c) => c.parsed.step === void 0) ? rest[rest.length - 1].hash : position.hash;
|
|
30079
30203
|
}
|
|
30080
30204
|
};
|
|
30205
|
+
skipPins();
|
|
30081
30206
|
installContext(context);
|
|
30082
30207
|
try {
|
|
30083
30208
|
const flowDone = input.flow({}).then(() => {
|
|
@@ -30477,7 +30602,8 @@ const load = gen(function* () {
|
|
|
30477
30602
|
},
|
|
30478
30603
|
...compiled.ui !== void 0 ? { ui: compiled.ui } : {},
|
|
30479
30604
|
warnings: diagnostics.filter((d) => d.severity === "warning"),
|
|
30480
|
-
fingerprint: pinned?.fingerprint ?? configFingerprint(loaded.file.dependencyFiles, host.root, module.read ?? readFromDisk)
|
|
30605
|
+
fingerprint: pinned?.fingerprint ?? configFingerprint(loaded.file.dependencyFiles, host.root, module.read ?? readFromDisk),
|
|
30606
|
+
drift: pinned === void 0 ? [] : changedConfigFiles(pinned.fingerprint, configFingerprint(Object.keys(pinned.fingerprint).map((file) => resolve(host.root, file)), host.root, readFromDisk))
|
|
30481
30607
|
};
|
|
30482
30608
|
});
|
|
30483
30609
|
const readFromDisk = (path) => {
|
|
@@ -31131,8 +31257,6 @@ const knownModes = (def) => Object.keys(def.modes);
|
|
|
31131
31257
|
//#endregion
|
|
31132
31258
|
//#region src/workflow/pin.ts
|
|
31133
31259
|
init_dist$1();
|
|
31134
|
-
/** Bumped when a launcher could no longer run an older pin the same way. */
|
|
31135
|
-
const PIN_FORMAT = 1;
|
|
31136
31260
|
/**
|
|
31137
31261
|
* The engine a pin carries, as ES module source. Live, the `gtd.core.mjs` the
|
|
31138
31262
|
* running gtd ships beside its own bundle.
|
|
@@ -31158,7 +31282,505 @@ const pin = (module, root, version) => gen(function* () {
|
|
|
31158
31282
|
try: () => pinnedWorkflowSource(module, root, version),
|
|
31159
31283
|
catch: (e) => new GtdError(`gtd bundle: ${module.filepath}: ${e instanceof Error ? e.message : String(e)}`)
|
|
31160
31284
|
})) + core.replace(/^#!.*\n/, "");
|
|
31161
|
-
return Buffer.concat([Buffer.from(`gtdpin
|
|
31285
|
+
return Buffer.concat([Buffer.from(`gtdpin 1 gtd ${version}\n`), gzipSync(source, { level: 9 })]);
|
|
31286
|
+
});
|
|
31287
|
+
/** A pin's header line, read without unpacking it; undefined when the bytes are no pin. */
|
|
31288
|
+
const pinHeader = (bytes) => {
|
|
31289
|
+
const end = bytes.indexOf(10);
|
|
31290
|
+
const header = end < 0 ? null : /^gtdpin (\d+) gtd (\S+)$/.exec(Buffer.from(bytes.subarray(0, end)).toString());
|
|
31291
|
+
return header === null ? void 0 : {
|
|
31292
|
+
format: Number(header[1]),
|
|
31293
|
+
version: header[2]
|
|
31294
|
+
};
|
|
31295
|
+
};
|
|
31296
|
+
/** A pin's header and module source. */
|
|
31297
|
+
const readPin = (bytes) => {
|
|
31298
|
+
const header = pinHeader(bytes);
|
|
31299
|
+
if (header === void 0) throw new Error("not a gtd pin");
|
|
31300
|
+
return {
|
|
31301
|
+
...header,
|
|
31302
|
+
source: gunzipSync(bytes.subarray(bytes.indexOf(10) + 1)).toString("utf8")
|
|
31303
|
+
};
|
|
31304
|
+
};
|
|
31305
|
+
//#endregion
|
|
31306
|
+
//#region src/emit/GitScript.ts
|
|
31307
|
+
const shellQuote$1 = (value) => `'${value.replace(/'/g, "'\\''")}'`;
|
|
31308
|
+
/**
|
|
31309
|
+
* `--allow-empty` because gtd's workflow commits may be empty on purpose.
|
|
31310
|
+
* Mirrors `Git.ts`'s retry: only re-tries without hooks on the specific
|
|
31311
|
+
* "empty git commit" rejection message, discriminated from the captured
|
|
31312
|
+
* output rather than the exit code, so a genuinely rejected commit (a lint
|
|
31313
|
+
* error, a commit-msg hook) still fails.
|
|
31314
|
+
*/
|
|
31315
|
+
const commitAllowEmpty = (message, only) => {
|
|
31316
|
+
const m = shellQuote$1(message);
|
|
31317
|
+
const paths = only === void 0 ? "" : ` -- ${shellQuote$1(only)}`;
|
|
31318
|
+
return [
|
|
31319
|
+
`if ! out=$(git commit --allow-empty -m ${m}${paths} 2>&1); then`,
|
|
31320
|
+
` case "$out" in`,
|
|
31321
|
+
` *"empty git commit"*) git commit --allow-empty --no-verify -m ${m}${paths} || exit 1 ;;`,
|
|
31322
|
+
` *) printf '%s\\n' "$out" >&2; exit 1 ;;`,
|
|
31323
|
+
` esac`,
|
|
31324
|
+
`fi`
|
|
31325
|
+
].join("\n");
|
|
31326
|
+
};
|
|
31327
|
+
const CHANGED_SINCE_LAND = "gtd: the working tree changed since `gtd land` — revert the edits and run `gtd land` again to approve";
|
|
31328
|
+
/** `git add -A || exit 1` (an `&&` left side would not abort under `set -e`) then `commitAllowEmpty`. */
|
|
31329
|
+
const commitAll = (message) => `git add -A || exit 1\n${commitAllowEmpty(message)}`;
|
|
31330
|
+
/** Commits `path` alone, as the worktree has it; every other pending change stays pending. */
|
|
31331
|
+
const commitPath = (path, message) => `git add -A -- ${shellQuote$1(path)} || exit 1\n${commitAllowEmpty(message, path)}`;
|
|
31332
|
+
/** A pin beat's script. A failed `gtd bundle` leaves no half-written pin behind. */
|
|
31333
|
+
const pinScript = (path) => {
|
|
31334
|
+
const p = shellQuote$1(path);
|
|
31335
|
+
const slash = path.lastIndexOf("/");
|
|
31336
|
+
return `${slash === -1 ? "" : `mkdir -p ${shellQuote$1(path.slice(0, slash))}\n`}gtd bundle >${p} || { rm -f -- ${p}; exit 1; }`;
|
|
31337
|
+
};
|
|
31338
|
+
/** An unpin beat's script, and `gtd abandon`'s removal of the pin. */
|
|
31339
|
+
const unpinScript = (path) => `rm -f -- ${shellQuote$1(path)}`;
|
|
31340
|
+
/**
|
|
31341
|
+
* An approval records exactly the judged tree: HEAD is pinned before the
|
|
31342
|
+
* clean check and the commit is built over its tree with `commit-tree`, so no
|
|
31343
|
+
* hook runs and none can add to it. A HEAD that moved fails the guarded
|
|
31344
|
+
* `update-ref`. `commit-tree` ignores `commit.gpgSign`, so it is read here.
|
|
31345
|
+
*/
|
|
31346
|
+
const commitClean = (message) => [
|
|
31347
|
+
`gtd_head=$(git rev-parse --verify HEAD) || exit 1`,
|
|
31348
|
+
`if [ -n "$(git status --porcelain --untracked-files=all --ignore-submodules=none)" ]; then`,
|
|
31349
|
+
` printf '%s\\n' ${shellQuote$1(CHANGED_SINCE_LAND)} >&2`,
|
|
31350
|
+
` exit 1`,
|
|
31351
|
+
`fi`,
|
|
31352
|
+
`gtd_sign_cfg=$(git config --type=bool --default=false commit.gpgSign) || exit 1`,
|
|
31353
|
+
`if [ "$gtd_sign_cfg" = true ]; then gtd_sign=-S; else gtd_sign=--no-gpg-sign; fi`,
|
|
31354
|
+
`gtd_tip=$(git commit-tree "$gtd_sign" "$gtd_head^{tree}" -p "$gtd_head" -m ${shellQuote$1(message)}) || exit 1`,
|
|
31355
|
+
`git update-ref -m ${shellQuote$1(`commit: ${message}`)} HEAD "$gtd_tip" "$gtd_head" || exit 1`
|
|
31356
|
+
].join("\n");
|
|
31357
|
+
const mixedResetTo = (ref) => `git reset --mixed ${shellQuote$1(ref)}`;
|
|
31358
|
+
const hardResetTo = (ref) => `git reset --hard ${shellQuote$1(ref)}`;
|
|
31359
|
+
/**
|
|
31360
|
+
* POSIX sh (runs under dash). The marker is a `commit-tree` over HEAD's own
|
|
31361
|
+
* tree, so no hook runs on it and its tree is the pre-turn tree by
|
|
31362
|
+
* construction; `commit-tree` ignores `commit.gpgSign`, hence the explicit
|
|
31363
|
+
* `-S`, read (and failing closed) before any object or ref is written; the
|
|
31364
|
+
* index is already staged by then, so an early exit leaves the turn staged.
|
|
31365
|
+
* The kept ref is deleted only after `reset -q` succeeds: it is then the
|
|
31366
|
+
* turn's only handle.
|
|
31367
|
+
* The branch moves before the final hard reset, so if that reset fails the
|
|
31368
|
+
* marker has landed and the turn is still in the working tree (kept ref
|
|
31369
|
+
* present); every other failure leaves the branch untouched.
|
|
31370
|
+
*/
|
|
31371
|
+
const keepTurn = (refPrefix, message) => {
|
|
31372
|
+
const keptRef = `${shellQuote$1(`${refPrefix}/`)}"$gtd_kept"`;
|
|
31373
|
+
return [
|
|
31374
|
+
`git add -A || exit 1`,
|
|
31375
|
+
`gtd_sign_cfg=$(git config --type=bool --default=false commit.gpgSign) || exit 1`,
|
|
31376
|
+
`if [ "$gtd_sign_cfg" = true ]; then gtd_sign=-S; else gtd_sign=--no-gpg-sign; fi`,
|
|
31377
|
+
`gtd_head=$(git rev-parse --verify HEAD) || exit 1`,
|
|
31378
|
+
`gtd_tree=$(git write-tree) || exit 1`,
|
|
31379
|
+
`gtd_kept=$(git commit-tree "$gtd_tree" -p "$gtd_head" -m ${shellQuote$1(message)}) || exit 1`,
|
|
31380
|
+
`git update-ref ${keptRef} "$gtd_kept" || exit 1`,
|
|
31381
|
+
`gtd_marker=$(printf '%s\\nGtd-Kept: %s' ${shellQuote$1(message)} "$gtd_kept")`,
|
|
31382
|
+
`if ! gtd_tip=$(git commit-tree "$gtd_sign" "$gtd_head^{tree}" -p "$gtd_head" -m "$gtd_marker") || ! git update-ref -m ${shellQuote$1(`commit: ${message}`)} HEAD "$gtd_tip" "$gtd_head"; then`,
|
|
31383
|
+
` git reset -q && git update-ref -d ${keptRef}`,
|
|
31384
|
+
` exit 1`,
|
|
31385
|
+
`fi`,
|
|
31386
|
+
`git reset --hard HEAD`
|
|
31387
|
+
].join("\n");
|
|
31388
|
+
};
|
|
31389
|
+
const updateRef = (ref, hash) => `git update-ref ${shellQuote$1(ref)} ${shellQuote$1(hash)}`;
|
|
31390
|
+
/** `git update-ref -d <ref>` — idempotent: deleting a missing ref is already a no-op in real git. */
|
|
31391
|
+
const deleteRef = (ref) => `git update-ref -d ${shellQuote$1(ref)}`;
|
|
31392
|
+
/**
|
|
31393
|
+
* No `+` on the refspec: a local ref the remote's tip does not descend from
|
|
31394
|
+
* fails the fetch instead of being overwritten (a fast-forward still applies).
|
|
31395
|
+
*/
|
|
31396
|
+
const fetchHistoryRefs = (remote) => [
|
|
31397
|
+
`set -eu`,
|
|
31398
|
+
`before=$(git for-each-ref --format='x' 'refs/gtd/history/' | wc -l)`,
|
|
31399
|
+
`git fetch ${shellQuote$1(remote)} 'refs/gtd/history/*:refs/gtd/history/*'`,
|
|
31400
|
+
`after=$(git for-each-ref --format='x' 'refs/gtd/history/' | wc -l)`,
|
|
31401
|
+
`printf '%s history refs arrived\\n' "$((after - before))"`
|
|
31402
|
+
].join("\n");
|
|
31403
|
+
/** Marks an emitted block as print-only — mirrors `OutcomeScript.ts`'s `OUTCOME_MARKER` verbatim, so `src/twins/EmittedScriptRecognizer.ts` recognizes either path's output identically. */
|
|
31404
|
+
const OUTCOME_MARKER$1 = "# gtd: outcome (print-only)";
|
|
31405
|
+
const printfLine$1 = (fmt, args) => `${OUTCOME_MARKER$1}\nprintf ${shellQuote$1(fmt.replace(/\n/g, "\\n"))} ${args.join(" ")}`;
|
|
31406
|
+
const renderOutcome = (outcome) => {
|
|
31407
|
+
switch (outcome.kind) {
|
|
31408
|
+
case "transition": return printfLine$1("%s %s → %s\n", [
|
|
31409
|
+
"'->'",
|
|
31410
|
+
shellQuote$1(outcome.from),
|
|
31411
|
+
shellQuote$1(outcome.to)
|
|
31412
|
+
]);
|
|
31413
|
+
case "commit": return printfLine$1("%s %s\n", ["'[commit]'", shellQuote$1(outcome.subject)]);
|
|
31414
|
+
case "note": return printfLine$1("%s\n", [shellQuote$1(outcome.text)]);
|
|
31415
|
+
}
|
|
31416
|
+
};
|
|
31417
|
+
const renderGitWrite = (write) => {
|
|
31418
|
+
switch (write.kind) {
|
|
31419
|
+
case "commitAll": return commitAll(write.message);
|
|
31420
|
+
case "commitPath": return commitPath(write.path, write.message);
|
|
31421
|
+
case "commitClean": return commitClean(write.message);
|
|
31422
|
+
case "updateRef": return updateRef(write.ref, write.target);
|
|
31423
|
+
case "keepTurn": return keepTurn(write.ref, write.message);
|
|
31424
|
+
}
|
|
31425
|
+
};
|
|
31426
|
+
/**
|
|
31427
|
+
* Wraps `command` so a non-zero exit prints `prompt` plus the command's
|
|
31428
|
+
* captured output before propagating that exit code — mirrors `Emit.ts`'s
|
|
31429
|
+
* `failurePromptWrapper` (same reverse-import concern as `renderOutcome`
|
|
31430
|
+
* above: `Emit.ts` imports `shellQuote` from here already).
|
|
31431
|
+
*/
|
|
31432
|
+
const failurePromptWrapper$1 = (command, prompt) => {
|
|
31433
|
+
return [
|
|
31434
|
+
`gtd_validate_status=0`,
|
|
31435
|
+
`gtd_validate_out="$( {`,
|
|
31436
|
+
command,
|
|
31437
|
+
`} 2>&1 )" || gtd_validate_status=$?`,
|
|
31438
|
+
`if [ "$gtd_validate_status" -ne 0 ]; then`,
|
|
31439
|
+
` printf '%s\\n\\n%s\\n' ${shellQuote$1(prompt)} "$gtd_validate_out"`,
|
|
31440
|
+
` exit "$gtd_validate_status"`,
|
|
31441
|
+
`fi`
|
|
31442
|
+
].join("\n");
|
|
31443
|
+
};
|
|
31444
|
+
const renderLandStep = (step) => {
|
|
31445
|
+
if (step.kind === "gitWrite") return renderGitWrite(step.write);
|
|
31446
|
+
if (step.kind === "outcome") return renderOutcome(step.outcome);
|
|
31447
|
+
if (step.kind === "uncheck") return `gtd uncheck ${shellQuote$1(step.file)}`;
|
|
31448
|
+
return step.onFailure !== void 0 ? failurePromptWrapper$1(step.command, step.onFailure) : step.command;
|
|
31449
|
+
};
|
|
31450
|
+
const ScriptSurface = { render: (steps) => {
|
|
31451
|
+
const rendered = steps.map(renderLandStep);
|
|
31452
|
+
return rendered.length === 0 ? "" : ["set -eu", ...rendered].join("\n\n");
|
|
31453
|
+
} };
|
|
31454
|
+
//#endregion
|
|
31455
|
+
//#region src/emit/Emit.ts
|
|
31456
|
+
/** Exits 0 when the declared steering file is absent — nothing to format or validate yet. An OR list, not `if`: its left side failing never trips `set -e`. */
|
|
31457
|
+
const fileExistsGuard = (file) => `[ -f ${shellQuote$1(file)} ] || exit 0`;
|
|
31458
|
+
/** A mode or `ui.format` command finds the file it works on in `$GTD_FILE`. */
|
|
31459
|
+
const withFileVar$1 = (command, file) => `export GTD_FILE=${shellQuote$1(file)}\n${command}`;
|
|
31460
|
+
const SIMPLE_LEADING_WORD_RE = /^[A-Za-z0-9_./-]+(?=[ \t]|$)/;
|
|
31461
|
+
const SHELL_METACHARACTER_RE = /[|&;$<>(){}`]/;
|
|
31462
|
+
const FILE_VAR_RE = /"\$GTD_FILE"|"\$\{GTD_FILE\}"|\$GTD_FILE\b|\$\{GTD_FILE\}/g;
|
|
31463
|
+
/** `command` with `file` written in for `$GTD_FILE` — one line an agent can copy, equivalent to running it under `withFileVar`. */
|
|
31464
|
+
const commandForFile = (command, file) => command.replace(FILE_VAR_RE, shellQuote$1(file));
|
|
31465
|
+
/**
|
|
31466
|
+
* The leading word of a rendered `format:`/`validate:` command, when the
|
|
31467
|
+
* command has exactly one unambiguous binary to probe — `undefined` for
|
|
31468
|
+
* anything a shell metacharacter could hide a second command behind. A missing
|
|
31469
|
+
* guard degrades to a raw exit; a wrong guard would refuse a working command.
|
|
31470
|
+
*/
|
|
31471
|
+
const extractLeadingBinary = (command) => {
|
|
31472
|
+
if (command.includes("\n")) return void 0;
|
|
31473
|
+
if (SHELL_METACHARACTER_RE.test(command.replace(FILE_VAR_RE, ""))) return void 0;
|
|
31474
|
+
const match = SIMPLE_LEADING_WORD_RE.exec(command);
|
|
31475
|
+
return match === null ? void 0 : match[0];
|
|
31476
|
+
};
|
|
31477
|
+
/**
|
|
31478
|
+
* Names a typo'd or uninstalled binary and the `$PATH` it was looked up on.
|
|
31479
|
+
* `$PATH` is a shell word the DRIVER's shell expands at run time, so gtd never
|
|
31480
|
+
* resolves it in-process. Exits 127, matching bash's "command not found".
|
|
31481
|
+
*/
|
|
31482
|
+
const binaryGuard = (binary, mode, key) => {
|
|
31483
|
+
const messageQ = shellQuote$1(`gtd: mode "${mode}": "${key}" command not found: ${binary}`);
|
|
31484
|
+
return [
|
|
31485
|
+
`command -v ${shellQuote$1(binary)} >/dev/null 2>&1 || {`,
|
|
31486
|
+
` printf '%s\\n%s\\n' ${messageQ} '$PATH: '"$PATH"`,
|
|
31487
|
+
` exit 127`,
|
|
31488
|
+
`}`
|
|
31489
|
+
].join("\n");
|
|
31490
|
+
};
|
|
31491
|
+
/**
|
|
31492
|
+
* Prints `prompt` plus the captured output on a non-zero exit, then propagates
|
|
31493
|
+
* it. The `{ … }` group inlines a multi-line `command` verbatim; the
|
|
31494
|
+
* assignment sits left of `||` so `set -e` never trips on the failure itself.
|
|
31495
|
+
*/
|
|
31496
|
+
const failurePromptWrapper = (command, prompt) => {
|
|
31497
|
+
return [
|
|
31498
|
+
`gtd_validate_status=0`,
|
|
31499
|
+
`gtd_validate_out="$( {`,
|
|
31500
|
+
command,
|
|
31501
|
+
`} 2>&1 )" || gtd_validate_status=$?`,
|
|
31502
|
+
`if [ "$gtd_validate_status" -ne 0 ]; then`,
|
|
31503
|
+
` printf '%s\\n\\n%s\\n' ${shellQuote$1(prompt)} "$gtd_validate_out"`,
|
|
31504
|
+
` exit "$gtd_validate_status"`,
|
|
31505
|
+
`fi`
|
|
31506
|
+
].join("\n");
|
|
31507
|
+
};
|
|
31508
|
+
const renderStep = (step) => {
|
|
31509
|
+
if (step.kind === "command" && step.onFailure !== void 0) return failurePromptWrapper(step.command, step.onFailure);
|
|
31510
|
+
return step.command;
|
|
31511
|
+
};
|
|
31512
|
+
const assembleScript = (steps) => steps.length === 0 ? "" : ["set -eu", ...steps.map(renderStep)].join("\n\n");
|
|
31513
|
+
/** Build both halves from already-rendered command strings; an empty (or omitted) array produces the empty-string result, so a driver checking `if [ -n "$required" ]` never has to special-case a bare preamble. */
|
|
31514
|
+
const emitScripts = (required = [], optional = []) => ({
|
|
31515
|
+
required: assembleScript(required),
|
|
31516
|
+
optional: assembleScript(optional)
|
|
31517
|
+
});
|
|
31518
|
+
/** Exported so `src/twins/EmittedScriptRecognizer.ts` can recognize these by exact string comparison. `printf`, not `echo`, avoids shell-specific backslash-escape quirks. */
|
|
31519
|
+
const DID_NOT_RUN_COMMENT = "# gtd emitted this and did NOT run it — run it with `sh` to apply it";
|
|
31520
|
+
const PRESENTATION_ONLY_COMMENT = "# presentation only — safe to skip";
|
|
31521
|
+
const PRESENTATION_FAILURE_WARNING = "printf 'gtd: presentation-only follow-up failed — continuing\\n' >&2";
|
|
31522
|
+
/**
|
|
31523
|
+
* A write command's single pasteable script (`gtd abandon | sh`,
|
|
31524
|
+
* `gtd land --json=script`'s own field). `optional`, when non-empty, is
|
|
31525
|
+
* wrapped in a subshell whose failure is swallowed — presentation-only, so it
|
|
31526
|
+
* must never turn a landed turn into a non-zero exit.
|
|
31527
|
+
*/
|
|
31528
|
+
const combinedScript = (required, optional) => {
|
|
31529
|
+
if (required.length === 0) return "";
|
|
31530
|
+
if (optional.length === 0) return `${DID_NOT_RUN_COMMENT}\n\n${required}`;
|
|
31531
|
+
return [
|
|
31532
|
+
DID_NOT_RUN_COMMENT,
|
|
31533
|
+
"",
|
|
31534
|
+
required,
|
|
31535
|
+
"",
|
|
31536
|
+
PRESENTATION_ONLY_COMMENT,
|
|
31537
|
+
`(\n${optional}\n) || ${PRESENTATION_FAILURE_WARNING}`
|
|
31538
|
+
].join("\n");
|
|
31539
|
+
};
|
|
31540
|
+
//#endregion
|
|
31541
|
+
//#region src/emit/OutcomeScript.ts
|
|
31542
|
+
/**
|
|
31543
|
+
* A `printf '<fmt>' <args...>` bash statement — `args` are already-valid bash
|
|
31544
|
+
* tokens (a `shellQuote`d literal, a `"$(git …)"` substitution), never
|
|
31545
|
+
* re-quoted here: a value reaches the script as a printf ARGUMENT, never
|
|
31546
|
+
* interpolated into the format itself, so a subject or state name containing
|
|
31547
|
+
* `%` can never be read as a conversion spec. Markers like `->` are arguments
|
|
31548
|
+
* too, keeping every format string free of a leading `-` that a shell's
|
|
31549
|
+
* `printf` could mistake for an option. A real newline in `fmt` is emitted as
|
|
31550
|
+
* the two-character `\n` escape `printf` itself expands, keeping every emitted
|
|
31551
|
+
* statement on one line.
|
|
31552
|
+
*/
|
|
31553
|
+
const printfLine = (fmt, args) => `${OUTCOME_MARKER}\nprintf ${shellQuote$1(fmt.replace(/\n/g, "\\n"))} ${args.join(" ")}`;
|
|
31554
|
+
/**
|
|
31555
|
+
* Marks an emitted block as print-only. Load-bearing beyond documentation:
|
|
31556
|
+
* `src/twins/EmittedScriptRecognizer.ts` recognizes an outcome block by
|
|
31557
|
+
* this line, and a bare `printf` would be ambiguous with a workflow
|
|
31558
|
+
* `script:` command that happens to start the same way.
|
|
31559
|
+
*/
|
|
31560
|
+
const OUTCOME_MARKER = "# gtd: outcome (print-only)";
|
|
31561
|
+
const FMT_ABANDON_NOOP = "no gtd process is underway (resting at \"%s\") — nothing to abandon\n";
|
|
31562
|
+
const FMT_ABANDONED = "abandoned the process resting at \"%s\" — HEAD is back at %s (\"%s\"), resting at \"%s\".\nEverything the process produced is kept as uncommitted changes (`git status`); discard them with `git checkout -- . && git clean -fd .gtd` for a clean tree.\n";
|
|
31563
|
+
const FMT_RESTORED = "restored the retained history — HEAD is back at %s (\"%s\"), resting at \"%s\". Resume with the loop, or `git reset` to any earlier turn to restart from there.\n";
|
|
31564
|
+
/** `no gtd process is underway (resting at "<initial>") — nothing to abandon` — `gtd abandon`'s no-op plain-text line. */
|
|
31565
|
+
const abandonNoopText = (initial) => renderFormat(FMT_ABANDON_NOOP, initial);
|
|
31566
|
+
/** One already-rendered plain line. */
|
|
31567
|
+
const noteOutcome = (text) => printfLine("%s\n", [shellQuote$1(text)]);
|
|
31568
|
+
/**
|
|
31569
|
+
* Resolves the post-hoc short hash/subject from `head` (a commitish) in-script
|
|
31570
|
+
* — this line runs AFTER the reset that made `head` the new HEAD, so neither
|
|
31571
|
+
* value is knowable when gtd generates it.
|
|
31572
|
+
*/
|
|
31573
|
+
const abandonedOutcome = (from, head, state) => printfLine(FMT_ABANDONED, [
|
|
31574
|
+
shellQuote$1(from),
|
|
31575
|
+
`"$(git rev-parse --short ${shellQuote$1(head)})"`,
|
|
31576
|
+
`"$(git log -1 --format=%s ${shellQuote$1(head)})"`,
|
|
31577
|
+
shellQuote$1(state)
|
|
31578
|
+
]);
|
|
31579
|
+
/** `gtd abandon`'s no-op outcome — the same wording `abandonNoopText` renders. */
|
|
31580
|
+
const abandonNoopOutcome = (initial) => noteOutcome(abandonNoopText(initial));
|
|
31581
|
+
/** Resolves the post-hoc short hash/subject from `to` (a commitish) in-script, for the same reason `abandonedOutcome` does. */
|
|
31582
|
+
const restoredOutcome = (to, state) => printfLine(FMT_RESTORED, [
|
|
31583
|
+
`"$(git rev-parse --short ${shellQuote$1(to)})"`,
|
|
31584
|
+
`"$(git log -1 --format=%s ${shellQuote$1(to)})"`,
|
|
31585
|
+
shellQuote$1(state)
|
|
31586
|
+
]);
|
|
31587
|
+
//#endregion
|
|
31588
|
+
//#region src/emit/ModeContradiction.ts
|
|
31589
|
+
/**
|
|
31590
|
+
* Written for two readers at once, since gtd validate's exit code carries no
|
|
31591
|
+
* second meaning to distinguish them: the agent (this is a CONFIGURATION BUG,
|
|
31592
|
+
* not the steering file — stop, don't edit it, or a fix-retry loop will
|
|
31593
|
+
* thrash on the document) and the human (which mode, and the exact rendered
|
|
31594
|
+
* `format:` command, to fix `gtd.config.ts` without reproducing the failure).
|
|
31595
|
+
*/
|
|
31596
|
+
const contradictionMessage = (mode, formatCommand) => `gtd: mode "${mode}"'s format: command breaks its own validator — this is a CONFIGURATION BUG, not a problem with the steering file. Do NOT edit the steering file — stop and end your turn now.\nThe rendered format: command that ran:\n${formatCommand}\nWhat it turned mode "${mode}"'s own canonical sample into:`;
|
|
31597
|
+
/**
|
|
31598
|
+
* Formats a copy of the mode's own canonical sample and re-validates it with
|
|
31599
|
+
* `gtd check <mode>`, failing loudly when the round-trip breaks it — detects a
|
|
31600
|
+
* mode whose `format:` contradicts its own `validate:` without ever touching
|
|
31601
|
+
* the real steering file. `printf '%s'` over a `shellQuote`d sample, never a
|
|
31602
|
+
* heredoc, since no delimiter chosen ahead of time can collide with the
|
|
31603
|
+
* sample's own bytes. One bash fragment with no blank lines (so
|
|
31604
|
+
* `src/emit/Emit.ts`'s `assembleScript` never splits it from a sibling step), meant
|
|
31605
|
+
* to run BEFORE `[ -f <file> ] || exit 0` in the emitted validate script so it
|
|
31606
|
+
* still fires at a first-write beat where the real steering file doesn't
|
|
31607
|
+
* exist yet.
|
|
31608
|
+
*/
|
|
31609
|
+
const buildModeContradictionCheck = (inputs) => {
|
|
31610
|
+
const { mode, samplePath, sample, formatCommand } = inputs;
|
|
31611
|
+
const pathQ = shellQuote$1(samplePath);
|
|
31612
|
+
const messageQ = shellQuote$1(contradictionMessage(mode, formatCommand));
|
|
31613
|
+
return [
|
|
31614
|
+
`printf '%s' ${shellQuote$1(sample)} > ${pathQ}`,
|
|
31615
|
+
formatCommand,
|
|
31616
|
+
`gtd check ${mode} ${pathQ} >/dev/null 2>&1 || {`,
|
|
31617
|
+
` printf '%s\\n' ${messageQ} >&2`,
|
|
31618
|
+
` cat ${pathQ} >&2`,
|
|
31619
|
+
` rm -f ${pathQ}`,
|
|
31620
|
+
` exit 1`,
|
|
31621
|
+
`}`,
|
|
31622
|
+
`rm -f ${pathQ}`
|
|
31623
|
+
].join("\n");
|
|
31624
|
+
};
|
|
31625
|
+
/**
|
|
31626
|
+
* Skip notice for a mode whose validator is EXTERNAL (a user `validate:`
|
|
31627
|
+
* command, not one of the two built-in modes) — there's no in-process parser
|
|
31628
|
+
* to round-trip a sample through. Printed to stderr since silence would read
|
|
31629
|
+
* as a clean bill of health.
|
|
31630
|
+
*/
|
|
31631
|
+
const modeContradictionSkipNotice = (mode) => {
|
|
31632
|
+
return `printf '%s\\n' ${shellQuote$1(`gtd: mode "${mode}" has an external validate: command — skipping the format/validate contradiction check`)} >&2`;
|
|
31633
|
+
};
|
|
31634
|
+
//#endregion
|
|
31635
|
+
//#region src/emit/SteeringMode.ts
|
|
31636
|
+
init_dist$1();
|
|
31637
|
+
init_steering();
|
|
31638
|
+
/**
|
|
31639
|
+
* `format` alone (no live/external verdict) when there's no validator at
|
|
31640
|
+
* all; a live in-process validator for a `"builtin"` validate OR a command
|
|
31641
|
+
* that is exactly the format's own seeded string; `externalValidate` for any
|
|
31642
|
+
* other command. Each arm returns directly — a flat sequence, not nested
|
|
31643
|
+
* ternaries — so the four outcomes stay each their own case to read, not a
|
|
31644
|
+
* boolean expression to re-derive.
|
|
31645
|
+
*/
|
|
31646
|
+
const capabilitiesFor = (mode, format, validate) => {
|
|
31647
|
+
if (format === void 0) return validate?.kind === "command" ? { externalValidate: true } : {};
|
|
31648
|
+
if (validate?.kind === "builtin") return {
|
|
31649
|
+
format,
|
|
31650
|
+
liveValidate: format.validate
|
|
31651
|
+
};
|
|
31652
|
+
if (validate === void 0) return { format };
|
|
31653
|
+
if (isSeededValidateCommand(mode, validate.command)) return {
|
|
31654
|
+
format,
|
|
31655
|
+
liveValidate: format.validate
|
|
31656
|
+
};
|
|
31657
|
+
return {
|
|
31658
|
+
format,
|
|
31659
|
+
externalValidate: true
|
|
31660
|
+
};
|
|
31661
|
+
};
|
|
31662
|
+
/**
|
|
31663
|
+
* Resolve a `mode:` name against `def.modes` plus the built-in registry
|
|
31664
|
+
* (`src/workflow/SteeringFormats.ts`) — half by half: a declared `format:`/`validate:`
|
|
31665
|
+
* wins, and an undeclared `validate:` falls back to the built-in format's own
|
|
31666
|
+
* parser when the name is registered. `format` is set from the registry
|
|
31667
|
+
* ALONE, independent of which half of `validate` wins — a declared validator
|
|
31668
|
+
* overrides validation, not the format identity.
|
|
31669
|
+
*
|
|
31670
|
+
* `def: undefined` is the LSP's built-in-only fallback — resolving `mode`
|
|
31671
|
+
* against the registry alone, with no workflow definition in hand (e.g. the
|
|
31672
|
+
* basename dispatch for a `REVIEW.md` no config maps).
|
|
31673
|
+
*
|
|
31674
|
+
* The `"unknown"` arm carries the whole refusal message, naming what IS known
|
|
31675
|
+
* when `def` is given — this is the one place that message is built, so it
|
|
31676
|
+
* can never drift from what actually resolved.
|
|
31677
|
+
*/
|
|
31678
|
+
const resolveMode = (def, state, mode) => {
|
|
31679
|
+
const declared = def?.modes?.[mode];
|
|
31680
|
+
const format = steeringFormatFor(mode);
|
|
31681
|
+
if (declared === void 0 && format === void 0) return {
|
|
31682
|
+
kind: "unknown",
|
|
31683
|
+
message: def !== void 0 ? `state "${state}": mode "${mode}" is not defined by the active workflow (known modes: ${knownModes(def).join(", ")})` : `mode "${mode}" is not a built-in steering format`
|
|
31684
|
+
};
|
|
31685
|
+
const validate = declared?.validate !== void 0 ? {
|
|
31686
|
+
kind: "command",
|
|
31687
|
+
command: declared.validate
|
|
31688
|
+
} : format !== void 0 ? {
|
|
31689
|
+
kind: "builtin",
|
|
31690
|
+
format
|
|
31691
|
+
} : void 0;
|
|
31692
|
+
return {
|
|
31693
|
+
kind: "resolved",
|
|
31694
|
+
mode,
|
|
31695
|
+
...format !== void 0 ? { format } : {},
|
|
31696
|
+
...declared?.format !== void 0 ? { formatCommand: declared.format } : {},
|
|
31697
|
+
...validate !== void 0 ? { validate } : {},
|
|
31698
|
+
capabilities: capabilitiesFor(mode, format, validate)
|
|
31699
|
+
};
|
|
31700
|
+
};
|
|
31701
|
+
/** `gtd next`'s fix-retry instruction, printed ahead of the failing command's captured output. */
|
|
31702
|
+
const fixPromptInstruction = (file) => `Your last turn does not pass its own validation script. Fix these format violations in ${file}, then finish:`;
|
|
31703
|
+
/**
|
|
31704
|
+
* Pushes a `binaryGuard` step ahead of `command` when its own leading word
|
|
31705
|
+
* is unambiguous — BEFORE, not wrapped around: a guard that fails must stop
|
|
31706
|
+
* the script on its own `exit 127`, without touching `failurePromptWrapper`'s
|
|
31707
|
+
* captured-output path. Each command guards on its OWN leading word — a
|
|
31708
|
+
* guard naming the wrong binary is worse than none.
|
|
31709
|
+
*/
|
|
31710
|
+
const pushGuardedCommand = (steps, mode, key, command, file, onFailure) => {
|
|
31711
|
+
const binary = extractLeadingBinary(command);
|
|
31712
|
+
if (binary !== void 0) steps.push({
|
|
31713
|
+
kind: "command",
|
|
31714
|
+
command: binaryGuard(binary, mode, key)
|
|
31715
|
+
});
|
|
31716
|
+
steps.push({
|
|
31717
|
+
kind: "command",
|
|
31718
|
+
command: withFileVar$1(command, file),
|
|
31719
|
+
...onFailure !== void 0 ? { onFailure } : {}
|
|
31720
|
+
});
|
|
31721
|
+
};
|
|
31722
|
+
/**
|
|
31723
|
+
* `resolved`'s `format:`/`validate:` as `EmitStep[]` — format first,
|
|
31724
|
+
* validate last, the last one wrapped with `onFailure` when the validator is
|
|
31725
|
+
* a command (a built-in's own in-process parser has no shell step to wrap:
|
|
31726
|
+
* `gtd check`'s seeded command carries that instead). Each command gets its
|
|
31727
|
+
* own `binaryGuard` immediately ahead of it.
|
|
31728
|
+
*/
|
|
31729
|
+
const modeCommandSteps = (resolved, file) => {
|
|
31730
|
+
const steps = [];
|
|
31731
|
+
if (resolved.formatCommand !== void 0) pushGuardedCommand(steps, resolved.mode, "format", resolved.formatCommand, file, void 0);
|
|
31732
|
+
if (resolved.validate?.kind === "command") pushGuardedCommand(steps, resolved.mode, "validate", resolved.validate.command, file, fixPromptInstruction(file));
|
|
31733
|
+
return steps;
|
|
31734
|
+
};
|
|
31735
|
+
/** `<Host.scratchDir>/gtd-mode-sample-<mode>-<pid>.md` — an absolute literal baked in at emit time, never a shell variable. `<pid>` avoids collisions between concurrent `gtd` processes. Never a `/tmp` literal or `mktemp` (`tests/tooling/no-tmp-assumption.test.ts` scans for both). */
|
|
31736
|
+
const scratchSamplePath = (scratchDir, mode) => join(scratchDir, `gtd-mode-sample-${mode}-${process.pid}.md`);
|
|
31737
|
+
/**
|
|
31738
|
+
* The contradiction round-trip/skip-notice steps for `resolved`'s `mode:`.
|
|
31739
|
+
* No `formatCommand` means nothing to round-trip: empty. Otherwise: a live
|
|
31740
|
+
* built-in validator runs the round-trip against that format's own canonical
|
|
31741
|
+
* sample; an external validator prints a one-line skip notice instead, since
|
|
31742
|
+
* silence there would read as a clean bill of health; a format-only mode with
|
|
31743
|
+
* neither has nothing to round-trip either.
|
|
31744
|
+
*/
|
|
31745
|
+
const modeContradictionSteps = (resolved) => gen(function* () {
|
|
31746
|
+
if (resolved.formatCommand === void 0) return [];
|
|
31747
|
+
const { capabilities } = resolved;
|
|
31748
|
+
if (capabilities.format !== void 0 && capabilities.liveValidate !== void 0) {
|
|
31749
|
+
const { scratchDir } = yield* Host;
|
|
31750
|
+
const samplePath = scratchSamplePath(scratchDir, resolved.mode);
|
|
31751
|
+
const formatCommand = withFileVar$1(resolved.formatCommand, samplePath);
|
|
31752
|
+
return [{
|
|
31753
|
+
kind: "command",
|
|
31754
|
+
command: buildModeContradictionCheck({
|
|
31755
|
+
mode: resolved.mode,
|
|
31756
|
+
samplePath,
|
|
31757
|
+
sample: capabilities.format.sample,
|
|
31758
|
+
formatCommand
|
|
31759
|
+
})
|
|
31760
|
+
}];
|
|
31761
|
+
}
|
|
31762
|
+
if (capabilities.externalValidate === true) return [{
|
|
31763
|
+
kind: "command",
|
|
31764
|
+
command: modeContradictionSkipNotice(resolved.mode)
|
|
31765
|
+
}];
|
|
31766
|
+
return [];
|
|
31767
|
+
});
|
|
31768
|
+
/**
|
|
31769
|
+
* Renders `resolved`'s complete validate script for `file`: the mode's
|
|
31770
|
+
* format/validate contradiction check, a guard for a not-yet-written file,
|
|
31771
|
+
* then the format/validate commands themselves (format first). gtd never
|
|
31772
|
+
* runs this script itself — `resolveMode`+`validateScriptFor` only render
|
|
31773
|
+
* text for a driver to execute (`gtd renders; the driver executes`).
|
|
31774
|
+
*/
|
|
31775
|
+
const validateScriptFor = (resolved, file) => gen(function* () {
|
|
31776
|
+
return { script: emitScripts([
|
|
31777
|
+
...yield* modeContradictionSteps(resolved),
|
|
31778
|
+
{
|
|
31779
|
+
kind: "command",
|
|
31780
|
+
command: fileExistsGuard(file)
|
|
31781
|
+
},
|
|
31782
|
+
...modeCommandSteps(resolved, file)
|
|
31783
|
+
]).required };
|
|
31162
31784
|
});
|
|
31163
31785
|
//#endregion
|
|
31164
31786
|
//#region src/step/planStep.ts
|
|
@@ -31171,6 +31793,26 @@ const uncheckStep = (snapshot) => snapshot.stepDef.actor === "human" && snapshot
|
|
|
31171
31793
|
kind: "uncheck",
|
|
31172
31794
|
file: snapshot.file
|
|
31173
31795
|
}] : [];
|
|
31796
|
+
const gitWriteOf = (landing, message) => {
|
|
31797
|
+
if (landing.only !== void 0) return {
|
|
31798
|
+
kind: "commitPath",
|
|
31799
|
+
path: landing.only,
|
|
31800
|
+
message
|
|
31801
|
+
};
|
|
31802
|
+
if (landing.keepTurn !== void 0) return {
|
|
31803
|
+
kind: "keepTurn",
|
|
31804
|
+
ref: landing.keepTurn,
|
|
31805
|
+
message
|
|
31806
|
+
};
|
|
31807
|
+
if (landing.spec.approved !== void 0) return {
|
|
31808
|
+
kind: "commitClean",
|
|
31809
|
+
message
|
|
31810
|
+
};
|
|
31811
|
+
return {
|
|
31812
|
+
kind: "commitAll",
|
|
31813
|
+
message
|
|
31814
|
+
};
|
|
31815
|
+
};
|
|
31174
31816
|
const planStep = (snapshot, opts = {}) => {
|
|
31175
31817
|
const landing = snapshot.landing;
|
|
31176
31818
|
if (landing.kind === "refusal") return {
|
|
@@ -31241,17 +31883,7 @@ const planStep = (snapshot, opts = {}) => {
|
|
|
31241
31883
|
...uncheckStep(snapshot),
|
|
31242
31884
|
{
|
|
31243
31885
|
kind: "gitWrite",
|
|
31244
|
-
write: landing
|
|
31245
|
-
kind: "keepTurn",
|
|
31246
|
-
ref: landing.keepTurn,
|
|
31247
|
-
message
|
|
31248
|
-
} : landing.spec.approved !== void 0 ? {
|
|
31249
|
-
kind: "commitClean",
|
|
31250
|
-
message
|
|
31251
|
-
} : {
|
|
31252
|
-
kind: "commitAll",
|
|
31253
|
-
message
|
|
31254
|
-
}
|
|
31886
|
+
write: gitWriteOf(landing, message)
|
|
31255
31887
|
},
|
|
31256
31888
|
...landing.keepHistory !== void 0 ? [{
|
|
31257
31889
|
kind: "gitWrite",
|
|
@@ -31299,8 +31931,7 @@ const checkOverrides = (commandLabel, varOverrides, declaredNames, envNames) =>
|
|
|
31299
31931
|
* `gtd start <name>`: start a brand NEW process on a workflow, writing an
|
|
31300
31932
|
* opening commit that names the workflow's first step and carries a
|
|
31301
31933
|
* `Gtd-Workflow:` trailer, every process setting as a `Gtd-Var:` trailer, plus
|
|
31302
|
-
* a `Gtd-Review-Base:` trailer when the workflow fixes the process's diff base
|
|
31303
|
-
* and a `Gtd-Config:` trailer per config file.
|
|
31934
|
+
* and a `Gtd-Review-Base:` trailer when the workflow fixes the process's diff base.
|
|
31304
31935
|
* All validation is a REFUSAL, not an Effect failure.
|
|
31305
31936
|
*/
|
|
31306
31937
|
const planStart = (current, actor, start) => gen(function* () {
|
|
@@ -31338,8 +31969,7 @@ const planStart = (current, actor, start) => gen(function* () {
|
|
|
31338
31969
|
to: firstStep,
|
|
31339
31970
|
workflow: name,
|
|
31340
31971
|
...base !== void 0 ? { reviewBase: base } : {},
|
|
31341
|
-
vars
|
|
31342
|
-
config: start.config
|
|
31972
|
+
vars
|
|
31343
31973
|
})
|
|
31344
31974
|
}
|
|
31345
31975
|
}, {
|
|
@@ -31382,6 +32012,8 @@ const ACTORS = /* @__PURE__ */ new Set([
|
|
|
31382
32012
|
"check",
|
|
31383
32013
|
"judge"
|
|
31384
32014
|
]);
|
|
32015
|
+
const isOpening = ({ parsed, step }) => parsed !== void 0 && step === void 0 && parsed.from === void 0 && parsed.actor === "human";
|
|
32016
|
+
const boundsEpisode = (def, { parsed, pin }) => pin !== "add" && (parsed === void 0 || !ACTORS.has(parsed.actor) || parsed.to === def.initial);
|
|
31385
32017
|
/**
|
|
31386
32018
|
* Where the episode HEAD belongs to begins, read off subjects alone. Walking
|
|
31387
32019
|
* back from HEAD: a bare `gtd(human): <step>` commit with no `Gtd-Step` is what
|
|
@@ -31394,14 +32026,13 @@ const ACTORS = /* @__PURE__ */ new Set([
|
|
|
31394
32026
|
const locateEpisode = (def, history) => {
|
|
31395
32027
|
for (let i = history.length - 1; i >= 0; i--) {
|
|
31396
32028
|
const message = parseCommitMessage(history[i].message);
|
|
31397
|
-
|
|
31398
|
-
if (subject !== void 0 && message.step === void 0 && subject.from === void 0 && subject.actor === "human") return {
|
|
32029
|
+
if (isOpening(message)) return {
|
|
31399
32030
|
workflow: message.workflow,
|
|
31400
32031
|
legacyOpening: message.workflow === void 0,
|
|
31401
32032
|
baseIndex: i,
|
|
31402
32033
|
processStart: i
|
|
31403
32034
|
};
|
|
31404
|
-
if (
|
|
32035
|
+
if (boundsEpisode(def, message)) return {
|
|
31405
32036
|
workflow: void 0,
|
|
31406
32037
|
legacyOpening: false,
|
|
31407
32038
|
baseIndex: i,
|
|
@@ -31419,7 +32050,7 @@ const headTurnOf = (head) => {
|
|
|
31419
32050
|
if (head === void 0) return void 0;
|
|
31420
32051
|
const message = parseCommitMessage(head.message);
|
|
31421
32052
|
const subject = message.parsed;
|
|
31422
|
-
if (subject === void 0 || !ACTORS.has(subject.actor)) return void 0;
|
|
32053
|
+
if (subject === void 0 || !ACTORS.has(subject.actor) || message.pin !== void 0) return void 0;
|
|
31423
32054
|
return {
|
|
31424
32055
|
state: subject.to,
|
|
31425
32056
|
actor: subject.actor,
|
|
@@ -31447,15 +32078,6 @@ const runOf = (history, location, closingHash = void 0) => {
|
|
|
31447
32078
|
const fault = historyFormatFault(history, location);
|
|
31448
32079
|
return fault === void 0 ? succeed$1(buildRun(history, location, closingHash)) : fail$8(new Error(fault));
|
|
31449
32080
|
};
|
|
31450
|
-
const pinnedConfigOf = (history) => {
|
|
31451
|
-
for (let i = history.length - 1; i >= 0; i--) {
|
|
31452
|
-
const files = parseCommitMessage(history[i].message).config;
|
|
31453
|
-
if (Object.keys(files).length > 0) return {
|
|
31454
|
-
commit: history[i].hash,
|
|
31455
|
-
files
|
|
31456
|
-
};
|
|
31457
|
-
}
|
|
31458
|
-
};
|
|
31459
32081
|
const approvalEntryOf = (hash, message) => message.approved === void 0 ? [] : [{
|
|
31460
32082
|
step: formatStepId(message.approved.step),
|
|
31461
32083
|
name: message.approved.name,
|
|
@@ -31463,8 +32085,10 @@ const approvalEntryOf = (hash, message) => message.approved === void 0 ? [] : [{
|
|
|
31463
32085
|
commit: hash
|
|
31464
32086
|
}];
|
|
31465
32087
|
const buildRun = (history, location, closingHash) => {
|
|
31466
|
-
const
|
|
31467
|
-
const
|
|
32088
|
+
const allProcessCommits = history.slice(location.processStart);
|
|
32089
|
+
const allParsed = allProcessCommits.map((c) => parseCommitMessage(c.message));
|
|
32090
|
+
const processCommits = allProcessCommits.filter((_, i) => allParsed[i].pin !== "add");
|
|
32091
|
+
const parsed = allParsed.filter((m) => m.pin !== "add");
|
|
31468
32092
|
const first = parsed[0];
|
|
31469
32093
|
const startParentHash = hashAt(history, location.processStart - 1) ?? EMPTY_TREE$1;
|
|
31470
32094
|
const head = history[history.length - 1];
|
|
@@ -31477,10 +32101,10 @@ const buildRun = (history, location, closingHash) => {
|
|
|
31477
32101
|
trace: processCommits.map((c, i) => traceEntryOf(c.hash, parsed[i])),
|
|
31478
32102
|
costEntries: parsed.flatMap((m, i) => costEntriesOf(processCommits[i].hash, m)),
|
|
31479
32103
|
judgeVerdicts: parsed.flatMap((m) => m.judge),
|
|
32104
|
+
pinCommitted: allParsed.some((m) => m.pin === "add"),
|
|
31480
32105
|
marks: parsed.flatMap((m) => m.marks),
|
|
31481
32106
|
approvals: processCommits.flatMap((c, i) => approvalEntryOf(c.hash, parsed[i])),
|
|
31482
32107
|
pinnedVars: { ...first?.vars },
|
|
31483
|
-
pinnedConfig: pinnedConfigOf(history.slice(Math.max(location.baseIndex, 0))),
|
|
31484
32108
|
headTurn: headTurnOf(head),
|
|
31485
32109
|
closingHash,
|
|
31486
32110
|
episode: {
|
|
@@ -31516,6 +32140,7 @@ const subjectIsBoundary = (def, subject) => {
|
|
|
31516
32140
|
const parsed = parseCommitMessage(subject).parsed;
|
|
31517
32141
|
if (parsed === void 0 || !ACTORS.has(parsed.actor)) return true;
|
|
31518
32142
|
if (parsed.actor === "human" && parsed.from === void 0) return false;
|
|
32143
|
+
if (parsed.actor === "check" && parsed.from === PIN_STATES.add) return false;
|
|
31519
32144
|
return parsed.to === def.initial;
|
|
31520
32145
|
};
|
|
31521
32146
|
/** The last page index (walking newest→oldest, same direction as `locateEpisode`) whose subject is a boundary, or `undefined` if none in `page`. */
|
|
@@ -31569,18 +32194,6 @@ const findBoundaryBase = (git, def, head) => gen(function* () {
|
|
|
31569
32194
|
});
|
|
31570
32195
|
const historyUpTo = (git, def, head) => flatMap(findBoundaryBase(git, def, head), ({ base, head: resolvedHead }) => git.commitHistory(base, resolvedHead));
|
|
31571
32196
|
const computeProcessRun = (git, def, head) => flatMap(historyUpTo(git, def, head), (history) => runOf(history, locateEpisode(def, history)));
|
|
31572
|
-
const changedSinceStart = (pinned, current, workspace) => {
|
|
31573
|
-
if (pinned === void 0) return [];
|
|
31574
|
-
return changedConfigFiles(pinned.files, current).filter((path) => {
|
|
31575
|
-
const committed = workspace.readCommittedSync(path, pinned.commit);
|
|
31576
|
-
return current[path] === void 0 || committed === void 0 || contentHash(committed) !== current[path];
|
|
31577
|
-
});
|
|
31578
|
-
};
|
|
31579
|
-
const warnConfigChanges = (run, current, workspace) => gen(function* () {
|
|
31580
|
-
const changed = changedSinceStart(run.pinnedConfig, current, workspace);
|
|
31581
|
-
if (changed.length === 0) return;
|
|
31582
|
-
yield* (yield* Narrator).warn(`gtd: warning: the config changed since this process started (${changed.join(", ")}) — the rest of the process replays under the changed config`);
|
|
31583
|
-
});
|
|
31584
32197
|
/** The run alone, never replaying — `gtd abandon` must work even when replay would refuse, short of a history it cannot read. */
|
|
31585
32198
|
const currentRun = gen(function* () {
|
|
31586
32199
|
const git = yield* GitService;
|
|
@@ -31884,31 +32497,32 @@ const restAt = (ref, startOverrides = {}) => gen(function* () {
|
|
|
31884
32497
|
const run = yield* computeProcessRun(git, config.workflow, ref);
|
|
31885
32498
|
const { resolved, def, vars, env, setup } = yield* setupFor(config, run, workspace, host, startOverrides);
|
|
31886
32499
|
const outcome = yield* promise(() => replayFor(setup));
|
|
31887
|
-
const
|
|
31888
|
-
|
|
31889
|
-
const
|
|
31890
|
-
|
|
31891
|
-
const
|
|
32500
|
+
const pinned = headHasPin(workspace, run);
|
|
32501
|
+
const pin = pinBeatAt(outcome, run, def, pinned);
|
|
32502
|
+
const resting = restingOn(outcome, pin);
|
|
32503
|
+
if (resting === void 0) return yield* fail$8(replayError(outcome) ?? /* @__PURE__ */ new Error("gtd: replay found no rest"));
|
|
32504
|
+
const { reached, step } = resting;
|
|
32505
|
+
const { stepDef, state, memory } = restFacts(pin, step, reached.trace, run);
|
|
32506
|
+
yield* (yield* Narrator).narrate(`rest resolved: ${state} (awaits ${stepDef.actor})`);
|
|
31892
32507
|
if (stepDef.mode !== void 0 && def.modes[stepDef.mode] === void 0) return yield* fail$8(/* @__PURE__ */ new Error(`gtd: config: step "${step.name}": mode "${stepDef.mode}" is not a mode this workflow knows (${knownModes(def).join(", ")})`));
|
|
31893
32508
|
const changes = ref === void 0 ? (yield* pendingChanges(git, stepDef.approve === true)).map((e) => ({
|
|
31894
32509
|
status: normalizeStatus(e.status),
|
|
31895
32510
|
path: e.path
|
|
31896
32511
|
})) : [];
|
|
31897
|
-
|
|
31898
|
-
const configNow = config.fingerprint;
|
|
31899
|
-
if (ref === void 0) yield* warnConfigChanges(run, configNow, workspace);
|
|
32512
|
+
if (ref === void 0 && config.drift.length > 0) yield* (yield* Narrator).warn(`gtd: notice: the live config differs from the pinned one (${config.drift.join(", ")}); \`gtd repin\` to switch`);
|
|
31900
32513
|
return {
|
|
31901
32514
|
def,
|
|
31902
32515
|
workflowName: resolved.name,
|
|
31903
|
-
state
|
|
32516
|
+
state,
|
|
31904
32517
|
stepDef,
|
|
31905
|
-
actor:
|
|
32518
|
+
actor: stepDef.actor,
|
|
32519
|
+
pin,
|
|
32520
|
+
pinned,
|
|
31906
32521
|
step,
|
|
31907
|
-
trace:
|
|
32522
|
+
trace: reached.trace,
|
|
31908
32523
|
run,
|
|
31909
32524
|
vars,
|
|
31910
32525
|
env,
|
|
31911
|
-
config: configNow,
|
|
31912
32526
|
changes,
|
|
31913
32527
|
memory: memory.key,
|
|
31914
32528
|
memoryResumed: memory.resumed,
|
|
@@ -31918,6 +32532,52 @@ const restAt = (ref, startOverrides = {}) => gen(function* () {
|
|
|
31918
32532
|
};
|
|
31919
32533
|
});
|
|
31920
32534
|
const currentRest = restAt(void 0);
|
|
32535
|
+
/** The step the process rests at and the trace that reached it — at an unpin, the flow's last step. */
|
|
32536
|
+
const restingOn = (outcome, pin) => {
|
|
32537
|
+
if (outcome.kind === "rest") return {
|
|
32538
|
+
reached: outcome,
|
|
32539
|
+
step: outcome.rest
|
|
32540
|
+
};
|
|
32541
|
+
if (outcome.kind !== "ended" || pin !== "remove") return void 0;
|
|
32542
|
+
const step = outcome.trace.at(-1);
|
|
32543
|
+
return step === void 0 ? void 0 : {
|
|
32544
|
+
reached: outcome,
|
|
32545
|
+
step
|
|
32546
|
+
};
|
|
32547
|
+
};
|
|
32548
|
+
/** A pin or unpin beat has its own script, no step's, and no agent memory. */
|
|
32549
|
+
const restFacts = (pin, step, trace, run) => pin === void 0 ? {
|
|
32550
|
+
stepDef: stepDefOf(step),
|
|
32551
|
+
state: step.name,
|
|
32552
|
+
memory: memoryOf(trace, run)
|
|
32553
|
+
} : {
|
|
32554
|
+
stepDef: pinStepDef(pin),
|
|
32555
|
+
state: PIN_STATES[pin],
|
|
32556
|
+
memory: {
|
|
32557
|
+
key: void 0,
|
|
32558
|
+
resumed: false
|
|
32559
|
+
}
|
|
32560
|
+
};
|
|
32561
|
+
const headHasPin = (workspace, run) => {
|
|
32562
|
+
const head = run.episode.commits.at(-1)?.hash ?? run.episode.base;
|
|
32563
|
+
return head !== void 0 && workspace.treeSync(head).has(".gtd/process.gtdpin");
|
|
32564
|
+
};
|
|
32565
|
+
const underwayAt = (run, step, def) => run.workflow !== void 0 || step.name !== def.initial || step.id.occurrence !== 1;
|
|
32566
|
+
/**
|
|
32567
|
+
* A started process pins first — again, should a turn have deleted the pin;
|
|
32568
|
+
* a pinned flow that ended unpins before the closing commit.
|
|
32569
|
+
*/
|
|
32570
|
+
const pinBeatAt = (outcome, run, def, pinned) => {
|
|
32571
|
+
if (flowEnded(outcome, run, def)) return pinned || run.pinCommitted ? "remove" : void 0;
|
|
32572
|
+
if (outcome.kind !== "rest" || pinned) return void 0;
|
|
32573
|
+
return underwayAt(run, outcome.rest, def) ? "add" : void 0;
|
|
32574
|
+
};
|
|
32575
|
+
const flowEnded = (outcome, run, def) => outcome.kind === "ended" || outcome.kind === "rest" && run.workflow === void 0 && outcome.rest.name === def.initial && underwayAt(run, outcome.rest, def);
|
|
32576
|
+
const pinStepDef = (pin) => ({
|
|
32577
|
+
actor: "check",
|
|
32578
|
+
kind: "script",
|
|
32579
|
+
content: pin === "add" ? pinScript(PIN_PATH) : unpinScript(PIN_PATH)
|
|
32580
|
+
});
|
|
31921
32581
|
/**
|
|
31922
32582
|
* Whether `gtd start <name>` can start a process here: the workflow must
|
|
31923
32583
|
* reach a step from the tree the opening commit would capture. That step is
|
|
@@ -31961,7 +32621,8 @@ const renderRest = (rest) => succeed$1({
|
|
|
31961
32621
|
content: rest.stepDef.content,
|
|
31962
32622
|
...rest.hints,
|
|
31963
32623
|
...rest.memory !== void 0 ? { memory: rest.memory } : {},
|
|
31964
|
-
memoryResumed: rest.memoryResumed
|
|
32624
|
+
memoryResumed: rest.memoryResumed,
|
|
32625
|
+
...rest.pin !== void 0 ? { pin: rest.pin } : {}
|
|
31965
32626
|
});
|
|
31966
32627
|
/**
|
|
31967
32628
|
* Derived, restart-proof stall detection: the tree is clean, HEAD is an empty
|
|
@@ -31969,7 +32630,7 @@ const renderRest = (rest) => succeed$1({
|
|
|
31969
32630
|
*/
|
|
31970
32631
|
const stalledAt = (rest) => rest.changes.length === 0 && rest.stepDef.kind === "prompt" && rest.stepDef.allowEmpty !== true && rest.run.headTurn?.state === rest.state && rest.run.headTurn.actor === rest.actor && !rest.run.headTurn.step && rest.run.headTurn.empty;
|
|
31971
32632
|
/** No process is underway: the default workflow's first step, first visit — a dirty tree there is a turn not yet landed, not a process. */
|
|
31972
|
-
const noProcessUnderway = (rest) => rest.
|
|
32633
|
+
const noProcessUnderway = (rest) => rest.pin === void 0 && !underwayAt(rest.run, rest.step, rest.def);
|
|
31973
32634
|
/** `idle` means exactly one thing: no process underway, clean tree. */
|
|
31974
32635
|
const restIsIdle = (rest) => noProcessUnderway(rest) && rest.changes.length === 0;
|
|
31975
32636
|
const isHumanReviewGate = (def) => def.actor === "human" && def.mode === "review";
|
|
@@ -32059,14 +32720,14 @@ const landingTo = (rest, to, branch, approver, marks) => {
|
|
|
32059
32720
|
const starts = !underway && to !== rest.def.initial;
|
|
32060
32721
|
const refused = starts ? multilineRefusal(rest.vars) : void 0;
|
|
32061
32722
|
if (refused !== void 0) return refused;
|
|
32062
|
-
const finishes =
|
|
32723
|
+
const { finishes, landsAt } = endingOf(rest, to, underway);
|
|
32063
32724
|
return {
|
|
32064
32725
|
kind: "commit",
|
|
32065
|
-
to,
|
|
32726
|
+
to: landsAt,
|
|
32066
32727
|
spec: {
|
|
32067
32728
|
actor: rest.actor,
|
|
32068
32729
|
from: rest.state,
|
|
32069
|
-
to,
|
|
32730
|
+
to: landsAt,
|
|
32070
32731
|
step: rest.step.id,
|
|
32071
32732
|
...marks.length > 0 && { marks },
|
|
32072
32733
|
...approvedTrailer(rest, approver),
|
|
@@ -32077,10 +32738,55 @@ const landingTo = (rest, to, branch, approver, marks) => {
|
|
|
32077
32738
|
...keptTurn(rest)
|
|
32078
32739
|
};
|
|
32079
32740
|
};
|
|
32080
|
-
const
|
|
32081
|
-
|
|
32082
|
-
|
|
32083
|
-
|
|
32741
|
+
const endingOf = (rest, to, underway) => {
|
|
32742
|
+
const ends = underway && to === rest.def.initial;
|
|
32743
|
+
return {
|
|
32744
|
+
finishes: ends && !rest.pinned,
|
|
32745
|
+
landsAt: ends && rest.pinned ? PIN_STATES.remove : to
|
|
32746
|
+
};
|
|
32747
|
+
};
|
|
32748
|
+
/** A pin or unpin commit carries the pin alone; the unpin commit closes the process. */
|
|
32749
|
+
const pinLanding = (rest, pin, branch) => {
|
|
32750
|
+
const change = rest.changes.find((c) => c.path === PIN_PATH)?.status;
|
|
32751
|
+
return pin === "add" ? pinCommit(rest, change) : unpinCommit(rest, change, branch);
|
|
32752
|
+
};
|
|
32753
|
+
const pinCommit = (rest, change) => {
|
|
32754
|
+
if (change === void 0 || change === "D") return pinRefusal("add");
|
|
32755
|
+
const to = rest.step.name;
|
|
32756
|
+
return {
|
|
32757
|
+
kind: "commit",
|
|
32758
|
+
to,
|
|
32759
|
+
spec: {
|
|
32760
|
+
actor: rest.actor,
|
|
32761
|
+
from: rest.state,
|
|
32762
|
+
to,
|
|
32763
|
+
pin: "add"
|
|
32764
|
+
},
|
|
32765
|
+
only: PIN_PATH
|
|
32766
|
+
};
|
|
32767
|
+
};
|
|
32768
|
+
const unpinCommit = (rest, change, branch) => {
|
|
32769
|
+
if (change !== "D" && rest.pinned) return pinRefusal("remove");
|
|
32770
|
+
const to = rest.def.initial;
|
|
32771
|
+
return {
|
|
32772
|
+
kind: "commit",
|
|
32773
|
+
to,
|
|
32774
|
+
spec: {
|
|
32775
|
+
actor: rest.actor,
|
|
32776
|
+
from: rest.state,
|
|
32777
|
+
to,
|
|
32778
|
+
pin: "remove",
|
|
32779
|
+
...closingTrailers(branch)
|
|
32780
|
+
},
|
|
32781
|
+
keepHistory: processHistoryRef(rest.run.startHash),
|
|
32782
|
+
...rest.pinned ? { only: PIN_PATH } : {}
|
|
32783
|
+
};
|
|
32784
|
+
};
|
|
32785
|
+
const pinRefusal = (pin) => ({
|
|
32786
|
+
kind: "refusal",
|
|
32787
|
+
message: pin === "add" ? `gtd land: ${PIN_PATH} is missing — run the pin beat's script (\`gtd next --json=content\`); if its \`gtd bundle\` fails, fix what it reports, or run \`gtd abandon\`` : `gtd land: ${PIN_PATH} is still there — run the unpin beat's script (\`gtd next --json=content\`), then land`
|
|
32788
|
+
});
|
|
32789
|
+
const startTrailers = (rest, starts) => starts ? { vars: rest.vars } : {};
|
|
32084
32790
|
const outcomeMarks = (outcome) => outcome.kind === "rest" || outcome.kind === "ended" ? outcome.marks : [];
|
|
32085
32791
|
/**
|
|
32086
32792
|
* What landing the pending turn at `rest` does — a pure decision from a
|
|
@@ -32088,6 +32794,7 @@ const outcomeMarks = (outcome) => outcome.kind === "rest" || outcome.kind === "e
|
|
|
32088
32794
|
* computed by replaying, never matched.
|
|
32089
32795
|
*/
|
|
32090
32796
|
const decideLanding = (rest, verdicts, branch, approver) => gen(function* () {
|
|
32797
|
+
if (rest.pin !== void 0) return pinLanding(rest, rest.pin, branch);
|
|
32091
32798
|
const unapproved = approvalRefusal(rest, approver);
|
|
32092
32799
|
if (unapproved !== void 0) return unapproved;
|
|
32093
32800
|
const early = cleanLanding(rest);
|
|
@@ -32150,16 +32857,20 @@ const summaryFor = (run) => gen(function* () {
|
|
|
32150
32857
|
if (summary === void 0 || run.trace.length === 0) return { prompt: void 0 };
|
|
32151
32858
|
return { prompt: summary(summaryInput(run, settingsFor(resolved, run.pinnedVars, host.env, {}, {}))) };
|
|
32152
32859
|
});
|
|
32153
|
-
/**
|
|
32154
|
-
|
|
32155
|
-
|
|
32860
|
+
/**
|
|
32861
|
+
* Repository-relative paths of today's config files; one outside the
|
|
32862
|
+
* repository cannot be read at a ref, so it is left out. A file only the old
|
|
32863
|
+
* config read needs no check: whichever file imported it differs too.
|
|
32864
|
+
*/
|
|
32865
|
+
const inRepoConfigPaths = (root, files) => {
|
|
32866
|
+
const paths = /* @__PURE__ */ new Set();
|
|
32156
32867
|
for (const file of files) paths.add(relative(root, resolve(root, file)).split(sep).join("/"));
|
|
32157
32868
|
return [...paths].filter((p) => p !== "" && !p.startsWith("..") && !isAbsolute(p)).sort();
|
|
32158
32869
|
};
|
|
32159
32870
|
const configRefusal = (command, tip, paths, reason = "A history ref replays under today's config, so it is unreadable after the config changes") => /* @__PURE__ */ new Error(`${command}: refused — config differs at ${tip.slice(0, 7)}: ${paths.join(", ")}. ${reason}`);
|
|
32160
|
-
const differingConfigPaths = (git, workspace, root, config,
|
|
32871
|
+
const differingConfigPaths = (git, workspace, root, config, hash) => gen(function* () {
|
|
32161
32872
|
const differing = [];
|
|
32162
|
-
for (const path of inRepoConfigPaths(root, Object.keys(config.fingerprint)
|
|
32873
|
+
for (const path of inRepoConfigPaths(root, Object.keys(config.fingerprint))) if ((yield* orElseSucceed(git.readFileAtRef(hash, path), () => void 0)) !== workspace.readSync(path)) differing.push(path);
|
|
32163
32874
|
return differing;
|
|
32164
32875
|
});
|
|
32165
32876
|
const notClosedError = (hash, outcome) => outcome.kind === "rest" ? /* @__PURE__ */ new Error(`gtd history: ${hash.slice(0, 7)} does not close a process under today's config`) : replayError(outcome) ?? /* @__PURE__ */ new Error("gtd history: replay failed");
|
|
@@ -32216,7 +32927,7 @@ const replayFinishedProcess = (config, run, hash, how) => gen(function* () {
|
|
|
32216
32927
|
const recorded = parseCommitMessage(run.episode.commits.find((c) => c.hash === hash)?.message ?? "").version;
|
|
32217
32928
|
const versionWarning = versionWarningFor(recorded);
|
|
32218
32929
|
const withWarning = (err) => versionWarning === void 0 ? err : /* @__PURE__ */ new Error(`${err.message}\n${versionWarning}`);
|
|
32219
|
-
const differing = yield* differingConfigPaths(git, workspace, host.root, config,
|
|
32930
|
+
const differing = yield* differingConfigPaths(git, workspace, host.root, config, hash);
|
|
32220
32931
|
if (differing.length > 0) return yield* fail$8(withWarning(configRefusal(how.command, hash, differing, how.configReason)));
|
|
32221
32932
|
const { setup } = yield* setupFor(config, run, workspace, host);
|
|
32222
32933
|
const outcome = yield* promise(() => replayFor(setup));
|
|
@@ -32228,474 +32939,6 @@ const replayFinishedProcess = (config, run, hash, how) => gen(function* () {
|
|
|
32228
32939
|
};
|
|
32229
32940
|
});
|
|
32230
32941
|
//#endregion
|
|
32231
|
-
//#region src/emit/GitScript.ts
|
|
32232
|
-
const shellQuote$1 = (value) => `'${value.replace(/'/g, "'\\''")}'`;
|
|
32233
|
-
/**
|
|
32234
|
-
* `--allow-empty` because gtd's workflow commits may be empty on purpose.
|
|
32235
|
-
* Mirrors `Git.ts`'s retry: only re-tries without hooks on the specific
|
|
32236
|
-
* "empty git commit" rejection message, discriminated from the captured
|
|
32237
|
-
* output rather than the exit code, so a genuinely rejected commit (a lint
|
|
32238
|
-
* error, a commit-msg hook) still fails.
|
|
32239
|
-
*/
|
|
32240
|
-
const commitAllowEmpty = (message) => {
|
|
32241
|
-
const m = shellQuote$1(message);
|
|
32242
|
-
return [
|
|
32243
|
-
`if ! out=$(git commit --allow-empty -m ${m} 2>&1); then`,
|
|
32244
|
-
` case "$out" in`,
|
|
32245
|
-
` *"empty git commit"*) git commit --allow-empty --no-verify -m ${m} || exit 1 ;;`,
|
|
32246
|
-
` *) printf '%s\\n' "$out" >&2; exit 1 ;;`,
|
|
32247
|
-
` esac`,
|
|
32248
|
-
`fi`
|
|
32249
|
-
].join("\n");
|
|
32250
|
-
};
|
|
32251
|
-
const CHANGED_SINCE_LAND = "gtd: the working tree changed since `gtd land` — revert the edits and run `gtd land` again to approve";
|
|
32252
|
-
/** `git add -A || exit 1` (an `&&` left side would not abort under `set -e`) then `commitAllowEmpty`. */
|
|
32253
|
-
const commitAll = (message) => `git add -A || exit 1\n${commitAllowEmpty(message)}`;
|
|
32254
|
-
/**
|
|
32255
|
-
* An approval records exactly the judged tree: HEAD is pinned before the
|
|
32256
|
-
* clean check and the commit is built over its tree with `commit-tree`, so no
|
|
32257
|
-
* hook runs and none can add to it. A HEAD that moved fails the guarded
|
|
32258
|
-
* `update-ref`. `commit-tree` ignores `commit.gpgSign`, so it is read here.
|
|
32259
|
-
*/
|
|
32260
|
-
const commitClean = (message) => [
|
|
32261
|
-
`gtd_head=$(git rev-parse --verify HEAD) || exit 1`,
|
|
32262
|
-
`if [ -n "$(git status --porcelain --untracked-files=all --ignore-submodules=none)" ]; then`,
|
|
32263
|
-
` printf '%s\\n' ${shellQuote$1(CHANGED_SINCE_LAND)} >&2`,
|
|
32264
|
-
` exit 1`,
|
|
32265
|
-
`fi`,
|
|
32266
|
-
`gtd_sign_cfg=$(git config --type=bool --default=false commit.gpgSign) || exit 1`,
|
|
32267
|
-
`if [ "$gtd_sign_cfg" = true ]; then gtd_sign=-S; else gtd_sign=--no-gpg-sign; fi`,
|
|
32268
|
-
`gtd_tip=$(git commit-tree "$gtd_sign" "$gtd_head^{tree}" -p "$gtd_head" -m ${shellQuote$1(message)}) || exit 1`,
|
|
32269
|
-
`git update-ref -m ${shellQuote$1(`commit: ${message}`)} HEAD "$gtd_tip" "$gtd_head" || exit 1`
|
|
32270
|
-
].join("\n");
|
|
32271
|
-
const mixedResetTo = (ref) => `git reset --mixed ${shellQuote$1(ref)}`;
|
|
32272
|
-
const hardResetTo = (ref) => `git reset --hard ${shellQuote$1(ref)}`;
|
|
32273
|
-
/**
|
|
32274
|
-
* POSIX sh (runs under dash). The marker is a `commit-tree` over HEAD's own
|
|
32275
|
-
* tree, so no hook runs on it and its tree is the pre-turn tree by
|
|
32276
|
-
* construction; `commit-tree` ignores `commit.gpgSign`, hence the explicit
|
|
32277
|
-
* `-S`, read (and failing closed) before any object or ref is written; the
|
|
32278
|
-
* index is already staged by then, so an early exit leaves the turn staged.
|
|
32279
|
-
* The kept ref is deleted only after `reset -q` succeeds: it is then the
|
|
32280
|
-
* turn's only handle.
|
|
32281
|
-
* The branch moves before the final hard reset, so if that reset fails the
|
|
32282
|
-
* marker has landed and the turn is still in the working tree (kept ref
|
|
32283
|
-
* present); every other failure leaves the branch untouched.
|
|
32284
|
-
*/
|
|
32285
|
-
const keepTurn = (refPrefix, message) => {
|
|
32286
|
-
const keptRef = `${shellQuote$1(`${refPrefix}/`)}"$gtd_kept"`;
|
|
32287
|
-
return [
|
|
32288
|
-
`git add -A || exit 1`,
|
|
32289
|
-
`gtd_sign_cfg=$(git config --type=bool --default=false commit.gpgSign) || exit 1`,
|
|
32290
|
-
`if [ "$gtd_sign_cfg" = true ]; then gtd_sign=-S; else gtd_sign=--no-gpg-sign; fi`,
|
|
32291
|
-
`gtd_head=$(git rev-parse --verify HEAD) || exit 1`,
|
|
32292
|
-
`gtd_tree=$(git write-tree) || exit 1`,
|
|
32293
|
-
`gtd_kept=$(git commit-tree "$gtd_tree" -p "$gtd_head" -m ${shellQuote$1(message)}) || exit 1`,
|
|
32294
|
-
`git update-ref ${keptRef} "$gtd_kept" || exit 1`,
|
|
32295
|
-
`gtd_marker=$(printf '%s\\nGtd-Kept: %s' ${shellQuote$1(message)} "$gtd_kept")`,
|
|
32296
|
-
`if ! gtd_tip=$(git commit-tree "$gtd_sign" "$gtd_head^{tree}" -p "$gtd_head" -m "$gtd_marker") || ! git update-ref -m ${shellQuote$1(`commit: ${message}`)} HEAD "$gtd_tip" "$gtd_head"; then`,
|
|
32297
|
-
` git reset -q && git update-ref -d ${keptRef}`,
|
|
32298
|
-
` exit 1`,
|
|
32299
|
-
`fi`,
|
|
32300
|
-
`git reset --hard HEAD`
|
|
32301
|
-
].join("\n");
|
|
32302
|
-
};
|
|
32303
|
-
const updateRef = (ref, hash) => `git update-ref ${shellQuote$1(ref)} ${shellQuote$1(hash)}`;
|
|
32304
|
-
/** `git update-ref -d <ref>` — idempotent: deleting a missing ref is already a no-op in real git. */
|
|
32305
|
-
const deleteRef = (ref) => `git update-ref -d ${shellQuote$1(ref)}`;
|
|
32306
|
-
/**
|
|
32307
|
-
* No `+` on the refspec: a local ref the remote's tip does not descend from
|
|
32308
|
-
* fails the fetch instead of being overwritten (a fast-forward still applies).
|
|
32309
|
-
*/
|
|
32310
|
-
const fetchHistoryRefs = (remote) => [
|
|
32311
|
-
`set -eu`,
|
|
32312
|
-
`before=$(git for-each-ref --format='x' 'refs/gtd/history/' | wc -l)`,
|
|
32313
|
-
`git fetch ${shellQuote$1(remote)} 'refs/gtd/history/*:refs/gtd/history/*'`,
|
|
32314
|
-
`after=$(git for-each-ref --format='x' 'refs/gtd/history/' | wc -l)`,
|
|
32315
|
-
`printf '%s history refs arrived\\n' "$((after - before))"`
|
|
32316
|
-
].join("\n");
|
|
32317
|
-
/** Marks an emitted block as print-only — mirrors `OutcomeScript.ts`'s `OUTCOME_MARKER` verbatim, so `src/twins/EmittedScriptRecognizer.ts` recognizes either path's output identically. */
|
|
32318
|
-
const OUTCOME_MARKER$1 = "# gtd: outcome (print-only)";
|
|
32319
|
-
const printfLine$1 = (fmt, args) => `${OUTCOME_MARKER$1}\nprintf ${shellQuote$1(fmt.replace(/\n/g, "\\n"))} ${args.join(" ")}`;
|
|
32320
|
-
const renderOutcome = (outcome) => {
|
|
32321
|
-
switch (outcome.kind) {
|
|
32322
|
-
case "transition": return printfLine$1("%s %s → %s\n", [
|
|
32323
|
-
"'->'",
|
|
32324
|
-
shellQuote$1(outcome.from),
|
|
32325
|
-
shellQuote$1(outcome.to)
|
|
32326
|
-
]);
|
|
32327
|
-
case "commit": return printfLine$1("%s %s\n", ["'[commit]'", shellQuote$1(outcome.subject)]);
|
|
32328
|
-
case "note": return printfLine$1("%s\n", [shellQuote$1(outcome.text)]);
|
|
32329
|
-
}
|
|
32330
|
-
};
|
|
32331
|
-
const renderGitWrite = (write) => {
|
|
32332
|
-
switch (write.kind) {
|
|
32333
|
-
case "commitAll": return commitAll(write.message);
|
|
32334
|
-
case "commitClean": return commitClean(write.message);
|
|
32335
|
-
case "updateRef": return updateRef(write.ref, write.target);
|
|
32336
|
-
case "keepTurn": return keepTurn(write.ref, write.message);
|
|
32337
|
-
}
|
|
32338
|
-
};
|
|
32339
|
-
/**
|
|
32340
|
-
* Wraps `command` so a non-zero exit prints `prompt` plus the command's
|
|
32341
|
-
* captured output before propagating that exit code — mirrors `Emit.ts`'s
|
|
32342
|
-
* `failurePromptWrapper` (same reverse-import concern as `renderOutcome`
|
|
32343
|
-
* above: `Emit.ts` imports `shellQuote` from here already).
|
|
32344
|
-
*/
|
|
32345
|
-
const failurePromptWrapper$1 = (command, prompt) => {
|
|
32346
|
-
return [
|
|
32347
|
-
`gtd_validate_status=0`,
|
|
32348
|
-
`gtd_validate_out="$( {`,
|
|
32349
|
-
command,
|
|
32350
|
-
`} 2>&1 )" || gtd_validate_status=$?`,
|
|
32351
|
-
`if [ "$gtd_validate_status" -ne 0 ]; then`,
|
|
32352
|
-
` printf '%s\\n\\n%s\\n' ${shellQuote$1(prompt)} "$gtd_validate_out"`,
|
|
32353
|
-
` exit "$gtd_validate_status"`,
|
|
32354
|
-
`fi`
|
|
32355
|
-
].join("\n");
|
|
32356
|
-
};
|
|
32357
|
-
const renderLandStep = (step) => {
|
|
32358
|
-
if (step.kind === "gitWrite") return renderGitWrite(step.write);
|
|
32359
|
-
if (step.kind === "outcome") return renderOutcome(step.outcome);
|
|
32360
|
-
if (step.kind === "uncheck") return `gtd uncheck ${shellQuote$1(step.file)}`;
|
|
32361
|
-
return step.onFailure !== void 0 ? failurePromptWrapper$1(step.command, step.onFailure) : step.command;
|
|
32362
|
-
};
|
|
32363
|
-
const ScriptSurface = { render: (steps) => {
|
|
32364
|
-
const rendered = steps.map(renderLandStep);
|
|
32365
|
-
return rendered.length === 0 ? "" : ["set -eu", ...rendered].join("\n\n");
|
|
32366
|
-
} };
|
|
32367
|
-
//#endregion
|
|
32368
|
-
//#region src/emit/Emit.ts
|
|
32369
|
-
/** Exits 0 when the declared steering file is absent — nothing to format or validate yet. An OR list, not `if`: its left side failing never trips `set -e`. */
|
|
32370
|
-
const fileExistsGuard = (file) => `[ -f ${shellQuote$1(file)} ] || exit 0`;
|
|
32371
|
-
/** A mode or `ui.format` command finds the file it works on in `$GTD_FILE`. */
|
|
32372
|
-
const withFileVar$1 = (command, file) => `export GTD_FILE=${shellQuote$1(file)}\n${command}`;
|
|
32373
|
-
const SIMPLE_LEADING_WORD_RE = /^[A-Za-z0-9_./-]+(?=[ \t]|$)/;
|
|
32374
|
-
const SHELL_METACHARACTER_RE = /[|&;$<>(){}`]/;
|
|
32375
|
-
const FILE_VAR_RE = /"\$GTD_FILE"|"\$\{GTD_FILE\}"|\$GTD_FILE\b|\$\{GTD_FILE\}/g;
|
|
32376
|
-
/** `command` with `file` written in for `$GTD_FILE` — one line an agent can copy, equivalent to running it under `withFileVar`. */
|
|
32377
|
-
const commandForFile = (command, file) => command.replace(FILE_VAR_RE, shellQuote$1(file));
|
|
32378
|
-
/**
|
|
32379
|
-
* The leading word of a rendered `format:`/`validate:` command, when the
|
|
32380
|
-
* command has exactly one unambiguous binary to probe — `undefined` for
|
|
32381
|
-
* anything a shell metacharacter could hide a second command behind. A missing
|
|
32382
|
-
* guard degrades to a raw exit; a wrong guard would refuse a working command.
|
|
32383
|
-
*/
|
|
32384
|
-
const extractLeadingBinary = (command) => {
|
|
32385
|
-
if (command.includes("\n")) return void 0;
|
|
32386
|
-
if (SHELL_METACHARACTER_RE.test(command.replace(FILE_VAR_RE, ""))) return void 0;
|
|
32387
|
-
const match = SIMPLE_LEADING_WORD_RE.exec(command);
|
|
32388
|
-
return match === null ? void 0 : match[0];
|
|
32389
|
-
};
|
|
32390
|
-
/**
|
|
32391
|
-
* Names a typo'd or uninstalled binary and the `$PATH` it was looked up on.
|
|
32392
|
-
* `$PATH` is a shell word the DRIVER's shell expands at run time, so gtd never
|
|
32393
|
-
* resolves it in-process. Exits 127, matching bash's "command not found".
|
|
32394
|
-
*/
|
|
32395
|
-
const binaryGuard = (binary, mode, key) => {
|
|
32396
|
-
const messageQ = shellQuote$1(`gtd: mode "${mode}": "${key}" command not found: ${binary}`);
|
|
32397
|
-
return [
|
|
32398
|
-
`command -v ${shellQuote$1(binary)} >/dev/null 2>&1 || {`,
|
|
32399
|
-
` printf '%s\\n%s\\n' ${messageQ} '$PATH: '"$PATH"`,
|
|
32400
|
-
` exit 127`,
|
|
32401
|
-
`}`
|
|
32402
|
-
].join("\n");
|
|
32403
|
-
};
|
|
32404
|
-
/**
|
|
32405
|
-
* Prints `prompt` plus the captured output on a non-zero exit, then propagates
|
|
32406
|
-
* it. The `{ … }` group inlines a multi-line `command` verbatim; the
|
|
32407
|
-
* assignment sits left of `||` so `set -e` never trips on the failure itself.
|
|
32408
|
-
*/
|
|
32409
|
-
const failurePromptWrapper = (command, prompt) => {
|
|
32410
|
-
return [
|
|
32411
|
-
`gtd_validate_status=0`,
|
|
32412
|
-
`gtd_validate_out="$( {`,
|
|
32413
|
-
command,
|
|
32414
|
-
`} 2>&1 )" || gtd_validate_status=$?`,
|
|
32415
|
-
`if [ "$gtd_validate_status" -ne 0 ]; then`,
|
|
32416
|
-
` printf '%s\\n\\n%s\\n' ${shellQuote$1(prompt)} "$gtd_validate_out"`,
|
|
32417
|
-
` exit "$gtd_validate_status"`,
|
|
32418
|
-
`fi`
|
|
32419
|
-
].join("\n");
|
|
32420
|
-
};
|
|
32421
|
-
const renderStep = (step) => {
|
|
32422
|
-
if (step.kind === "command" && step.onFailure !== void 0) return failurePromptWrapper(step.command, step.onFailure);
|
|
32423
|
-
return step.command;
|
|
32424
|
-
};
|
|
32425
|
-
const assembleScript = (steps) => steps.length === 0 ? "" : ["set -eu", ...steps.map(renderStep)].join("\n\n");
|
|
32426
|
-
/** Build both halves from already-rendered command strings; an empty (or omitted) array produces the empty-string result, so a driver checking `if [ -n "$required" ]` never has to special-case a bare preamble. */
|
|
32427
|
-
const emitScripts = (required = [], optional = []) => ({
|
|
32428
|
-
required: assembleScript(required),
|
|
32429
|
-
optional: assembleScript(optional)
|
|
32430
|
-
});
|
|
32431
|
-
/** Exported so `src/twins/EmittedScriptRecognizer.ts` can recognize these by exact string comparison. `printf`, not `echo`, avoids shell-specific backslash-escape quirks. */
|
|
32432
|
-
const DID_NOT_RUN_COMMENT = "# gtd emitted this and did NOT run it — run it with `sh` to apply it";
|
|
32433
|
-
const PRESENTATION_ONLY_COMMENT = "# presentation only — safe to skip";
|
|
32434
|
-
const PRESENTATION_FAILURE_WARNING = "printf 'gtd: presentation-only follow-up failed — continuing\\n' >&2";
|
|
32435
|
-
/**
|
|
32436
|
-
* A write command's single pasteable script (`gtd abandon | sh`,
|
|
32437
|
-
* `gtd land --json=script`'s own field). `optional`, when non-empty, is
|
|
32438
|
-
* wrapped in a subshell whose failure is swallowed — presentation-only, so it
|
|
32439
|
-
* must never turn a landed turn into a non-zero exit.
|
|
32440
|
-
*/
|
|
32441
|
-
const combinedScript = (required, optional) => {
|
|
32442
|
-
if (required.length === 0) return "";
|
|
32443
|
-
if (optional.length === 0) return `${DID_NOT_RUN_COMMENT}\n\n${required}`;
|
|
32444
|
-
return [
|
|
32445
|
-
DID_NOT_RUN_COMMENT,
|
|
32446
|
-
"",
|
|
32447
|
-
required,
|
|
32448
|
-
"",
|
|
32449
|
-
PRESENTATION_ONLY_COMMENT,
|
|
32450
|
-
`(\n${optional}\n) || ${PRESENTATION_FAILURE_WARNING}`
|
|
32451
|
-
].join("\n");
|
|
32452
|
-
};
|
|
32453
|
-
//#endregion
|
|
32454
|
-
//#region src/emit/OutcomeScript.ts
|
|
32455
|
-
/**
|
|
32456
|
-
* A `printf '<fmt>' <args...>` bash statement — `args` are already-valid bash
|
|
32457
|
-
* tokens (a `shellQuote`d literal, a `"$(git …)"` substitution), never
|
|
32458
|
-
* re-quoted here: a value reaches the script as a printf ARGUMENT, never
|
|
32459
|
-
* interpolated into the format itself, so a subject or state name containing
|
|
32460
|
-
* `%` can never be read as a conversion spec. Markers like `->` are arguments
|
|
32461
|
-
* too, keeping every format string free of a leading `-` that a shell's
|
|
32462
|
-
* `printf` could mistake for an option. A real newline in `fmt` is emitted as
|
|
32463
|
-
* the two-character `\n` escape `printf` itself expands, keeping every emitted
|
|
32464
|
-
* statement on one line.
|
|
32465
|
-
*/
|
|
32466
|
-
const printfLine = (fmt, args) => `${OUTCOME_MARKER}\nprintf ${shellQuote$1(fmt.replace(/\n/g, "\\n"))} ${args.join(" ")}`;
|
|
32467
|
-
/**
|
|
32468
|
-
* Marks an emitted block as print-only. Load-bearing beyond documentation:
|
|
32469
|
-
* `src/twins/EmittedScriptRecognizer.ts` recognizes an outcome block by
|
|
32470
|
-
* this line, and a bare `printf` would be ambiguous with a workflow
|
|
32471
|
-
* `script:` command that happens to start the same way.
|
|
32472
|
-
*/
|
|
32473
|
-
const OUTCOME_MARKER = "# gtd: outcome (print-only)";
|
|
32474
|
-
const FMT_ABANDON_NOOP = "no gtd process is underway (resting at \"%s\") — nothing to abandon\n";
|
|
32475
|
-
const FMT_ABANDONED = "abandoned the process resting at \"%s\" — HEAD is back at %s (\"%s\"), resting at \"%s\".\nEverything the process produced is kept as uncommitted changes (`git status`); discard them with `git checkout -- . && git clean -fd .gtd` for a clean tree.\n";
|
|
32476
|
-
const FMT_RESTORED = "restored the retained history — HEAD is back at %s (\"%s\"), resting at \"%s\". Resume with the loop, or `git reset` to any earlier turn to restart from there.\n";
|
|
32477
|
-
/** `no gtd process is underway (resting at "<initial>") — nothing to abandon` — `gtd abandon`'s no-op plain-text line. */
|
|
32478
|
-
const abandonNoopText = (initial) => renderFormat(FMT_ABANDON_NOOP, initial);
|
|
32479
|
-
/** One already-rendered plain line. */
|
|
32480
|
-
const noteOutcome = (text) => printfLine("%s\n", [shellQuote$1(text)]);
|
|
32481
|
-
/**
|
|
32482
|
-
* Resolves the post-hoc short hash/subject from `head` (a commitish) in-script
|
|
32483
|
-
* — this line runs AFTER the reset that made `head` the new HEAD, so neither
|
|
32484
|
-
* value is knowable when gtd generates it.
|
|
32485
|
-
*/
|
|
32486
|
-
const abandonedOutcome = (from, head, state) => printfLine(FMT_ABANDONED, [
|
|
32487
|
-
shellQuote$1(from),
|
|
32488
|
-
`"$(git rev-parse --short ${shellQuote$1(head)})"`,
|
|
32489
|
-
`"$(git log -1 --format=%s ${shellQuote$1(head)})"`,
|
|
32490
|
-
shellQuote$1(state)
|
|
32491
|
-
]);
|
|
32492
|
-
/** `gtd abandon`'s no-op outcome — the same wording `abandonNoopText` renders. */
|
|
32493
|
-
const abandonNoopOutcome = (initial) => noteOutcome(abandonNoopText(initial));
|
|
32494
|
-
/** Resolves the post-hoc short hash/subject from `to` (a commitish) in-script, for the same reason `abandonedOutcome` does. */
|
|
32495
|
-
const restoredOutcome = (to, state) => printfLine(FMT_RESTORED, [
|
|
32496
|
-
`"$(git rev-parse --short ${shellQuote$1(to)})"`,
|
|
32497
|
-
`"$(git log -1 --format=%s ${shellQuote$1(to)})"`,
|
|
32498
|
-
shellQuote$1(state)
|
|
32499
|
-
]);
|
|
32500
|
-
//#endregion
|
|
32501
|
-
//#region src/emit/ModeContradiction.ts
|
|
32502
|
-
/**
|
|
32503
|
-
* Written for two readers at once, since gtd validate's exit code carries no
|
|
32504
|
-
* second meaning to distinguish them: the agent (this is a CONFIGURATION BUG,
|
|
32505
|
-
* not the steering file — stop, don't edit it, or a fix-retry loop will
|
|
32506
|
-
* thrash on the document) and the human (which mode, and the exact rendered
|
|
32507
|
-
* `format:` command, to fix `gtd.config.ts` without reproducing the failure).
|
|
32508
|
-
*/
|
|
32509
|
-
const contradictionMessage = (mode, formatCommand) => `gtd: mode "${mode}"'s format: command breaks its own validator — this is a CONFIGURATION BUG, not a problem with the steering file. Do NOT edit the steering file — stop and end your turn now.\nThe rendered format: command that ran:\n${formatCommand}\nWhat it turned mode "${mode}"'s own canonical sample into:`;
|
|
32510
|
-
/**
|
|
32511
|
-
* Formats a copy of the mode's own canonical sample and re-validates it with
|
|
32512
|
-
* `gtd check <mode>`, failing loudly when the round-trip breaks it — detects a
|
|
32513
|
-
* mode whose `format:` contradicts its own `validate:` without ever touching
|
|
32514
|
-
* the real steering file. `printf '%s'` over a `shellQuote`d sample, never a
|
|
32515
|
-
* heredoc, since no delimiter chosen ahead of time can collide with the
|
|
32516
|
-
* sample's own bytes. One bash fragment with no blank lines (so
|
|
32517
|
-
* `src/emit/Emit.ts`'s `assembleScript` never splits it from a sibling step), meant
|
|
32518
|
-
* to run BEFORE `[ -f <file> ] || exit 0` in the emitted validate script so it
|
|
32519
|
-
* still fires at a first-write beat where the real steering file doesn't
|
|
32520
|
-
* exist yet.
|
|
32521
|
-
*/
|
|
32522
|
-
const buildModeContradictionCheck = (inputs) => {
|
|
32523
|
-
const { mode, samplePath, sample, formatCommand } = inputs;
|
|
32524
|
-
const pathQ = shellQuote$1(samplePath);
|
|
32525
|
-
const messageQ = shellQuote$1(contradictionMessage(mode, formatCommand));
|
|
32526
|
-
return [
|
|
32527
|
-
`printf '%s' ${shellQuote$1(sample)} > ${pathQ}`,
|
|
32528
|
-
formatCommand,
|
|
32529
|
-
`gtd check ${mode} ${pathQ} >/dev/null 2>&1 || {`,
|
|
32530
|
-
` printf '%s\\n' ${messageQ} >&2`,
|
|
32531
|
-
` cat ${pathQ} >&2`,
|
|
32532
|
-
` rm -f ${pathQ}`,
|
|
32533
|
-
` exit 1`,
|
|
32534
|
-
`}`,
|
|
32535
|
-
`rm -f ${pathQ}`
|
|
32536
|
-
].join("\n");
|
|
32537
|
-
};
|
|
32538
|
-
/**
|
|
32539
|
-
* Skip notice for a mode whose validator is EXTERNAL (a user `validate:`
|
|
32540
|
-
* command, not one of the two built-in modes) — there's no in-process parser
|
|
32541
|
-
* to round-trip a sample through. Printed to stderr since silence would read
|
|
32542
|
-
* as a clean bill of health.
|
|
32543
|
-
*/
|
|
32544
|
-
const modeContradictionSkipNotice = (mode) => {
|
|
32545
|
-
return `printf '%s\\n' ${shellQuote$1(`gtd: mode "${mode}" has an external validate: command — skipping the format/validate contradiction check`)} >&2`;
|
|
32546
|
-
};
|
|
32547
|
-
//#endregion
|
|
32548
|
-
//#region src/emit/SteeringMode.ts
|
|
32549
|
-
init_dist$1();
|
|
32550
|
-
init_steering();
|
|
32551
|
-
/**
|
|
32552
|
-
* `format` alone (no live/external verdict) when there's no validator at
|
|
32553
|
-
* all; a live in-process validator for a `"builtin"` validate OR a command
|
|
32554
|
-
* that is exactly the format's own seeded string; `externalValidate` for any
|
|
32555
|
-
* other command. Each arm returns directly — a flat sequence, not nested
|
|
32556
|
-
* ternaries — so the four outcomes stay each their own case to read, not a
|
|
32557
|
-
* boolean expression to re-derive.
|
|
32558
|
-
*/
|
|
32559
|
-
const capabilitiesFor = (mode, format, validate) => {
|
|
32560
|
-
if (format === void 0) return validate?.kind === "command" ? { externalValidate: true } : {};
|
|
32561
|
-
if (validate?.kind === "builtin") return {
|
|
32562
|
-
format,
|
|
32563
|
-
liveValidate: format.validate
|
|
32564
|
-
};
|
|
32565
|
-
if (validate === void 0) return { format };
|
|
32566
|
-
if (isSeededValidateCommand(mode, validate.command)) return {
|
|
32567
|
-
format,
|
|
32568
|
-
liveValidate: format.validate
|
|
32569
|
-
};
|
|
32570
|
-
return {
|
|
32571
|
-
format,
|
|
32572
|
-
externalValidate: true
|
|
32573
|
-
};
|
|
32574
|
-
};
|
|
32575
|
-
/**
|
|
32576
|
-
* Resolve a `mode:` name against `def.modes` plus the built-in registry
|
|
32577
|
-
* (`src/workflow/SteeringFormats.ts`) — half by half: a declared `format:`/`validate:`
|
|
32578
|
-
* wins, and an undeclared `validate:` falls back to the built-in format's own
|
|
32579
|
-
* parser when the name is registered. `format` is set from the registry
|
|
32580
|
-
* ALONE, independent of which half of `validate` wins — a declared validator
|
|
32581
|
-
* overrides validation, not the format identity.
|
|
32582
|
-
*
|
|
32583
|
-
* `def: undefined` is the LSP's built-in-only fallback — resolving `mode`
|
|
32584
|
-
* against the registry alone, with no workflow definition in hand (e.g. the
|
|
32585
|
-
* basename dispatch for a `REVIEW.md` no config maps).
|
|
32586
|
-
*
|
|
32587
|
-
* The `"unknown"` arm carries the whole refusal message, naming what IS known
|
|
32588
|
-
* when `def` is given — this is the one place that message is built, so it
|
|
32589
|
-
* can never drift from what actually resolved.
|
|
32590
|
-
*/
|
|
32591
|
-
const resolveMode = (def, state, mode) => {
|
|
32592
|
-
const declared = def?.modes?.[mode];
|
|
32593
|
-
const format = steeringFormatFor(mode);
|
|
32594
|
-
if (declared === void 0 && format === void 0) return {
|
|
32595
|
-
kind: "unknown",
|
|
32596
|
-
message: def !== void 0 ? `state "${state}": mode "${mode}" is not defined by the active workflow (known modes: ${knownModes(def).join(", ")})` : `mode "${mode}" is not a built-in steering format`
|
|
32597
|
-
};
|
|
32598
|
-
const validate = declared?.validate !== void 0 ? {
|
|
32599
|
-
kind: "command",
|
|
32600
|
-
command: declared.validate
|
|
32601
|
-
} : format !== void 0 ? {
|
|
32602
|
-
kind: "builtin",
|
|
32603
|
-
format
|
|
32604
|
-
} : void 0;
|
|
32605
|
-
return {
|
|
32606
|
-
kind: "resolved",
|
|
32607
|
-
mode,
|
|
32608
|
-
...format !== void 0 ? { format } : {},
|
|
32609
|
-
...declared?.format !== void 0 ? { formatCommand: declared.format } : {},
|
|
32610
|
-
...validate !== void 0 ? { validate } : {},
|
|
32611
|
-
capabilities: capabilitiesFor(mode, format, validate)
|
|
32612
|
-
};
|
|
32613
|
-
};
|
|
32614
|
-
/** `gtd next`'s fix-retry instruction, printed ahead of the failing command's captured output. */
|
|
32615
|
-
const fixPromptInstruction = (file) => `Your last turn does not pass its own validation script. Fix these format violations in ${file}, then finish:`;
|
|
32616
|
-
/**
|
|
32617
|
-
* Pushes a `binaryGuard` step ahead of `command` when its own leading word
|
|
32618
|
-
* is unambiguous — BEFORE, not wrapped around: a guard that fails must stop
|
|
32619
|
-
* the script on its own `exit 127`, without touching `failurePromptWrapper`'s
|
|
32620
|
-
* captured-output path. Each command guards on its OWN leading word — a
|
|
32621
|
-
* guard naming the wrong binary is worse than none.
|
|
32622
|
-
*/
|
|
32623
|
-
const pushGuardedCommand = (steps, mode, key, command, file, onFailure) => {
|
|
32624
|
-
const binary = extractLeadingBinary(command);
|
|
32625
|
-
if (binary !== void 0) steps.push({
|
|
32626
|
-
kind: "command",
|
|
32627
|
-
command: binaryGuard(binary, mode, key)
|
|
32628
|
-
});
|
|
32629
|
-
steps.push({
|
|
32630
|
-
kind: "command",
|
|
32631
|
-
command: withFileVar$1(command, file),
|
|
32632
|
-
...onFailure !== void 0 ? { onFailure } : {}
|
|
32633
|
-
});
|
|
32634
|
-
};
|
|
32635
|
-
/**
|
|
32636
|
-
* `resolved`'s `format:`/`validate:` as `EmitStep[]` — format first,
|
|
32637
|
-
* validate last, the last one wrapped with `onFailure` when the validator is
|
|
32638
|
-
* a command (a built-in's own in-process parser has no shell step to wrap:
|
|
32639
|
-
* `gtd check`'s seeded command carries that instead). Each command gets its
|
|
32640
|
-
* own `binaryGuard` immediately ahead of it.
|
|
32641
|
-
*/
|
|
32642
|
-
const modeCommandSteps = (resolved, file) => {
|
|
32643
|
-
const steps = [];
|
|
32644
|
-
if (resolved.formatCommand !== void 0) pushGuardedCommand(steps, resolved.mode, "format", resolved.formatCommand, file, void 0);
|
|
32645
|
-
if (resolved.validate?.kind === "command") pushGuardedCommand(steps, resolved.mode, "validate", resolved.validate.command, file, fixPromptInstruction(file));
|
|
32646
|
-
return steps;
|
|
32647
|
-
};
|
|
32648
|
-
/** `<Host.scratchDir>/gtd-mode-sample-<mode>-<pid>.md` — an absolute literal baked in at emit time, never a shell variable. `<pid>` avoids collisions between concurrent `gtd` processes. Never a `/tmp` literal or `mktemp` (`tests/tooling/no-tmp-assumption.test.ts` scans for both). */
|
|
32649
|
-
const scratchSamplePath = (scratchDir, mode) => join(scratchDir, `gtd-mode-sample-${mode}-${process.pid}.md`);
|
|
32650
|
-
/**
|
|
32651
|
-
* The contradiction round-trip/skip-notice steps for `resolved`'s `mode:`.
|
|
32652
|
-
* No `formatCommand` means nothing to round-trip: empty. Otherwise: a live
|
|
32653
|
-
* built-in validator runs the round-trip against that format's own canonical
|
|
32654
|
-
* sample; an external validator prints a one-line skip notice instead, since
|
|
32655
|
-
* silence there would read as a clean bill of health; a format-only mode with
|
|
32656
|
-
* neither has nothing to round-trip either.
|
|
32657
|
-
*/
|
|
32658
|
-
const modeContradictionSteps = (resolved) => gen(function* () {
|
|
32659
|
-
if (resolved.formatCommand === void 0) return [];
|
|
32660
|
-
const { capabilities } = resolved;
|
|
32661
|
-
if (capabilities.format !== void 0 && capabilities.liveValidate !== void 0) {
|
|
32662
|
-
const { scratchDir } = yield* Host;
|
|
32663
|
-
const samplePath = scratchSamplePath(scratchDir, resolved.mode);
|
|
32664
|
-
const formatCommand = withFileVar$1(resolved.formatCommand, samplePath);
|
|
32665
|
-
return [{
|
|
32666
|
-
kind: "command",
|
|
32667
|
-
command: buildModeContradictionCheck({
|
|
32668
|
-
mode: resolved.mode,
|
|
32669
|
-
samplePath,
|
|
32670
|
-
sample: capabilities.format.sample,
|
|
32671
|
-
formatCommand
|
|
32672
|
-
})
|
|
32673
|
-
}];
|
|
32674
|
-
}
|
|
32675
|
-
if (capabilities.externalValidate === true) return [{
|
|
32676
|
-
kind: "command",
|
|
32677
|
-
command: modeContradictionSkipNotice(resolved.mode)
|
|
32678
|
-
}];
|
|
32679
|
-
return [];
|
|
32680
|
-
});
|
|
32681
|
-
/**
|
|
32682
|
-
* Renders `resolved`'s complete validate script for `file`: the mode's
|
|
32683
|
-
* format/validate contradiction check, a guard for a not-yet-written file,
|
|
32684
|
-
* then the format/validate commands themselves (format first). gtd never
|
|
32685
|
-
* runs this script itself — `resolveMode`+`validateScriptFor` only render
|
|
32686
|
-
* text for a driver to execute (`gtd renders; the driver executes`).
|
|
32687
|
-
*/
|
|
32688
|
-
const validateScriptFor = (resolved, file) => gen(function* () {
|
|
32689
|
-
return { script: emitScripts([
|
|
32690
|
-
...yield* modeContradictionSteps(resolved),
|
|
32691
|
-
{
|
|
32692
|
-
kind: "command",
|
|
32693
|
-
command: fileExistsGuard(file)
|
|
32694
|
-
},
|
|
32695
|
-
...modeCommandSteps(resolved, file)
|
|
32696
|
-
]).required };
|
|
32697
|
-
});
|
|
32698
|
-
//#endregion
|
|
32699
32942
|
//#region src/cli/ToolCommands.ts
|
|
32700
32943
|
init_dist$1();
|
|
32701
32944
|
/**
|
|
@@ -32753,7 +32996,7 @@ const runStraysCommand = (command, json, out) => gen(function* () {
|
|
|
32753
32996
|
const narrator = yield* Narrator;
|
|
32754
32997
|
const base = yield* resolveSide(git, command.base);
|
|
32755
32998
|
const tip = yield* resolveSide(git, command.tip);
|
|
32756
|
-
const touched = yield* git.diffPaths(base, tip);
|
|
32999
|
+
const touched = (yield* git.diffPaths(base, tip)).filter((path) => path !== PIN_PATH);
|
|
32757
33000
|
const strays = strayPaths(touched, command.globs);
|
|
32758
33001
|
const total = touched.length;
|
|
32759
33002
|
if (json.kind === "select") yield* writeSelection(out, strays, json.path).pipe(catchIf((error) => error instanceof SelectorUsageError && /^\d+$/.test(json.path), () => void_));
|
|
@@ -33655,8 +33898,7 @@ const startWorkflow = (workflow, varOverrides, json, out, commandLabel) => gen(f
|
|
|
33655
33898
|
workflow: resolved,
|
|
33656
33899
|
first: started,
|
|
33657
33900
|
commandLabel,
|
|
33658
|
-
vars: varOverrides
|
|
33659
|
-
config: rest.config
|
|
33901
|
+
vars: varOverrides
|
|
33660
33902
|
});
|
|
33661
33903
|
if (plan.kind === "refusal") return yield* fail$8(new Error(plan.message));
|
|
33662
33904
|
yield* writeScript(json, out, listingCommitted(landingScript(ScriptSurface.render(plan.steps)), rest.changes));
|
|
@@ -33768,6 +34010,10 @@ const runAbandonCommand = (json, out) => gen(function* () {
|
|
|
33768
34010
|
kind: "gitWrite",
|
|
33769
34011
|
command: mixedResetTo(run.startParentHash)
|
|
33770
34012
|
},
|
|
34013
|
+
{
|
|
34014
|
+
kind: "gitWrite",
|
|
34015
|
+
command: unpinScript(PIN_PATH)
|
|
34016
|
+
},
|
|
33771
34017
|
{
|
|
33772
34018
|
kind: "outcome",
|
|
33773
34019
|
command: abandonedOutcome(restState, run.startParentHash, initial)
|
|
@@ -33925,7 +34171,7 @@ const runCodeThreadsCheckCommand = () => gen(function* () {
|
|
|
33925
34171
|
if (rest.run.trace.length === 0) return;
|
|
33926
34172
|
const git = yield* GitService;
|
|
33927
34173
|
const workspace = yield* Workspace;
|
|
33928
|
-
const lines = (yield* git.changedPaths(rest.run.diffBase)).filter((c) => c.status !== "D").flatMap(({ path }) => {
|
|
34174
|
+
const lines = (yield* git.changedPaths(rest.run.diffBase)).filter((c) => c.status !== "D" && c.path !== ".gtd/process.gtdpin").flatMap(({ path }) => {
|
|
33929
34175
|
const { threads, findings } = parseCodeThreads(path, workspace.atPath(path) ?? "");
|
|
33930
34176
|
return [...threads.filter((t) => t.waitingOn === "human").map((t) => `${path}:${t.line + 1}: ${t.entries[0].text}`), ...findings.map((f) => `${path}:${(f.line ?? 0) + 1}: ${f.message}`)];
|
|
33931
34177
|
});
|
|
@@ -34655,6 +34901,7 @@ const flagHeader = (row) => {
|
|
|
34655
34901
|
};
|
|
34656
34902
|
const GLOBAL_OPTIONS = [
|
|
34657
34903
|
["-v, --verbose", "Narrate on stderr what gtd resolved"],
|
|
34904
|
+
["--live", "Run this gtd, not the process's pin; it may misread the process"],
|
|
34658
34905
|
["-h, --help", "Print help; gtd <command> --help for one command"],
|
|
34659
34906
|
["-V, --version", "Print the version"],
|
|
34660
34907
|
["--", "End the options: every word after it is an argument"]
|
|
@@ -34666,7 +34913,7 @@ const EXTRA_TOPICS = [[
|
|
|
34666
34913
|
], [
|
|
34667
34914
|
"version",
|
|
34668
34915
|
"Print the version",
|
|
34669
|
-
"Print gtd's version."
|
|
34916
|
+
"Print gtd's version, and the version of a pinned process's gtd."
|
|
34670
34917
|
]];
|
|
34671
34918
|
const EXAMPLES = ` $ gtd start list what can start
|
|
34672
34919
|
$ gtd start <name> start a process
|
|
@@ -35164,6 +35411,108 @@ const runCli = (argv, io) => {
|
|
|
35164
35411
|
}), provide(io.layers(plan.verbose)), sandbox, catch_((cause) => hasInterruptsOnly(cause) ? failCause$1(cause) : report(io, plan.json.kind !== "off")(cause)));
|
|
35165
35412
|
};
|
|
35166
35413
|
//#endregion
|
|
35414
|
+
//#region src/cli/Launch.ts
|
|
35415
|
+
/** What the core a pin carries lacks, or what must read the live state: these never hand over. */
|
|
35416
|
+
const LIVE_KINDS = /* @__PURE__ */ new Set([
|
|
35417
|
+
"ui",
|
|
35418
|
+
"lsp",
|
|
35419
|
+
"guide",
|
|
35420
|
+
"bundle",
|
|
35421
|
+
"historyList",
|
|
35422
|
+
"historyShow",
|
|
35423
|
+
"historyMetrics",
|
|
35424
|
+
"historyJudges",
|
|
35425
|
+
"historyFetch"
|
|
35426
|
+
]);
|
|
35427
|
+
const LIVE_FLAG = "--live";
|
|
35428
|
+
const optionWords = (argv) => {
|
|
35429
|
+
const end = argv.indexOf("--", 2);
|
|
35430
|
+
return argv.slice(2, end < 0 ? void 0 : end);
|
|
35431
|
+
};
|
|
35432
|
+
const withoutLiveFlag = (argv) => {
|
|
35433
|
+
const end = argv.indexOf("--", 2);
|
|
35434
|
+
return argv.filter((word, i) => word !== LIVE_FLAG || i < 2 || end >= 0 && i > end);
|
|
35435
|
+
};
|
|
35436
|
+
/**
|
|
35437
|
+
* Whether this gtd runs the command or hands it to the process's pin. A pin
|
|
35438
|
+
* this gtd cannot run is refused, never guessed at; a usage error the live
|
|
35439
|
+
* parser finds is still the pin's to judge, since its engine may read
|
|
35440
|
+
* another CLI.
|
|
35441
|
+
*/
|
|
35442
|
+
const launchFor = (argv, pinFile) => {
|
|
35443
|
+
if (globalThis[PIN_KEY] !== void 0) return {
|
|
35444
|
+
kind: "live",
|
|
35445
|
+
argv
|
|
35446
|
+
};
|
|
35447
|
+
const live = optionWords(argv).includes(LIVE_FLAG);
|
|
35448
|
+
const rest = withoutLiveFlag(argv);
|
|
35449
|
+
if (live) return {
|
|
35450
|
+
kind: "live",
|
|
35451
|
+
argv: rest
|
|
35452
|
+
};
|
|
35453
|
+
const plan = parseArgv(rest);
|
|
35454
|
+
const version = plan.kind === "output" && plan.stdout === `${GTD_VERSION}\n`;
|
|
35455
|
+
if (plan.kind === "output" && !version) return {
|
|
35456
|
+
kind: "live",
|
|
35457
|
+
argv: rest
|
|
35458
|
+
};
|
|
35459
|
+
if (plan.kind === "command" && LIVE_KINDS.has(plan.command.kind)) return {
|
|
35460
|
+
kind: "live",
|
|
35461
|
+
argv: rest
|
|
35462
|
+
};
|
|
35463
|
+
const found = pinFile();
|
|
35464
|
+
if (found === void 0) return {
|
|
35465
|
+
kind: "live",
|
|
35466
|
+
argv: rest
|
|
35467
|
+
};
|
|
35468
|
+
const header = pinHeader(found.bytes);
|
|
35469
|
+
if (version) return {
|
|
35470
|
+
kind: "output",
|
|
35471
|
+
stdout: header === void 0 ? plan.stdout : `${GTD_VERSION} (this process is pinned to gtd ${header.version})\n`
|
|
35472
|
+
};
|
|
35473
|
+
if (header === void 0 || header.format !== 1) {
|
|
35474
|
+
const what = header === void 0 ? "is no pin this gtd can read" : `is pin format ${header.format}, made by gtd ${header.version}`;
|
|
35475
|
+
return {
|
|
35476
|
+
kind: "refuse",
|
|
35477
|
+
message: `gtd: ${found.path} ${what}; gtd ${GTD_VERSION} runs pin format 1 — upgrade gtd (\`gtd --live\` runs this gtd instead, and may misread the process)`
|
|
35478
|
+
};
|
|
35479
|
+
}
|
|
35480
|
+
return {
|
|
35481
|
+
kind: "pin",
|
|
35482
|
+
argv: rest,
|
|
35483
|
+
path: found.path,
|
|
35484
|
+
version: header.version,
|
|
35485
|
+
source: () => readPin(found.bytes).source
|
|
35486
|
+
};
|
|
35487
|
+
};
|
|
35488
|
+
const isJsonFlag = (word) => word === "--json" || word.startsWith("--json=");
|
|
35489
|
+
const refusalText = (argv, message) => (optionWords(argv).some(isJsonFlag) ? `${JSON.stringify({
|
|
35490
|
+
state: "error",
|
|
35491
|
+
prompt: message
|
|
35492
|
+
})}\n` : "") + `${message}\n`;
|
|
35493
|
+
const runPin = async (launch, launcher) => {
|
|
35494
|
+
const words = optionWords(launch.argv);
|
|
35495
|
+
if (words.includes("--verbose") || words.includes("-v")) launcher.stderr(`launcher: running the pin ${launch.path} (gtd ${launch.version})\n`);
|
|
35496
|
+
try {
|
|
35497
|
+
await launcher.pin(launch);
|
|
35498
|
+
} catch (error) {
|
|
35499
|
+
const reason = error instanceof Error ? error.message : String(error);
|
|
35500
|
+
launcher.stderr(`gtd: could not run the pin ${launch.path}: ${reason}\n`);
|
|
35501
|
+
launcher.exit(1);
|
|
35502
|
+
}
|
|
35503
|
+
};
|
|
35504
|
+
const runLauncher = async (argv, pinFile, launcher) => {
|
|
35505
|
+
const launch = launchFor(argv, pinFile);
|
|
35506
|
+
switch (launch.kind) {
|
|
35507
|
+
case "live": return launcher.live(launch.argv);
|
|
35508
|
+
case "output": return launcher.stdout(launch.stdout);
|
|
35509
|
+
case "refuse":
|
|
35510
|
+
launcher.stderr(refusalText(argv, launch.message));
|
|
35511
|
+
return launcher.exit(1);
|
|
35512
|
+
case "pin": return runPin(launch, launcher);
|
|
35513
|
+
}
|
|
35514
|
+
};
|
|
35515
|
+
//#endregion
|
|
35167
35516
|
//#region node_modules/@trpc/server/dist/codes-DagpWZLc.mjs
|
|
35168
35517
|
/**
|
|
35169
35518
|
* Ensures there are no duplicate keys when building a procedure.
|
|
@@ -51190,25 +51539,40 @@ const liveToolCommands = effect(ToolCommands, gen(function* () {
|
|
|
51190
51539
|
//#endregion
|
|
51191
51540
|
//#region src/main.ts
|
|
51192
51541
|
init_dist$1();
|
|
51193
|
-
|
|
51194
|
-
|
|
51195
|
-
|
|
51196
|
-
|
|
51197
|
-
|
|
51198
|
-
|
|
51199
|
-
|
|
51200
|
-
|
|
51201
|
-
|
|
51202
|
-
|
|
51542
|
+
const runLive = (argv) => {
|
|
51543
|
+
let deathSignal;
|
|
51544
|
+
process.once("SIGINT", () => {
|
|
51545
|
+
deathSignal = "SIGINT";
|
|
51546
|
+
});
|
|
51547
|
+
process.once("SIGTERM", () => {
|
|
51548
|
+
deathSignal ??= "SIGTERM";
|
|
51549
|
+
});
|
|
51550
|
+
process.once("SIGHUP", () => {
|
|
51551
|
+
deathSignal = "SIGHUP";
|
|
51552
|
+
process.kill(process.pid, "SIGTERM");
|
|
51553
|
+
});
|
|
51554
|
+
runMain(runCli(argv, nodeCliIo(liveToolCommands)), { teardown: (exit, onExit) => {
|
|
51555
|
+
if (deathSignal === void 0) {
|
|
51556
|
+
defaultTeardown(exit, onExit);
|
|
51557
|
+
return;
|
|
51558
|
+
}
|
|
51559
|
+
const signal = deathSignal;
|
|
51560
|
+
process.removeAllListeners(signal);
|
|
51561
|
+
process.kill(process.pid, signal);
|
|
51562
|
+
} });
|
|
51563
|
+
};
|
|
51564
|
+
await runLauncher(process.argv, livePinFile(process.cwd(), PIN_PATH), {
|
|
51565
|
+
stdout: (chunk) => {
|
|
51566
|
+
process.stdout.write(chunk);
|
|
51567
|
+
},
|
|
51568
|
+
stderr: (chunk) => {
|
|
51569
|
+
process.stderr.write(chunk);
|
|
51570
|
+
},
|
|
51571
|
+
exit: (code) => {
|
|
51572
|
+
process.exitCode = code;
|
|
51573
|
+
},
|
|
51574
|
+
live: runLive,
|
|
51575
|
+
pin: (launch) => runPinModule(launch.path, launch.source())
|
|
51203
51576
|
});
|
|
51204
|
-
runMain(runCli(process.argv, nodeCliIo(liveToolCommands)), { teardown: (exit, onExit) => {
|
|
51205
|
-
if (deathSignal === void 0) {
|
|
51206
|
-
defaultTeardown(exit, onExit);
|
|
51207
|
-
return;
|
|
51208
|
-
}
|
|
51209
|
-
const signal = deathSignal;
|
|
51210
|
-
process.removeAllListeners(signal);
|
|
51211
|
-
process.kill(process.pid, signal);
|
|
51212
|
-
} });
|
|
51213
51577
|
//#endregion
|
|
51214
51578
|
export {};
|