@pmelab/gtd 24.3.0 → 25.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 +28 -22
- package/dist/chunks/{reserved-DJwU5C4z.mjs → reserved-6pAgFGgB.mjs} +119 -135
- package/dist/chunks/{steering-D-PvRC5W.mjs → steering-DVILanVf.mjs} +16 -5
- package/dist/flows.mjs +9 -1
- package/dist/gtd.bundle.mjs +780 -813
- package/dist/steering.mjs +2 -2
- package/dist/testkit.mjs +666 -161
- package/dist/workflow-kit.mjs +55 -96
- package/dist/workflows/feature.mjs +171 -76
- package/dist/workflows/fix.mjs +2 -2
- package/dist/workflows/review.mjs +1 -1
- package/dist/workflows.mjs +353 -310
- package/package.json +1 -2
- package/src/cli/Program.test.ts +3 -8
- package/src/cli/Program.ts +1 -8
- package/src/edge/Edge.test.ts +5 -2
- package/src/edge/Edge.ts +3 -4
- package/src/emit/index.ts +1 -0
- package/src/flows/scripts.ts +6 -0
- package/src/platform/Git.test.ts +6 -9
- package/src/replay/Replay.ts +2 -1
- package/src/steering/FORMATS.md +1 -1
- package/src/steering/SteeringFormat.test.ts +9 -1
- package/src/steering/SteeringFormat.ts +7 -0
- package/src/steering/contract.test.ts +3 -0
- package/src/steering/index.ts +1 -0
- package/src/steering/review.test.ts +30 -0
- package/src/steering/review.ts +8 -2
- package/src/testkit/EmittedScriptRecognizer.test.ts +29 -30
- package/src/testkit/EmittedScriptRecognizer.ts +148 -51
- package/src/testkit/EmittedScripts.test.ts +166 -0
- package/src/testkit/FlowScripts.test.ts +242 -0
- package/src/testkit/FlowScripts.ts +326 -0
- package/src/testkit/GitTiers.ts +140 -49
- package/src/testkit/InMemRepo.ts +35 -2
- package/src/testkit/Layers.ts +10 -2
- package/src/testkit/TestRepo.ts +4 -1
- package/src/testkit/index.ts +1 -1
- package/src/workflow/discovery.test.ts +147 -0
- package/src/workflow/discovery.ts +5 -0
- package/src/workflow/fingerprint.test.ts +20 -4
- package/src/workflow/fingerprint.ts +8 -9
- package/src/workflow/index.ts +1 -6
- package/src/workflow/load.test.ts +5 -19
- package/src/workflow/load.ts +94 -10
- package/src/workflow-kit/review.test.ts +84 -0
- package/src/workflow-kit/review.ts +9 -6
- package/src/workflow-kit/scenarios.test.ts +116 -0
- package/src/workflow-kit/scenarios.ts +53 -113
- package/src/workflows/feature/feature.ts +2 -5
- package/src/workflows/feature/packages.test.ts +17 -34
- package/src/workflows/feature/packages.ts +13 -52
- package/src/workflows/feature/planning.ts +1 -1
- package/src/workflows/feature/review.test.ts +233 -54
- package/src/workflows/feature/review.ts +178 -41
- package/src/workflows/feature/steps.test.ts +12 -0
- package/src/workflows/feature/steps.ts +8 -1
- package/src/workflows/index.ts +0 -2
- package/src/workflows/shared/access.test.ts +4 -2
- package/src/workflows/shared/access.ts +1 -0
- package/src/workflows/shared/health.test.ts +1 -21
- package/src/workflows/shared/health.ts +4 -39
- package/src/workflows/shared/options.test.ts +8 -0
- package/src/workflows/shared/options.ts +1 -1
- package/src/workflows/shared/steps.ts +18 -11
- package/src/workflows/shared/tail.test.ts +1 -41
- package/src/workflows/shared/tail.ts +17 -41
- package/src/workflows/shared/text.fixture.ts +1 -0
- package/src/workflows/shared/text.test.ts +129 -41
- package/src/workflows/shared/text.ts +111 -75
- package/src/workflows/shared/vars.ts +1 -1
- package/src/workflows/shared/scenarios.test.ts +0 -256
- package/src/workflows/shared/scenarios.ts +0 -35
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@pmelab/gtd",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "25.0.0",
|
|
4
4
|
"private": false,
|
|
5
5
|
"description": "Git-aware CLI that emits the next prompt for an autonomous coding agent based on the current repository state",
|
|
6
6
|
"bin": {
|
|
@@ -106,7 +106,6 @@
|
|
|
106
106
|
"watch": "tsdown --watch",
|
|
107
107
|
"bench:replay": "node scripts/bench-replay.mjs",
|
|
108
108
|
"test": "turbo run format:check typecheck lint lint:sh lint:boundaries test:unit test:workflows test:e2e:inmem test:e2e:live test:web analyze",
|
|
109
|
-
"test:fast": "turbo run format:check typecheck lint lint:sh lint:boundaries test:unit test:workflows test:web analyze",
|
|
110
109
|
"test:unit": "vitest run --project unit",
|
|
111
110
|
"test:workflows": "vitest run --project workflows",
|
|
112
111
|
"test:watch": "vitest",
|
package/src/cli/Program.test.ts
CHANGED
|
@@ -17,7 +17,7 @@ vi.mock("node:os", async (importOriginal) => ({
|
|
|
17
17
|
|
|
18
18
|
import { runCli, type Command } from "./index.js"
|
|
19
19
|
import { EXIT_USAGE_ERROR, stallDiagnosis, noopText, WIRE_SCHEMA } from "../wire/index.js"
|
|
20
|
-
import {
|
|
20
|
+
import { needsOf, runCommand, SelectorUsageError } from "./Program.js"
|
|
21
21
|
import { InMemRepo, makeCapturingCliIo, testLayers, applyEmittedScript } from "../testkit/index.js"
|
|
22
22
|
import {
|
|
23
23
|
commitAll,
|
|
@@ -28,6 +28,7 @@ import {
|
|
|
28
28
|
} from "../emit/index.js"
|
|
29
29
|
import { HISTORY_REF } from "../platform/index.js"
|
|
30
30
|
import { CONFIG_SCAFFOLD } from "../install/index.js"
|
|
31
|
+
import { contentHash } from "../workflow/index.js"
|
|
31
32
|
|
|
32
33
|
const run = async (
|
|
33
34
|
repo: InMemRepo,
|
|
@@ -102,7 +103,7 @@ export const defaults = { greeting: "hello" }
|
|
|
102
103
|
expect(exitCode).toBe(0)
|
|
103
104
|
expect(stdout).toContain(
|
|
104
105
|
commitAll(
|
|
105
|
-
|
|
106
|
+
`gtd(human): working\n\nGtd-Workflow: hotfix\nGtd-Var: greeting=hello\nGtd-Config: ${contentHash(WORKFLOW)} gtd.config.ts\nGtd-Format: 1`,
|
|
106
107
|
),
|
|
107
108
|
)
|
|
108
109
|
expect(repo.commitHistory()).toHaveLength(before)
|
|
@@ -958,12 +959,6 @@ describe("gtd check <mode> <file>", () => {
|
|
|
958
959
|
)
|
|
959
960
|
})
|
|
960
961
|
|
|
961
|
-
it("a line-carrying finding with no range prints '<file>:<line>: <message>' — the flat-shape fallback no built-in format reaches", async () => {
|
|
962
|
-
expect(formatFinding("FAKE.md", { message: "positioned but rangeless", line: 4 })).toBe(
|
|
963
|
-
"FAKE.md:5: positioned but rangeless",
|
|
964
|
-
)
|
|
965
|
-
})
|
|
966
|
-
|
|
967
962
|
it("an absent file exits 0 with no output", async () => {
|
|
968
963
|
const repo = bareRepo()
|
|
969
964
|
const { stdout, exitCode } = await run(repo, "check", "qa", ".gtd/TODO.md")
|
package/src/cli/Program.ts
CHANGED
|
@@ -66,7 +66,7 @@ import {
|
|
|
66
66
|
openThreads,
|
|
67
67
|
parseCodeThreads,
|
|
68
68
|
unansweredQuestions,
|
|
69
|
-
|
|
69
|
+
formatFinding,
|
|
70
70
|
type SteeringFormat,
|
|
71
71
|
} from "../steering/index.js"
|
|
72
72
|
import {
|
|
@@ -851,13 +851,6 @@ const runValidateCommand = (out: ArtifactOut): Effect.Effect<void, Error, Comman
|
|
|
851
851
|
)
|
|
852
852
|
})
|
|
853
853
|
|
|
854
|
-
/** `file:line:col: message` — the shape editors and grep already jump on. Stored 0-based, printed 1-based. A finding with a `line` but no `range`, or none at all, degrades to the shorter forms. */
|
|
855
|
-
export const formatFinding = (file: string, finding: SteeringFinding): string => {
|
|
856
|
-
if (finding.line === undefined) return finding.message
|
|
857
|
-
const col = finding.range !== undefined ? `:${finding.range.start.character + 1}` : ""
|
|
858
|
-
return `${file}:${finding.line + 1}${col}: ${finding.message}`
|
|
859
|
-
}
|
|
860
|
-
|
|
861
854
|
/**
|
|
862
855
|
* Runs `<mode>`'s parser over `<file>`, exiting non-zero on any finding.
|
|
863
856
|
* Resolves no workflow state and reads no config — this is what a seeded
|
package/src/edge/Edge.test.ts
CHANGED
|
@@ -22,6 +22,7 @@ import { GTD_VERSION } from "../install/index.js"
|
|
|
22
22
|
import { processHistoryRef } from "../platform/index.js"
|
|
23
23
|
import { GitService, Workspace, type GitOperations, type WorkspaceOps } from "../platform/index.js"
|
|
24
24
|
import { formatCommitMessage, type CommitSpec } from "../replay/index.js"
|
|
25
|
+
import { contentHash } from "../workflow/index.js"
|
|
25
26
|
import {
|
|
26
27
|
fakeGitOperations,
|
|
27
28
|
InMemRepo,
|
|
@@ -201,7 +202,7 @@ describe("currentRun", () => {
|
|
|
201
202
|
const before = headOf(repo)
|
|
202
203
|
const opening = await enter(repo, "side")
|
|
203
204
|
expect(repo.lastCommitMessage()).toBe(
|
|
204
|
-
|
|
205
|
+
`gtd(human): fixing\n\nGtd-Workflow: side\nGtd-Var: base=\nGtd-Var: reviewer=nobody\nGtd-Config: ${contentHash(LINEAR)} gtd.config.ts\nGtd-Format: 1`,
|
|
205
206
|
)
|
|
206
207
|
const fixed = await land(repo, { "fix.txt": "x\n" })
|
|
207
208
|
const run = await provide(currentRun, repo)
|
|
@@ -233,7 +234,9 @@ export const again = async () => {
|
|
|
233
234
|
const repo = repoWith(STARTS_AT_INITIAL)
|
|
234
235
|
const before = headOf(repo)
|
|
235
236
|
const opening = await enter(repo, "again")
|
|
236
|
-
expect(repo.lastCommitMessage()).toBe(
|
|
237
|
+
expect(repo.lastCommitMessage()).toBe(
|
|
238
|
+
`gtd(human): idle\n\nGtd-Workflow: again\nGtd-Config: ${contentHash(STARTS_AT_INITIAL)} gtd.config.ts\nGtd-Format: 1`,
|
|
239
|
+
)
|
|
237
240
|
const fixed = await land(repo, { "fix.txt": "x\n" })
|
|
238
241
|
const run = await provide(currentRun, repo)
|
|
239
242
|
expect(run.workflow).toBe("again")
|
package/src/edge/Edge.ts
CHANGED
|
@@ -12,7 +12,6 @@ import {
|
|
|
12
12
|
ConfigDiscovery,
|
|
13
13
|
ConfigService,
|
|
14
14
|
changedConfigFiles,
|
|
15
|
-
configFingerprint,
|
|
16
15
|
contentHash,
|
|
17
16
|
multilineSetting,
|
|
18
17
|
resolveVars,
|
|
@@ -972,7 +971,7 @@ export const restAt = (
|
|
|
972
971
|
}))
|
|
973
972
|
: []
|
|
974
973
|
const memory = memoryOf(outcome.trace, run)
|
|
975
|
-
const configNow =
|
|
974
|
+
const configNow = config.fingerprint
|
|
976
975
|
// Only the current rest warns, so a command must read it once — a second
|
|
977
976
|
// read warns twice. An earlier ref (restore's tip) would warn about a
|
|
978
977
|
// process the command is not acting on.
|
|
@@ -1320,7 +1319,7 @@ const differingConfigPaths = (
|
|
|
1320
1319
|
git: GitService["Type"],
|
|
1321
1320
|
workspace: Workspace["Type"],
|
|
1322
1321
|
root: string,
|
|
1323
|
-
config: { readonly
|
|
1322
|
+
config: { readonly fingerprint: ConfigFingerprint },
|
|
1324
1323
|
run: ProcessRun,
|
|
1325
1324
|
hash: string,
|
|
1326
1325
|
) =>
|
|
@@ -1328,7 +1327,7 @@ const differingConfigPaths = (
|
|
|
1328
1327
|
const differing: string[] = []
|
|
1329
1328
|
for (const path of inRepoConfigPaths(
|
|
1330
1329
|
root,
|
|
1331
|
-
config.
|
|
1330
|
+
Object.keys(config.fingerprint),
|
|
1332
1331
|
run.pinnedConfig?.files ?? {},
|
|
1333
1332
|
)) {
|
|
1334
1333
|
const atTip = yield* Effect.orElseSucceed(git.readFileAtRef(hash, path), () => undefined)
|
package/src/emit/index.ts
CHANGED
package/src/flows/scripts.ts
CHANGED
|
@@ -118,3 +118,9 @@ export const moveScript = (from: string, to: string): string =>
|
|
|
118
118
|
`mv -- ${quote(from)} ${quote(to)}`,
|
|
119
119
|
"",
|
|
120
120
|
].join("\n")
|
|
121
|
+
|
|
122
|
+
/** Write `path` as committed at `ref` to `to`. */
|
|
123
|
+
export const showScript = (ref: string, path: string, to: string): string =>
|
|
124
|
+
["#!/usr/bin/env sh", "set -e", `git show ${quote(`${ref}:${path}`)} > ${quote(to)}`, ""].join(
|
|
125
|
+
"\n",
|
|
126
|
+
)
|
package/src/platform/Git.test.ts
CHANGED
|
@@ -39,12 +39,9 @@ describe("resolvedRefOrCorrupted", () => {
|
|
|
39
39
|
})
|
|
40
40
|
})
|
|
41
41
|
|
|
42
|
-
/**
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
describe("GitScript", () => {
|
|
49
|
-
runGitScriptContract()
|
|
50
|
-
})
|
|
42
|
+
/** `GitScript.ts`'s builders as a driver runs them: real `sh` against the in-memory twin. */
|
|
43
|
+
for (const makeTier of gitTiers) {
|
|
44
|
+
describe(`GitScript [${makeTier().name}]`, () => {
|
|
45
|
+
runGitScriptContract(makeTier)
|
|
46
|
+
})
|
|
47
|
+
}
|
package/src/replay/Replay.ts
CHANGED
|
@@ -260,7 +260,8 @@ export const replay = async (input: ReplayInput): Promise<ReplayOutcome> => {
|
|
|
260
260
|
const treeAt = (hash: string): TreeView | undefined =>
|
|
261
261
|
hash === input.episode.base.hash
|
|
262
262
|
? input.episode.base.tree
|
|
263
|
-
: commits.find((c) => c.hash === hash)?.tree
|
|
263
|
+
: (commits.find((c) => c.hash === hash)?.tree ??
|
|
264
|
+
(hash === input.start ? input.startTree : undefined))
|
|
264
265
|
|
|
265
266
|
let cursor = 0
|
|
266
267
|
let pendingUsed = false
|
package/src/steering/FORMATS.md
CHANGED
|
@@ -47,7 +47,7 @@ Breaking — committed files, or files an older gtd must read, are affected:
|
|
|
47
47
|
questions, options, chunks, hunks, ticks, threads or notes.
|
|
48
48
|
- A writer (a tick, a note, a reply) produces text an older gtd cannot read.
|
|
49
49
|
- Renaming or removing a marker: `## Open Questions`, `_your answer_`,
|
|
50
|
-
`# Review:`, `<!-- base: -->`, `Risk:`, `H:` / `A:`.
|
|
50
|
+
`# Review:`, `<!-- base: -->`, `Risk:`, a leading `! `, `H:` / `A:`.
|
|
51
51
|
|
|
52
52
|
## Format changes bump `Gtd-Format`
|
|
53
53
|
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { describe, expect, it } from "vitest"
|
|
2
|
-
import { steeringFormatFor } from "./index.js"
|
|
2
|
+
import { formatFinding, steeringFormatFor } from "./index.js"
|
|
3
3
|
import type { SteeringFinding, SteeringFormat } from "./SteeringFormat.js"
|
|
4
4
|
|
|
5
5
|
const QA_FORMAT = steeringFormatFor("qa")!
|
|
@@ -89,3 +89,11 @@ describe("SteeringFinding range invariants", () => {
|
|
|
89
89
|
assertRangeInvariants(findings)
|
|
90
90
|
})
|
|
91
91
|
})
|
|
92
|
+
|
|
93
|
+
describe("formatFinding", () => {
|
|
94
|
+
it("a line-carrying finding with no range prints '<file>:<line>: <message>' — the flat-shape fallback no built-in format reaches", () => {
|
|
95
|
+
expect(formatFinding("FAKE.md", { message: "positioned but rangeless", line: 4 })).toBe(
|
|
96
|
+
"FAKE.md:5: positioned but rangeless",
|
|
97
|
+
)
|
|
98
|
+
})
|
|
99
|
+
})
|
|
@@ -41,6 +41,13 @@ export interface SteeringFinding {
|
|
|
41
41
|
}
|
|
42
42
|
}
|
|
43
43
|
|
|
44
|
+
/** `file:line:col: message` — the shape editors and grep already jump on. Stored 0-based, printed 1-based. A finding with a `line` but no `range`, or none at all, degrades to the shorter forms. */
|
|
45
|
+
export const formatFinding = (file: string, finding: SteeringFinding): string => {
|
|
46
|
+
if (finding.line === undefined) return finding.message
|
|
47
|
+
const col = finding.range !== undefined ? `:${finding.range.start.character + 1}` : ""
|
|
48
|
+
return `${file}:${finding.line + 1}${col}: ${finding.message}`
|
|
49
|
+
}
|
|
50
|
+
|
|
44
51
|
/** A hunk pointer's document-link target: `range` covers the pointer token itself; `line` is 0-based, so a `#42` suffix resolves down by one and a bare `./path` lands at 0. */
|
|
45
52
|
export interface SteeringLink {
|
|
46
53
|
readonly range: {
|
|
@@ -174,6 +174,7 @@ describe("review notes, the review → fix hand-off", () => {
|
|
|
174
174
|
anchor: "Parser accepts CRLF",
|
|
175
175
|
before: "**Line endings are normalized before the split.**",
|
|
176
176
|
text: "**Line endings are normalized before the split.** Also normalize in the writer.",
|
|
177
|
+
forced: false,
|
|
177
178
|
},
|
|
178
179
|
{
|
|
179
180
|
id: "note-2",
|
|
@@ -181,6 +182,7 @@ describe("review notes, the review → fix hand-off", () => {
|
|
|
181
182
|
anchor: "Parser accepts CRLF ./src/parse.ts#30-31",
|
|
182
183
|
before: "Risk: a lone `\\r` is still kept as content",
|
|
183
184
|
text: "Risk: a lone `\\r` is still kept as content. Treat it as a line break too.",
|
|
185
|
+
forced: false,
|
|
184
186
|
},
|
|
185
187
|
{
|
|
186
188
|
id: "note-3",
|
|
@@ -188,6 +190,7 @@ describe("review notes, the review → fix hand-off", () => {
|
|
|
188
190
|
anchor: "- [x] ./src/parse.test.ts#1-40 — one case per line ending[^fn1]",
|
|
189
191
|
before: "",
|
|
190
192
|
text: "Add a mixed-endings case.",
|
|
193
|
+
forced: false,
|
|
191
194
|
},
|
|
192
195
|
])
|
|
193
196
|
})
|
package/src/steering/index.ts
CHANGED
|
@@ -5,6 +5,7 @@ import type {
|
|
|
5
5
|
SteeringOutlineNode,
|
|
6
6
|
SteeringPointer,
|
|
7
7
|
} from "./SteeringFormat.js"
|
|
8
|
+
export { formatFinding } from "./SteeringFormat.js"
|
|
8
9
|
import { qaDescriptor } from "./qa.js"
|
|
9
10
|
import { reviewDescriptor } from "./review.js"
|
|
10
11
|
export { reviewNotes, reviewRisks, type ReviewNote } from "./review.js"
|
|
@@ -1733,6 +1733,7 @@ describe("reviewNotes", async () => {
|
|
|
1733
1733
|
anchor: "Chunk one ./src/a.ts#1-3",
|
|
1734
1734
|
before: "",
|
|
1735
1735
|
text: "rename this",
|
|
1736
|
+
forced: false,
|
|
1736
1737
|
},
|
|
1737
1738
|
])
|
|
1738
1739
|
})
|
|
@@ -1753,6 +1754,7 @@ describe("reviewNotes", async () => {
|
|
|
1753
1754
|
anchor: "- [ ] ./src/a.ts#1-3[^n1]",
|
|
1754
1755
|
before: "",
|
|
1755
1756
|
text: "please split",
|
|
1757
|
+
forced: false,
|
|
1756
1758
|
},
|
|
1757
1759
|
])
|
|
1758
1760
|
})
|
|
@@ -1774,6 +1776,34 @@ describe("reviewNotes", async () => {
|
|
|
1774
1776
|
expect(reviewNotes(answered, more).map((n) => n.kind)).toEqual(["footnote"])
|
|
1775
1777
|
})
|
|
1776
1778
|
|
|
1779
|
+
it("a leading ! forces a pointer note and keeps its text", () => {
|
|
1780
|
+
const [n] = reviewNotes(base, base.replace("#1-3", "#1-3 ! rename this"))
|
|
1781
|
+
expect(n).toMatchObject({ kind: "pointer", text: "! rename this", forced: true })
|
|
1782
|
+
})
|
|
1783
|
+
|
|
1784
|
+
it("a leading ! forces a footnote note", () => {
|
|
1785
|
+
const after = base.replace("#1-3", "#1-3[^n1]") + "\n[^n1]: ! please split\n"
|
|
1786
|
+
expect(reviewNotes(base, after)[0]).toMatchObject({ text: "! please split", forced: true })
|
|
1787
|
+
})
|
|
1788
|
+
|
|
1789
|
+
it("a leading ! forces chunk prose", () => {
|
|
1790
|
+
const [n] = reviewNotes(base, doc("! Prose is wrong."))
|
|
1791
|
+
expect(n).toMatchObject({ kind: "chunk", text: "! Prose is wrong.", forced: true })
|
|
1792
|
+
})
|
|
1793
|
+
|
|
1794
|
+
it("a ! glued to the next word is not a marker", () => {
|
|
1795
|
+
const [n] = reviewNotes(base, base.replace("#1-3", "#1-3 != should be ==="))
|
|
1796
|
+
expect(n).toMatchObject({ text: "!= should be ===", forced: false })
|
|
1797
|
+
})
|
|
1798
|
+
|
|
1799
|
+
it("a mid-text ! or a ! in the pointer path does not force", () => {
|
|
1800
|
+
const [n] = reviewNotes(base, base.replace("#1-3", "#1-3 rename this!"))
|
|
1801
|
+
expect(n).toMatchObject({ text: "rename this!", forced: false })
|
|
1802
|
+
const [m] = reviewNotes(base, base.replace("./src/a.ts#1-3", "./src/a!b.ts#1-3 x"))
|
|
1803
|
+
expect(m).toMatchObject({ forced: false })
|
|
1804
|
+
expect(m!.anchor).toContain("a!b.ts")
|
|
1805
|
+
})
|
|
1806
|
+
|
|
1777
1807
|
it("ids run in document order", () => {
|
|
1778
1808
|
const after = base.replace("#1-3", "#1-3 a[^n1]") + "\n[^n1]: b\n"
|
|
1779
1809
|
expect(reviewNotes(base, after).map((n) => [n.id, n.kind])).toEqual([
|
package/src/steering/review.ts
CHANGED
|
@@ -1085,12 +1085,14 @@ export interface ReviewNote {
|
|
|
1085
1085
|
readonly before: string
|
|
1086
1086
|
/** The human's text. */
|
|
1087
1087
|
readonly text: string
|
|
1088
|
+
/** The human opened `text` with `! ` (kept in `text`; the feature review strips it). */
|
|
1089
|
+
readonly forced?: boolean
|
|
1088
1090
|
}
|
|
1089
1091
|
|
|
1090
1092
|
const pointerLabel = (f: ReviewFile): string =>
|
|
1091
1093
|
`${f.path}${f.line === undefined ? "" : `#${f.line}${f.rangeEnd === undefined ? "" : `-${f.rangeEnd}`}`}`
|
|
1092
1094
|
|
|
1093
|
-
type Found = { line: number; note: Omit<ReviewNote, "id"> }
|
|
1095
|
+
type Found = { line: number; note: Omit<ReviewNote, "id" | "forced"> }
|
|
1094
1096
|
|
|
1095
1097
|
const chunkNote = (chunk: Changeset, prior: Changeset | undefined): Found[] =>
|
|
1096
1098
|
chunk.description === (prior?.description ?? "")
|
|
@@ -1162,7 +1164,11 @@ export const reviewNotes = (before: string, after: string): readonly ReviewNote[
|
|
|
1162
1164
|
}),
|
|
1163
1165
|
...footnoteNotes(before, after),
|
|
1164
1166
|
]
|
|
1165
|
-
return found
|
|
1167
|
+
return found
|
|
1168
|
+
.sort((a, b) => a.line - b.line)
|
|
1169
|
+
.map((f, i) => {
|
|
1170
|
+
return { id: `note-${i + 1}`, ...f.note, forced: /^!(\s|$)/.test(f.note.text) }
|
|
1171
|
+
})
|
|
1166
1172
|
}
|
|
1167
1173
|
|
|
1168
1174
|
/** Pointer notes the reviewer opened with `Risk:`, in document order — the ones the workflow fixes before the human gate. */
|
|
@@ -42,7 +42,7 @@ describe("applyEmittedScript — the 8 GitScript builders", () => {
|
|
|
42
42
|
const result = applyEmittedScript(repo, NO_COMMANDS, commitAll("gtd(agent): building"))
|
|
43
43
|
twin.commitAllWithPrefix("gtd(agent): building")
|
|
44
44
|
|
|
45
|
-
expect(result).
|
|
45
|
+
expect(result).toMatchObject({ ok: true })
|
|
46
46
|
expect(snapshot(repo)).toEqual(snapshot(twin))
|
|
47
47
|
})
|
|
48
48
|
|
|
@@ -60,7 +60,7 @@ describe("applyEmittedScript — the 8 GitScript builders", () => {
|
|
|
60
60
|
|
|
61
61
|
const result = applyEmittedScript(repo, NO_COMMANDS, commitAll(message))
|
|
62
62
|
|
|
63
|
-
expect(result).
|
|
63
|
+
expect(result).toMatchObject({ ok: true })
|
|
64
64
|
expect(repo.lastCommitMessage()).toBe(message)
|
|
65
65
|
})
|
|
66
66
|
|
|
@@ -77,7 +77,7 @@ describe("applyEmittedScript — the 8 GitScript builders", () => {
|
|
|
77
77
|
const result = applyEmittedScript(repo, NO_COMMANDS, commitAsIs("gtd(agent): as-is"))
|
|
78
78
|
twin.commitAsIs("gtd(agent): as-is")
|
|
79
79
|
|
|
80
|
-
expect(result).
|
|
80
|
+
expect(result).toMatchObject({ ok: true })
|
|
81
81
|
expect(snapshot(repo)).toEqual(snapshot(twin))
|
|
82
82
|
})
|
|
83
83
|
|
|
@@ -97,7 +97,7 @@ describe("applyEmittedScript — the 8 GitScript builders", () => {
|
|
|
97
97
|
const result = applyEmittedScript(repo, NO_COMMANDS, softResetTo(first))
|
|
98
98
|
twin.softResetTo(first)
|
|
99
99
|
|
|
100
|
-
expect(result).
|
|
100
|
+
expect(result).toMatchObject({ ok: true })
|
|
101
101
|
expect(snapshot(repo)).toEqual(snapshot(twin))
|
|
102
102
|
expect(repo.resolveRef("HEAD")).toBe(first)
|
|
103
103
|
})
|
|
@@ -118,7 +118,7 @@ describe("applyEmittedScript — the 8 GitScript builders", () => {
|
|
|
118
118
|
const result = applyEmittedScript(repo, NO_COMMANDS, mixedResetTo(first))
|
|
119
119
|
twin.mixedResetTo(first)
|
|
120
120
|
|
|
121
|
-
expect(result).
|
|
121
|
+
expect(result).toMatchObject({ ok: true })
|
|
122
122
|
expect(snapshot(repo)).toEqual(snapshot(twin))
|
|
123
123
|
})
|
|
124
124
|
|
|
@@ -138,7 +138,7 @@ describe("applyEmittedScript — the 8 GitScript builders", () => {
|
|
|
138
138
|
const result = applyEmittedScript(repo, NO_COMMANDS, hardResetTo(first))
|
|
139
139
|
twin.hardResetTo(first)
|
|
140
140
|
|
|
141
|
-
expect(result).
|
|
141
|
+
expect(result).toMatchObject({ ok: true })
|
|
142
142
|
expect(snapshot(repo)).toEqual(snapshot(twin))
|
|
143
143
|
expect(repo.hasPath("b.txt")).toBe(false)
|
|
144
144
|
})
|
|
@@ -158,7 +158,7 @@ describe("applyEmittedScript — the 8 GitScript builders", () => {
|
|
|
158
158
|
const result = applyEmittedScript(repo, NO_COMMANDS, discardPending())
|
|
159
159
|
twin.discardPending()
|
|
160
160
|
|
|
161
|
-
expect(result).
|
|
161
|
+
expect(result).toMatchObject({ ok: true })
|
|
162
162
|
expect(snapshot(repo)).toEqual(snapshot(twin))
|
|
163
163
|
expect(repo.readFile("a.txt")).toBe("1")
|
|
164
164
|
expect(repo.hasPath("untracked.txt")).toBe(false)
|
|
@@ -176,7 +176,7 @@ describe("applyEmittedScript — the 8 GitScript builders", () => {
|
|
|
176
176
|
const result = applyEmittedScript(repo, NO_COMMANDS, updateRef("refs/gtd/base", hash))
|
|
177
177
|
twin.updateRef("refs/gtd/base", hash)
|
|
178
178
|
|
|
179
|
-
expect(result).
|
|
179
|
+
expect(result).toMatchObject({ ok: true })
|
|
180
180
|
expect(repo.resolveRef("refs/gtd/base")).toBe(hash)
|
|
181
181
|
expect(snapshot(repo)).toEqual(snapshot(twin))
|
|
182
182
|
})
|
|
@@ -194,7 +194,7 @@ describe("applyEmittedScript — the 8 GitScript builders", () => {
|
|
|
194
194
|
const result = applyEmittedScript(repo, NO_COMMANDS, deleteRef("refs/gtd/base"))
|
|
195
195
|
twin.deleteRef("refs/gtd/base")
|
|
196
196
|
|
|
197
|
-
expect(result).
|
|
197
|
+
expect(result).toMatchObject({ ok: true })
|
|
198
198
|
expect(repo.resolveRef("refs/gtd/base")).toBeNull()
|
|
199
199
|
expect(snapshot(repo)).toEqual(snapshot(twin))
|
|
200
200
|
})
|
|
@@ -210,7 +210,7 @@ describe("applyEmittedScript — gtd check <mode> <file>", () => {
|
|
|
210
210
|
|
|
211
211
|
const result = applyEmittedScript(repo, NO_COMMANDS, "gtd check qa '.gtd/TODO.md'")
|
|
212
212
|
|
|
213
|
-
expect(result).
|
|
213
|
+
expect(result).toMatchObject({ ok: true })
|
|
214
214
|
})
|
|
215
215
|
|
|
216
216
|
it("fails on invalid qa-mode content (a bare ### heading with no question text)", () => {
|
|
@@ -220,7 +220,7 @@ describe("applyEmittedScript — gtd check <mode> <file>", () => {
|
|
|
220
220
|
const result = applyEmittedScript(repo, NO_COMMANDS, "gtd check qa '.gtd/TODO.md'")
|
|
221
221
|
|
|
222
222
|
expect(result.ok).toBe(false)
|
|
223
|
-
expect(result.
|
|
223
|
+
expect(result.output).toContain('.gtd/TODO.md is not valid under mode "qa"')
|
|
224
224
|
})
|
|
225
225
|
})
|
|
226
226
|
|
|
@@ -231,7 +231,7 @@ describe("applyEmittedScript — Emit.ts's fileExistsGuard", () => {
|
|
|
231
231
|
|
|
232
232
|
const result = applyEmittedScript(repo, NO_COMMANDS, fileExistsGuard(".gtd/PLAN.md"))
|
|
233
233
|
|
|
234
|
-
expect(result).
|
|
234
|
+
expect(result).toMatchObject({ ok: true })
|
|
235
235
|
})
|
|
236
236
|
|
|
237
237
|
it("stops the whole script (successfully) when the file is absent — unlike every other guard, it does not fail", () => {
|
|
@@ -240,7 +240,7 @@ describe("applyEmittedScript — Emit.ts's fileExistsGuard", () => {
|
|
|
240
240
|
const script = [fileExistsGuard(".gtd/PLAN.md"), "gtd check qa '.gtd/PLAN.md'"].join("\n\n")
|
|
241
241
|
const result = applyEmittedScript(repo, NO_COMMANDS, script)
|
|
242
242
|
|
|
243
|
-
expect(result).
|
|
243
|
+
expect(result).toMatchObject({ ok: true })
|
|
244
244
|
})
|
|
245
245
|
|
|
246
246
|
it("a subsequent block after the guard trips is never applied", () => {
|
|
@@ -262,7 +262,7 @@ describe("applyEmittedScript — ModeContradiction.ts's skip notice", () => {
|
|
|
262
262
|
|
|
263
263
|
const result = applyEmittedScript(repo, NO_COMMANDS, modeContradictionSkipNotice("adr"))
|
|
264
264
|
|
|
265
|
-
expect(result).
|
|
265
|
+
expect(result).toMatchObject({ ok: true })
|
|
266
266
|
})
|
|
267
267
|
})
|
|
268
268
|
|
|
@@ -284,7 +284,7 @@ describe("applyEmittedScript — ModeContradiction.ts's contradiction round-trip
|
|
|
284
284
|
|
|
285
285
|
const result = applyEmittedScript(repo, commands, block)
|
|
286
286
|
|
|
287
|
-
expect(result).
|
|
287
|
+
expect(result).toMatchObject({ ok: true })
|
|
288
288
|
expect(repo.readFile(samplePath)).toBeUndefined()
|
|
289
289
|
})
|
|
290
290
|
|
|
@@ -359,7 +359,7 @@ describe("applyEmittedScript — the scripted-command table", () => {
|
|
|
359
359
|
|
|
360
360
|
const result = applyEmittedScript(repo, commands, "./run-check.sh")
|
|
361
361
|
|
|
362
|
-
expect(result).
|
|
362
|
+
expect(result).toMatchObject({ ok: true })
|
|
363
363
|
})
|
|
364
364
|
|
|
365
365
|
it("an 'exit' hit with a non-zero status fails the script", () => {
|
|
@@ -370,8 +370,7 @@ describe("applyEmittedScript — the scripted-command table", () => {
|
|
|
370
370
|
|
|
371
371
|
const result = applyEmittedScript(repo, commands, "./run-check.sh")
|
|
372
372
|
|
|
373
|
-
expect(result
|
|
374
|
-
expect(result.error).toContain("./run-check.sh")
|
|
373
|
+
expect(result).toMatchObject({ ok: false, status: 1, output: "boom\n" })
|
|
375
374
|
})
|
|
376
375
|
|
|
377
376
|
it("a 'rewrite' hit mutates the repo's file", () => {
|
|
@@ -383,7 +382,7 @@ describe("applyEmittedScript — the scripted-command table", () => {
|
|
|
383
382
|
|
|
384
383
|
const result = applyEmittedScript(repo, commands, "./format-feedback.sh")
|
|
385
384
|
|
|
386
|
-
expect(result).
|
|
385
|
+
expect(result).toMatchObject({ ok: true })
|
|
387
386
|
expect(repo.readFile("FEEDBACK.md")).toBe("new")
|
|
388
387
|
})
|
|
389
388
|
})
|
|
@@ -434,7 +433,7 @@ describe("applyEmittedScript — a tripped precondition stops the script", () =>
|
|
|
434
433
|
|
|
435
434
|
const result = applyEmittedScript(repo, NO_COMMANDS, script)
|
|
436
435
|
|
|
437
|
-
expect(result).
|
|
436
|
+
expect(result).toMatchObject({ ok: true })
|
|
438
437
|
expect(repo.lastCommitMessage()).toBe("gtd(agent): lands")
|
|
439
438
|
})
|
|
440
439
|
})
|
|
@@ -455,7 +454,7 @@ describe("applyEmittedScript — a realistic assembled multi-block script", () =
|
|
|
455
454
|
|
|
456
455
|
const result = applyEmittedScript(repo, NO_COMMANDS, script)
|
|
457
456
|
|
|
458
|
-
expect(result).
|
|
457
|
+
expect(result).toMatchObject({ ok: true })
|
|
459
458
|
expect(repo.lastCommitMessage()).toBe("gtd(agent): second")
|
|
460
459
|
expect(repo.resolveRef("refs/gtd/base")).toBe(first)
|
|
461
460
|
})
|
|
@@ -479,7 +478,7 @@ describe("applyEmittedScript — a realistic src/emit/Emit.ts-assembled script",
|
|
|
479
478
|
const result = applyEmittedScript(repo, NO_COMMANDS, required)
|
|
480
479
|
twin.commitAllWithPrefix("gtd(agent): building")
|
|
481
480
|
|
|
482
|
-
expect(result).
|
|
481
|
+
expect(result).toMatchObject({ ok: true })
|
|
483
482
|
expect(snapshot(repo)).toEqual(snapshot(twin))
|
|
484
483
|
})
|
|
485
484
|
|
|
@@ -505,7 +504,7 @@ describe("applyEmittedScript — a realistic src/emit/Emit.ts-assembled script",
|
|
|
505
504
|
const { required } = emitScripts(steps)
|
|
506
505
|
const result = applyEmittedScript(repo, NO_COMMANDS, required)
|
|
507
506
|
|
|
508
|
-
expect(result).
|
|
507
|
+
expect(result).toMatchObject({ ok: true })
|
|
509
508
|
expect(repo.resolveRef("refs/worktree/gtd/history")).toBe(tip)
|
|
510
509
|
expect(repo.lastCommitMessage()).toBe("chore: squashed")
|
|
511
510
|
expect(repo.hasPath(".gtd/TODO.md")).toBe(false)
|
|
@@ -520,7 +519,7 @@ describe("applyEmittedScript — outcome blocks are inert (no git effect to miss
|
|
|
520
519
|
|
|
521
520
|
const result = applyEmittedScript(repo, NO_COMMANDS, transitionOutcome("idle", "working"))
|
|
522
521
|
|
|
523
|
-
expect(result).
|
|
522
|
+
expect(result).toMatchObject({ ok: true })
|
|
524
523
|
expect(snapshot(repo)).toEqual(before)
|
|
525
524
|
})
|
|
526
525
|
|
|
@@ -531,7 +530,7 @@ describe("applyEmittedScript — outcome blocks are inert (no git effect to miss
|
|
|
531
530
|
|
|
532
531
|
const result = applyEmittedScript(repo, NO_COMMANDS, commitOutcome("gtd(agent): x"))
|
|
533
532
|
|
|
534
|
-
expect(result).
|
|
533
|
+
expect(result).toMatchObject({ ok: true })
|
|
535
534
|
expect(snapshot(repo)).toEqual(before)
|
|
536
535
|
})
|
|
537
536
|
|
|
@@ -553,7 +552,7 @@ describe("applyEmittedScript — outcome blocks are inert (no git effect to miss
|
|
|
553
552
|
const result = applyEmittedScript(repo, NO_COMMANDS, required)
|
|
554
553
|
twin.commitAllWithPrefix("gtd(agent): building")
|
|
555
554
|
|
|
556
|
-
expect(result).
|
|
555
|
+
expect(result).toMatchObject({ ok: true })
|
|
557
556
|
expect(snapshot(repo)).toEqual(snapshot(twin))
|
|
558
557
|
})
|
|
559
558
|
|
|
@@ -567,7 +566,7 @@ describe("applyEmittedScript — outcome blocks are inert (no git effect to miss
|
|
|
567
566
|
])
|
|
568
567
|
const result = applyEmittedScript(repo, NO_COMMANDS, required)
|
|
569
568
|
|
|
570
|
-
expect(result).
|
|
569
|
+
expect(result).toMatchObject({ ok: true })
|
|
571
570
|
expect(snapshot(repo)).toEqual(before)
|
|
572
571
|
})
|
|
573
572
|
})
|
|
@@ -583,7 +582,7 @@ describe("applyEmittedScript — Emit.ts's combinedScript (the whole stdout of a
|
|
|
583
582
|
const script = combinedScript(required, "")
|
|
584
583
|
const result = applyEmittedScript(repo, NO_COMMANDS, script)
|
|
585
584
|
|
|
586
|
-
expect(result).
|
|
585
|
+
expect(result).toMatchObject({ ok: true })
|
|
587
586
|
expect(repo.lastCommitMessage()).toBe("gtd(human): idle")
|
|
588
587
|
})
|
|
589
588
|
|
|
@@ -605,7 +604,7 @@ describe("applyEmittedScript — Emit.ts's combinedScript (the whole stdout of a
|
|
|
605
604
|
|
|
606
605
|
const result = applyEmittedScript(repo, NO_COMMANDS, script)
|
|
607
606
|
|
|
608
|
-
expect(result).
|
|
607
|
+
expect(result).toMatchObject({ ok: true })
|
|
609
608
|
expect(repo.lastCommitMessage()).toBe("gtd(human): idle")
|
|
610
609
|
expect(repo.resolveRef("refs/gtd/marker")).toBe(repo.resolveRef("HEAD"))
|
|
611
610
|
})
|
|
@@ -625,7 +624,7 @@ describe("applyEmittedScript — Emit.ts's combinedScript (the whole stdout of a
|
|
|
625
624
|
|
|
626
625
|
const result = applyEmittedScript(repo, NO_COMMANDS, script)
|
|
627
626
|
|
|
628
|
-
expect(result).
|
|
627
|
+
expect(result).toMatchObject({ ok: true })
|
|
629
628
|
expect(repo.lastCommitMessage()).toBe("gtd(human): idle")
|
|
630
629
|
})
|
|
631
630
|
|