@pmelab/gtd 62.0.0 → 63.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 +14 -8
- package/dist/chunks/{testkit-XQeYCNMn.mjs → testkit-DwypA7cH.mjs} +183 -28
- package/dist/gtd.bundle.mjs +182 -26
- package/dist/gtd.core.mjs +80 -77
- package/dist/testkit/gherkin.mjs +1 -1
- package/dist/testkit.mjs +1 -1
- package/package.json +1 -1
- package/src/cli/Cli.test.ts +2 -0
- package/src/cli/Cli.ts +12 -0
- package/src/cli/Launch.ts +1 -0
- package/src/cli/Program.test.ts +9 -7
- package/src/cli/Program.ts +51 -0
- package/src/edge/Edge.test.ts +3 -3
- package/src/edge/Edge.ts +68 -3
- package/src/edge/index.ts +1 -0
- package/src/emit/GitScript.ts +7 -0
- package/src/emit/index.ts +1 -0
- package/src/replay/Format.test.ts +26 -7
- package/src/replay/Format.ts +3 -2
- package/src/replay/Replay.test.ts +87 -0
- package/src/replay/Replay.ts +111 -23
- package/src/replay/Trailers.test.ts +10 -10
- package/src/replay/Trailers.ts +9 -0
- package/src/replay/index.ts +1 -0
- package/src/step/planStart.test.ts +2 -2
- package/src/step/planStep.test.ts +4 -4
- package/src/twins/EmittedScriptRecognizer.ts +3 -2
package/src/replay/Replay.ts
CHANGED
|
@@ -127,7 +127,14 @@ export type ReplayOutcome =
|
|
|
127
127
|
readonly landed: ReachedStep | undefined
|
|
128
128
|
readonly marks: readonly Mark[]
|
|
129
129
|
}
|
|
130
|
-
| {
|
|
130
|
+
| {
|
|
131
|
+
readonly kind: "divergence"
|
|
132
|
+
readonly message: string
|
|
133
|
+
/** The commit replay last stood on — where a repin anchors. */
|
|
134
|
+
readonly at: string
|
|
135
|
+
/** The step replay reached there; `undefined` when the flow ended instead. */
|
|
136
|
+
readonly reached: string | undefined
|
|
137
|
+
}
|
|
131
138
|
| { readonly kind: "refused"; readonly message: string }
|
|
132
139
|
| { readonly kind: "failed"; readonly message: string }
|
|
133
140
|
|
|
@@ -261,6 +268,38 @@ interface ParsedCommit extends EpisodeCommit {
|
|
|
261
268
|
readonly parsed: CommitMessage
|
|
262
269
|
}
|
|
263
270
|
|
|
271
|
+
const anchorOf = (parsed: CommitMessage): string | undefined =>
|
|
272
|
+
parsed.pin === "add" ? parsed.anchor : undefined
|
|
273
|
+
|
|
274
|
+
const isAnchor = (commit: ParsedCommit): boolean => anchorOf(commit.parsed) !== undefined
|
|
275
|
+
|
|
276
|
+
/**
|
|
277
|
+
* The commits every anchor sets aside — those between the commit it names and
|
|
278
|
+
* itself — or a fault for an anchor naming no commit still read.
|
|
279
|
+
*/
|
|
280
|
+
export const setAsideBy = (
|
|
281
|
+
base: string,
|
|
282
|
+
all: readonly { readonly hash: string; readonly parsed: CommitMessage }[],
|
|
283
|
+
): { readonly setAside: ReadonlySet<string> } | { readonly fault: string } => {
|
|
284
|
+
const setAside = new Set<string>()
|
|
285
|
+
if (all.every((c) => anchorOf(c.parsed) === undefined)) return { setAside }
|
|
286
|
+
let kept: string[] = []
|
|
287
|
+
for (const commit of all) {
|
|
288
|
+
const anchor = anchorOf(commit.parsed)
|
|
289
|
+
if (anchor !== undefined) {
|
|
290
|
+
const at = anchor === base ? -1 : kept.indexOf(anchor)
|
|
291
|
+
if (anchor !== base && at === -1) {
|
|
292
|
+
return {
|
|
293
|
+
fault: `gtd: commit ${short(commit.hash)} anchors the process at ${short(anchor)}, which is no commit replay reads in this episode`,
|
|
294
|
+
}
|
|
295
|
+
}
|
|
296
|
+
for (const hash of kept.splice(at + 1)) setAside.add(hash)
|
|
297
|
+
}
|
|
298
|
+
kept.push(commit.hash)
|
|
299
|
+
}
|
|
300
|
+
return { setAside }
|
|
301
|
+
}
|
|
302
|
+
|
|
264
303
|
/**
|
|
265
304
|
* Replay one episode: run its entry flow, answering each step from the
|
|
266
305
|
* episode's commits in order, until a step has no commit left — the rest.
|
|
@@ -323,10 +362,17 @@ const withoutPin = (input: ReplayInput): ReplayInput => {
|
|
|
323
362
|
|
|
324
363
|
export const replay = async (raw: ReplayInput): Promise<ReplayOutcome> => {
|
|
325
364
|
const input = withoutPin(raw)
|
|
326
|
-
const
|
|
365
|
+
const all: readonly ParsedCommit[] = input.episode.commits.map((c) => ({
|
|
327
366
|
...c,
|
|
328
367
|
parsed: parseCommitMessage(c.message),
|
|
329
368
|
}))
|
|
369
|
+
const resolved = setAsideBy(input.episode.base.hash, all)
|
|
370
|
+
if ("fault" in resolved) return { kind: "failed", message: resolved.fault }
|
|
371
|
+
const setAside = resolved.setAside
|
|
372
|
+
const commits = all.filter((c) => !setAside.has(c.hash))
|
|
373
|
+
const pinned = (raw.episode.commits.at(-1)?.tree ?? raw.episode.base.tree)
|
|
374
|
+
.paths()
|
|
375
|
+
.includes(PIN_PATH)
|
|
330
376
|
|
|
331
377
|
// Every replay re-derives base, commits and head() from whatever history
|
|
332
378
|
// exists right now, so a hash a flow reads this same run (head()/start())
|
|
@@ -343,12 +389,27 @@ export const replay = async (raw: ReplayInput): Promise<ReplayOutcome> => {
|
|
|
343
389
|
let pendingUsed = false
|
|
344
390
|
let position: Position = input.episode.base
|
|
345
391
|
// Never stands on a pin commit: `head()` names the turn, so a flow
|
|
346
|
-
// reverting it reverts the turn, not the pin.
|
|
392
|
+
// reverting it reverts the turn, not the pin. An anchor is the exception:
|
|
393
|
+
// replay stands on it for HEAD's tree, so the flow code leading to the step
|
|
394
|
+
// it names, and that step's actor, see the set-aside work in place.
|
|
395
|
+
// `changes()` keeps the previous position: the set-aside range counts as
|
|
396
|
+
// part of the last step replay read.
|
|
347
397
|
const skipPins = (): void => {
|
|
348
|
-
while (cursor < commits.length && commits[cursor]!.parsed.pin !== undefined)
|
|
398
|
+
while (cursor < commits.length && commits[cursor]!.parsed.pin !== undefined) {
|
|
399
|
+
const commit = commits[cursor]!
|
|
400
|
+
cursor++
|
|
401
|
+
if (!isAnchor(commit)) continue
|
|
402
|
+
position = commit
|
|
403
|
+
const to = commit.parsed.parsed?.to
|
|
404
|
+
expectedNext =
|
|
405
|
+
to === undefined || to === UNPIN_STATE ? undefined : { name: to, hash: commit.hash }
|
|
406
|
+
anchorPending = expectedNext !== undefined
|
|
407
|
+
}
|
|
349
408
|
}
|
|
350
409
|
let previousPosition: Position = input.episode.base
|
|
351
410
|
let expectedNext: { readonly name: string; readonly hash: string } | undefined
|
|
411
|
+
// Whether `expectedNext` is an anchor's, which a flow that ends contradicts.
|
|
412
|
+
let anchorPending = false
|
|
352
413
|
const occurrences = new Map<string, number>()
|
|
353
414
|
const personas = new Map<string, Identity>()
|
|
354
415
|
const scopes: ScopeOptions[] = []
|
|
@@ -547,18 +608,28 @@ export const replay = async (raw: ReplayInput): Promise<ReplayOutcome> => {
|
|
|
547
608
|
: `gtd: "${step.name}" runs with a different model, system prompt, skills or file access than an earlier agent step in its conversation in memory scope "${step.memoryScope || "root"}" — a conversation has one identity`
|
|
548
609
|
}
|
|
549
610
|
|
|
550
|
-
const DIVERGED =
|
|
611
|
+
const DIVERGED = pinned
|
|
612
|
+
? "the workflow changed under this process; run `gtd repin` to continue under it from there, or `gtd abandon` to start over"
|
|
613
|
+
: "the workflow changed under this process; run `gtd abandon` to start over"
|
|
614
|
+
|
|
615
|
+
const diverged = (message: string, reached: ReachedStep | undefined): ReplayOutcome => ({
|
|
616
|
+
kind: "divergence",
|
|
617
|
+
message: `${message} — ${DIVERGED}`,
|
|
618
|
+
at: position.hash,
|
|
619
|
+
reached: reached?.name,
|
|
620
|
+
})
|
|
551
621
|
|
|
552
622
|
// Checks a reached step against what the previous commit's subject promised
|
|
553
623
|
// and against the persona of its scope — the reasons replay cannot go on.
|
|
554
624
|
const blockerAt = (step: ReachedStep): ReplayOutcome | undefined => {
|
|
555
625
|
const expected = expectedNext
|
|
556
626
|
expectedNext = undefined
|
|
627
|
+
anchorPending = false
|
|
557
628
|
if (expected !== undefined && expected.name !== step.name) {
|
|
558
|
-
return
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
629
|
+
return diverged(
|
|
630
|
+
`gtd: commit ${short(expected.hash)} names "${expected.name}" as the next step, but replay reached "${step.name}"`,
|
|
631
|
+
step,
|
|
632
|
+
)
|
|
562
633
|
}
|
|
563
634
|
const error =
|
|
564
635
|
unknownOptions(step) ?? approveConflict(step) ?? modeWithoutFile(step) ?? checkIdentity(step)
|
|
@@ -569,10 +640,10 @@ export const replay = async (raw: ReplayInput): Promise<ReplayOutcome> => {
|
|
|
569
640
|
if (step.request.kind !== "human" || step.request.options.approve !== true) return undefined
|
|
570
641
|
const approved = commit.parsed.approved
|
|
571
642
|
if (approved !== undefined && formatStepId(approved.step) === expected) return undefined
|
|
572
|
-
return
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
643
|
+
return diverged(
|
|
644
|
+
`gtd: commit ${short(commit.hash)} completes approval gate "${expected}" but records no Gtd-Approved for it`,
|
|
645
|
+
step,
|
|
646
|
+
)
|
|
576
647
|
}
|
|
577
648
|
|
|
578
649
|
const consumeCommit = (step: ReachedStep, commit: ParsedCommit): unknown => {
|
|
@@ -581,21 +652,27 @@ export const replay = async (raw: ReplayInput): Promise<ReplayOutcome> => {
|
|
|
581
652
|
if (recorded === undefined || formatStepId(recorded) !== expected) {
|
|
582
653
|
const what =
|
|
583
654
|
recorded === undefined ? "records no step" : `records step "${formatStepId(recorded)}"`
|
|
584
|
-
return finish(
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
655
|
+
return finish(
|
|
656
|
+
diverged(
|
|
657
|
+
`gtd: commit ${short(commit.hash)} ${what}, but replay expected "${expected}"`,
|
|
658
|
+
step,
|
|
659
|
+
),
|
|
660
|
+
)
|
|
588
661
|
}
|
|
589
662
|
const unapproved = missingApproval(step, commit, expected)
|
|
590
663
|
if (unapproved !== undefined) return finish(unapproved)
|
|
591
664
|
cursor++
|
|
592
665
|
advance(commit)
|
|
666
|
+
expectNextOf(commit)
|
|
593
667
|
skipPins()
|
|
594
668
|
noteKept(step, commit)
|
|
669
|
+
return replyFor(step.request, commit.parsed.judge)
|
|
670
|
+
}
|
|
671
|
+
|
|
672
|
+
// A turn that ends a pinned process names the unpin, which is no step.
|
|
673
|
+
const expectNextOf = (commit: ParsedCommit): void => {
|
|
595
674
|
const to = commit.parsed.parsed?.to
|
|
596
|
-
// A turn that ends a pinned process names the unpin, which is no step.
|
|
597
675
|
if (to !== undefined && to !== UNPIN_STATE) expectedNext = { name: to, hash: commit.hash }
|
|
598
|
-
return replyFor(step.request, commit.parsed.judge)
|
|
599
676
|
}
|
|
600
677
|
|
|
601
678
|
const noteKept = (step: ReachedStep, commit: ParsedCommit): void => {
|
|
@@ -679,16 +756,27 @@ export const replay = async (raw: ReplayInput): Promise<ReplayOutcome> => {
|
|
|
679
756
|
|
|
680
757
|
const endOfFlow = (via: "return" | "restart"): ReplayOutcome => {
|
|
681
758
|
skipPins()
|
|
759
|
+
if (anchorPending && cursor === commits.length) {
|
|
760
|
+
return diverged(
|
|
761
|
+
`gtd: the flow ended, but commit ${short(expectedNext!.hash)} continues the process at "${expectedNext!.name}"`,
|
|
762
|
+
undefined,
|
|
763
|
+
)
|
|
764
|
+
}
|
|
682
765
|
return cursor < commits.length
|
|
683
|
-
?
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
766
|
+
? diverged(
|
|
767
|
+
`gtd: the flow ended, but commit ${short(commits[cursor]!.hash)} continues the episode`,
|
|
768
|
+
undefined,
|
|
769
|
+
)
|
|
687
770
|
: { kind: "ended", via, trace, landed, marks }
|
|
688
771
|
}
|
|
689
772
|
|
|
690
773
|
const changesSince = (hash: string): readonly Change[] => {
|
|
691
774
|
const tree = treeAt(hash)
|
|
775
|
+
if (setAside.has(hash)) {
|
|
776
|
+
throw new Error(
|
|
777
|
+
`gtd: changesSince(${hash}): a repin set ${short(hash)} aside — pass a hash this run read from head() or start()`,
|
|
778
|
+
)
|
|
779
|
+
}
|
|
692
780
|
if (tree === undefined) {
|
|
693
781
|
throw new Error(
|
|
694
782
|
`gtd: changesSince(${hash}): ${hash} is not the episode base or one of its commits — pass a hash this run read from head() or start(), not one captured earlier, read from state, or from another branch`,
|
|
@@ -46,7 +46,7 @@ describe("the commit-message codec", () => {
|
|
|
46
46
|
reviewBase: "abc",
|
|
47
47
|
})
|
|
48
48
|
expect(message).toBe(
|
|
49
|
-
"gtd(human): fix-precheck\n\nGtd-Workflow: fix\nGtd-Review-Base: abc\nGtd-Format:
|
|
49
|
+
"gtd(human): fix-precheck\n\nGtd-Workflow: fix\nGtd-Review-Base: abc\nGtd-Format: 3",
|
|
50
50
|
)
|
|
51
51
|
expect(parseCommitMessage(message).workflow).toBe("fix")
|
|
52
52
|
expect(parseCommitMessage("gtd(human): fix-precheck").workflow).toBeUndefined()
|
|
@@ -56,7 +56,7 @@ describe("the commit-message codec", () => {
|
|
|
56
56
|
const one = formatCommitMessage({ actor: "human", to: "x", vars: { b: "2", a: "1", c: "" } })
|
|
57
57
|
const two = formatCommitMessage({ actor: "human", to: "x", vars: { c: "", a: "1", b: "2" } })
|
|
58
58
|
expect(one).toBe(two)
|
|
59
|
-
expect(one).toBe("gtd(human): x\n\nGtd-Var: a=1\nGtd-Var: b=2\nGtd-Var: c=\nGtd-Format:
|
|
59
|
+
expect(one).toBe("gtd(human): x\n\nGtd-Var: a=1\nGtd-Var: b=2\nGtd-Var: c=\nGtd-Format: 3")
|
|
60
60
|
expect(parseCommitMessage(one).vars).toEqual({ a: "1", b: "2", c: "" })
|
|
61
61
|
})
|
|
62
62
|
|
|
@@ -81,8 +81,8 @@ describe("the commit-message codec", () => {
|
|
|
81
81
|
it("reads a Gtd-Version carrying control bytes as unknown, and keeps spaces and parens", () => {
|
|
82
82
|
const withVersion = (value: string) =>
|
|
83
83
|
formatCommitMessage({ actor: "a", to: "b" }).replace(
|
|
84
|
-
"Gtd-Format:
|
|
85
|
-
`Gtd-Version: ${value}\nGtd-Format:
|
|
84
|
+
"Gtd-Format: 3",
|
|
85
|
+
`Gtd-Version: ${value}\nGtd-Format: 3`,
|
|
86
86
|
)
|
|
87
87
|
expect(parseCommitMessage(withVersion("1.0\u001b]0;x\u0007")).version).toBeUndefined()
|
|
88
88
|
expect(parseCommitMessage(withVersion("1.0\u009b")).version).toBeUndefined()
|
|
@@ -100,18 +100,18 @@ describe("the commit-message codec", () => {
|
|
|
100
100
|
|
|
101
101
|
it("writes the current Gtd-Format when branch and version are written", () => {
|
|
102
102
|
const message = formatCommitMessage({ actor: "a", to: "b", branch: "m", version: "1" })
|
|
103
|
-
expect(message.endsWith("Gtd-Format:
|
|
103
|
+
expect(message.endsWith("Gtd-Format: 3")).toBe(true)
|
|
104
104
|
})
|
|
105
105
|
|
|
106
106
|
it("writes Gtd-Format last on every message, even one with no other trailer", () => {
|
|
107
107
|
const message = formatCommitMessage({ actor: "agent", to: "building" })
|
|
108
|
-
expect(message).toBe("gtd(agent): building\n\nGtd-Format:
|
|
109
|
-
expect(parseCommitMessage(message).format).toBe(
|
|
108
|
+
expect(message).toBe("gtd(agent): building\n\nGtd-Format: 3")
|
|
109
|
+
expect(parseCommitMessage(message).format).toBe(3)
|
|
110
110
|
})
|
|
111
111
|
|
|
112
112
|
it("reads a message without Gtd-Format as format 1, and an explicit one as written", () => {
|
|
113
113
|
expect(parseCommitMessage("gtd(agent): a → b\n\nGtd-Step: a#1").format).toBe(1)
|
|
114
|
-
expect(parseCommitMessage("gtd(agent): a → b\n\nGtd-Format:
|
|
114
|
+
expect(parseCommitMessage("gtd(agent): a → b\n\nGtd-Format: 3").format).toBe(3)
|
|
115
115
|
expect(parseCommitMessage("gtd(agent): a → b\n\nGtd-Format: two").format).toBe(1)
|
|
116
116
|
})
|
|
117
117
|
|
|
@@ -149,7 +149,7 @@ describe("Gtd-Kept", () => {
|
|
|
149
149
|
it("reads undefined when absent, and leaves Gtd-Format at the current format", () => {
|
|
150
150
|
const parsed = parseCommitMessage(formatCommitMessage({ actor: "agent", to: "b" }))
|
|
151
151
|
expect(parsed.kept).toBeUndefined()
|
|
152
|
-
expect(parsed.format).toBe(
|
|
152
|
+
expect(parsed.format).toBe(3)
|
|
153
153
|
})
|
|
154
154
|
})
|
|
155
155
|
|
|
@@ -289,7 +289,7 @@ describe("Gtd-Mark", () => {
|
|
|
289
289
|
const at = lines.findIndex((l) => l.startsWith("Gtd-Mark:"))
|
|
290
290
|
expect(lines[at - 1]).toMatch(/^Gtd-Judge:/)
|
|
291
291
|
expect(lines[at + 1]).toMatch(/^Gtd-Approved:/)
|
|
292
|
-
expect(lines).toContain("Gtd-Format:
|
|
292
|
+
expect(lines).toContain("Gtd-Format: 3")
|
|
293
293
|
})
|
|
294
294
|
|
|
295
295
|
it("writes nothing without marks", () => {
|
package/src/replay/Trailers.ts
CHANGED
|
@@ -94,6 +94,8 @@ export interface CommitMessage {
|
|
|
94
94
|
readonly approved: Approval | undefined
|
|
95
95
|
/** `Gtd-Pin`: a commit that adds or removes the pin — never a turn. */
|
|
96
96
|
readonly pin: PinChange | undefined
|
|
97
|
+
/** `Gtd-Anchor`, on a repin's commit: replay sets aside every commit after this one; a non-hash value is skipped. */
|
|
98
|
+
readonly anchor: string | undefined
|
|
97
99
|
readonly format: number
|
|
98
100
|
}
|
|
99
101
|
|
|
@@ -153,6 +155,7 @@ interface Trailers {
|
|
|
153
155
|
kept: string | undefined
|
|
154
156
|
approved: Approval | undefined
|
|
155
157
|
pin: PinChange | undefined
|
|
158
|
+
anchor: string | undefined
|
|
156
159
|
readonly judge: JudgeVerdict[]
|
|
157
160
|
readonly marks: Mark[]
|
|
158
161
|
readonly vars: Record<string, string>
|
|
@@ -200,6 +203,9 @@ const TRAILER_READERS: Readonly<Record<string, (value: string, into: Trailers) =
|
|
|
200
203
|
"Gtd-Pin": (value, into) => {
|
|
201
204
|
if (value === "add" || value === "remove") into.pin ??= value
|
|
202
205
|
},
|
|
206
|
+
"Gtd-Anchor": (value, into) => {
|
|
207
|
+
if (HASH_RE.test(value)) into.anchor ??= value
|
|
208
|
+
},
|
|
203
209
|
"Gtd-Format": (value, into) => {
|
|
204
210
|
if (FORMAT_RE.test(value)) into.format ??= Number(value)
|
|
205
211
|
},
|
|
@@ -221,6 +227,7 @@ export const parseCommitMessage = (message: string): CommitMessage => {
|
|
|
221
227
|
kept: undefined,
|
|
222
228
|
approved: undefined,
|
|
223
229
|
pin: undefined,
|
|
230
|
+
anchor: undefined,
|
|
224
231
|
judge: [],
|
|
225
232
|
marks: [],
|
|
226
233
|
vars: {},
|
|
@@ -250,6 +257,7 @@ export interface CommitSpec {
|
|
|
250
257
|
/** Written after `Gtd-Mark`, before `Gtd-Branch`. */
|
|
251
258
|
readonly approved?: Approval
|
|
252
259
|
readonly pin?: PinChange
|
|
260
|
+
readonly anchor?: string
|
|
253
261
|
readonly branch?: string
|
|
254
262
|
readonly version?: string
|
|
255
263
|
}
|
|
@@ -275,6 +283,7 @@ export const formatCommitMessage = (spec: CommitSpec): string => {
|
|
|
275
283
|
...(spec.marks ?? []).map((m) => `Gtd-Mark: ${m.name}=${m.value}`),
|
|
276
284
|
...trailer("Approved", spec.approved && encodeApproval(spec.approved)),
|
|
277
285
|
...trailer("Pin", spec.pin),
|
|
286
|
+
...trailer("Anchor", spec.anchor),
|
|
278
287
|
...trailer("Branch", spec.branch),
|
|
279
288
|
...trailer("Version", spec.version),
|
|
280
289
|
`Gtd-Format: ${HISTORY_FORMAT}`,
|
package/src/replay/index.ts
CHANGED
|
@@ -176,7 +176,7 @@ describe("planStart", () => {
|
|
|
176
176
|
kind: "gitWrite",
|
|
177
177
|
write: {
|
|
178
178
|
kind: "commitAll",
|
|
179
|
-
message: "gtd(human): work\n\nGtd-Workflow: working\nGtd-Var: base=\nGtd-Format:
|
|
179
|
+
message: "gtd(human): work\n\nGtd-Workflow: working\nGtd-Var: base=\nGtd-Format: 3",
|
|
180
180
|
},
|
|
181
181
|
},
|
|
182
182
|
{ kind: "outcome", outcome: { kind: "commit", subject: "gtd(human): work" } },
|
|
@@ -195,7 +195,7 @@ describe("planStart", () => {
|
|
|
195
195
|
if (write?.kind !== "gitWrite" || write.write.kind !== "commitAll")
|
|
196
196
|
throw new Error("expected a gitWrite step")
|
|
197
197
|
expect(write.write.message).toBe(
|
|
198
|
-
"gtd(human): work\n\nGtd-Workflow: working\nGtd-Var: base=main\nGtd-Format:
|
|
198
|
+
"gtd(human): work\n\nGtd-Workflow: working\nGtd-Var: base=main\nGtd-Format: 3",
|
|
199
199
|
)
|
|
200
200
|
const outcome = plan.steps.find((s) => s.kind === "outcome")
|
|
201
201
|
expect(outcome).toEqual({
|
|
@@ -39,7 +39,7 @@ describe("planStep — commit", () => {
|
|
|
39
39
|
expect(outcome.steps).toEqual([
|
|
40
40
|
{
|
|
41
41
|
kind: "gitWrite",
|
|
42
|
-
write: { kind: "commitAll", message: "gtd(agent): await-answers\n\nGtd-Format:
|
|
42
|
+
write: { kind: "commitAll", message: "gtd(agent): await-answers\n\nGtd-Format: 3" },
|
|
43
43
|
},
|
|
44
44
|
{ kind: "outcome", outcome: { kind: "commit", subject: "gtd(agent): await-answers" } },
|
|
45
45
|
])
|
|
@@ -59,7 +59,7 @@ describe("planStep — commit", () => {
|
|
|
59
59
|
kind: "gitWrite",
|
|
60
60
|
write: {
|
|
61
61
|
kind: "commitAll",
|
|
62
|
-
message: "gtd(human): building → done\n\nGtd-Step: building#1\nGtd-Format:
|
|
62
|
+
message: "gtd(human): building → done\n\nGtd-Step: building#1\nGtd-Format: 3",
|
|
63
63
|
},
|
|
64
64
|
})
|
|
65
65
|
expect(outcome.steps.at(-1)).toEqual({
|
|
@@ -99,7 +99,7 @@ describe("planStep — commit", () => {
|
|
|
99
99
|
expect(trailers[1]).toBe("Gtd-Cost: 1.5 m")
|
|
100
100
|
expect(trailers[2]).toBe('Gtd-Judge: {"id":"q1","answer":true,"p":0.9}')
|
|
101
101
|
expect(trailers[3]).toBe('Gtd-Judge: {"id":"q2","answer":"x","p":0.4}')
|
|
102
|
-
expect(trailers[4]).toBe("Gtd-Format:
|
|
102
|
+
expect(trailers[4]).toBe("Gtd-Format: 3")
|
|
103
103
|
expect(trailers).toHaveLength(5)
|
|
104
104
|
})
|
|
105
105
|
})
|
|
@@ -182,7 +182,7 @@ describe("planStep — keepTurn", () => {
|
|
|
182
182
|
write: {
|
|
183
183
|
kind: "keepTurn",
|
|
184
184
|
ref: kept,
|
|
185
|
-
message: "gtd(agent): building → checking\n\nGtd-Step: building#1\nGtd-Format:
|
|
185
|
+
message: "gtd(agent): building → checking\n\nGtd-Step: building#1\nGtd-Format: 3",
|
|
186
186
|
},
|
|
187
187
|
})
|
|
188
188
|
})
|
|
@@ -26,6 +26,7 @@ import {
|
|
|
26
26
|
modeContradictionSkipNotice,
|
|
27
27
|
OUTCOME_MARKER,
|
|
28
28
|
pinScript,
|
|
29
|
+
repinScript,
|
|
29
30
|
unpinScript,
|
|
30
31
|
} from "../emit/index.js"
|
|
31
32
|
import {
|
|
@@ -272,9 +273,9 @@ const recognizeGitBuilders = (repo: InMemRepo, block: string): BlockOutcome | un
|
|
|
272
273
|
/** What the in-memory `gtd bundle` prints: a pin header, and no engine behind it. */
|
|
273
274
|
const IN_MEMORY_PIN = "gtdpin 1 gtd in-memory\n"
|
|
274
275
|
|
|
275
|
-
/** A pin or unpin beat's script. */
|
|
276
|
+
/** A pin or unpin beat's script, or `gtd repin`'s. */
|
|
276
277
|
const recognizePinScripts = (repo: InMemRepo, block: string): BlockOutcome | undefined => {
|
|
277
|
-
if (block === pinScript(PIN_PATH)) {
|
|
278
|
+
if (block === pinScript(PIN_PATH) || block === repinScript(PIN_PATH)) {
|
|
278
279
|
repo.writeFile(PIN_PATH, IN_MEMORY_PIN)
|
|
279
280
|
return { kind: "applied" }
|
|
280
281
|
}
|