@pmelab/gtd 52.1.0 → 53.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 +91 -129
- package/claude/hooks/drive.ts +0 -3
- package/claude/hooks/register.tsx +3 -68
- package/claude/hooks/ship.ts +85 -1595
- package/dist/chunks/{reserved-DxB5CROj.mjs → reserved-CQ7amcKG.mjs} +45 -132
- package/dist/chunks/{review-CKMSdvgW.mjs → review-CNgN_V8T.mjs} +2 -2
- package/dist/chunks/{tail-QZc7Uz2U.mjs → tail-JZ9PAOwp.mjs} +2 -14
- package/dist/chunks/{wire-BJuZWR3C.mjs → wire-C12dahrH.mjs} +2 -130
- package/dist/flows.mjs +1 -16
- package/dist/gtd.bundle.mjs +476 -982
- package/dist/testkit.mjs +58 -417
- package/dist/wire.mjs +2 -2
- package/dist/workflows/cleanup.mjs +2 -2
- package/dist/workflows/feature.mjs +112 -363
- package/dist/workflows/fix.mjs +2 -2
- package/dist/workflows/review.mjs +3 -3
- package/dist/workflows/rollout.mjs +1 -1
- package/dist/workflows.mjs +311 -674
- package/package.json +1 -1
- package/src/cli/Cli.test.ts +1 -86
- package/src/cli/Cli.ts +13 -133
- package/src/cli/Metrics.test.ts +21 -31
- package/src/cli/Metrics.ts +25 -36
- package/src/cli/Program.test.ts +1 -35
- package/src/cli/Program.ts +5 -64
- package/src/cli/ToolCommands.ts +0 -3
- package/src/edge/Edge.test.ts +20 -322
- package/src/edge/Edge.ts +10 -181
- package/src/edge/index.ts +0 -1
- package/src/flows/runtime.test.ts +0 -27
- package/src/flows/runtime.ts +1 -49
- package/src/install/Install.ts +15 -24
- package/src/replay/Replay.test.ts +0 -169
- package/src/replay/Replay.ts +4 -93
- package/src/replay/index.ts +0 -1
- package/src/testkit/Layers.ts +0 -1
- package/src/wire/BeatDocument.ts +2 -12
- package/src/wire/BeatStatus.ts +0 -3
- package/src/wire/WireSchema.test.ts +0 -18
- package/src/wire/index.ts +0 -2
- package/src/wire/types.ts +0 -7
- package/src/workflow/Workflow.ts +1 -9
- package/src/workflows/feature/criteria.test.ts +1 -14
- package/src/workflows/feature/criteria.ts +6 -38
- package/src/workflows/feature/feature.ts +34 -118
- package/src/workflows/feature/hands-on-skip.feature +2 -70
- package/src/workflows/feature/index.ts +0 -1
- package/src/workflows/feature/one-step-land.feature +0 -4
- package/src/workflows/feature/prototype-rounds.feature +1 -1
- package/src/workflows/feature/prototype.feature +3 -3
- package/src/workflows/feature/prototype.ts +18 -29
- package/src/workflows/feature/refactor-absorbs.feature +3 -8
- package/src/workflows/feature/refactor-first.feature +0 -5
- package/src/workflows/feature/refactor-rebuild.feature +0 -6
- package/src/workflows/feature/refactor-review.feature +11 -11
- package/src/workflows/feature/refactor.ts +30 -64
- package/src/workflows/feature/review-gate.feature +1 -1
- package/src/workflows/feature/seam-rejection.feature +0 -4
- package/src/workflows/feature/seam.test.ts +7 -26
- package/src/workflows/feature/seam.ts +52 -165
- package/src/workflows/index.ts +0 -1
- package/src/workflows/shared/lenses.ts +0 -13
- package/src/workflows/shared/options.test.ts +2 -11
- package/src/workflows/shared/options.ts +4 -21
- package/src/workflows/shared/review.test.ts +3 -3
- package/src/workflows/shared/steps.ts +0 -2
- package/src/workflows/shared/text.test.ts +10 -80
- package/src/workflows/shared/text.ts +35 -211
- package/src/workflows/shared/vars.ts +2 -8
- package/src/workflows/shared/workflows.steps.ts +0 -13
- package/claude/hooks/context.ts +0 -66
- package/src/replay/Slices.test.ts +0 -73
- package/src/replay/Slices.ts +0 -45
- package/src/wire/ContextLimit.fixture.ts +0 -30
- package/src/wire/ContextLimit.test.ts +0 -96
- package/src/wire/ContextLimit.ts +0 -169
- package/src/wire/Slices.ts +0 -25
- package/src/wire/golden/1/slices.json +0 -11
- package/src/workflows/feature/context-limit.feature +0 -37
- package/src/workflows/feature/continuation-cap.feature +0 -69
- package/src/workflows/feature/continuation-rebuild.feature +0 -92
- package/src/workflows/feature/continuation-slices.feature +0 -68
- package/src/workflows/feature/continuation.feature +0 -209
- package/src/workflows/feature/slice-findings.feature +0 -256
- package/src/workflows/feature/slicing-cap.feature +0 -23
- package/src/workflows/feature/slicing.feature +0 -297
- package/src/workflows/feature/slicing.test.ts +0 -49
- package/src/workflows/feature/slicing.ts +0 -105
- package/src/workflows/shared/continuation.ts +0 -92
|
@@ -9,8 +9,6 @@ import {
|
|
|
9
9
|
landFields,
|
|
10
10
|
renderBeatJson,
|
|
11
11
|
renderLandJson,
|
|
12
|
-
renderSlicesJson,
|
|
13
|
-
slicesDocument,
|
|
14
12
|
statusOf,
|
|
15
13
|
WIRE_SCHEMA,
|
|
16
14
|
type BeatKind,
|
|
@@ -118,9 +116,6 @@ const documents: Readonly<Record<string, string>> = {
|
|
|
118
116
|
}),
|
|
119
117
|
),
|
|
120
118
|
"judge.json": JUDGE,
|
|
121
|
-
"slices.json": renderSlicesJson(
|
|
122
|
-
slicesDocument([{ name: "build", base: "a".repeat(40), tip: "b".repeat(40), closed: true }]),
|
|
123
|
-
),
|
|
124
119
|
}
|
|
125
120
|
|
|
126
121
|
describe(`wire schema ${WIRE_SCHEMA}`, () => {
|
|
@@ -144,19 +139,6 @@ describe(`wire schema ${WIRE_SCHEMA}`, () => {
|
|
|
144
139
|
}
|
|
145
140
|
})
|
|
146
141
|
|
|
147
|
-
describe("slicesDocument", () => {
|
|
148
|
-
it("stamps WIRE_SCHEMA and drops any field beyond the four", () => {
|
|
149
|
-
const doc = slicesDocument([
|
|
150
|
-
{ name: "n", base: "b", tip: "t", closed: false, extra: 1 } as never,
|
|
151
|
-
])
|
|
152
|
-
expect(doc).toEqual({
|
|
153
|
-
schema: WIRE_SCHEMA,
|
|
154
|
-
slices: [{ name: "n", base: "b", tip: "t", closed: false }],
|
|
155
|
-
})
|
|
156
|
-
expect(Object.keys(doc.slices[0]!)).toEqual(["name", "base", "tip", "closed"])
|
|
157
|
-
})
|
|
158
|
-
})
|
|
159
|
-
|
|
160
142
|
describe("the shape check", () => {
|
|
161
143
|
const land = JSON.parse(documents["land.json"]!) as Record<string, unknown>
|
|
162
144
|
const { subject, ...rest } = land
|
package/src/wire/index.ts
CHANGED
package/src/wire/types.ts
CHANGED
|
@@ -38,12 +38,6 @@ export interface ApprovalEntry {
|
|
|
38
38
|
readonly commit: string
|
|
39
39
|
}
|
|
40
40
|
|
|
41
|
-
/** The context-limit shares a step sets; a driver fills an absent one from its defaults. */
|
|
42
|
-
export interface StepContextLimit {
|
|
43
|
-
readonly soft?: number
|
|
44
|
-
readonly hard?: number
|
|
45
|
-
}
|
|
46
|
-
|
|
47
41
|
export interface RenderedDemandSource {
|
|
48
42
|
readonly state: StateName
|
|
49
43
|
readonly actor: Actor
|
|
@@ -57,5 +51,4 @@ export interface RenderedDemandSource {
|
|
|
57
51
|
readonly judge?: string
|
|
58
52
|
readonly skills?: readonly string[]
|
|
59
53
|
readonly access?: StepAccess
|
|
60
|
-
readonly context?: StepContextLimit
|
|
61
54
|
}
|
package/src/workflow/Workflow.ts
CHANGED
|
@@ -1,12 +1,5 @@
|
|
|
1
1
|
import type { WorkflowBase, Flow, ScopeAccess, Summary } from "../flows/index.js"
|
|
2
|
-
import type {
|
|
3
|
-
Actor,
|
|
4
|
-
ContentKind,
|
|
5
|
-
StateMode,
|
|
6
|
-
StateName,
|
|
7
|
-
StepAccess,
|
|
8
|
-
StepContextLimit,
|
|
9
|
-
} from "../wire/index.js"
|
|
2
|
+
import type { Actor, ContentKind, StateMode, StateName, StepAccess } from "../wire/index.js"
|
|
10
3
|
|
|
11
4
|
export type { Actor, ContentKind, StateMode, StateName }
|
|
12
5
|
|
|
@@ -51,7 +44,6 @@ export interface StepDef {
|
|
|
51
44
|
readonly approve?: boolean
|
|
52
45
|
readonly skills?: readonly string[]
|
|
53
46
|
readonly access?: StepAccess
|
|
54
|
-
readonly context?: StepContextLimit
|
|
55
47
|
}
|
|
56
48
|
|
|
57
49
|
/**
|
|
@@ -139,17 +139,9 @@ describe("statedCriteria", () => {
|
|
|
139
139
|
describe("criterionIds", () => {
|
|
140
140
|
const requirements = "## Acceptance Criteria\n\n- AC-1: a\n- AC-2: b\n- AC-4: added\n- unnamed\n"
|
|
141
141
|
|
|
142
|
-
it("lists every ID'd criterion
|
|
142
|
+
it("lists every ID'd criterion", () => {
|
|
143
143
|
expect(criterionIds(requirements)).toEqual(["AC-1", "AC-2", "AC-4"])
|
|
144
144
|
})
|
|
145
|
-
|
|
146
|
-
it("keeps `only` to the IDs the file still lists", () => {
|
|
147
|
-
expect(criterionIds(requirements, { only: ["AC-1", "AC-3"] })).toEqual(["AC-1"])
|
|
148
|
-
})
|
|
149
|
-
|
|
150
|
-
it("gives `except` every other ID, one added since included", () => {
|
|
151
|
-
expect(criterionIds(requirements, { except: ["AC-1", "AC-3"] })).toEqual(["AC-2", "AC-4"])
|
|
152
|
-
})
|
|
153
145
|
})
|
|
154
146
|
|
|
155
147
|
describe("handsOnIds", () => {
|
|
@@ -159,11 +151,6 @@ describe("handsOnIds", () => {
|
|
|
159
151
|
it("leaves out every criterion tagged (no hands-on)", () => {
|
|
160
152
|
expect(handsOnIds(requirements)).toEqual(["AC-1", "AC-3"])
|
|
161
153
|
})
|
|
162
|
-
|
|
163
|
-
it("keeps to the slice", () => {
|
|
164
|
-
expect(handsOnIds(requirements, { only: ["AC-2", "AC-3"] })).toEqual(["AC-3"])
|
|
165
|
-
expect(handsOnIds(requirements, { only: ["AC-2", "AC-4"] })).toEqual([])
|
|
166
|
-
})
|
|
167
154
|
})
|
|
168
155
|
|
|
169
156
|
describe("unstableIds", () => {
|
|
@@ -79,37 +79,16 @@ export const criteriaOf = (text: string): readonly Criterion[] | undefined =>
|
|
|
79
79
|
return id === null ? { id: "", text: item } : criterion(id)
|
|
80
80
|
})
|
|
81
81
|
|
|
82
|
-
/**
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
* steer added.
|
|
86
|
-
*/
|
|
87
|
-
export interface CriteriaSlice {
|
|
88
|
-
readonly only?: readonly string[]
|
|
89
|
-
readonly except?: readonly string[]
|
|
90
|
-
}
|
|
91
|
-
|
|
92
|
-
/** The ID'd criteria of `text`, only `slice`'s when given. */
|
|
93
|
-
export const criterionIds = (text: string, slice?: CriteriaSlice): string[] =>
|
|
94
|
-
(criteriaOf(text) ?? [])
|
|
95
|
-
.map((c) => c.id)
|
|
96
|
-
.filter(
|
|
97
|
-
(id) =>
|
|
98
|
-
id !== "" &&
|
|
99
|
-
(slice?.only === undefined || slice.only.includes(id)) &&
|
|
100
|
-
!(slice?.except ?? []).includes(id),
|
|
101
|
-
)
|
|
82
|
+
/** The ID'd criteria of `text`. */
|
|
83
|
+
export const criterionIds = (text: string): string[] =>
|
|
84
|
+
(criteriaOf(text) ?? []).map((c) => c.id).filter((id) => id !== "")
|
|
102
85
|
|
|
103
|
-
/** The ID'd criteria of `text`
|
|
104
|
-
export const handsOnIds = (text: string
|
|
86
|
+
/** The ID'd criteria of `text` a person can try by hand: none tagged `(no hands-on)`. */
|
|
87
|
+
export const handsOnIds = (text: string): string[] => {
|
|
105
88
|
const tagged = (criteriaOf(text) ?? []).filter((c) => c.noHandsOn).map((c) => c.id)
|
|
106
|
-
return criterionIds(text
|
|
89
|
+
return criterionIds(text).filter((id) => !tagged.includes(id))
|
|
107
90
|
}
|
|
108
91
|
|
|
109
|
-
/** `slice`'s IDs in `.gtd/REQUIREMENTS.md` as it is now; `undefined` for an unsliced change. */
|
|
110
|
-
export const sliceIds = (slice: CriteriaSlice | undefined): string[] | undefined =>
|
|
111
|
-
slice === undefined ? undefined : criterionIds(read(REQUIREMENTS) ?? "", slice)
|
|
112
|
-
|
|
113
92
|
/** Items numbered `AC-1`…, unless every one already carries its own distinct ID. */
|
|
114
93
|
const numbered = (items: readonly string[]): Criterion[] => {
|
|
115
94
|
const ids = items.map((item) => ID.exec(item))
|
|
@@ -292,14 +271,3 @@ export const skipDesign = (stated: StatedCriteria | undefined): Promise<boolean>
|
|
|
292
271
|
await impact()
|
|
293
272
|
return true
|
|
294
273
|
})
|
|
295
|
-
|
|
296
|
-
/** After a gate that invites narrowing the requirements: an edit must leave valid criteria. */
|
|
297
|
-
export const requireCriteriaIfEdited = (): void => {
|
|
298
|
-
const change = changes().get(REQUIREMENTS)
|
|
299
|
-
if (change === undefined) return
|
|
300
|
-
if (change.status === "deleted")
|
|
301
|
-
refuse(
|
|
302
|
-
`gtd land: criteria-missing: ${REQUIREMENTS} must stay — narrow its criteria, never delete it`,
|
|
303
|
-
)
|
|
304
|
-
requireCriteria(REQUIREMENTS)
|
|
305
|
-
}
|
|
@@ -6,44 +6,36 @@ import {
|
|
|
6
6
|
read,
|
|
7
7
|
revertScript,
|
|
8
8
|
run,
|
|
9
|
-
slice,
|
|
10
9
|
start,
|
|
11
10
|
} from "../../flows/index.js"
|
|
12
11
|
import { gate } from "../shared/health.js"
|
|
13
|
-
import { joinReports, type LensReport } from "../shared/lenses.js"
|
|
14
12
|
import { flagModeOn, shipped } from "../shared/options.js"
|
|
15
13
|
import { FEEDBACK, REQUIREMENTS } from "../shared/steps.js"
|
|
16
14
|
import { refusePendingHandOff, tail } from "../shared/tail.js"
|
|
17
15
|
import * as t from "../shared/text.js"
|
|
18
16
|
import {
|
|
19
17
|
criterionIds,
|
|
20
|
-
requireCriteriaFrozen,
|
|
21
18
|
requireStableIds,
|
|
22
19
|
type StableIds,
|
|
23
20
|
skipDesign,
|
|
24
|
-
sliceIds,
|
|
25
21
|
statedCriteria,
|
|
26
|
-
type CriteriaSlice,
|
|
27
22
|
type StatedCriteria,
|
|
28
23
|
} from "./criteria.js"
|
|
29
24
|
import { design } from "./planning.js"
|
|
30
25
|
import { buildTail, humanReview, isCode, reUnwind, type ReviewOutcome } from "../shared/review.js"
|
|
31
26
|
import { prototypeRounds } from "./prototype.js"
|
|
32
27
|
import { implement, implementing, seamBuild } from "./seam.js"
|
|
33
|
-
import {
|
|
34
|
-
import { recordRisk, riskTierOn, type RiskTier } from "./tier.js"
|
|
28
|
+
import { recordRisk, riskTierOn } from "./tier.js"
|
|
35
29
|
|
|
36
30
|
// The ordinary start. Any change to the tree starts a process: `idle` →
|
|
37
31
|
// `unwind` reverts the sketch (its intent survives in history) → a
|
|
38
|
-
// green-baseline gate → design →
|
|
39
|
-
//
|
|
40
|
-
//
|
|
41
|
-
//
|
|
42
|
-
//
|
|
43
|
-
//
|
|
44
|
-
//
|
|
45
|
-
// signs off (the episode ends back at `idle`) or sends a design → implement
|
|
46
|
-
// lap.
|
|
32
|
+
// green-baseline gate → design → a naive pass, judged with `flagMode` on: it
|
|
33
|
+
// lands as the implementation, or (refactoring first when the judge asks) is
|
|
34
|
+
// built fresh from → the health loop → a hands-on test of every criterion,
|
|
35
|
+
// fixed and re-tested → the triaged quality lap, whose leftovers are carried
|
|
36
|
+
// on → a reviewer's walkthrough of the whole change, its findings among its
|
|
37
|
+
// chunks, for the human, which signs off (the episode ends back at `idle`) or
|
|
38
|
+
// sends a design → implement lap.
|
|
47
39
|
|
|
48
40
|
/** Revert the sketch that started the process out of the working tree; its intent survives in history. */
|
|
49
41
|
export const unwind = (): Promise<void> => {
|
|
@@ -56,36 +48,32 @@ export const unwind = (): Promise<void> => {
|
|
|
56
48
|
export { reUnwind }
|
|
57
49
|
|
|
58
50
|
/**
|
|
59
|
-
*
|
|
60
|
-
*
|
|
51
|
+
* A naive pass (judged only with `flagMode` on), then implement and the
|
|
52
|
+
* quality lap, then the review; design must have run since `firstBase`.
|
|
61
53
|
*/
|
|
62
54
|
export const buildFeature = async (firstBase: string): Promise<void> => {
|
|
63
55
|
const tier = riskTierOn() ? recordRisk() : undefined
|
|
64
|
-
|
|
65
|
-
// Every ID handed out, gathered before a later narrowing can drop one: none is ever reused.
|
|
56
|
+
// Every ID handed out, gathered before a later edit can drop one: none is ever reused.
|
|
66
57
|
const handedOut = new Set(listedIds())
|
|
67
|
-
|
|
68
|
-
const
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
laps: reports,
|
|
87
|
-
})
|
|
88
|
-
}
|
|
58
|
+
const seam = await seamBuild(firstBase)
|
|
59
|
+
const used = gather(handedOut)
|
|
60
|
+
const closed = await implementing({
|
|
61
|
+
reference: seam.reference,
|
|
62
|
+
tier,
|
|
63
|
+
afterRefactor: seam.refactored,
|
|
64
|
+
naive: seam.land === true ? seam.reference : undefined,
|
|
65
|
+
rebuild: seam.rebuild,
|
|
66
|
+
})
|
|
67
|
+
// A feature with no code beyond the carried-over tests: the refactor carried
|
|
68
|
+
// it, so the lap covers the whole change.
|
|
69
|
+
const carried = seam.rebuild?.acceptance?.tests ?? []
|
|
70
|
+
const base =
|
|
71
|
+
seam.refactored === true &&
|
|
72
|
+
!changesSince(seam.base).some((c) => isCode(c.path) && !carried.includes(c.path))
|
|
73
|
+
? firstBase
|
|
74
|
+
: seam.base
|
|
75
|
+
const outcome = await reviewedTail(base, base)
|
|
76
|
+
await featureFeedback(outcome, base, { closed, used })
|
|
89
77
|
}
|
|
90
78
|
|
|
91
79
|
const listedIds = (): readonly string[] => criterionIds(read(REQUIREMENTS) ?? "")
|
|
@@ -95,85 +83,14 @@ const gather = (handedOut: Set<string>): readonly string[] => {
|
|
|
95
83
|
return [...handedOut]
|
|
96
84
|
}
|
|
97
85
|
|
|
98
|
-
|
|
99
|
-
/** The slice's number, from 1. */
|
|
100
|
-
readonly number: string
|
|
101
|
-
readonly tier: RiskTier | undefined
|
|
102
|
-
readonly criteria: CriteriaSlice | undefined
|
|
103
|
-
readonly refactor: string
|
|
104
|
-
readonly feature: string
|
|
105
|
-
readonly more: () => boolean
|
|
106
|
-
/** Absent: the slice's own base. */
|
|
107
|
-
readonly reviewBase?: string | undefined
|
|
108
|
-
/** Every criterion ID handed out so far; the slice adds those a steer adds. */
|
|
109
|
-
readonly handedOut: Set<string>
|
|
110
|
-
/** Each earlier slice's lap report, by slice number, for the review after the last one. */
|
|
111
|
-
readonly laps: { slice: string; report: LensReport }[]
|
|
112
|
-
}
|
|
113
|
-
|
|
114
|
-
/** A naive pass (judged only with `flagMode` on), then implement and the quality lap; after the last slice, the review. */
|
|
115
|
-
const buildSlice = async (sliceBase: string, build: SliceBuild): Promise<void> => {
|
|
116
|
-
const seam = await seamBuild(sliceBase, { criteria: build.criteria, refactor: build.refactor })
|
|
117
|
-
const used = gather(build.handedOut)
|
|
118
|
-
await slice(build.feature, async () => {
|
|
119
|
-
const closed = await implementing({
|
|
120
|
-
reference: seam.reference,
|
|
121
|
-
tier: build.tier,
|
|
122
|
-
afterRefactor: seam.refactored,
|
|
123
|
-
naive: seam.land === true ? seam.reference : undefined,
|
|
124
|
-
rebuild: seam.rebuild,
|
|
125
|
-
slice: build.criteria,
|
|
126
|
-
more: build.more,
|
|
127
|
-
})
|
|
128
|
-
// Closing kept the criteria exactly when there is more to build, so this reads as it did.
|
|
129
|
-
const more = build.more()
|
|
130
|
-
// A feature slice with no code beyond the carried-over tests: the refactor carried the
|
|
131
|
-
// feature, so the lap covers the whole slice.
|
|
132
|
-
const carried = seam.rebuild?.acceptance?.tests ?? []
|
|
133
|
-
const base =
|
|
134
|
-
seam.refactored === true &&
|
|
135
|
-
!changesSince(seam.base).some((c) => isCode(c.path) && !carried.includes(c.path))
|
|
136
|
-
? sliceBase
|
|
137
|
-
: seam.base
|
|
138
|
-
if (more) {
|
|
139
|
-
// The criteria stay in the tree for the next slice; no quality fix may move them.
|
|
140
|
-
const guard = () => requireCriteriaFrozen(REQUIREMENTS)
|
|
141
|
-
// No gate between slices: what the lap left waits for the review after the last one.
|
|
142
|
-
const report = await tail(async (_, report) => report, {
|
|
143
|
-
green: true,
|
|
144
|
-
lensBase: base,
|
|
145
|
-
guard,
|
|
146
|
-
guardRests: false,
|
|
147
|
-
})
|
|
148
|
-
build.laps.push({ slice: build.number, report })
|
|
149
|
-
return
|
|
150
|
-
}
|
|
151
|
-
const reviewBase = build.reviewBase ?? base
|
|
152
|
-
const outcome = await reviewedTail(reviewBase, base, { slice: build.number, laps: build.laps })
|
|
153
|
-
await featureFeedback(outcome, base, { closed, used })
|
|
154
|
-
})
|
|
155
|
-
}
|
|
156
|
-
|
|
157
|
-
/**
|
|
158
|
-
* The quality lap of slice `last.slice`, then a reviewer's walkthrough since
|
|
159
|
-
* `base` for the human, carrying what earlier slices' `laps` left too.
|
|
160
|
-
*/
|
|
86
|
+
/** The quality lap, then a reviewer's walkthrough since `base` for the human. */
|
|
161
87
|
const reviewedTail = (
|
|
162
88
|
base: string,
|
|
163
89
|
lensBase: string,
|
|
164
|
-
last: { readonly slice: string; readonly laps: SliceBuild["laps"] } = { slice: "", laps: [] },
|
|
165
90
|
lap: "first" | "feedback" = "first",
|
|
166
91
|
): Promise<ReviewOutcome> =>
|
|
167
92
|
tail(
|
|
168
|
-
(escalations, report) =>
|
|
169
|
-
humanReview(base, escalations, {
|
|
170
|
-
report:
|
|
171
|
-
last.laps.length === 0
|
|
172
|
-
? report
|
|
173
|
-
: joinReports([...last.laps, { slice: last.slice, report }]),
|
|
174
|
-
walkthrough: true,
|
|
175
|
-
lap,
|
|
176
|
-
}),
|
|
93
|
+
(escalations, report) => humanReview(base, escalations, { report, walkthrough: true, lap }),
|
|
177
94
|
{ green: true, lensBase },
|
|
178
95
|
)
|
|
179
96
|
|
|
@@ -197,7 +114,7 @@ const featureFeedback = async (
|
|
|
197
114
|
})
|
|
198
115
|
const tier = riskTierOn() ? recordRisk() : undefined
|
|
199
116
|
closed = await implementing({ tier, amend: true })
|
|
200
|
-
outcome = await reviewedTail(outcome.base, lensBase,
|
|
117
|
+
outcome = await reviewedTail(outcome.base, lensBase, "feedback")
|
|
201
118
|
}
|
|
202
119
|
}
|
|
203
120
|
|
|
@@ -209,7 +126,7 @@ export const planAndBuild = async (firstBase: string, stated?: StatedCriteria):
|
|
|
209
126
|
}
|
|
210
127
|
|
|
211
128
|
// seam.naive / seam.judge never run here: a re-plan's seam is built unjudged and unrefactored.
|
|
212
|
-
/** After a tail that skipped planning: stop on sign-off, re-plan on feedback.
|
|
129
|
+
/** After a tail that skipped planning: stop on sign-off, re-plan on feedback. */
|
|
213
130
|
export const afterTail = async (first: ReviewOutcome): Promise<void> => {
|
|
214
131
|
let outcome = first
|
|
215
132
|
while (outcome.verdict === "feedback") {
|
|
@@ -227,7 +144,6 @@ export const ordinaryStart = async (): Promise<void> => {
|
|
|
227
144
|
refusePendingHandOff()
|
|
228
145
|
// Read right after idle, so a bad value refuses the landing that would pin it.
|
|
229
146
|
flagModeOn()
|
|
230
|
-
sliceCap()
|
|
231
147
|
prototypeRounds()
|
|
232
148
|
const stated = statedCriteria(changes(), head())
|
|
233
149
|
await unwind()
|
|
@@ -7,14 +7,13 @@ Feature: the hands-on test tries only the criteria a person can observe
|
|
|
7
7
|
per untagged criterion, whether a person can observe it by using the
|
|
8
8
|
software; a tagged one is never asked about. Every ID it does not answer
|
|
9
9
|
`no` reaches implement.prototype.testing, which must report each once and
|
|
10
|
-
needs no chunk for the rest. A
|
|
11
|
-
nothing rests to say so.
|
|
10
|
+
needs no chunk for the rest. A change with no such criterion is not tested by
|
|
11
|
+
hand, and nothing rests to say so.
|
|
12
12
|
|
|
13
13
|
Background:
|
|
14
14
|
Given the shipped workflows, as gtd install scaffolds them
|
|
15
15
|
And an environment variable "GTD_QUALITYREVIEWS" set to ""
|
|
16
16
|
And an environment variable "GTD_FLAGMODE" set to "false"
|
|
17
|
-
And an environment variable "GTD_SLICEMAXCRITERIA" set to "2"
|
|
18
17
|
And a file "src/calc.ts" with:
|
|
19
18
|
"""
|
|
20
19
|
export const add = (a: number, b: number) => a + b
|
|
@@ -275,73 +274,6 @@ Feature: the hands-on test tries only the criteria a person can observe
|
|
|
275
274
|
gtd(check): implement.closing → build.review.reviewing
|
|
276
275
|
"""
|
|
277
276
|
|
|
278
|
-
Scenario: each slice's hands-on test judges its own criteria afresh
|
|
279
|
-
Given step "design.triage" writes ".gtd/REQUIREMENTS.md" with:
|
|
280
|
-
"""
|
|
281
|
-
# Requirements
|
|
282
|
-
|
|
283
|
-
## Impact
|
|
284
|
-
|
|
285
|
-
- the greeting module
|
|
286
|
-
|
|
287
|
-
## Acceptance Criteria
|
|
288
|
-
|
|
289
|
-
- AC-1: subtract(a, b) returns a - b.
|
|
290
|
-
- AC-2 (no hands-on): the build prompt names subtract.
|
|
291
|
-
- AC-3: the review prompt names subtract.
|
|
292
|
-
"""
|
|
293
|
-
And step "design.slicing" writes ".gtd/SLICES.md" with:
|
|
294
|
-
"""
|
|
295
|
-
# Slices
|
|
296
|
-
|
|
297
|
-
## Prompts
|
|
298
|
-
|
|
299
|
-
- AC-2
|
|
300
|
-
- AC-3
|
|
301
|
-
|
|
302
|
-
## Subtract
|
|
303
|
-
|
|
304
|
-
- AC-1
|
|
305
|
-
"""
|
|
306
|
-
And step "seam.naive", when prompted with "This slice builds AC-2, AC-3 only", writes "src/prompts.test.ts" with:
|
|
307
|
-
"""
|
|
308
|
-
it("[AC-2] names subtract in the build", () => {})
|
|
309
|
-
it("[AC-3] names subtract in the review", () => {})
|
|
310
|
-
"""
|
|
311
|
-
And step "seam.naive", when prompted with "This slice builds AC-1 only", writes "src/calc.test.ts" with:
|
|
312
|
-
"""
|
|
313
|
-
it("[AC-1] subtracts", () => {})
|
|
314
|
-
"""
|
|
315
|
-
And judge step "implement.prototype.observable", when its questions contain "criterion AC-3 describes", answers:
|
|
316
|
-
"""
|
|
317
|
-
[{"id":"AC-3","answer":"no","p":0.9}]
|
|
318
|
-
"""
|
|
319
|
-
And judge step "implement.prototype.observable", when its questions contain "criterion AC-1 describes", answers:
|
|
320
|
-
"""
|
|
321
|
-
[{"id":"AC-1","answer":"yes","p":0.9}]
|
|
322
|
-
"""
|
|
323
|
-
And step "implement.prototype.testing" writes ".gtd/PROTOTYPE.md" with:
|
|
324
|
-
"""
|
|
325
|
-
## AC-1: pass
|
|
326
|
-
|
|
327
|
-
Printed 2.
|
|
328
|
-
"""
|
|
329
|
-
When the process is driven
|
|
330
|
-
Then the process rests at "build.review.reviewing"
|
|
331
|
-
And the commit subjects end with:
|
|
332
|
-
"""
|
|
333
|
-
gtd(check): implement.health.check → implement.prototype.observable
|
|
334
|
-
gtd(judge): implement.prototype.observable → implement.closing
|
|
335
|
-
gtd(check): implement.closing → seam.naive
|
|
336
|
-
gtd(agent): seam.naive → seam.check
|
|
337
|
-
gtd(check): seam.check → implement.applying
|
|
338
|
-
gtd(check): implement.applying → implement.health.check
|
|
339
|
-
gtd(check): implement.health.check → implement.prototype.observable
|
|
340
|
-
gtd(judge): implement.prototype.observable → implement.prototype.testing
|
|
341
|
-
gtd(agent): implement.prototype.testing → implement.closing
|
|
342
|
-
gtd(check): implement.closing → build.review.reviewing
|
|
343
|
-
"""
|
|
344
|
-
|
|
345
277
|
Scenario: the tag is frozen once design is settled
|
|
346
278
|
Given step "design.triage" writes ".gtd/REQUIREMENTS.md" with:
|
|
347
279
|
"""
|
|
@@ -87,8 +87,4 @@ Feature: a naive pass the judge rules land is itself the implementation
|
|
|
87
87
|
And the commit "gtd(agent): seam.naive → seam.check" has a body containing "Gtd-Kept:"
|
|
88
88
|
And the commit "gtd(judge): seam.judge → implement.applying" has a body containing "\"answer\":\"land\""
|
|
89
89
|
And no commit body contains "Gtd-Approved:"
|
|
90
|
-
And the output lines of gtd "slices" start with:
|
|
91
|
-
"""
|
|
92
|
-
feature
|
|
93
|
-
"""
|
|
94
90
|
And no commit body contains "Gtd-Workflow:"
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
@inmem
|
|
2
2
|
Feature: past its round cap the hands-on test rests at a person's gate
|
|
3
3
|
|
|
4
|
-
`prototypeRounds` caps the fix turns
|
|
4
|
+
`prototypeRounds` caps the fix turns the hands-on test gets. Once they
|
|
5
5
|
are spent and the report still has open items, the process rests at
|
|
6
6
|
implement.prototype.open naming them; a person's fix there is tested again
|
|
7
7
|
with a fresh budget.
|
|
@@ -3,10 +3,10 @@ Feature: every observable criterion is tested by hand before the quality lap
|
|
|
3
3
|
|
|
4
4
|
Once the suite is green, implement.prototype.observable judges which
|
|
5
5
|
criteria a person can observe, and implement.prototype.testing uses the
|
|
6
|
-
|
|
6
|
+
change the way a person would and writes .gtd/PROTOTYPE.md, one chunk per
|
|
7
7
|
such criterion headed `## AC-<n>: pass|fail|blocked`, plus one per problem
|
|
8
8
|
found beside them. A failure goes to implement.prototype.fix.fixing and is tested again; only a clean
|
|
9
|
-
report closes the
|
|
9
|
+
report closes the test. Past `prototypeRounds` fix rounds, or with nothing a
|
|
10
10
|
fixer can take, a person decides at implement.prototype.open.
|
|
11
11
|
|
|
12
12
|
Background:
|
|
@@ -67,7 +67,7 @@ Feature: every observable criterion is tested by hand before the quality lap
|
|
|
67
67
|
And the resting step's content contains "against the user's real data or this working tree"
|
|
68
68
|
And the resting step's content contains "`## AC-<n>: pass`"
|
|
69
69
|
|
|
70
|
-
Scenario: a clean report closes the
|
|
70
|
+
Scenario: a clean report closes the test and moves on to the quality lap
|
|
71
71
|
Given step "implement.prototype.testing" writes ".gtd/PROTOTYPE.md" with:
|
|
72
72
|
"""
|
|
73
73
|
# Prototype test
|
|
@@ -5,14 +5,7 @@ import * as t from "../shared/text.js"
|
|
|
5
5
|
import { defaults } from "../shared/vars.js"
|
|
6
6
|
import type { Guard } from "../shared/health.js"
|
|
7
7
|
import { green } from "./green.js"
|
|
8
|
-
import {
|
|
9
|
-
criteriaOf,
|
|
10
|
-
criterionIds,
|
|
11
|
-
handsOnIds,
|
|
12
|
-
requireCriteriaFrozen,
|
|
13
|
-
sliceIds,
|
|
14
|
-
type CriteriaSlice,
|
|
15
|
-
} from "./criteria.js"
|
|
8
|
+
import { criteriaOf, criterionIds, handsOnIds, requireCriteriaFrozen } from "./criteria.js"
|
|
16
9
|
|
|
17
10
|
/** Only a `no` at this or above skips a criterion: no verdict, or an unsure one, sends it to the hands-on test. */
|
|
18
11
|
const UNOBSERVABLE_MIN_P = 0.5
|
|
@@ -102,12 +95,12 @@ const requireReport = (
|
|
|
102
95
|
}
|
|
103
96
|
|
|
104
97
|
/**
|
|
105
|
-
* The
|
|
106
|
-
*
|
|
98
|
+
* The criteria a person can observe by using the change, by the judge: those
|
|
99
|
+
* tagged `(no hands-on)` are never asked about.
|
|
107
100
|
*/
|
|
108
|
-
const observable = async (
|
|
101
|
+
const observable = async (): Promise<string[]> => {
|
|
109
102
|
const requirements = read(REQUIREMENTS) ?? ""
|
|
110
|
-
const own = handsOnIds(requirements
|
|
103
|
+
const own = handsOnIds(requirements)
|
|
111
104
|
const criteria = (criteriaOf(requirements) ?? []).filter((c) => own.includes(c.id))
|
|
112
105
|
if (criteria.length === 0) return []
|
|
113
106
|
const { answers } = await judge("observable", {
|
|
@@ -122,13 +115,12 @@ const observable = async (slice: CriteriaSlice | undefined): Promise<string[]> =
|
|
|
122
115
|
const frozen = (): void => requireCriteriaFrozen(REQUIREMENTS)
|
|
123
116
|
|
|
124
117
|
const testing = async (
|
|
125
|
-
slice: CriteriaSlice | undefined,
|
|
126
118
|
ids: readonly string[],
|
|
127
119
|
retest: boolean,
|
|
128
120
|
): Promise<ReturnType<typeof requireReport>> => {
|
|
129
|
-
const skipped = criterionIds(read(REQUIREMENTS) ?? ""
|
|
130
|
-
await t.agentWithSkills("testing", t.prototypeTestingPrompt(
|
|
131
|
-
label: retest ? "Re-testing the
|
|
121
|
+
const skipped = criterionIds(read(REQUIREMENTS) ?? "").filter((id) => !ids.includes(id))
|
|
122
|
+
await t.agentWithSkills("testing", t.prototypeTestingPrompt(skipped, retest), {
|
|
123
|
+
label: retest ? "Re-testing the change by hand" : "Testing the change by hand",
|
|
132
124
|
file: PROTOTYPE,
|
|
133
125
|
model: coder(),
|
|
134
126
|
system: t.testerSystem(),
|
|
@@ -138,9 +130,9 @@ const testing = async (
|
|
|
138
130
|
return requireReport(ids, retest)
|
|
139
131
|
}
|
|
140
132
|
|
|
141
|
-
const fixing = async (
|
|
133
|
+
const fixing = async (guard?: Guard): Promise<void> => {
|
|
142
134
|
await scope("fix", () =>
|
|
143
|
-
t.agentWithSkills("fixing", t.prototypeFixingPrompt(
|
|
135
|
+
t.agentWithSkills("fixing", t.prototypeFixingPrompt(), {
|
|
144
136
|
label: "Fixing what the hands-on test found",
|
|
145
137
|
file: PROTOTYPE,
|
|
146
138
|
model: coder(),
|
|
@@ -166,25 +158,22 @@ const askPerson = async (name: string, message: string, label: string): Promise<
|
|
|
166
158
|
}
|
|
167
159
|
|
|
168
160
|
/**
|
|
169
|
-
* Use the
|
|
170
|
-
* can observe, a fix turn takes each failure, and the
|
|
161
|
+
* Use the change by hand: a testing turn reports on every criterion a person
|
|
162
|
+
* can observe, a fix turn takes each failure, and the change is tested again —
|
|
171
163
|
* until the report is clean, or `prototypeRounds` fixes are spent and a person
|
|
172
|
-
* decides. A
|
|
173
|
-
* every fix turn, as it holds the implementation's.
|
|
164
|
+
* decides. A change with no such criterion is not tested by hand. `guard`
|
|
165
|
+
* holds every fix turn, as it holds the implementation's.
|
|
174
166
|
*/
|
|
175
|
-
export const prototypeTest = async (
|
|
176
|
-
slice: CriteriaSlice | undefined,
|
|
177
|
-
guard?: Guard,
|
|
178
|
-
): Promise<void> => {
|
|
167
|
+
export const prototypeTest = async (guard?: Guard): Promise<void> => {
|
|
179
168
|
const cap = prototypeRounds()
|
|
180
169
|
if (cap === 0) return
|
|
181
170
|
await scope("prototype", async () => {
|
|
182
|
-
const ids = await observable(
|
|
171
|
+
const ids = await observable()
|
|
183
172
|
if (ids.length === 0) return
|
|
184
173
|
let fixes = 0
|
|
185
174
|
let retest = false
|
|
186
175
|
for (;;) {
|
|
187
|
-
const { unguided, open } = await testing(
|
|
176
|
+
const { unguided, open } = await testing(ids, retest)
|
|
188
177
|
retest = !unguided
|
|
189
178
|
if (unguided) {
|
|
190
179
|
const instructed = await askPerson(
|
|
@@ -196,7 +185,7 @@ export const prototypeTest = async (
|
|
|
196
185
|
} else if (open.length === 0) return
|
|
197
186
|
else if (fixes < cap && open.some((i) => i.verdict === "fail")) {
|
|
198
187
|
fixes++
|
|
199
|
-
await fixing(
|
|
188
|
+
await fixing(guard)
|
|
200
189
|
} else {
|
|
201
190
|
const changed = await askPerson(
|
|
202
191
|
"open",
|