@pmelab/gtd 24.0.0 → 24.1.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 +31 -5
- package/claude/hooks/access.ts +1 -1
- package/dist/chunks/Schema-Dh-mkwkq.mjs +25353 -0
- package/dist/chunks/reserved-ejn-LVYO.mjs +1388 -0
- package/dist/chunks/rolldown-runtime-8H4AJuhK.mjs +14 -0
- package/dist/chunks/steering-D-PvRC5W.mjs +16296 -0
- package/dist/chunks/wire-DA7q2ikx.mjs +473 -0
- package/dist/flows.mjs +525 -0
- package/dist/gtd.bundle.mjs +43339 -55575
- package/dist/steering.mjs +2 -0
- package/dist/testkit.mjs +13006 -0
- package/dist/wire.mjs +2 -0
- package/dist/workflow-kit.mjs +352 -0
- package/dist/workflows/feature.mjs +315 -0
- package/dist/workflows/fix.mjs +40 -0
- package/dist/workflows/review.mjs +25 -0
- package/dist/workflows.mjs +8 -6
- package/package.json +69 -8
- package/src/cli/Cli.test.ts +1329 -0
- package/src/cli/Cli.ts +1348 -0
- package/src/cli/Program.test.ts +2262 -0
- package/src/cli/Program.ts +1331 -0
- package/src/cli/ToolCommands.ts +43 -0
- package/src/cli/index.ts +14 -0
- package/src/edge/Edge.test.ts +1016 -0
- package/src/edge/Edge.ts +1225 -0
- package/src/edge/index.ts +16 -0
- package/src/emit/Emit.test.ts +425 -0
- package/src/emit/Emit.ts +139 -0
- package/src/emit/GitScript.test.ts +260 -0
- package/src/emit/GitScript.ts +131 -0
- package/src/emit/ModeContradiction.test.ts +120 -0
- package/src/emit/ModeContradiction.ts +66 -0
- package/src/emit/OutcomeScript.test.ts +238 -0
- package/src/emit/OutcomeScript.ts +73 -0
- package/src/emit/SteeringMode.test.ts +274 -0
- package/src/emit/SteeringMode.ts +256 -0
- package/src/emit/index.ts +40 -0
- package/src/install/ConfigScaffold.ts +19 -0
- package/src/install/Install.test.ts +426 -0
- package/src/install/Install.ts +634 -0
- package/src/install/Version.ts +27 -0
- package/src/install/index.ts +3 -0
- package/src/platform/CommandRunner.ts +74 -0
- package/src/platform/Commentary.test.ts +126 -0
- package/src/platform/Commentary.ts +92 -0
- package/src/platform/Git.test.ts +50 -0
- package/src/platform/Git.ts +552 -0
- package/src/platform/Host.ts +42 -0
- package/src/platform/RetainedHistory.test.ts +65 -0
- package/src/platform/RetainedHistory.ts +34 -0
- package/src/platform/Workspace.test.ts +669 -0
- package/src/platform/Workspace.ts +631 -0
- package/src/platform/WorktreeState.test.ts +162 -0
- package/src/platform/WorktreeState.ts +50 -0
- package/src/platform/index.ts +22 -0
- package/src/replay/Access.test.ts +53 -0
- package/src/replay/Access.ts +46 -0
- package/src/replay/Format.test.ts +20 -0
- package/src/replay/Format.ts +14 -0
- package/src/replay/Replay.test.ts +887 -0
- package/src/replay/Replay.ts +669 -0
- package/src/replay/Trailers.test.ts +117 -0
- package/src/replay/Trailers.ts +213 -0
- package/src/replay/Tree.test.ts +25 -0
- package/src/replay/Tree.ts +45 -0
- package/src/replay/index.ts +11 -0
- package/src/steering/Blocks.fixture.ts +6 -0
- package/src/steering/Blocks.test.ts +167 -0
- package/src/steering/Blocks.ts +321 -0
- package/src/steering/CodeThreads.test.ts +127 -0
- package/src/steering/CodeThreads.ts +197 -0
- package/src/steering/Edits.test.ts +19 -0
- package/src/steering/Edits.ts +20 -0
- package/src/steering/Eol.test.ts +16 -0
- package/src/steering/Eol.ts +2 -0
- package/src/steering/FORMATS.md +58 -0
- package/src/steering/Footnotes.test.ts +977 -0
- package/src/steering/Footnotes.ts +661 -0
- package/src/steering/Inline.fixture.ts +7 -0
- package/src/steering/Inline.test.ts +227 -0
- package/src/steering/Inline.ts +195 -0
- package/src/steering/MarkdownTree.test.ts +247 -0
- package/src/steering/MarkdownTree.ts +260 -0
- package/src/steering/Outline.ts +19 -0
- package/src/steering/SteeringFormat.test.ts +91 -0
- package/src/steering/SteeringFormat.ts +267 -0
- package/src/steering/SteeringFormats.test.ts +163 -0
- package/src/steering/Threads.ts +163 -0
- package/src/steering/contract.test.ts +264 -0
- package/src/steering/fixtures/code-threads/cache.stripped.ts.txt +6 -0
- package/src/steering/fixtures/code-threads/cache.ts.md +15 -0
- package/src/steering/fixtures/free-form/notes.md +12 -0
- package/src/steering/fixtures/free-form/notes.read.json +42 -0
- package/src/steering/fixtures/qa/answered.md +23 -0
- package/src/steering/fixtures/qa/answered.read.json +119 -0
- package/src/steering/fixtures/qa/requirements.md +160 -0
- package/src/steering/fixtures/qa/requirements.read.json +475 -0
- package/src/steering/fixtures/review/review.md +76 -0
- package/src/steering/fixtures/review/review.read.json +237 -0
- package/src/steering/fixtures/review/ticked.md +23 -0
- package/src/steering/fixtures/review/ticked.read.json +99 -0
- package/src/steering/fixtures/review-notes/baseline.md +14 -0
- package/src/steering/fixtures/review-notes/noted.md +19 -0
- package/src/steering/freeform.test.ts +311 -0
- package/src/steering/freeform.ts +148 -0
- package/src/steering/index.test.ts +62 -0
- package/src/steering/index.ts +140 -0
- package/src/steering/qa.test.ts +2160 -0
- package/src/steering/qa.ts +1104 -0
- package/src/steering/review.test.ts +1829 -0
- package/src/steering/review.ts +1180 -0
- package/src/step/LandStep.ts +31 -0
- package/src/step/RepoSnapshot.ts +29 -0
- package/src/step/accessRefusal.test.ts +58 -0
- package/src/step/accessRefusal.ts +26 -0
- package/src/step/index.ts +5 -0
- package/src/step/planStart.test.ts +202 -0
- package/src/step/planStart.ts +167 -0
- package/src/step/planStep.test.ts +104 -0
- package/src/step/planStep.ts +89 -0
- package/src/step/snapshot.fixture.ts +28 -0
- package/src/testkit/EmittedScriptRecognizer.test.ts +639 -0
- package/src/testkit/EmittedScriptRecognizer.ts +642 -0
- package/src/testkit/FakeGitOperations.ts +95 -0
- package/src/testkit/GitTiers.test.ts +11 -0
- package/src/testkit/GitTiers.ts +1052 -0
- package/src/testkit/InMemRepo.ts +467 -0
- package/src/testkit/Layers.test.ts +58 -0
- package/src/testkit/Layers.ts +241 -0
- package/src/testkit/StrictGitOperations.ts +31 -0
- package/src/testkit/TestRepo.test.ts +119 -0
- package/src/testkit/TestRepo.ts +274 -0
- package/src/testkit/cliIo.ts +36 -0
- package/src/testkit/index.ts +9 -0
- package/src/testkit/steps.ts +177 -0
- package/src/testkit/testkit.feature +65 -0
- package/src/wire/BeatDocument.test.ts +419 -0
- package/src/wire/BeatDocument.ts +114 -0
- package/src/wire/BeatPlain.test.ts +113 -0
- package/src/wire/BeatPlain.ts +127 -0
- package/src/wire/BeatStatus.test.ts +59 -0
- package/src/wire/BeatStatus.ts +81 -0
- package/src/wire/Demand.test.ts +142 -0
- package/src/wire/Demand.ts +108 -0
- package/src/wire/ExitCodes.test.ts +21 -0
- package/src/wire/ExitCodes.ts +16 -0
- package/src/wire/Glob.ts +29 -0
- package/src/wire/Judge.ts +22 -0
- package/src/wire/Land.test.ts +120 -0
- package/src/wire/Land.ts +75 -0
- package/src/wire/Select.test.ts +148 -0
- package/src/wire/Select.ts +78 -0
- package/src/wire/Sessions.test.ts +59 -0
- package/src/wire/Sessions.ts +39 -0
- package/src/wire/WireSchema.test.ts +162 -0
- package/src/wire/constants.ts +22 -0
- package/src/wire/golden/1/beat-capture.json +34 -0
- package/src/wire/golden/1/beat-message.json +34 -0
- package/src/wire/golden/1/beat-prompt.json +44 -0
- package/src/wire/golden/1/beat-script.json +34 -0
- package/src/wire/golden/1/beat-stalled.json +34 -0
- package/src/wire/golden/1/judge.json +14 -0
- package/src/wire/golden/1/land-noop.json +10 -0
- package/src/wire/golden/1/land.json +10 -0
- package/src/wire/index.ts +12 -0
- package/src/wire/types.ts +46 -0
- package/src/workflow/Diagnostic.test.ts +93 -0
- package/src/workflow/Diagnostic.ts +54 -0
- package/src/workflow/SteeringFormats.test.ts +27 -0
- package/src/workflow/SteeringFormats.ts +6 -0
- package/src/workflow/Workflow.ts +77 -0
- package/src/workflow/compile.test.ts +53 -0
- package/src/workflow/compile.ts +56 -0
- package/src/workflow/discovery.ts +122 -0
- package/src/workflow/fingerprint.test.ts +52 -0
- package/src/workflow/fingerprint.ts +53 -0
- package/src/workflow/index.ts +33 -0
- package/src/workflow/load.test.ts +1017 -0
- package/src/workflow/load.ts +836 -0
- package/src/workflow/skills.test.ts +109 -0
- package/src/workflow/skills.ts +90 -0
- package/src/workflow/vars.ts +33 -0
- package/src/workflows/feature/feature.test.ts +27 -33
- package/src/workflows/feature/index.ts +10 -0
- package/src/workflows/fix/fix.feature +80 -0
- package/src/workflows/fix/fix.test.ts +58 -78
- package/src/workflows/fix/index.ts +6 -0
- package/src/workflows/index.ts +2 -10
- package/src/workflows/review/index.ts +6 -0
- package/src/workflows/review/review.test.ts +85 -29
- package/src/workflows/review/review.ts +3 -1
- package/src/workflows/shared/config.fixture.ts +12 -0
- package/src/workflows/shared/reserved.ts +11 -0
- package/src/workflows/shared/workflows.steps.ts +7 -0
|
@@ -0,0 +1,473 @@
|
|
|
1
|
+
import { createHash, randomUUID } from "node:crypto";
|
|
2
|
+
//#region src/wire/constants.ts
|
|
3
|
+
/**
|
|
4
|
+
* The top-level `schema` of every JSON document a driver reads. Bumped only
|
|
5
|
+
* when a field is removed or renamed — never for an added one — and never tied
|
|
6
|
+
* to the package version, since a driver can be installed apart from gtd.
|
|
7
|
+
*/
|
|
8
|
+
const WIRE_SCHEMA = 1;
|
|
9
|
+
/** The bucket a cost with no `--model` tag is grouped under, kept distinct so a mixed history still totals correctly. */
|
|
10
|
+
const UNATTRIBUTED_MODEL = "unspecified";
|
|
11
|
+
/** The plain-text twin of `OutcomeScript.ts`'s `printfLine`. Lives here so `src/wire/`'s encoders reach it without importing out of the leaf. */
|
|
12
|
+
const renderFormat = (fmt, ...args) => {
|
|
13
|
+
let i = 0;
|
|
14
|
+
return fmt.replace(/%s/g, () => args[i++] ?? "");
|
|
15
|
+
};
|
|
16
|
+
/** The first line of the landing refusal for a turn that wrote outside its `write` access — a stable marker drivers and tests grep for. */
|
|
17
|
+
const ACCESS_REFUSAL = "gtd land: this turn wrote outside its write access — revert these paths, then run `gtd land` again:";
|
|
18
|
+
//#endregion
|
|
19
|
+
//#region src/wire/ExitCodes.ts
|
|
20
|
+
const EXIT_OK = 0;
|
|
21
|
+
/** Something failed that was never a matter of how gtd was invoked — a refusal or an unexpected error. */
|
|
22
|
+
const EXIT_RUNTIME_ERROR = 1;
|
|
23
|
+
/** gtd was invoked wrong — an unknown option/command, bad arity, a scope violation, a decode failure. */
|
|
24
|
+
const EXIT_USAGE_ERROR = 2;
|
|
25
|
+
const EXIT_SIGINT = 130;
|
|
26
|
+
const EXIT_SIGTERM = 143;
|
|
27
|
+
/** Frozen: adding a code is a driver-protocol break, the same as removing a wire field. */
|
|
28
|
+
const EXIT_CODES = /* @__PURE__ */ new Set([
|
|
29
|
+
0,
|
|
30
|
+
1,
|
|
31
|
+
2,
|
|
32
|
+
130,
|
|
33
|
+
143
|
|
34
|
+
]);
|
|
35
|
+
//#endregion
|
|
36
|
+
//#region src/wire/Demand.ts
|
|
37
|
+
/**
|
|
38
|
+
* `BeatKind`'s own member list, as a value — the ONE place the five kinds are
|
|
39
|
+
* enumerated. Every test that needs to loop "every kind" (the JSON document,
|
|
40
|
+
* `DEMAND_BRIEFING`, the plain header) imports this instead of re-typing its
|
|
41
|
+
* own `as const satisfies readonly BeatKind[]` literal, so the type and the
|
|
42
|
+
* loop can never drift apart.
|
|
43
|
+
*/
|
|
44
|
+
const BEAT_KINDS = [
|
|
45
|
+
"capture",
|
|
46
|
+
"message",
|
|
47
|
+
"script",
|
|
48
|
+
"prompt",
|
|
49
|
+
"stalled"
|
|
50
|
+
];
|
|
51
|
+
/**
|
|
52
|
+
* The beat kind for a rest, in precedence order:
|
|
53
|
+
*
|
|
54
|
+
* 1. `stalled` — `Edge.ts`'s `stalledAt` verdict, passed in as a plain
|
|
55
|
+
* boolean (this module never resolves it itself). `stalledAt` already
|
|
56
|
+
* requires a clean tree, so this can never collide with `capture` below.
|
|
57
|
+
* 2. `capture` — a `message` rest with a DIRTY tree: the human already
|
|
58
|
+
* acted; a driver lands it immediately.
|
|
59
|
+
* 3. Otherwise, the content kind verbatim (`script`/`prompt`, or `message`
|
|
60
|
+
* on a clean tree).
|
|
61
|
+
*/
|
|
62
|
+
const beatKindOf = (input) => {
|
|
63
|
+
if (input.stalled) return "stalled";
|
|
64
|
+
if (input.contentKind === "message" && input.dirty) return "capture";
|
|
65
|
+
return input.contentKind;
|
|
66
|
+
};
|
|
67
|
+
/**
|
|
68
|
+
* The `stalled` beat's own content: a diagnosis naming the stuck step and
|
|
69
|
+
* the ways out. The first line is shaped so a stderr grep for
|
|
70
|
+
* `stalled at "<state>"` stays a stable substring (see
|
|
71
|
+
* `tests/integration/features/driver-doc.feature`).
|
|
72
|
+
*/
|
|
73
|
+
const stallDiagnosis = (state, actor) => `stalled at "${state}": the last gtd(${actor}): ${state} turn landed an empty attempt, the tree is clean, and another dispatch would repeat it.\n\nTwo ways out:\n - sharpen the step's prompt so the turn has something concrete to author\n - pass allowEmpty: true to the step, so an empty turn completes it and the\n flow decides what follows — finishing, or an escalation step after N\n empty turns\n`;
|
|
74
|
+
/**
|
|
75
|
+
* Assemble one `Demand` — the ONLY place a rest's resolved content becomes
|
|
76
|
+
* `Demand.content` (`stallDiagnosis` at `kind === "stalled"`, the rendered
|
|
77
|
+
* content otherwise), and the only place `session`/`validate`/`skills`/`access` are
|
|
78
|
+
* attached, which the `prompt` variant's own type makes impossible to do at
|
|
79
|
+
* any other kind.
|
|
80
|
+
*/
|
|
81
|
+
const demandOf = (input) => {
|
|
82
|
+
const { rendered, kind, session, validate } = input;
|
|
83
|
+
const content = kind === "stalled" ? stallDiagnosis(rendered.state, rendered.actor) : rendered.content;
|
|
84
|
+
const skills = rendered.skills !== void 0 && rendered.skills.length > 0 ? rendered.skills : void 0;
|
|
85
|
+
const access = rendered.access ?? {
|
|
86
|
+
read: null,
|
|
87
|
+
write: null
|
|
88
|
+
};
|
|
89
|
+
return kind === "prompt" ? {
|
|
90
|
+
kind,
|
|
91
|
+
content,
|
|
92
|
+
session,
|
|
93
|
+
validate,
|
|
94
|
+
skills,
|
|
95
|
+
access
|
|
96
|
+
} : {
|
|
97
|
+
kind,
|
|
98
|
+
content
|
|
99
|
+
};
|
|
100
|
+
};
|
|
101
|
+
//#endregion
|
|
102
|
+
//#region src/wire/BeatStatus.ts
|
|
103
|
+
/**
|
|
104
|
+
* Assemble one `BeatStatus`. `system` is omitted (not just falsy) when its
|
|
105
|
+
* rendered value is the empty string, unlike `model`: an empty
|
|
106
|
+
* `--system-prompt ""` would silently delete the harness's own default
|
|
107
|
+
* instead of failing loudly.
|
|
108
|
+
*/
|
|
109
|
+
const statusOf = (input) => {
|
|
110
|
+
const { rendered, idle, initial, workflow, log, changes, next, cost, costByModel } = input;
|
|
111
|
+
return {
|
|
112
|
+
idle,
|
|
113
|
+
initial,
|
|
114
|
+
model: rendered.model,
|
|
115
|
+
system: rendered.system !== void 0 && rendered.system !== "" ? rendered.system : void 0,
|
|
116
|
+
log,
|
|
117
|
+
state: rendered.state,
|
|
118
|
+
actor: rendered.actor,
|
|
119
|
+
workflow,
|
|
120
|
+
label: rendered.label,
|
|
121
|
+
memory: rendered.memory,
|
|
122
|
+
file: rendered.file,
|
|
123
|
+
mode: rendered.mode,
|
|
124
|
+
changes,
|
|
125
|
+
next,
|
|
126
|
+
cost,
|
|
127
|
+
costByModel,
|
|
128
|
+
judge: rendered.judge
|
|
129
|
+
};
|
|
130
|
+
};
|
|
131
|
+
//#endregion
|
|
132
|
+
//#region src/wire/BeatDocument.ts
|
|
133
|
+
/** `gtd next --json`'s `next` key — `null` when landing would commit nothing. */
|
|
134
|
+
const nextField = (next) => next === null ? null : { target: next.target };
|
|
135
|
+
/** Unreachable in a well-typed call — used as the `default` arm of an exhaustive switch over `Demand["kind"]`, so a future `BeatKind` member that isn't handled fails `tsc`, not just review. */
|
|
136
|
+
const assertNeverDemand = (demand) => {
|
|
137
|
+
throw new Error(`unreachable demand: ${JSON.stringify(demand)}`);
|
|
138
|
+
};
|
|
139
|
+
/**
|
|
140
|
+
* The ONE exhaustive switch translating a `Demand` into its wire
|
|
141
|
+
* `session`/`validate`/`skills`/`access` set — only the `prompt` variant carries
|
|
142
|
+
* any of them, and every other member is listed explicitly (not a
|
|
143
|
+
* `default:` fallthrough) so adding a sixth `BeatKind` without adding its
|
|
144
|
+
* case here fails `tsc` at the `assertNeverDemand` call, not silently
|
|
145
|
+
* falling through to "no session."
|
|
146
|
+
*/
|
|
147
|
+
const dispatchFieldsOf = (demand) => {
|
|
148
|
+
switch (demand.kind) {
|
|
149
|
+
case "prompt": return {
|
|
150
|
+
session: demand.session,
|
|
151
|
+
validate: demand.validate,
|
|
152
|
+
skills: demand.skills,
|
|
153
|
+
access: demand.access
|
|
154
|
+
};
|
|
155
|
+
case "capture":
|
|
156
|
+
case "message":
|
|
157
|
+
case "script":
|
|
158
|
+
case "stalled": return {
|
|
159
|
+
session: void 0,
|
|
160
|
+
validate: void 0,
|
|
161
|
+
skills: void 0,
|
|
162
|
+
access: void 0
|
|
163
|
+
};
|
|
164
|
+
default: return assertNeverDemand(demand);
|
|
165
|
+
}
|
|
166
|
+
};
|
|
167
|
+
/**
|
|
168
|
+
* Flatten a `Demand` plus its `BeatStatus` into the single 24-key document
|
|
169
|
+
* `gtd next --json` emits — the ONLY place the two are joined. `cost`/
|
|
170
|
+
* `costByModel` are omitted together, exactly when no cost was recorded
|
|
171
|
+
* (`cost <= 0`).
|
|
172
|
+
*/
|
|
173
|
+
const beatDocument = (demand, status) => {
|
|
174
|
+
const { session, validate, skills, access } = dispatchFieldsOf(demand);
|
|
175
|
+
const hasCost = status.cost > 0;
|
|
176
|
+
return {
|
|
177
|
+
schema: 1,
|
|
178
|
+
kind: demand.kind,
|
|
179
|
+
content: demand.content,
|
|
180
|
+
idle: status.idle,
|
|
181
|
+
initial: status.initial,
|
|
182
|
+
session,
|
|
183
|
+
model: status.model,
|
|
184
|
+
system: status.system,
|
|
185
|
+
validate,
|
|
186
|
+
log: status.log,
|
|
187
|
+
state: status.state,
|
|
188
|
+
actor: status.actor,
|
|
189
|
+
workflow: status.workflow,
|
|
190
|
+
label: status.label,
|
|
191
|
+
memory: status.memory,
|
|
192
|
+
file: status.file,
|
|
193
|
+
mode: status.mode,
|
|
194
|
+
changes: status.changes,
|
|
195
|
+
next: nextField(status.next),
|
|
196
|
+
cost: hasCost ? status.cost : void 0,
|
|
197
|
+
costByModel: hasCost ? status.costByModel : void 0,
|
|
198
|
+
judge: status.judge,
|
|
199
|
+
skills,
|
|
200
|
+
access
|
|
201
|
+
};
|
|
202
|
+
};
|
|
203
|
+
const renderBeatJson = (document) => JSON.stringify(document) + "\n";
|
|
204
|
+
//#endregion
|
|
205
|
+
//#region src/wire/BeatPlain.ts
|
|
206
|
+
/**
|
|
207
|
+
* True when the per-model breakdown adds information beyond the `Cost:`
|
|
208
|
+
* total: more than one model, or a single model that carries an actual
|
|
209
|
+
* `--model` tag (a lone `unspecified` bucket just restates the total, so
|
|
210
|
+
* it's suppressed).
|
|
211
|
+
*/
|
|
212
|
+
const breakdownIsInformative = (byModel) => byModel.length > 1 || byModel.length === 1 && byModel[0].model !== "unspecified";
|
|
213
|
+
const costStatusLines = (cost, byModel) => {
|
|
214
|
+
if (cost <= 0) return [];
|
|
215
|
+
const lines = [`Cost: ${cost}`];
|
|
216
|
+
if (breakdownIsInformative(byModel)) for (const m of byModel) lines.push(` ${m.model}: ${m.cost}`);
|
|
217
|
+
return lines;
|
|
218
|
+
};
|
|
219
|
+
const pendingStatusLines = (statusChanges) => statusChanges.length === 0 ? ["Pending: (clean)"] : ["Pending:", ...statusChanges.map((c) => ` ${c.status} ${c.path}`)];
|
|
220
|
+
const nextStatusLine = (next) => next === null ? "Next: (nothing would land)" : `Next: → ${next.target}`;
|
|
221
|
+
/** Builds `{[key]: value}` for each entry whose value isn't `undefined` — the shared "omit absent optional fields" shape the header uses. */
|
|
222
|
+
const definedFields = (entries) => {
|
|
223
|
+
const result = {};
|
|
224
|
+
for (const [key, value] of entries) if (value !== void 0) result[key] = value;
|
|
225
|
+
return result;
|
|
226
|
+
};
|
|
227
|
+
/**
|
|
228
|
+
* The header block's own lines — `State:`/`Awaits:`/`Workflow:`/optional `Label:`/
|
|
229
|
+
* `Model:`/`Memory:`/`File:`/`Mode:`/`Cost:`(+breakdown)/`Pending:`/`Next:`,
|
|
230
|
+
* in that order. Used by `renderBeatPlain`'s header-showing kinds below.
|
|
231
|
+
*/
|
|
232
|
+
const beatHeaderLines = (document) => {
|
|
233
|
+
const optional = definedFields([
|
|
234
|
+
["Label", document.label],
|
|
235
|
+
["Model", document.model],
|
|
236
|
+
["Memory", document.memory],
|
|
237
|
+
["File", document.file],
|
|
238
|
+
["Mode", document.mode]
|
|
239
|
+
]);
|
|
240
|
+
return [
|
|
241
|
+
`State: ${document.state}`,
|
|
242
|
+
`Awaits: ${document.actor}`,
|
|
243
|
+
`Workflow: ${document.workflow}`,
|
|
244
|
+
...Object.entries(optional).map(([key, value]) => `${key}: ${value}`),
|
|
245
|
+
...costStatusLines(document.cost ?? 0, document.costByModel ?? []),
|
|
246
|
+
...pendingStatusLines(document.changes),
|
|
247
|
+
nextStatusLine(document.next)
|
|
248
|
+
];
|
|
249
|
+
};
|
|
250
|
+
/**
|
|
251
|
+
* One entry per `BeatKind`, typed as `Record<BeatKind, DemandBriefing>` so
|
|
252
|
+
* the object literal itself is exhaustive: adding or removing a `BeatKind`
|
|
253
|
+
* member without updating this map fails `tsc`, the same guarantee
|
|
254
|
+
* `dispatchFieldsOf`'s switch (`BeatDocument.ts`) gives the JSON side. Exported
|
|
255
|
+
* so the one cross-cutting exhaustiveness test (`BeatDocument.test.ts`) can
|
|
256
|
+
* assert against it directly, rather than re-deriving it from
|
|
257
|
+
* `renderBeatPlain`'s output. Replaces the old `renderBeatPlain`'s hand-written
|
|
258
|
+
* `if (kind === "script") … if (kind === "capture") …` pair.
|
|
259
|
+
*/
|
|
260
|
+
const DEMAND_BRIEFING = {
|
|
261
|
+
capture: {
|
|
262
|
+
instructionLine: "The edit is already made — run `gtd land` to land it.",
|
|
263
|
+
suppressHeader: false
|
|
264
|
+
},
|
|
265
|
+
message: {
|
|
266
|
+
instructionLine: void 0,
|
|
267
|
+
suppressHeader: false
|
|
268
|
+
},
|
|
269
|
+
script: {
|
|
270
|
+
instructionLine: "Run this script:",
|
|
271
|
+
suppressHeader: false
|
|
272
|
+
},
|
|
273
|
+
stalled: {
|
|
274
|
+
instructionLine: void 0,
|
|
275
|
+
suppressHeader: false
|
|
276
|
+
},
|
|
277
|
+
prompt: {
|
|
278
|
+
instructionLine: void 0,
|
|
279
|
+
suppressHeader: true
|
|
280
|
+
}
|
|
281
|
+
};
|
|
282
|
+
/**
|
|
283
|
+
* The self-validation instruction appended to a `prompt` beat's plain output
|
|
284
|
+
* only — a structured (`--json`) driver runs the `validate` field
|
|
285
|
+
* itself instead. Advisory either way: `gtd land` refuses a turn whose
|
|
286
|
+
* steering file is invalid regardless of whether this was followed.
|
|
287
|
+
*/
|
|
288
|
+
const selfValidateInstruction = (command, file) => `\nBefore finishing your turn, run \`${command}\` — it checks ${file} — and fix every violation it reports until it exits cleanly. Do not finish while it still reports violations.\n`;
|
|
289
|
+
/**
|
|
290
|
+
* `gtd next`'s plain-text encoding, driven by `DEMAND_BRIEFING`. At
|
|
291
|
+
* `kind === "prompt"` the status header is suppressed and only `content`
|
|
292
|
+
* plus the self-validation instruction is emitted — those bytes ARE the
|
|
293
|
+
* agent's input, so gtd's own bookkeeping (state name, edges, pending
|
|
294
|
+
* changes) must not be prefixed onto it. Every other kind gets the header
|
|
295
|
+
* plus its instruction line, since its plain output is read by a human or
|
|
296
|
+
* driver, never an agent. `selfValidateCommand` is already resolved; this
|
|
297
|
+
* function never resolves it itself.
|
|
298
|
+
*/
|
|
299
|
+
const renderBeatPlain = (document, selfValidateCommand) => {
|
|
300
|
+
const content = document.content.endsWith("\n") ? document.content : document.content + "\n";
|
|
301
|
+
const briefing = DEMAND_BRIEFING[document.kind];
|
|
302
|
+
if (briefing.suppressHeader) return selfValidateCommand !== void 0 && document.file !== void 0 ? content + selfValidateInstruction(selfValidateCommand, document.file) : content;
|
|
303
|
+
const body = beatHeaderLines(document).join("\n") + "\n\n" + content;
|
|
304
|
+
return briefing.instructionLine !== void 0 ? `${briefing.instructionLine}\n` + body : body;
|
|
305
|
+
};
|
|
306
|
+
//#endregion
|
|
307
|
+
//#region src/wire/Land.ts
|
|
308
|
+
/** Assembles one `gtd land` result's fields in `LandFields`' declared order — the ONLY place `gtd land --json`'s wire shape is built. */
|
|
309
|
+
const landFields = (input) => ({
|
|
310
|
+
schema: 1,
|
|
311
|
+
script: input.script,
|
|
312
|
+
settled: input.settled,
|
|
313
|
+
idle: input.idle,
|
|
314
|
+
state: input.state,
|
|
315
|
+
subject: input.subject,
|
|
316
|
+
cost: input.cost,
|
|
317
|
+
model: input.model
|
|
318
|
+
});
|
|
319
|
+
const renderLandJson = (fields) => JSON.stringify(fields) + "\n";
|
|
320
|
+
const FMT_NOOP = "nothing to do at \"%s\"\n";
|
|
321
|
+
const FMT_LAND_PROSE = "commit everything with this message: %s\n";
|
|
322
|
+
/** `nothing to do at "<state>"` — a no-op step's plain-text line, and the text a print-only script's own `printf` carries. */
|
|
323
|
+
const noopText = (state) => renderFormat(FMT_NOOP, state);
|
|
324
|
+
/** `commit everything with this message: <subject>` — plain `gtd land`'s own stdout at a pending diff (no script); `--json` keeps emitting the script itself, unaffected. */
|
|
325
|
+
const landProseText = (subject) => renderFormat(FMT_LAND_PROSE, subject);
|
|
326
|
+
/**
|
|
327
|
+
* `gtd land`'s plain-text encoding — names the commit subject at a real
|
|
328
|
+
* landing and points at `--json=script`, since the script itself is
|
|
329
|
+
* unreachable from plain output; prints the existing no-op note otherwise.
|
|
330
|
+
* `command` names the invocation to point at — `"land"` by default, but
|
|
331
|
+
* `gtd judge answer` (the only other caller of `planLanding`/`landFields`)
|
|
332
|
+
* passes its own two-word name so the hint stays runnable verbatim.
|
|
333
|
+
*/
|
|
334
|
+
const renderLandPlain = (fields, command = "land") => fields.subject !== null ? `${landProseText(fields.subject).trimEnd()}\n(run \`gtd ${command} --json=script | sh\` to get the landing script)\n` : noopText(fields.state);
|
|
335
|
+
//#endregion
|
|
336
|
+
//#region src/wire/Judge.ts
|
|
337
|
+
const judgeJson = (state, questions) => JSON.stringify({
|
|
338
|
+
schema: 1,
|
|
339
|
+
state,
|
|
340
|
+
questions
|
|
341
|
+
});
|
|
342
|
+
//#endregion
|
|
343
|
+
//#region src/wire/Select.ts
|
|
344
|
+
/**
|
|
345
|
+
* Renders a fully-walked leaf value: scalars/booleans stringify directly,
|
|
346
|
+
* arrays become one `JSON.stringify` per entry, newline-joined.
|
|
347
|
+
* `selectPath`'s loop already returns `absent` for `undefined`/`null` after
|
|
348
|
+
* every segment (including the last), so neither ever reaches here.
|
|
349
|
+
*/
|
|
350
|
+
const toSelection = (value) => {
|
|
351
|
+
if (Array.isArray(value)) return {
|
|
352
|
+
kind: "value",
|
|
353
|
+
text: value.map((entry) => JSON.stringify(entry)).join("\n")
|
|
354
|
+
};
|
|
355
|
+
if (typeof value === "object") return {
|
|
356
|
+
kind: "value",
|
|
357
|
+
text: JSON.stringify(value)
|
|
358
|
+
};
|
|
359
|
+
return {
|
|
360
|
+
kind: "value",
|
|
361
|
+
text: String(value)
|
|
362
|
+
};
|
|
363
|
+
};
|
|
364
|
+
/** A segment that resolves to no key at all (missing from an object, out-of-range or non-numeric against an array) — distinct from a present key holding `undefined`. */
|
|
365
|
+
const NOT_FOUND = Symbol("not-found");
|
|
366
|
+
/** An all-digits segment (`"0"`, `"12"`) — the only shape an array segment resolves against. */
|
|
367
|
+
const ARRAY_INDEX = /^\d+$/;
|
|
368
|
+
/**
|
|
369
|
+
* One step of the walk: looks `segment` up on `current`, or reports
|
|
370
|
+
* `NOT_FOUND` for a key that was never there (a primitive has no keys).
|
|
371
|
+
* Against an array, only an all-digits segment resolves — an in-range index
|
|
372
|
+
* (`skills.0` reads the first declared skill), never a non-index own key
|
|
373
|
+
* (`length`/`map`/every other inherited array member stays `NOT_FOUND`).
|
|
374
|
+
* Against an object, presence is an OWN-property test
|
|
375
|
+
* (`Object.prototype.hasOwnProperty`), never the `in` operator — `in` walks
|
|
376
|
+
* the prototype chain, so it would resolve inherited members
|
|
377
|
+
* (`constructor`, `toString`, `hasOwnProperty`, `valueOf`, ...) as real
|
|
378
|
+
* document fields.
|
|
379
|
+
*/
|
|
380
|
+
const resolveSegment = (current, segment) => {
|
|
381
|
+
if (Array.isArray(current)) return ARRAY_INDEX.test(segment) && Number(segment) < current.length ? current[Number(segment)] : NOT_FOUND;
|
|
382
|
+
if (typeof current === "object" && current !== null) {
|
|
383
|
+
const record = current;
|
|
384
|
+
return Object.prototype.hasOwnProperty.call(record, segment) ? record[segment] : NOT_FOUND;
|
|
385
|
+
}
|
|
386
|
+
return NOT_FOUND;
|
|
387
|
+
};
|
|
388
|
+
/**
|
|
389
|
+
* Walks `fields` by dotted key path. Never throws — any unwalkable shape
|
|
390
|
+
* (a primitive mid-path, an out-of-range or non-numeric array segment)
|
|
391
|
+
* degrades to `unknown`, and a present-but-`undefined` value anywhere along
|
|
392
|
+
* the path short-circuits the whole remaining path to `absent` rather than
|
|
393
|
+
* reporting a key that was never actually missing.
|
|
394
|
+
*/
|
|
395
|
+
const selectPath = (fields, path) => {
|
|
396
|
+
try {
|
|
397
|
+
let current = fields;
|
|
398
|
+
for (const segment of path.split(".")) {
|
|
399
|
+
current = resolveSegment(current, segment);
|
|
400
|
+
if (current === NOT_FOUND) return {
|
|
401
|
+
kind: "unknown",
|
|
402
|
+
path
|
|
403
|
+
};
|
|
404
|
+
if (current === void 0 || current === null) return { kind: "absent" };
|
|
405
|
+
}
|
|
406
|
+
return toSelection(current);
|
|
407
|
+
} catch {
|
|
408
|
+
return {
|
|
409
|
+
kind: "unknown",
|
|
410
|
+
path
|
|
411
|
+
};
|
|
412
|
+
}
|
|
413
|
+
};
|
|
414
|
+
//#endregion
|
|
415
|
+
//#region src/wire/Sessions.ts
|
|
416
|
+
/** Fixed forever — every derived session id descends from this one constant. */
|
|
417
|
+
const GTD_SESSION_NAMESPACE = "ca4be249-805e-41c1-8b6a-75a11c011e25";
|
|
418
|
+
const toBytes = (uuid) => Buffer.from(uuid.replace(/-/g, ""), "hex");
|
|
419
|
+
const toUuidString = (bytes) => {
|
|
420
|
+
const hex = bytes.toString("hex");
|
|
421
|
+
return `${hex.slice(0, 8)}-${hex.slice(8, 12)}-${hex.slice(12, 16)}-${hex.slice(16, 20)}-${hex.slice(20, 32)}`;
|
|
422
|
+
};
|
|
423
|
+
/**
|
|
424
|
+
* UUIDv5 (RFC 4122 §4.3), hand-rolled over `node:crypto`'s SHA-1 rather than
|
|
425
|
+
* a dependency — correctness is checked against the RFC's own DNS-namespace
|
|
426
|
+
* test vector in `Sessions.test.ts`.
|
|
427
|
+
*/
|
|
428
|
+
const uuidv5 = (namespace, name) => {
|
|
429
|
+
const bytes = createHash("sha1").update(toBytes(namespace)).update(name, "utf8").digest().subarray(0, 16);
|
|
430
|
+
bytes[6] = bytes[6] & 15 | 80;
|
|
431
|
+
bytes[8] = bytes[8] & 63 | 128;
|
|
432
|
+
return toUuidString(bytes);
|
|
433
|
+
};
|
|
434
|
+
/**
|
|
435
|
+
* Resolves a `prompt` rest's session id: with no `memoryKey` there's no
|
|
436
|
+
* history to derive a stable id from, so it mints an ephemeral one and forces
|
|
437
|
+
* `resume: false`; otherwise the id is deterministic (`uuidv5`) and `resume`
|
|
438
|
+
* is passed through as given.
|
|
439
|
+
*/
|
|
440
|
+
const resolveSession = (memoryKey, resume, mint = randomUUID) => memoryKey === void 0 ? {
|
|
441
|
+
sessionId: mint(),
|
|
442
|
+
resume: false
|
|
443
|
+
} : {
|
|
444
|
+
sessionId: uuidv5(GTD_SESSION_NAMESPACE, memoryKey),
|
|
445
|
+
resume
|
|
446
|
+
};
|
|
447
|
+
//#endregion
|
|
448
|
+
//#region src/wire/Glob.ts
|
|
449
|
+
const ESCAPE_RE = /[.+^${}()|[\]\\?]/g;
|
|
450
|
+
const compile = (glob) => {
|
|
451
|
+
let pattern = "^";
|
|
452
|
+
let i = 0;
|
|
453
|
+
while (i < glob.length) {
|
|
454
|
+
const char = glob[i];
|
|
455
|
+
if (char !== "*") {
|
|
456
|
+
pattern += char.replace(ESCAPE_RE, "\\$&");
|
|
457
|
+
i += 1;
|
|
458
|
+
} else if (glob[i + 1] !== "*") {
|
|
459
|
+
pattern += "[^/]*";
|
|
460
|
+
i += 1;
|
|
461
|
+
} else if (glob[i + 2] === "/") {
|
|
462
|
+
pattern += "(?:.*/)?";
|
|
463
|
+
i += 3;
|
|
464
|
+
} else {
|
|
465
|
+
pattern += ".*";
|
|
466
|
+
i += 2;
|
|
467
|
+
}
|
|
468
|
+
}
|
|
469
|
+
return new RegExp(`${pattern}$`);
|
|
470
|
+
};
|
|
471
|
+
const globMatches = (path, glob) => compile(glob).test(path);
|
|
472
|
+
//#endregion
|
|
473
|
+
export { EXIT_SIGTERM as C, WIRE_SCHEMA as D, UNATTRIBUTED_MODEL as E, renderFormat as O, EXIT_SIGINT as S, ACCESS_REFUSAL as T, demandOf as _, landFields as a, EXIT_OK as b, renderLandJson as c, renderBeatPlain as d, beatDocument as f, beatKindOf as g, BEAT_KINDS as h, judgeJson as i, renderLandPlain as l, statusOf as m, resolveSession as n, landProseText as o, renderBeatJson as p, selectPath as r, noopText as s, globMatches as t, DEMAND_BRIEFING as u, stallDiagnosis as v, EXIT_USAGE_ERROR as w, EXIT_RUNTIME_ERROR as x, EXIT_CODES as y };
|