@pmelab/gtd 64.1.1 → 64.2.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 +27 -14
- package/dist/chunks/{flows-B7ZRCe-B.mjs → flows-dnIE_qbe.mjs} +7 -13
- package/dist/chunks/{rolldown-runtime-D7D4PA-g.mjs → rolldown-runtime-D8jQpDG7.mjs} +1 -2
- package/dist/chunks/{steering-Cem5i6Xl.mjs → steering-DEh_MS40.mjs} +181 -187
- package/dist/chunks/{testkit-DfWZQCWQ.mjs → testkit-BwCH8ky6.mjs} +412 -512
- package/dist/chunks/{wire-CPNSm_9N.mjs → wire-CaPJ9Tme.mjs} +12 -24
- package/dist/flows.mjs +1 -2
- package/dist/gtd.bundle.mjs +1010 -1785
- package/dist/gtd.core.mjs +191 -182
- package/dist/steering.mjs +1 -2
- package/dist/testkit/gherkin.mjs +3 -5
- package/dist/testkit.mjs +2 -2
- package/dist/web/main.css +1 -1
- package/dist/web/main.js +438 -413
- package/dist/wire.mjs +1 -2
- package/dist/workflow-kit.mjs +6 -10
- package/package.json +19 -32
- package/src/cli/Cli.test.ts +1 -0
- package/src/cli/Cli.ts +16 -1
- package/src/cli/Program.ts +6 -0
- package/src/cli/ToolCommands.ts +1 -0
- package/src/edge/Edge.test.ts +23 -1
- package/src/edge/Edge.ts +23 -1
- package/src/edge/index.ts +1 -0
- package/src/emit/GitScript.test.ts +1 -1
- package/src/flows/runtime.test.ts +1 -1
- package/src/platform/Git.ts +33 -10
- package/src/replay/Replay.test.ts +21 -7
- package/src/steering/Blocks.test.ts +3 -1
- package/src/steering/Blocks.ts +14 -6
- package/src/steering/SteeringFormat.ts +9 -1
- package/src/steering/freeform.test.ts +51 -1
- package/src/steering/freeform.ts +38 -0
- package/src/steering/qa.test.ts +3 -3
- package/src/steering/qa.ts +1 -1
- package/src/steering/review.test.ts +26 -1
- package/src/step/LandStep.ts +1 -1
- package/src/twins/FakeGitOperations.ts +2 -0
- package/src/twins/GitTiers.ts +27 -2
- package/src/twins/InMemRepo.ts +5 -0
- package/src/wire/BeatPlain.ts +3 -3
- package/src/wire/Select.ts +1 -1
package/dist/wire.mjs
CHANGED
|
@@ -1,2 +1 @@
|
|
|
1
|
-
|
|
2
|
-
export { ACCESS_REFUSAL, BEAT_KINDS, DEMAND_BRIEFING, EXIT_CODES, EXIT_OK, EXIT_RUNTIME_ERROR, EXIT_SIGINT, EXIT_SIGTERM, EXIT_STRAYS_FOUND, EXIT_USAGE_ERROR, PIN_PATH, UNATTRIBUTED_MODEL, WIRE_SCHEMA, beatDocument, beatKindOf, demandOf, globMatches, judgeJson, landFields, landProseText, noopText, renderBeatJson, renderBeatPlain, renderFormat, renderLandJson, renderLandPlain, renderScriptJson, resolveSession, scriptFields, selectPath, stallDiagnosis, statusOf };
|
|
1
|
+
export { O as ACCESS_REFUSAL, _ as BEAT_KINDS, f as DEMAND_BRIEFING, x as EXIT_CODES, S as EXIT_OK, C as EXIT_RUNTIME_ERROR, w as EXIT_SIGINT, T as EXIT_SIGTERM, E as EXIT_STRAYS_FOUND, D as EXIT_USAGE_ERROR, k as PIN_PATH, A as UNATTRIBUTED_MODEL, j as WIRE_SCHEMA, m as beatDocument, v as beatKindOf, y as demandOf, t as globMatches, i as judgeJson, s as landFields, c as landProseText, l as noopText, h as renderBeatJson, p as renderBeatPlain, M as renderFormat, u as renderLandJson, d as renderLandPlain, a as renderScriptJson, n as resolveSession, o as scriptFields, r as selectPath, b as stallDiagnosis, g as statusOf } from "./chunks/wire-CaPJ9Tme.mjs";
|
package/dist/workflow-kit.mjs
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
import { B as head, F as changesSince, G as mark, P as changes, X as read, Z as refuse, d as hasThreadFor, et as scope, g as requireReplies, v as requireThreadsClosed } from "./chunks/flows-
|
|
2
|
-
import { C as reviewRisks, N as parseThreads, S as reviewNotes, T as unansweredNotes, n as stripCodeThreads, x as reviewDecisions } from "./chunks/steering-
|
|
3
|
-
|
|
1
|
+
import { B as head, F as changesSince, G as mark, P as changes, X as read, Z as refuse, d as hasThreadFor, et as scope, g as requireReplies, v as requireThreadsClosed } from "./chunks/flows-dnIE_qbe.mjs";
|
|
2
|
+
import { C as reviewRisks, N as parseThreads, S as reviewNotes, T as unansweredNotes, n as stripCodeThreads, x as reviewDecisions } from "./chunks/steering-DEh_MS40.mjs";
|
|
3
|
+
// src/workflow-kit/health.ts
|
|
4
4
|
/** Hand a red suite that repeated attempts could not fix to a person, until `limit` rounds are spent. */
|
|
5
5
|
const escalation = async (count, steps) => {
|
|
6
6
|
if (count.rounds >= steps.limit) return steps.exhausted();
|
|
@@ -74,8 +74,7 @@ const healthy = async (fix, health) => {
|
|
|
74
74
|
const gate = (name, passes, blocked) => scope(name, async () => {
|
|
75
75
|
while (!await passes()) await blocked();
|
|
76
76
|
});
|
|
77
|
-
|
|
78
|
-
//#region src/workflow-kit/quality.ts
|
|
77
|
+
// src/workflow-kit/quality.ts
|
|
79
78
|
const lensOf = (entry) => {
|
|
80
79
|
const at = entry.indexOf("=");
|
|
81
80
|
return at < 0 ? {
|
|
@@ -106,8 +105,7 @@ const fixFindings = async (f) => {
|
|
|
106
105
|
await f.escalate();
|
|
107
106
|
return false;
|
|
108
107
|
};
|
|
109
|
-
|
|
110
|
-
//#region src/workflow-kit/scenarios.ts
|
|
108
|
+
// src/workflow-kit/scenarios.ts
|
|
111
109
|
const DOC_STRING = /^("""|```)/;
|
|
112
110
|
const THEN = /^Then\s/;
|
|
113
111
|
const CONTINUATION = /^(And|But|\*)\s/;
|
|
@@ -161,8 +159,7 @@ const removedAssertions = (changes) => changes.filter((c) => c.path.endsWith(".f
|
|
|
161
159
|
lines
|
|
162
160
|
};
|
|
163
161
|
}).filter((r) => r.lines.length > 0);
|
|
164
|
-
|
|
165
|
-
//#region src/workflow-kit/review.ts
|
|
162
|
+
// src/workflow-kit/review.ts
|
|
166
163
|
const collect = async (p, capture, round, requested) => {
|
|
167
164
|
await p.collecting(capture, round);
|
|
168
165
|
if (read(p.feedback) !== void 0) return "feedback";
|
|
@@ -426,5 +423,4 @@ const reviewToHandOff = async (base, p) => {
|
|
|
426
423
|
return;
|
|
427
424
|
}
|
|
428
425
|
};
|
|
429
|
-
//#endregion
|
|
430
426
|
export { assertionLines, escalation, fixFindings, gate, healthy, lensEntriesOf, lensOf, lensScopes, lensesOf, qualityLap, removedAssertions, review, reviewToHandOff, untilSettled };
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@pmelab/gtd",
|
|
3
|
-
"version": "64.
|
|
3
|
+
"version": "64.2.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": {
|
|
@@ -83,33 +83,17 @@
|
|
|
83
83
|
"scripts": {
|
|
84
84
|
"prepare": "husky",
|
|
85
85
|
"dev": "node dev/run.mjs",
|
|
86
|
-
"
|
|
87
|
-
"postbuild": "node scripts/assert-no-test-doubles.mjs",
|
|
88
|
-
"watch": "tsdown --watch",
|
|
86
|
+
"watch": "vp pack --watch",
|
|
89
87
|
"bench:replay": "node scripts/bench-replay.mjs",
|
|
90
88
|
"test": "jiti scripts/test-states.main.ts",
|
|
91
|
-
"test:unit": "vitest run --project unit",
|
|
92
|
-
"test:workflows": "vitest run --project workflows",
|
|
93
89
|
"test:watch": "vitest",
|
|
94
90
|
"test:changed": "vitest run --project unit --project e2e-inmem --project workflows --changed HEAD",
|
|
95
|
-
"test:e2e": "
|
|
96
|
-
"
|
|
97
|
-
"lint": "oxlint .",
|
|
98
|
-
"lint:sh": "jiti scripts/generate-shell-corpus.ts --check && shellcheck -s sh tests/shell/corpus/*.sh",
|
|
99
|
-
"lint:boundaries": "depcruise src tests claude",
|
|
100
|
-
"format": "oxfmt --write .",
|
|
101
|
-
"format:check": "oxfmt --check .",
|
|
91
|
+
"test:e2e": "vp run test:e2e:inmem && vp run test:e2e:live",
|
|
92
|
+
"format": "vp fmt",
|
|
102
93
|
"test:mutation": "stryker run",
|
|
103
|
-
"test:e2e:inmem": "vitest run --project e2e-inmem",
|
|
104
|
-
"test:e2e:live": "vitest run --project e2e-live --no-file-parallelism",
|
|
105
|
-
"analyze": "fallow --summary --quiet",
|
|
106
|
-
"test:web": "vitest run --project storybook",
|
|
107
94
|
"storybook": "storybook dev --host 0.0.0.0",
|
|
108
95
|
"coverage": "vitest run --project unit --coverage"
|
|
109
96
|
},
|
|
110
|
-
"lint-staged": {
|
|
111
|
-
"*": "oxfmt --no-error-on-unmatched-pattern --write"
|
|
112
|
-
},
|
|
113
97
|
"devDependencies": {
|
|
114
98
|
"@commitlint/cli": "^21.2.3",
|
|
115
99
|
"@commitlint/config-conventional": "^21.2.3",
|
|
@@ -119,8 +103,8 @@
|
|
|
119
103
|
"@semantic-release/git": "^10.0.1",
|
|
120
104
|
"@semantic-release/github": "^12.0.8",
|
|
121
105
|
"@semantic-release/npm": "^12.0.0",
|
|
122
|
-
"@storybook/addon-vitest": "^10.6.
|
|
123
|
-
"@storybook/react-vite": "^10.6.
|
|
106
|
+
"@storybook/addon-vitest": "^10.6.5",
|
|
107
|
+
"@storybook/react-vite": "^10.6.5",
|
|
124
108
|
"@stryker-mutator/core": "^9.6.1",
|
|
125
109
|
"@stryker-mutator/typescript-checker": "^9.6.1",
|
|
126
110
|
"@stryker-mutator/vitest-runner": "^9.6.1",
|
|
@@ -134,29 +118,27 @@
|
|
|
134
118
|
"@types/react": "^19.2.7",
|
|
135
119
|
"@types/react-dom": "^19.2.3",
|
|
136
120
|
"@vitejs/plugin-react": "^5.2.0",
|
|
137
|
-
"@vitest/browser": "
|
|
138
|
-
"@vitest/browser-playwright": "
|
|
139
|
-
"@vitest/coverage-v8": "
|
|
121
|
+
"@vitest/browser": "5.0.5",
|
|
122
|
+
"@vitest/browser-playwright": "5.0.5",
|
|
123
|
+
"@vitest/coverage-v8": "5.0.5",
|
|
140
124
|
"dependency-cruiser": "^18.3.1",
|
|
141
125
|
"fallow": "3.25.0",
|
|
142
126
|
"fast-check": "^4.8.0",
|
|
143
127
|
"husky": "^9.1.7",
|
|
144
128
|
"jiti": "^2.7.0",
|
|
145
|
-
"
|
|
146
|
-
"
|
|
147
|
-
"oxlint": "^1.74.0",
|
|
129
|
+
"oxfmt": "0.59.0",
|
|
130
|
+
"oxlint": "1.74.0",
|
|
148
131
|
"playwright": "^1.62.1",
|
|
149
132
|
"prettier": "^3.8.1",
|
|
150
133
|
"quickpickle": "1.11.2",
|
|
151
134
|
"react": "^19.2.0",
|
|
152
135
|
"react-dom": "^19.2.0",
|
|
153
136
|
"semantic-release": "^25.0.5",
|
|
154
|
-
"storybook": "^10.6.
|
|
137
|
+
"storybook": "^10.6.5",
|
|
155
138
|
"tailwind-merge": "^3.7.0",
|
|
156
139
|
"tailwindcss": "^4.3.3",
|
|
157
|
-
"
|
|
158
|
-
"
|
|
159
|
-
"vitest": "^4.1.12",
|
|
140
|
+
"vite-plus": "1.4.0",
|
|
141
|
+
"vitest": "5.0.5",
|
|
160
142
|
"yaml": "^2.8.2",
|
|
161
143
|
"zustand": "^5.0.15"
|
|
162
144
|
},
|
|
@@ -190,5 +172,10 @@
|
|
|
190
172
|
"rolldown": "^1.1.5",
|
|
191
173
|
"vscode-languageserver": "^10.1.0",
|
|
192
174
|
"vscode-languageserver-textdocument": "^1.0.13"
|
|
175
|
+
},
|
|
176
|
+
"overrides": {
|
|
177
|
+
"storybook": {
|
|
178
|
+
"vite-plus": "$vite-plus"
|
|
179
|
+
}
|
|
193
180
|
}
|
|
194
181
|
}
|
package/src/cli/Cli.test.ts
CHANGED
package/src/cli/Cli.ts
CHANGED
|
@@ -184,7 +184,7 @@ const FLAGS: readonly FlagRow[] = [
|
|
|
184
184
|
},
|
|
185
185
|
scopeError: "gtd: --port is only valid for `gtd ui`",
|
|
186
186
|
valueHint: "<n>",
|
|
187
|
-
help: "The tailscale serve port (default: the first free of 8443, 10000, 443), or with --host the port to bind (default: any free one)",
|
|
187
|
+
help: "The tailscale serve port (default: the first free of 8443, 10000, 443), or with --host or --http the port to bind (default: any free one)",
|
|
188
188
|
},
|
|
189
189
|
{
|
|
190
190
|
name: "--host",
|
|
@@ -209,6 +209,16 @@ const FLAGS: readonly FlagRow[] = [
|
|
|
209
209
|
valueHint: "",
|
|
210
210
|
help: "Bind directly with a throwaway self-signed certificate instead of the configured ui.cert/ui.key",
|
|
211
211
|
},
|
|
212
|
+
{
|
|
213
|
+
name: "--http",
|
|
214
|
+
arity: 0,
|
|
215
|
+
repeatable: false,
|
|
216
|
+
scope: (kind) => kind === "ui",
|
|
217
|
+
decode: () => Result.succeed(true),
|
|
218
|
+
scopeError: "gtd: --http is only valid for `gtd ui`",
|
|
219
|
+
valueHint: "",
|
|
220
|
+
help: "Serve plain HTTP for a local TLS-terminating proxy; binds 127.0.0.1 unless a loopback --host is given",
|
|
221
|
+
},
|
|
212
222
|
{
|
|
213
223
|
name: "--dev",
|
|
214
224
|
arity: 0,
|
|
@@ -1137,6 +1147,7 @@ export const parseArgv = (argv: readonly string[]): CliPlan => {
|
|
|
1137
1147
|
readonly "--spec"?: string
|
|
1138
1148
|
readonly "--host"?: string
|
|
1139
1149
|
readonly "--self-signed"?: boolean
|
|
1150
|
+
readonly "--http"?: boolean
|
|
1140
1151
|
readonly "--dev"?: boolean
|
|
1141
1152
|
readonly "--provider"?: "fixed" | "jev" | "llm"
|
|
1142
1153
|
readonly "--answers"?: string
|
|
@@ -1185,6 +1196,9 @@ export const parseArgv = (argv: readonly string[]): CliPlan => {
|
|
|
1185
1196
|
}
|
|
1186
1197
|
|
|
1187
1198
|
if (kind === "ui") {
|
|
1199
|
+
if (bag["--http"] === true && bag["--self-signed"] === true) {
|
|
1200
|
+
return usagePlan("gtd: --http and --self-signed cannot be combined", jsonSeen)
|
|
1201
|
+
}
|
|
1188
1202
|
return {
|
|
1189
1203
|
kind: "command",
|
|
1190
1204
|
command: {
|
|
@@ -1192,6 +1206,7 @@ export const parseArgv = (argv: readonly string[]): CliPlan => {
|
|
|
1192
1206
|
...(bag["--host"] !== undefined ? { host: bag["--host"] } : {}),
|
|
1193
1207
|
...(bag["--port"] !== undefined ? { port: bag["--port"] } : {}),
|
|
1194
1208
|
selfSigned: bag["--self-signed"] ?? false,
|
|
1209
|
+
...(bag["--http"] === true ? { http: true } : {}),
|
|
1195
1210
|
dev: bag["--dev"] ?? false,
|
|
1196
1211
|
},
|
|
1197
1212
|
json,
|
package/src/cli/Program.ts
CHANGED
|
@@ -51,6 +51,7 @@ import {
|
|
|
51
51
|
} from "../wire/index.js"
|
|
52
52
|
import {
|
|
53
53
|
currentRest,
|
|
54
|
+
refuseHugeUntracked,
|
|
54
55
|
repinAnchor,
|
|
55
56
|
startRefusal,
|
|
56
57
|
currentRun,
|
|
@@ -144,6 +145,7 @@ export type Command =
|
|
|
144
145
|
readonly host?: string
|
|
145
146
|
readonly port?: number
|
|
146
147
|
readonly selfSigned: boolean
|
|
148
|
+
readonly http?: boolean
|
|
147
149
|
readonly dev: boolean
|
|
148
150
|
}
|
|
149
151
|
| {
|
|
@@ -310,6 +312,7 @@ const planLanding = (
|
|
|
310
312
|
current?: Rest,
|
|
311
313
|
): Effect.Effect<LandResult, Error, CommandRequirements> =>
|
|
312
314
|
Effect.gen(function* () {
|
|
315
|
+
yield* refuseHugeUntracked
|
|
313
316
|
const rest = current ?? (yield* currentRest)
|
|
314
317
|
const snapshot = yield* snapshotFromRest(rest, opts.judge)
|
|
315
318
|
const outcome = planStepPure(snapshot, opts)
|
|
@@ -596,6 +599,7 @@ const startWorkflow = (
|
|
|
596
599
|
new GtdUsageError(`${commandLabel}: unknown workflow "${workflow}" — ${startable}`),
|
|
597
600
|
)
|
|
598
601
|
}
|
|
602
|
+
yield* refuseHugeUntracked
|
|
599
603
|
const rest = yield* restAt(undefined, varOverrides)
|
|
600
604
|
const started = yield* startRefusal(rest, resolved, varOverrides)
|
|
601
605
|
const plan = yield* planStart(
|
|
@@ -967,6 +971,7 @@ const runNextCommand = (
|
|
|
967
971
|
out: ArtifactOut,
|
|
968
972
|
): Effect.Effect<void, Error, CommandRequirements> =>
|
|
969
973
|
Effect.gen(function* () {
|
|
974
|
+
yield* refuseHugeUntracked
|
|
970
975
|
const rest = yield* currentRest
|
|
971
976
|
const rendered = yield* renderRest(rest)
|
|
972
977
|
const fields = yield* gatherBeatDocument(rest, rendered)
|
|
@@ -1260,6 +1265,7 @@ const runUiCliCommand = (
|
|
|
1260
1265
|
...(command.host !== undefined ? { host: command.host } : {}),
|
|
1261
1266
|
...(command.port !== undefined ? { port: command.port } : {}),
|
|
1262
1267
|
selfSigned: command.selfSigned,
|
|
1268
|
+
...(command.http === true ? { http: true } : {}),
|
|
1263
1269
|
dev: command.dev,
|
|
1264
1270
|
},
|
|
1265
1271
|
config: config.ui,
|
package/src/cli/ToolCommands.ts
CHANGED
package/src/edge/Edge.test.ts
CHANGED
|
@@ -6,6 +6,7 @@ import {
|
|
|
6
6
|
currentRun,
|
|
7
7
|
historyTrail,
|
|
8
8
|
noProcessUnderway,
|
|
9
|
+
refuseHugeUntracked,
|
|
9
10
|
renderRest,
|
|
10
11
|
restAt,
|
|
11
12
|
restIsIdle,
|
|
@@ -757,7 +758,7 @@ describe("restAt", () => {
|
|
|
757
758
|
`--model=${model}`,
|
|
758
759
|
)
|
|
759
760
|
expect(exitCode).toBe(0)
|
|
760
|
-
|
|
761
|
+
applied(repo, (JSON.parse(stdout) as { readonly script: string }).script)
|
|
761
762
|
await takePinBeat(repo, "add")
|
|
762
763
|
}
|
|
763
764
|
const { cost } = await provide(currentRest, repo)
|
|
@@ -768,6 +769,27 @@ describe("restAt", () => {
|
|
|
768
769
|
])
|
|
769
770
|
})
|
|
770
771
|
|
|
772
|
+
it("refuses over 1000 untracked files and proceeds at exactly 1000", async () => {
|
|
773
|
+
const repo = repoWith(LINEAR)
|
|
774
|
+
const files = (n: number) =>
|
|
775
|
+
Object.fromEntries(Array.from({ length: n }, (_, i) => [`big/f${i}.txt`, "x\n"]))
|
|
776
|
+
write(repo, files(1000))
|
|
777
|
+
await provide(refuseHugeUntracked, repo)
|
|
778
|
+
write(repo, files(1001))
|
|
779
|
+
await expect(provide(refuseHugeUntracked, repo)).rejects.toThrow(
|
|
780
|
+
/1001 untracked files.*big \(1001\).*\.gitignore/,
|
|
781
|
+
)
|
|
782
|
+
})
|
|
783
|
+
|
|
784
|
+
it("resolving the current rest never refuses a huge untracked tree", async () => {
|
|
785
|
+
const repo = repoWith(LINEAR)
|
|
786
|
+
write(
|
|
787
|
+
repo,
|
|
788
|
+
Object.fromEntries(Array.from({ length: 1001 }, (_, i) => [`big/f${i}.txt`, "x\n"])),
|
|
789
|
+
)
|
|
790
|
+
expect((await provide(currentRest, repo)).changes).toHaveLength(1001)
|
|
791
|
+
})
|
|
792
|
+
|
|
771
793
|
it("restAt(ref) resolves where the process rested at that commit", async () => {
|
|
772
794
|
const repo = repoWith(LINEAR)
|
|
773
795
|
const boundary = headOf(repo)
|
package/src/edge/Edge.ts
CHANGED
|
@@ -238,7 +238,7 @@ const historyFormatFault = (history: History, location: EpisodeLocation): string
|
|
|
238
238
|
const runOf = (
|
|
239
239
|
history: History,
|
|
240
240
|
location: EpisodeLocation,
|
|
241
|
-
closingHash
|
|
241
|
+
closingHash?: string,
|
|
242
242
|
): Effect.Effect<ProcessRun, Error> => {
|
|
243
243
|
const fault = historyFormatFault(history, location)
|
|
244
244
|
return fault === undefined
|
|
@@ -971,6 +971,28 @@ const setupFor = (
|
|
|
971
971
|
return { resolved, def, vars, env, setup }
|
|
972
972
|
})
|
|
973
973
|
|
|
974
|
+
const UNTRACKED_LIMIT = 1000
|
|
975
|
+
|
|
976
|
+
// `git add -A` would commit every untracked file, so a huge tree is refused —
|
|
977
|
+
// only by the commands that land (next, land, judge answer, start). Read-only
|
|
978
|
+
// rest readers never refuse.
|
|
979
|
+
export const refuseHugeUntracked = Effect.gen(function* () {
|
|
980
|
+
const paths = yield* (yield* GitService).untrackedPaths()
|
|
981
|
+
if (paths.length <= UNTRACKED_LIMIT) return
|
|
982
|
+
const dirs = new Map<string, number>()
|
|
983
|
+
for (const p of paths) {
|
|
984
|
+
const i = p.lastIndexOf("/")
|
|
985
|
+
const d = i === -1 ? "." : p.slice(0, i)
|
|
986
|
+
dirs.set(d, (dirs.get(d) ?? 0) + 1)
|
|
987
|
+
}
|
|
988
|
+
const top = [...dirs].sort((a, b) => b[1] - a[1]).slice(0, 3)
|
|
989
|
+
return yield* Effect.fail(
|
|
990
|
+
new Error(
|
|
991
|
+
`gtd: ${paths.length} untracked files (limit ${UNTRACKED_LIMIT}); largest untracked directories: ${top.map(([d, n]) => `${d} (${n})`).join(", ")}. Add them to .gitignore or delete them.`,
|
|
992
|
+
),
|
|
993
|
+
)
|
|
994
|
+
})
|
|
995
|
+
|
|
974
996
|
// Only an approval gate counts an index-only edit (one staged, then reverted
|
|
975
997
|
// in the worktree): its approval must cover exactly the judged diff. Every
|
|
976
998
|
// other rest keeps the worktree's view, so a net-zero turn stays an attempt.
|
package/src/edge/index.ts
CHANGED
|
@@ -234,7 +234,7 @@ describe("shellQuote", () => {
|
|
|
234
234
|
})
|
|
235
235
|
|
|
236
236
|
// 500 samples through 500 bash spawns starved the 30s test timeout whenever
|
|
237
|
-
// the suite ran under
|
|
237
|
+
// the suite ran under `npm test`'s parallel task load. The property is unchanged;
|
|
238
238
|
// all samples now round-trip through a SINGLE bash, NUL-delimited on the way
|
|
239
239
|
// back out (NUL is filtered from the inputs, so it can't collide).
|
|
240
240
|
it("round-trips arbitrary strings — including newlines and non-ASCII — through a real bash printf", () => {
|
|
@@ -146,7 +146,7 @@ describe("kept()", () => {
|
|
|
146
146
|
})
|
|
147
147
|
|
|
148
148
|
/** A context that records every step request and resolves it with `reply`. */
|
|
149
|
-
const recording = (reply
|
|
149
|
+
const recording = (reply?: unknown) => {
|
|
150
150
|
const requests: StepRequest[] = []
|
|
151
151
|
installContext({
|
|
152
152
|
step: async (request: StepRequest) => {
|
package/src/platform/Git.ts
CHANGED
|
@@ -94,6 +94,8 @@ export interface GitReaderOperations {
|
|
|
94
94
|
readonly changedPaths: (
|
|
95
95
|
base?: string,
|
|
96
96
|
) => Effect.Effect<ReadonlyArray<{ readonly path: string; readonly status: string }>, Error>
|
|
97
|
+
/** Untracked, non-ignored paths. */
|
|
98
|
+
readonly untrackedPaths: () => Effect.Effect<ReadonlyArray<string>, Error>
|
|
97
99
|
/**
|
|
98
100
|
* The index vs `HEAD`, as `{path, status}` pairs — what a commit would carry
|
|
99
101
|
* that `changedPaths` misses when the worktree reverted a staged edit. Empty
|
|
@@ -205,6 +207,14 @@ const stripCommitSeam = (tail: string): string => {
|
|
|
205
207
|
|
|
206
208
|
type GitExec = (...args: [string, ...Array<string>]) => Effect.Effect<string, Error>
|
|
207
209
|
|
|
210
|
+
// Unbounded argv throws E2BIG synchronously (a defect), so path lists are chunked.
|
|
211
|
+
const ARGV_CHUNK = 500
|
|
212
|
+
const chunked = <T>(items: ReadonlyArray<T>): Array<ReadonlyArray<T>> => {
|
|
213
|
+
const out: Array<ReadonlyArray<T>> = []
|
|
214
|
+
for (let i = 0; i < items.length; i += ARGV_CHUNK) out.push(items.slice(i, i + ARGV_CHUNK))
|
|
215
|
+
return out
|
|
216
|
+
}
|
|
217
|
+
|
|
208
218
|
/**
|
|
209
219
|
* `path → blob object id` at `ref`; a path absent from the map doesn't exist
|
|
210
220
|
* there. An unresolvable `ref` (an empty repo) yields an empty map, so every
|
|
@@ -215,11 +225,17 @@ const blobsAtRef = (
|
|
|
215
225
|
ref: string,
|
|
216
226
|
paths: ReadonlyArray<string>,
|
|
217
227
|
): Effect.Effect<Map<string, string>, Error> =>
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
228
|
+
Effect.forEach(
|
|
229
|
+
chunked(paths),
|
|
230
|
+
(chunk) =>
|
|
231
|
+
exec("git", "ls-tree", "-r", "-z", ref, "--", ...chunk).pipe(
|
|
232
|
+
Effect.catch(() => Effect.succeed("")),
|
|
233
|
+
),
|
|
234
|
+
{ concurrency: 1 },
|
|
235
|
+
).pipe(
|
|
236
|
+
Effect.map((outs) => {
|
|
221
237
|
const blobs = new Map<string, string>()
|
|
222
|
-
for (const entry of splitNul
|
|
238
|
+
for (const entry of outs.flatMap(splitNul)) {
|
|
223
239
|
const tab = entry.indexOf("\t")
|
|
224
240
|
if (tab === -1) continue
|
|
225
241
|
const [, type, oid] = entry.slice(0, tab).split(" ")
|
|
@@ -246,16 +262,20 @@ const hashObjects = (
|
|
|
246
262
|
exec: GitExec,
|
|
247
263
|
paths: ReadonlyArray<string>,
|
|
248
264
|
): Effect.Effect<Map<string, string>, Error> =>
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
265
|
+
Effect.forEach(
|
|
266
|
+
chunked(paths),
|
|
267
|
+
(chunk) =>
|
|
268
|
+
exec("git", "hash-object", "--", ...chunk).pipe(
|
|
269
|
+
Effect.map((out) =>
|
|
253
270
|
out
|
|
254
271
|
.trim()
|
|
255
272
|
.split("\n")
|
|
256
|
-
.map((oid, i) => [
|
|
273
|
+
.map((oid, i) => [chunk[i] ?? "", oid.trim()] as const),
|
|
257
274
|
),
|
|
258
|
-
|
|
275
|
+
),
|
|
276
|
+
{ concurrency: 1 },
|
|
277
|
+
).pipe(
|
|
278
|
+
Effect.map((entries) => new Map(entries.flat())),
|
|
259
279
|
Effect.catch(() => Effect.succeed(new Map<string, string>())),
|
|
260
280
|
)
|
|
261
281
|
|
|
@@ -471,6 +491,9 @@ const makeGitImpl = (
|
|
|
471
491
|
return all
|
|
472
492
|
}),
|
|
473
493
|
|
|
494
|
+
untrackedPaths: () =>
|
|
495
|
+
exec("git", "ls-files", "--others", "--exclude-standard", "-z").pipe(Effect.map(splitNul)),
|
|
496
|
+
|
|
474
497
|
stagedPaths: () =>
|
|
475
498
|
exec("git", "diff", "--cached", "--name-status", "--no-renames", "-z", "HEAD").pipe(
|
|
476
499
|
Effect.catch(() => Effect.succeed("")),
|
|
@@ -370,7 +370,9 @@ describe("replay", () => {
|
|
|
370
370
|
})
|
|
371
371
|
|
|
372
372
|
it("is reported when history continues past the end of the flow", async () => {
|
|
373
|
-
const short = async () =>
|
|
373
|
+
const short = async () => {
|
|
374
|
+
await human("only")
|
|
375
|
+
}
|
|
374
376
|
const h = new History().land("human", "only", 1, "only").land("human", "only", 2, "only")
|
|
375
377
|
expect((await replayOf(short, h)).kind).toBe("divergence")
|
|
376
378
|
})
|
|
@@ -606,7 +608,9 @@ describe("replay", () => {
|
|
|
606
608
|
})
|
|
607
609
|
|
|
608
610
|
it("ends the episode on return and on restart", async () => {
|
|
609
|
-
const returning = async () =>
|
|
611
|
+
const returning = async () => {
|
|
612
|
+
await human("only")
|
|
613
|
+
}
|
|
610
614
|
const h = new History().land("human", "only", 1, "only")
|
|
611
615
|
expect(await replayOf(returning, h)).toMatchObject({ kind: "ended", via: "return" })
|
|
612
616
|
|
|
@@ -1337,7 +1341,9 @@ describe("replay: a repin's anchor", () => {
|
|
|
1337
1341
|
})
|
|
1338
1342
|
|
|
1339
1343
|
it("diverges when the flow ends where it names a step", async () => {
|
|
1340
|
-
const outcome = await replayOf(async () =>
|
|
1344
|
+
const outcome = await replayOf(async () => {
|
|
1345
|
+
await human("idle")
|
|
1346
|
+
}, anchoredHistory())
|
|
1341
1347
|
expect(outcome).toMatchObject({ kind: "divergence", at: hashOf(3), reached: undefined })
|
|
1342
1348
|
})
|
|
1343
1349
|
|
|
@@ -1346,7 +1352,9 @@ describe("replay: a repin's anchor", () => {
|
|
|
1346
1352
|
.land("human", "idle", 1, "build", { "a.txt": "a" })
|
|
1347
1353
|
.land("agent", "build", 1, "old")
|
|
1348
1354
|
.raw(anchorTo("gtd:unpin", 1))
|
|
1349
|
-
const outcome = await replayOf(async () =>
|
|
1355
|
+
const outcome = await replayOf(async () => {
|
|
1356
|
+
await human("idle")
|
|
1357
|
+
}, h)
|
|
1350
1358
|
expect(outcome.kind).toBe("ended")
|
|
1351
1359
|
})
|
|
1352
1360
|
|
|
@@ -1355,13 +1363,17 @@ describe("replay: a repin's anchor", () => {
|
|
|
1355
1363
|
.land("human", "idle", 1, "build", { "a.txt": "a" })
|
|
1356
1364
|
.land("agent", "build", 1, "old")
|
|
1357
1365
|
.raw(`gtd(check): pin → unpin\n\nGtd-Pin: add\nGtd-Anchor: ${hashOf(1)}\nGtd-Format: 3`)
|
|
1358
|
-
const outcome = await replayOf(async () =>
|
|
1366
|
+
const outcome = await replayOf(async () => {
|
|
1367
|
+
await human("idle")
|
|
1368
|
+
}, h)
|
|
1359
1369
|
expect(outcome.kind).toBe("ended")
|
|
1360
1370
|
})
|
|
1361
1371
|
|
|
1362
1372
|
it("reads a format 4 anchor naming unpin as a step", async () => {
|
|
1363
1373
|
const outcome = await replayOf(
|
|
1364
|
-
async () =>
|
|
1374
|
+
async () => {
|
|
1375
|
+
await human("idle")
|
|
1376
|
+
},
|
|
1365
1377
|
new History()
|
|
1366
1378
|
.land("human", "idle", 1, "build", { "a.txt": "a" })
|
|
1367
1379
|
.land("agent", "build", 1, "old")
|
|
@@ -1372,7 +1384,9 @@ describe("replay: a repin's anchor", () => {
|
|
|
1372
1384
|
|
|
1373
1385
|
it("fails when it names no commit replay reads", async () => {
|
|
1374
1386
|
const h = anchoredHistory().raw(anchorTo("build", 2))
|
|
1375
|
-
const outcome = await replayOf(async () =>
|
|
1387
|
+
const outcome = await replayOf(async () => {
|
|
1388
|
+
await human("idle")
|
|
1389
|
+
}, h)
|
|
1376
1390
|
expect(outcome.kind === "failed" && outcome.message).toContain(
|
|
1377
1391
|
"which is no commit replay reads",
|
|
1378
1392
|
)
|
|
@@ -35,7 +35,9 @@ describe("blockRunInline — spec-feedback regression: a nested list item's own
|
|
|
35
35
|
const nodes = blockNodesOf(content, tree)
|
|
36
36
|
const topItem = nodes[0]?.block?.items?.[0]
|
|
37
37
|
expect(topItem?.inline).toEqual([{ kind: "text", value: "outer" }])
|
|
38
|
-
expect(topItem?.items).toEqual([
|
|
38
|
+
expect(topItem?.items).toEqual([
|
|
39
|
+
{ text: "inner", inline: [{ kind: "text", value: "inner" }], line: 1 },
|
|
40
|
+
])
|
|
39
41
|
})
|
|
40
42
|
})
|
|
41
43
|
|
package/src/steering/Blocks.ts
CHANGED
|
@@ -128,21 +128,28 @@ const nodeInline = (
|
|
|
128
128
|
return title.length > 0 ? [{ kind: "text", value: title }] : []
|
|
129
129
|
}
|
|
130
130
|
|
|
131
|
+
type NoteAt = ReturnType<typeof noteLookup>
|
|
132
|
+
|
|
131
133
|
/** One list item as a `BlockListItem`, recursing into a nested `list` child (there is at most one, CommonMark's own shape) as its own `items`. */
|
|
132
|
-
const blockListItemOf = (content: string, item: ListItem): BlockListItem => {
|
|
134
|
+
const blockListItemOf = (content: string, item: ListItem, noteAt: NoteAt): BlockListItem => {
|
|
133
135
|
const nested = item.children.filter((c): c is List => c.type === "list")
|
|
134
|
-
const items = nested.flatMap((list) => blockListItemsOf(content, list.children))
|
|
136
|
+
const items = nested.flatMap((list) => blockListItemsOf(content, list.children, noteAt))
|
|
137
|
+
const line = item.position === undefined ? undefined : toLspPosition(item.position.start).line
|
|
135
138
|
return {
|
|
136
139
|
text: listItemText(content, item),
|
|
137
140
|
inline: listItemInline(content, item),
|
|
138
141
|
...(item.checked === true || item.checked === false ? { checked: item.checked } : {}),
|
|
139
142
|
...(items.length > 0 ? { items } : {}),
|
|
143
|
+
...(line === undefined ? {} : { line, ...noteAt(line) }),
|
|
140
144
|
}
|
|
141
145
|
}
|
|
142
146
|
|
|
143
147
|
/** Every item of a `list` node's own `children`, as `BlockListItem`s, in document order. */
|
|
144
|
-
const blockListItemsOf = (
|
|
145
|
-
|
|
148
|
+
const blockListItemsOf = (
|
|
149
|
+
content: string,
|
|
150
|
+
items: readonly ListItem[],
|
|
151
|
+
noteAt: NoteAt,
|
|
152
|
+
): readonly BlockListItem[] => items.map((item) => blockListItemOf(content, item, noteAt))
|
|
146
153
|
|
|
147
154
|
/** The `title` an empty fenced code block (a `` ``` ``/`` ``` `` pair with nothing between them) falls back to — its real body is `""`, and every node still carries a non-empty title. */
|
|
148
155
|
const EMPTY_CODE_BLOCK_TITLE = "(empty code block)"
|
|
@@ -184,6 +191,7 @@ const blockTitle = (content: string, node: RootContent, options?: BlockWalkOptio
|
|
|
184
191
|
const blockOf = (
|
|
185
192
|
content: string,
|
|
186
193
|
node: RootContent,
|
|
194
|
+
noteAt: NoteAt,
|
|
187
195
|
options?: BlockWalkOptions,
|
|
188
196
|
): SteeringViewNode["block"] | undefined => {
|
|
189
197
|
switch (node.type) {
|
|
@@ -197,7 +205,7 @@ const blockOf = (
|
|
|
197
205
|
return {
|
|
198
206
|
kind: "list",
|
|
199
207
|
ordered: node.ordered === true,
|
|
200
|
-
items: blockListItemsOf(content, visibleListItems(node, options)),
|
|
208
|
+
items: blockListItemsOf(content, visibleListItems(node, options), noteAt),
|
|
201
209
|
}
|
|
202
210
|
case "code":
|
|
203
211
|
return {
|
|
@@ -285,7 +293,7 @@ export const blockNodesOfRun = (
|
|
|
285
293
|
const noteAt = noteLookup(content, options?.footnotes)
|
|
286
294
|
return visibleNodes(content, nodes, options).map((node) => {
|
|
287
295
|
const startLine = toLspPosition(node.position!.start).line
|
|
288
|
-
const block = blockOf(content, node, options)
|
|
296
|
+
const block = blockOf(content, node, noteAt, options)
|
|
289
297
|
return {
|
|
290
298
|
title: blockTitle(content, node, options),
|
|
291
299
|
anchor: { kind: "paragraph" as const, line: startLine },
|
|
@@ -74,10 +74,13 @@ export type SteeringAnchor =
|
|
|
74
74
|
| { readonly kind: "option"; readonly questionIndex: number; readonly index: number }
|
|
75
75
|
| { readonly kind: "paragraph"; readonly line: number }
|
|
76
76
|
|
|
77
|
+
/** `pick-never-clears`: a free-form pick-one tick only moves, so `checked: false` is refused by policy, not for a bad anchor. */
|
|
78
|
+
type SteeringRefusalReason = "anchor-not-found" | "id-collision" | "pick-never-clears"
|
|
79
|
+
|
|
77
80
|
/** `annotate`'s result: the edits to splice in, or a typed refusal — a stale/unresolvable `anchor`, or a derived note id that already names a definition. */
|
|
78
81
|
export type SteeringAnnotateResult =
|
|
79
82
|
| { readonly ok: true; readonly edits: readonly SteeringEdit[] }
|
|
80
|
-
| { readonly ok: false; readonly reason:
|
|
83
|
+
| { readonly ok: false; readonly reason: SteeringRefusalReason }
|
|
81
84
|
|
|
82
85
|
/** One item of a `list` block's `items` tree — recursive to whatever depth the source markdown nests. */
|
|
83
86
|
export interface BlockListItem {
|
|
@@ -89,6 +92,11 @@ export interface BlockListItem {
|
|
|
89
92
|
readonly items?: readonly BlockListItem[]
|
|
90
93
|
/** `text`'s own inline structure — the shared renderer walks this instead of `text` when present. Populated at every nesting depth. */
|
|
91
94
|
readonly inline?: readonly InlineNode[]
|
|
95
|
+
/** The item's own 0-based source line: a `paragraph` anchor at it notes this item alone. */
|
|
96
|
+
readonly line?: number
|
|
97
|
+
/** The note or thread whose marker sits on `line`, as on a view node. */
|
|
98
|
+
readonly note?: string
|
|
99
|
+
readonly thread?: SteeringViewThread
|
|
92
100
|
}
|
|
93
101
|
|
|
94
102
|
export interface SteeringViewThread {
|