@joshuafolkken/kit 1.1136.0 → 1.1178.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/settings.json +4 -4
- package/.claude/skills/epic-commands/SKILL.md +27 -239
- package/.claude/skills/epic-commands/epic-bundle.md +115 -0
- package/.claude/skills/epic-commands/execution-waves.md +126 -0
- package/.claude/skills/workflow-commands/SKILL.md +48 -194
- package/.claude/skills/workflow-commands/background-commands.md +1 -1
- package/.claude/skills/workflow-commands/backlogrun-child.md +12 -123
- package/.claude/skills/workflow-commands/backlogrun-lanes.md +14 -63
- package/.claude/skills/workflow-commands/backlogrun-park.md +16 -15
- package/.claude/skills/workflow-commands/backlogrun-progress.md +17 -173
- package/.claude/skills/workflow-commands/backlogrun-recovery.md +155 -0
- package/.claude/skills/workflow-commands/backlogrun-steps.md +65 -144
- package/.claude/skills/workflow-commands/backlogrun.md +5 -4
- package/.claude/skills/workflow-commands/chain-rule.md +3 -2
- package/.claude/skills/workflow-commands/delegation.md +2 -2
- package/.claude/skills/workflow-commands/entry-sequence.md +62 -0
- package/.claude/skills/workflow-commands/followup.md +104 -181
- package/.claude/skills/workflow-commands/fullrun-steps.md +8 -9
- package/.claude/skills/workflow-commands/fullrun.md +16 -73
- package/.claude/skills/workflow-commands/halfrun.md +10 -36
- package/.claude/skills/workflow-commands/into-target.md +4 -4
- package/.claude/skills/workflow-commands/issue-comments.md +7 -8
- package/.claude/skills/workflow-commands/issue-scout.md +6 -6
- package/.claude/skills/workflow-commands/kickoff.md +6 -6
- package/.claude/skills/workflow-commands/needs-human-review.md +6 -6
- package/.claude/skills/workflow-commands/observation-filing.md +21 -172
- package/.claude/skills/workflow-commands/observation-ledger.md +150 -0
- package/.claude/skills/workflow-commands/pre-gate-cut.md +35 -95
- package/.claude/skills/workflow-commands/prerequisite.md +6 -6
- package/.claude/skills/workflow-commands/progress-watcher.md +90 -0
- package/.claude/skills/workflow-commands/prrun.md +5 -5
- package/.claude/skills/workflow-commands/retrospective.md +9 -9
- package/.claude/skills/workflow-commands/split-assessment.md +2 -2
- package/.claude/skills/workflow-commands/target-repository.md +2 -2
- package/.claude/skills/workflow-commands/working-tree-hold.md +7 -5
- package/.codex/hooks.json +4 -4
- package/.github/workflows/ci.yml +15 -2
- package/AGENTS.md +1 -6
- package/CLAUDE.md +58 -120
- package/GEMINI.md +1 -6
- package/dist/CLAUDE.md +58 -120
- package/dist/hooks/batch-guard.js +4 -4
- package/dist/hooks/{chunk-NFZGFGOR.js → chunk-32GQTL4F.js} +14 -1
- package/dist/hooks/{chunk-HBKZLITF.js → chunk-DO6NWWHU.js} +4 -4
- package/dist/hooks/{chunk-KBXZLMRH.js → chunk-G6KRCKEC.js} +216 -76
- package/dist/hooks/{chunk-JKKTVDAG.js → chunk-GDS5N2KB.js} +186 -309
- package/dist/hooks/chunk-I4XSL4TT.js +56 -0
- package/dist/hooks/{chunk-GDU26K3J.js → chunk-S6X6WYKR.js} +886 -174
- package/dist/hooks/{chunk-FO2I7UT7.js → chunk-UAGLLIGZ.js} +1 -1
- package/dist/hooks/{chunk-RFWWUI5F.js → chunk-VESKRQOF.js} +3 -3
- package/dist/hooks/{chunk-KOKOAV6D.js → chunk-VQLLNFNT.js} +6 -4
- package/dist/hooks/{chunk-LJBLGHEH.js → chunk-ZAL3NRGL.js} +4 -6
- package/dist/hooks/codex-hook-adapter.js +10 -10
- package/dist/hooks/duplicate-read-guard.js +5 -5
- package/dist/hooks/format-edited.js +2 -2
- package/dist/hooks/inputs.json +178 -177
- package/dist/hooks/investigation-guard.js +5 -5
- package/dist/hooks/pretool-guard.js +11 -10
- package/dist/hooks/stop-guard.js +20 -6
- package/dist/josh.js +29 -3
- package/dist/version/index.js +5 -1
- package/package.json +2 -1
- package/prompts/collaboration-workflow/file-edits.md +11 -10
- package/prompts/collaboration-workflow/gh-rest.md +2 -4
- package/prompts/collaboration-workflow/glossary.md +1 -1
- package/prompts/collaboration-workflow/issue-citation.md +5 -9
- package/prompts/collaboration-workflow/issue-template.md +40 -115
- package/prompts/collaboration-workflow/operating-rules.md +4 -5
- package/prompts/collaboration-workflow/output-bounds.md +12 -78
- package/prompts/collaboration-workflow/overview.md +7 -13
- package/prompts/collaboration-workflow/plan-comment.md +11 -39
- package/prompts/collaboration-workflow/principles.md +3 -4
- package/prompts/collaboration-workflow/report-format.md +28 -74
- package/prompts/collaboration-workflow/residency.md +12 -109
- package/prompts/collaboration-workflow/rule-delivery.md +98 -97
- package/prompts/collaboration-workflow/shell-body.md +3 -13
- package/prompts/collaboration-workflow/turn-batching.md +14 -88
- package/prompts/collaboration-workflow/upstream-interrupt.md +12 -14
- package/prompts/collaboration-workflow/wip-cap.md +6 -45
- package/prompts/collaboration-workflow.md +1 -13
- package/prompts/review-rubric.md +8 -71
- package/prompts/review.md +25 -55
- package/prompts/testing-guide.md +18 -67
- package/scripts/agent/agent-role-profile.ts +1 -1
- package/scripts/agent/codex-hooks.ts +3 -12
- package/scripts/backlog/backlog-budget.ts +1 -1
- package/scripts/backlog/backlog-drive-cli.ts +4 -3
- package/scripts/backlog/backlog-drive-offer-argv.ts +10 -1
- package/scripts/backlog/backlog-next.ts +15 -4
- package/scripts/backlog/backlog-offer.ts +6 -6
- package/scripts/backlog/backlog-plan-cli.ts +6 -5
- package/scripts/backlog/backlog-rank.ts +18 -9
- package/scripts/backlog/backlog-restructure.ts +152 -0
- package/scripts/backlog/backlog-waves.ts +15 -17
- package/scripts/behavior/behavior-rules.ts +11 -4
- package/scripts/cost-runtime/context-cut-threshold.ts +1 -9
- package/scripts/cost-runtime/cost-verdict.ts +15 -28
- package/scripts/delegation/investigation-reads.ts +5 -5
- package/scripts/discovery/repo-discovery.ts +21 -7
- package/scripts/discovery/repo-origin.ts +26 -0
- package/scripts/discovery/repo-party.ts +42 -1
- package/scripts/document/backlogrun-parent-read-set.ts +14 -15
- package/scripts/document/document-byte-budget.ts +34 -20
- package/scripts/document/entry-read-budget.ts +21 -9
- package/scripts/document/lane-child-read-set.ts +12 -14
- package/scripts/document/read-set-cli.ts +4 -4
- package/scripts/document/read-set-point-of-use.ts +10 -2
- package/scripts/document/resident-budget.ts +47 -0
- package/scripts/epic/epic-shape.ts +10 -13
- package/scripts/epic/epic-solo.ts +1 -1
- package/scripts/exports/unused-members-cli.ts +7 -3
- package/scripts/followup/git-followup-stages.ts +9 -0
- package/scripts/followup/git-pr-followup.ts +1 -1
- package/scripts/gh/git-gh-api-path.ts +7 -0
- package/scripts/gh/git-gh-issue.ts +5 -3
- package/scripts/gh/git-gh-pr-read.ts +4 -0
- package/scripts/gh/git-pr-checks-eval.ts +8 -0
- package/scripts/gh/git-pr-checks.ts +11 -1
- package/scripts/git/constants.ts +10 -0
- package/scripts/git/main-merge.ts +57 -21
- package/scripts/git/schemas.ts +2 -0
- package/scripts/hooks/pretool-guard-cli.ts +2 -2
- package/scripts/hooks/pretool-guard.ts +45 -2
- package/scripts/hooks/run-hook.sh +41 -0
- package/scripts/init/hook-command-rewrite.ts +18 -35
- package/scripts/init/hook-launch.ts +21 -32
- package/scripts/issue/issue-auto-ok.ts +95 -0
- package/scripts/issue/issue-file-cli.ts +53 -7
- package/scripts/issue/issue-file.ts +28 -5
- package/scripts/issue/issue-labels.ts +12 -12
- package/scripts/issue/issue-rank.ts +1 -1
- package/scripts/issue/issue-read-cli.ts +3 -3
- package/scripts/issue/issue-read.ts +2 -2
- package/scripts/issue/issue-scout-cli.ts +2 -2
- package/scripts/issue/issue-state-cli.ts +1 -1
- package/scripts/issue/issue-state.ts +2 -2
- package/scripts/issue/issue-wip.ts +70 -0
- package/scripts/josh/hook-decision.ts +17 -0
- package/scripts/josh/josh-commands-development.ts +11 -1
- package/scripts/josh/josh-commands-issue.ts +2 -2
- package/scripts/lane/lane-launch-cli.ts +21 -3
- package/scripts/lines/bytes-command.ts +29 -6
- package/scripts/notify/telegram-notify.ts +26 -1
- package/scripts/observations/observation-ledger-line.ts +1 -1
- package/scripts/release/release-progress.ts +167 -0
- package/scripts/release/release-publish.ts +19 -8
- package/scripts/release/release-tag.ts +22 -8
- package/scripts/rules/decision-oracle-stage.ts +1 -1
- package/scripts/rules/decision-oracle.ts +6 -6
- package/scripts/rules/delivered-rules.ts +11 -11
- package/scripts/rules/direct-pr-create.ts +110 -0
- package/scripts/rules/file-body.ts +98 -42
- package/scripts/rules/filing-cap.ts +2 -2
- package/scripts/rules/filing-offer.ts +1 -1
- package/scripts/rules/gh-api.ts +18 -1
- package/scripts/rules/implementation-cut.ts +3 -1
- package/scripts/rules/lane-background.ts +67 -10
- package/scripts/rules/permission-guards.ts +10 -2
- package/scripts/rules/pre-gate-cut.ts +2 -2
- package/scripts/rules/stop-rules.ts +4 -4
- package/scripts/rules/test-declared-commit.ts +1 -1
- package/scripts/rules/third-party-write.ts +56 -12
- package/scripts/run/run-carry.ts +11 -5
- package/scripts/run/run-cut-cli.ts +1 -1
- package/scripts/run/run-cut-handoff.ts +21 -1
- package/scripts/run/run-cut-report.ts +2 -2
- package/scripts/run/run-entry-cli.ts +11 -3
- package/scripts/run/run-hold-cli.ts +16 -2
- package/scripts/run/run-label.ts +50 -0
- package/scripts/run/run-merge-steps.ts +8 -3
- package/scripts/run/run-merge.ts +1 -1
- package/scripts/run/run-prep-cli.ts +6 -2
- package/scripts/run/run-prep.ts +17 -0
- package/scripts/run/run-ship-cli.ts +27 -6
- package/scripts/run/run-ship-next.ts +13 -0
- package/scripts/run/run-ship-pre-detach.ts +66 -0
- package/scripts/run/run-ship-preflight.ts +34 -3
- package/scripts/run/run-ship-return.ts +7 -4
- package/scripts/run/run-ship-review-steps.ts +1 -1
- package/scripts/run/run-ship-review.ts +26 -5
- package/scripts/run/run-ship-stage.ts +13 -0
- package/scripts/run/run-ship-steps.ts +10 -2
- package/scripts/run/run-ship-sync.ts +114 -0
- package/scripts/run/run-ship.ts +6 -0
- package/scripts/run/run-step-cli.ts +20 -7
- package/scripts/run/run-wake-cli.ts +8 -1
- package/scripts/run/run-wake-driver.ts +62 -8
- package/scripts/run/run-wake-session.ts +2 -1
- package/scripts/test/test-declared-logic.ts +1 -1
- package/scripts/test/test-red-logic.ts +17 -2
- package/scripts/test/test-red.ts +8 -7
- package/scripts/time-runtime/time-batch-guard.ts +52 -16
- package/scripts/time-runtime/time-density-hook.ts +20 -2
- package/scripts/time-runtime/time-density.ts +4 -3
- package/scripts/time-runtime/time-markers.ts +15 -4
- package/scripts/time-runtime/time-transcript-line.ts +27 -1
- package/scripts/version/npm-registry.ts +11 -1
- package/templates/workflows/ci.yml +9 -1
- package/.claude/skills/workflow-commands/followup-reference.md +0 -118
- package/dist/hooks/chunk-HANSKHES.js +0 -464
|
@@ -23,6 +23,12 @@ const LINE_SEPARATOR = /\r?\n/u
|
|
|
23
23
|
const HIGH = 'high'
|
|
24
24
|
const MEDIUM = 'medium'
|
|
25
25
|
const FIXED_PREFIX = 'fixed '
|
|
26
|
+
// What a finding line carries after its spec: one sentence saying what is wrong, so a stopped ship's
|
|
27
|
+
// log tells the resumed lane child the defect rather than only its location (joshuafolkken/kit#3159).
|
|
28
|
+
const DESCRIPTION_SEPARATOR = ' — '
|
|
29
|
+
// Where the spec ends on read: a reviewer that types the dash as `-`, `--` or `–` still ends it there,
|
|
30
|
+
// so its sentence never lands in the ledger's file field.
|
|
31
|
+
const DESCRIPTION_BOUNDARY = / [—–-]{1,2} /u
|
|
26
32
|
|
|
27
33
|
const VERDICT = {
|
|
28
34
|
CLEAN: 'clean',
|
|
@@ -35,6 +41,7 @@ type ReviewVerdict =
|
|
|
35
41
|
| {
|
|
36
42
|
kind: typeof VERDICT.CLEAN | typeof VERDICT.FIXED | typeof VERDICT.BLOCKING
|
|
37
43
|
specs: ReadonlyArray<string>
|
|
44
|
+
report: ReadonlyArray<string>
|
|
38
45
|
}
|
|
39
46
|
| { kind: typeof VERDICT.INVALID; note: string }
|
|
40
47
|
|
|
@@ -49,6 +56,7 @@ interface RoundOutcome {
|
|
|
49
56
|
interface Finding {
|
|
50
57
|
spec: string
|
|
51
58
|
is_fixed: boolean
|
|
59
|
+
line: string
|
|
52
60
|
}
|
|
53
61
|
|
|
54
62
|
const MISSING_NOTE = 'the reviewer wrote no findings file — the review did not complete'
|
|
@@ -69,7 +77,8 @@ function findings_instruction(findings_path: string, fixed_clause: string): stri
|
|
|
69
77
|
const categories = review_finding_ledger.CATEGORIES.join(', ')
|
|
70
78
|
|
|
71
79
|
return [
|
|
72
|
-
`Then write every finding to ${findings_path}, one per line as
|
|
80
|
+
`Then write every finding to ${findings_path}, one per line as`,
|
|
81
|
+
`<category>:<severity>:<file>[:<line>]${DESCRIPTION_SEPARATOR}<one sentence: what is wrong and why>`,
|
|
73
82
|
`(category one of ${categories}; severity high, medium or low)${fixed_clause},`,
|
|
74
83
|
'and an empty file when there is none.',
|
|
75
84
|
].join(' ')
|
|
@@ -99,10 +108,17 @@ function verification_prompt(brief_path: string, findings_path: string): string
|
|
|
99
108
|
].join(' ')
|
|
100
109
|
}
|
|
101
110
|
|
|
111
|
+
// The spec is what `review:record` reads; the description after it travels only in the stage output.
|
|
112
|
+
function spec_of(unmarked: string): string {
|
|
113
|
+
const end = unmarked.search(DESCRIPTION_BOUNDARY)
|
|
114
|
+
|
|
115
|
+
return (end === -1 ? unmarked : unmarked.slice(0, end)).trim()
|
|
116
|
+
}
|
|
117
|
+
|
|
102
118
|
function finding_of(line: string): Finding {
|
|
103
|
-
|
|
119
|
+
const is_fixed = line.startsWith(FIXED_PREFIX)
|
|
104
120
|
|
|
105
|
-
return { spec: line.slice(FIXED_PREFIX.length)
|
|
121
|
+
return { spec: spec_of(is_fixed ? line.slice(FIXED_PREFIX.length) : line), is_fixed, line }
|
|
106
122
|
}
|
|
107
123
|
|
|
108
124
|
function spec_findings(text: string): ReadonlyArray<Finding> {
|
|
@@ -133,7 +149,8 @@ function scored_kind(findings: ReadonlyArray<Finding>): ScoredVerdict['kind'] {
|
|
|
133
149
|
|
|
134
150
|
// Absent means the reviewer never finished; a line outside the grammar means its verdict cannot be
|
|
135
151
|
// recorded — both stop the ship rather than read as clean. The specs are recorded without the `fixed`
|
|
136
|
-
// mark: a fixed finding was still a finding of that round.
|
|
152
|
+
// mark: a fixed finding was still a finding of that round. The report keeps each line whole — mark and
|
|
153
|
+
// description — for the stage output a resumed lane child reads.
|
|
137
154
|
function read_verdict(text: string | undefined): ReviewVerdict {
|
|
138
155
|
if (text === undefined) return { kind: VERDICT.INVALID, note: MISSING_NOTE }
|
|
139
156
|
|
|
@@ -144,7 +161,11 @@ function read_verdict(text: string | undefined): ReviewVerdict {
|
|
|
144
161
|
return { kind: VERDICT.INVALID, note: `unreadable finding: ${invalid.spec}` }
|
|
145
162
|
}
|
|
146
163
|
|
|
147
|
-
return {
|
|
164
|
+
return {
|
|
165
|
+
kind: scored_kind(findings),
|
|
166
|
+
specs: findings.map((finding) => finding.spec),
|
|
167
|
+
report: findings.map((finding) => finding.line),
|
|
168
|
+
}
|
|
148
169
|
}
|
|
149
170
|
|
|
150
171
|
// Round 1 passes clean or with every blocking finding fixed in place; round 2 follows the commit.
|
|
@@ -20,6 +20,7 @@ const STAGE = {
|
|
|
20
20
|
PREFLIGHT: 'preflight',
|
|
21
21
|
REVIEW: 'review',
|
|
22
22
|
GATE: 'gate',
|
|
23
|
+
SYNC: 'sync',
|
|
23
24
|
COMMIT: 'commit',
|
|
24
25
|
ROUND_TWO: 'round-2',
|
|
25
26
|
FOLLOWUP: 'followup',
|
|
@@ -99,6 +100,9 @@ const DONE_BY: Record<Stage, (done: ReadonlySet<string>, state: ShipState) => bo
|
|
|
99
100
|
// the recorded round, and only a commit pins the tree that round read.
|
|
100
101
|
[STAGE.REVIEW]: (_done, state) => state.is_merged || state.is_committed,
|
|
101
102
|
[STAGE.GATE]: (_done, state) => state.is_merged || state.is_committed,
|
|
103
|
+
// The merge of the default branch before the commit (joshuafolkken/kit#3221): once committed, a
|
|
104
|
+
// conflicting pull request is the followup's to merge again.
|
|
105
|
+
[STAGE.SYNC]: (_done, state) => state.is_merged || state.is_committed,
|
|
102
106
|
[STAGE.COMMIT]: (done, state) => state.is_merged || (done.has(STAGE.COMMIT) && is_shipped(state)),
|
|
103
107
|
// The round-2 pass (joshuafolkken/kit#2489) reads the pushed fix delta, so its record is honored only
|
|
104
108
|
// while that push still stands — new work pushed on top reruns it.
|
|
@@ -138,6 +142,14 @@ function event_text(issue: string, stage: Stage, phase: Phase): string {
|
|
|
138
142
|
return `#${issue} ${stage} ${phase}`
|
|
139
143
|
}
|
|
140
144
|
|
|
145
|
+
// A stop on a merge git could not finish is named `conflict` rather than by the stage it happened in
|
|
146
|
+
// (joshuafolkken/kit#3221), so the event stream reads the cause without anyone opening the report.
|
|
147
|
+
const CONFLICT = 'conflict'
|
|
148
|
+
|
|
149
|
+
function stop_reason(stage: Stage, conflicts: ReadonlyArray<string> = []): string {
|
|
150
|
+
return conflicts.length > 0 ? CONFLICT : stage
|
|
151
|
+
}
|
|
152
|
+
|
|
141
153
|
const run_ship_stage = {
|
|
142
154
|
PHASE,
|
|
143
155
|
STAGE,
|
|
@@ -148,6 +160,7 @@ const run_ship_stage = {
|
|
|
148
160
|
mark_done,
|
|
149
161
|
read_done,
|
|
150
162
|
record_path,
|
|
163
|
+
stop_reason,
|
|
151
164
|
}
|
|
152
165
|
|
|
153
166
|
export type { Phase, ShipState, Stage }
|
|
@@ -4,6 +4,7 @@ import { run_ship_preflight } from './run-ship-preflight'
|
|
|
4
4
|
import { run_ship_review_steps } from './run-ship-review-steps'
|
|
5
5
|
import { run_ship_scoped } from './run-ship-scoped'
|
|
6
6
|
import { run_ship_stage, type ShipState, type Stage } from './run-ship-stage'
|
|
7
|
+
import { run_ship_sync, type StepResult } from './run-ship-sync'
|
|
7
8
|
|
|
8
9
|
// The stages `josh ship` runs, in the order a change ships (joshuafolkken/kit#2398), kept apart from the
|
|
9
10
|
// chaining CLI so the order is read in one place.
|
|
@@ -24,7 +25,7 @@ interface ShipArguments {
|
|
|
24
25
|
interface Step {
|
|
25
26
|
stage: Stage
|
|
26
27
|
header: string
|
|
27
|
-
run: (args: ShipArguments, state: ShipState) => Promise<
|
|
28
|
+
run: (args: ShipArguments, state: ShipState) => Promise<StepResult>
|
|
28
29
|
}
|
|
29
30
|
|
|
30
31
|
const { STAGE } = run_ship_stage
|
|
@@ -56,6 +57,13 @@ const COMMIT_STEPS: ReadonlyArray<Step> = [
|
|
|
56
57
|
async () => await josh(['gate']),
|
|
57
58
|
]),
|
|
58
59
|
},
|
|
60
|
+
// joshuafolkken/kit#3221: the default branch merged again right before the commit, so the pull
|
|
61
|
+
// request opens on a branch that is current; a clean merge goes on without waking anyone.
|
|
62
|
+
{
|
|
63
|
+
stage: STAGE.SYNC,
|
|
64
|
+
header: run_ship.SYNC_HEADER,
|
|
65
|
+
run: async () => await run_ship_sync.sync_stage(),
|
|
66
|
+
},
|
|
59
67
|
{
|
|
60
68
|
stage: STAGE.COMMIT,
|
|
61
69
|
header: run_ship.COMMIT_HEADER,
|
|
@@ -69,7 +77,7 @@ const MERGE_STEPS: ReadonlyArray<Step> = [
|
|
|
69
77
|
{
|
|
70
78
|
stage: STAGE.FOLLOWUP,
|
|
71
79
|
header: run_ship.FOLLOWUP_HEADER,
|
|
72
|
-
run: async (args) => await
|
|
80
|
+
run: async (args) => await run_ship_sync.followup_stage(args.title, args.notify),
|
|
73
81
|
},
|
|
74
82
|
{
|
|
75
83
|
stage: STAGE.REPORT,
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
import { git_gh_pr_read } from '#scripts/gh/git-gh-pr-read'
|
|
2
|
+
import { is_merge_conflict } from '#scripts/gh/git-pr-checks-eval'
|
|
3
|
+
import { git_command } from '#scripts/git/git-command'
|
|
4
|
+
import { main_merge, type MergeOutcome } from '#scripts/git/main-merge'
|
|
5
|
+
import { josh_command, type JoshResult } from '#scripts/josh/josh-run'
|
|
6
|
+
import { error_text } from '#scripts/lib/error-message'
|
|
7
|
+
import { run_ship_scoped } from './run-ship-scoped'
|
|
8
|
+
|
|
9
|
+
// `josh ship` keeps the branch current with the default branch itself (joshuafolkken/kit#3221). The
|
|
10
|
+
// lane merged it once, before the hand-off, and nothing merged it again while the pull request opened
|
|
11
|
+
// and CI ran — so a default branch that moved meanwhile stopped the followup as `interrupted`, and a
|
|
12
|
+
// relaunched session spent its first turns finding out that the cause was a conflict. Most of those
|
|
13
|
+
// conflicts were not conflicts at all to git: merging the default branch again resolved them.
|
|
14
|
+
//
|
|
15
|
+
// So the merge happens twice more, mechanically: once before the commit (`sync_stage`), and once when
|
|
16
|
+
// the followup fails on a pull request GitHub reports `DIRTY` (`followup_stage`). Only a merge git
|
|
17
|
+
// itself cannot finish stops the ship, and it stops as `conflict`, naming the unmerged paths so the
|
|
18
|
+
// resumed session goes straight to them.
|
|
19
|
+
|
|
20
|
+
const should_forward_stderr = true
|
|
21
|
+
const SUCCESS_EXIT_CODE = 0
|
|
22
|
+
const FAILURE_EXIT_CODE = 1
|
|
23
|
+
// A default branch that keeps moving under a pull request is merged again at most this often per ship;
|
|
24
|
+
// past it the followup failure stands, so a busy default branch cannot hold a ship in a loop.
|
|
25
|
+
const MAX_MERGE_RETRIES = 2
|
|
26
|
+
const CONFLICT_NOTE = 'merging the default branch left unmerged paths:'
|
|
27
|
+
const DEFERRED_PREFIX = 'merge deferred to the followup: '
|
|
28
|
+
const CURRENT_SUFFIX = ' brings nothing in'
|
|
29
|
+
|
|
30
|
+
// A stage's result, plus the paths a stopped merge left unmerged — carried to the stop prompt.
|
|
31
|
+
interface StepResult extends JoshResult {
|
|
32
|
+
conflicts?: ReadonlyArray<string>
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
async function josh(argv: ReadonlyArray<string>): Promise<JoshResult> {
|
|
36
|
+
return await josh_command.josh_run(argv, should_forward_stderr)
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
// A merge that could not even be attempted — a fetch that failed — reads as a refusal: nothing was
|
|
40
|
+
// changed, and the followup still finds a real conflict on GitHub's side.
|
|
41
|
+
async function merge_outcome(): Promise<MergeOutcome> {
|
|
42
|
+
try {
|
|
43
|
+
return await main_merge.merge()
|
|
44
|
+
} catch (error) {
|
|
45
|
+
return { kind: 'refused', message: error_text.message_of(error) }
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function conflict_result(files: ReadonlyArray<string>, lead: string): StepResult {
|
|
50
|
+
const out = [lead, CONFLICT_NOTE, ...files].filter((line) => line !== '').join('\n')
|
|
51
|
+
|
|
52
|
+
return { code: FAILURE_EXIT_CODE, out, conflicts: files }
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
// A refusal does not stop the commit: the guard refuses only uncommitted work the default branch also
|
|
56
|
+
// touches, and once that work is committed the followup merges again on a clean tree. A merge that changed
|
|
57
|
+
// the tree meets the scoped pair again, so the commit never carries an unchecked merge.
|
|
58
|
+
async function sync_stage(): Promise<StepResult> {
|
|
59
|
+
const outcome = await merge_outcome()
|
|
60
|
+
|
|
61
|
+
if (outcome.kind === 'conflict') return conflict_result(outcome.files, '')
|
|
62
|
+
if (outcome.kind === 'merged') return await run_ship_scoped.scoped_pair()
|
|
63
|
+
|
|
64
|
+
const out =
|
|
65
|
+
outcome.kind === 'current'
|
|
66
|
+
? `${outcome.branch}${CURRENT_SUFFIX}`
|
|
67
|
+
: `${DEFERRED_PREFIX}${outcome.message}`
|
|
68
|
+
|
|
69
|
+
return { code: SUCCESS_EXIT_CODE, out }
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
// Any read that fails answers "not conflicting": the followup failure then stands as it was.
|
|
73
|
+
async function is_conflicting(): Promise<boolean> {
|
|
74
|
+
try {
|
|
75
|
+
const state = await git_gh_pr_read.pr_get_merge_state(await git_command.branch())
|
|
76
|
+
|
|
77
|
+
return is_merge_conflict(state?.merge_state_status)
|
|
78
|
+
} catch {
|
|
79
|
+
return false
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
// The result to stop on, or `undefined` once a clean merge is pushed and the followup can wait again.
|
|
84
|
+
async function merge_again(title: string, failed: JoshResult): Promise<StepResult | undefined> {
|
|
85
|
+
if (!(await is_conflicting())) return failed
|
|
86
|
+
|
|
87
|
+
const outcome = await merge_outcome()
|
|
88
|
+
|
|
89
|
+
if (outcome.kind === 'conflict') return conflict_result(outcome.files, failed.out)
|
|
90
|
+
if (outcome.kind !== 'merged') return failed
|
|
91
|
+
|
|
92
|
+
const push = await josh(['git', '-y', '--skip-commit', '--skip-pr', title])
|
|
93
|
+
|
|
94
|
+
return push.code === SUCCESS_EXIT_CODE ? undefined : push
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
async function followup_stage(
|
|
98
|
+
title: string,
|
|
99
|
+
notify: ReadonlyArray<string>,
|
|
100
|
+
retries = 0,
|
|
101
|
+
): Promise<StepResult> {
|
|
102
|
+
const result = await josh(['followup', title, ...notify])
|
|
103
|
+
|
|
104
|
+
if (result.code === SUCCESS_EXIT_CODE || retries >= MAX_MERGE_RETRIES) return result
|
|
105
|
+
|
|
106
|
+
const stop = await merge_again(title, result)
|
|
107
|
+
|
|
108
|
+
return stop ?? (await followup_stage(title, notify, retries + 1))
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
const run_ship_sync = { CONFLICT_NOTE, MAX_MERGE_RETRIES, followup_stage, sync_stage }
|
|
112
|
+
|
|
113
|
+
export type { StepResult }
|
|
114
|
+
export { run_ship_sync }
|
package/scripts/run/run-ship.ts
CHANGED
|
@@ -10,8 +10,10 @@
|
|
|
10
10
|
// composite ends at the failure, and the report names the step that failed so the run reads only it.
|
|
11
11
|
|
|
12
12
|
const PREFLIGHT_HEADER = '=== preflight ==='
|
|
13
|
+
const PRE_DETACH_HEADER = '=== pre-detach checks ==='
|
|
13
14
|
const REVIEW_HEADER = '=== review ==='
|
|
14
15
|
const GATE_HEADER = '=== gate ==='
|
|
16
|
+
const SYNC_HEADER = '=== sync origin/main ==='
|
|
15
17
|
const COMMIT_HEADER = '=== commit/push/PR ==='
|
|
16
18
|
const ROUND_TWO_HEADER = '=== round-2 review ==='
|
|
17
19
|
const FOLLOWUP_HEADER = '=== followup ==='
|
|
@@ -29,6 +31,8 @@ interface ShipSection {
|
|
|
29
31
|
header: string
|
|
30
32
|
body: string
|
|
31
33
|
code: number
|
|
34
|
+
// The paths a stopped merge left unmerged (joshuafolkken/kit#3221), handed to the stop prompt.
|
|
35
|
+
conflicts?: ReadonlyArray<string>
|
|
32
36
|
}
|
|
33
37
|
|
|
34
38
|
function section_report(section: ShipSection): string {
|
|
@@ -65,10 +69,12 @@ const run_ship = {
|
|
|
65
69
|
FOLLOWUP_HEADER,
|
|
66
70
|
GATE_HEADER,
|
|
67
71
|
PREFLIGHT_HEADER,
|
|
72
|
+
PRE_DETACH_HEADER,
|
|
68
73
|
REPORT_HEADER,
|
|
69
74
|
REVIEW_HEADER,
|
|
70
75
|
ROUND_TWO_HEADER,
|
|
71
76
|
SKIPPED_BODY,
|
|
77
|
+
SYNC_HEADER,
|
|
72
78
|
STOPPED_PREFIX,
|
|
73
79
|
exit_code,
|
|
74
80
|
failed_section,
|
|
@@ -10,6 +10,7 @@ import { run_carry, type CarryRead } from './run-carry'
|
|
|
10
10
|
import { run_event_scope } from './run-event-scope'
|
|
11
11
|
import { run_event_stream } from './run-event-stream'
|
|
12
12
|
import { run_headless } from './run-headless'
|
|
13
|
+
import { run_prep, type PrepParts } from './run-prep'
|
|
13
14
|
import { run_prep_cli } from './run-prep-cli'
|
|
14
15
|
import { run_retrospective } from './run-retrospective'
|
|
15
16
|
import { run_step, type StepInput } from './run-step'
|
|
@@ -85,14 +86,14 @@ function read_run(
|
|
|
85
86
|
}
|
|
86
87
|
}
|
|
87
88
|
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
const run_reads = read_run(await run_carry.repository_directory(), issue_number, is_lane_child)
|
|
89
|
+
interface Gathered {
|
|
90
|
+
input: StepInput
|
|
91
|
+
ship_problems: ReadonlyArray<string>
|
|
92
|
+
}
|
|
93
93
|
|
|
94
|
+
function step_input(parts: PrepParts, run_reads: RunReads, is_lane_child: boolean): StepInput {
|
|
94
95
|
return {
|
|
95
|
-
issue_number,
|
|
96
|
+
issue_number: parts.issue_number,
|
|
96
97
|
state: parts.state?.state,
|
|
97
98
|
is_human_review: parts.state?.is_human_review ?? false,
|
|
98
99
|
latest_scope: parts.latest_scope,
|
|
@@ -110,6 +111,15 @@ async function gather(issue_number: string): Promise<StepInput> {
|
|
|
110
111
|
}
|
|
111
112
|
}
|
|
112
113
|
|
|
114
|
+
async function gather(issue_number: string): Promise<Gathered> {
|
|
115
|
+
const reads = await run_prep_cli.gather(issue_number)
|
|
116
|
+
const parts = run_prep_cli.to_parts(issue_number, reads)
|
|
117
|
+
const is_lane_child = lane_child_marker.is_child_of(process.cwd())
|
|
118
|
+
const run_reads = read_run(await run_carry.repository_directory(), issue_number, is_lane_child)
|
|
119
|
+
|
|
120
|
+
return { input: step_input(parts, run_reads, is_lane_child), ship_problems: parts.ship_problems }
|
|
121
|
+
}
|
|
122
|
+
|
|
113
123
|
async function run(argv: ReadonlyArray<string>): Promise<number> {
|
|
114
124
|
const issue_number = run_prep_cli.parse_number(argv)
|
|
115
125
|
|
|
@@ -119,9 +129,12 @@ async function run(argv: ReadonlyArray<string>): Promise<number> {
|
|
|
119
129
|
return FAILURE_EXIT_CODE
|
|
120
130
|
}
|
|
121
131
|
|
|
122
|
-
const input = await gather(issue_number)
|
|
132
|
+
const { input, ship_problems } = await gather(issue_number)
|
|
123
133
|
|
|
124
134
|
console.info(run_step.next_action(input).line)
|
|
135
|
+
// On stderr, so stdout stays the one action line (joshuafolkken/kit#3154): what `josh ship` would
|
|
136
|
+
// refuse on is said at every step, while it is still cheap to meet.
|
|
137
|
+
if (ship_problems.length > 0) console.error(run_prep.ship_body(ship_problems))
|
|
125
138
|
|
|
126
139
|
// A state that could not be read exits non-zero, exactly as `run:prep` and `run:next` exit on the
|
|
127
140
|
// same failure, so a position missing its issue state is never read as a confident next step.
|
|
@@ -209,10 +209,16 @@ function log_hint(context: WakeContext): string {
|
|
|
209
209
|
return `The output of every session this supervisor started is in ${context.log_target}.`
|
|
210
210
|
}
|
|
211
211
|
|
|
212
|
+
// The log hint goes ahead of the note: the Telegram length limit cuts the tail, and a long failure note
|
|
213
|
+
// (stdout tail plus stderr tail) would otherwise take the one line that says where to look next.
|
|
214
|
+
function warning_body(stop: LoopStop, body: string, context: WakeContext): string {
|
|
215
|
+
return [body, log_hint(context), stop.note].filter(Boolean).join('\n')
|
|
216
|
+
}
|
|
217
|
+
|
|
212
218
|
async function warn_of_stop(stop: LoopStop, body: string, context: WakeContext): Promise<void> {
|
|
213
219
|
await telegram_notify.warn({
|
|
214
220
|
issue_title: WARNING_TITLE,
|
|
215
|
-
body:
|
|
221
|
+
body: warning_body(stop, body, context),
|
|
216
222
|
recovery: WARNING_RECOVERY,
|
|
217
223
|
})
|
|
218
224
|
}
|
|
@@ -408,6 +414,7 @@ const run_wake_cli = {
|
|
|
408
414
|
USAGE,
|
|
409
415
|
run,
|
|
410
416
|
stop_body,
|
|
417
|
+
warning_body,
|
|
411
418
|
}
|
|
412
419
|
|
|
413
420
|
if (process.argv[1] === SCRIPT_PATH) await main(process.argv.slice(ARGV_OFFSET))
|
|
@@ -8,6 +8,8 @@ const FIRST_LINE = 0
|
|
|
8
8
|
const RESUME_PREFIX = 'resume: '
|
|
9
9
|
const DETAILS_TAIL_LENGTH = 2048
|
|
10
10
|
const TRUNCATION_MARK = '... '
|
|
11
|
+
// The prefix `backlog-drive-cli.ts` gives the exception it catches.
|
|
12
|
+
const ERROR_PREFIX = 'error '
|
|
11
13
|
|
|
12
14
|
function driver_args(invocation: string): ReadonlyArray<string> | undefined {
|
|
13
15
|
const rebuilt = run_invocation.rebuild(invocation)
|
|
@@ -37,16 +39,54 @@ function details_tail(details: string): string {
|
|
|
37
39
|
: details
|
|
38
40
|
}
|
|
39
41
|
|
|
40
|
-
|
|
42
|
+
// **The hand-off names its next move, so the woken session never searches the procedure for it**
|
|
43
|
+
// (joshuafolkken/kit#3156). Whatever the branch, the session acts on it and hands the loop back to the
|
|
44
|
+
// supervisor with a cut; only a person's stop ends the run instead.
|
|
45
|
+
//
|
|
46
|
+
// **The claim comes first** (joshuafolkken/kit#3238). The supervisor hands the record off before the
|
|
47
|
+
// wake, so until the session adopts it with `--resume` every count it writes — `--merged`, `--done`,
|
|
48
|
+
// the closing `--cut` — is refused as advancing a handed-off budget, and the supervisor, seeing no
|
|
49
|
+
// claim, ends the run as a failed wake.
|
|
50
|
+
const HAND_BACK = 'then hand the loop back with pnpm josh run:carry --cut --owner "$PPID"'
|
|
51
|
+
const NEXT_BY_VERDICT: ReadonlyArray<readonly [string, string]> = [
|
|
52
|
+
[
|
|
53
|
+
'retrospective',
|
|
54
|
+
`run what pnpm josh run:step prints (retrospective.md), close it with pnpm josh run:carry --retrospective --summary "<result>" --owner "$PPID", ${HAND_BACK}`,
|
|
55
|
+
],
|
|
56
|
+
[
|
|
57
|
+
'merge human-review',
|
|
58
|
+
'stop the run, because the child stopped before its commit for a person (needs-human-review.md)',
|
|
59
|
+
],
|
|
60
|
+
['epic #', `follow backlogrun-steps.md → "Named issues run first, in order", ${HAND_BACK}`],
|
|
61
|
+
]
|
|
62
|
+
const NEXT_DEFAULT = `act on the result by backlogrun-steps.md → "The loop", ${HAND_BACK}`
|
|
63
|
+
|
|
64
|
+
function next_move(verdict: string, invocation: string): string {
|
|
65
|
+
const found = NEXT_BY_VERDICT.find(([prefix]) => verdict.startsWith(prefix))
|
|
66
|
+
|
|
67
|
+
return `\nNext: first claim the carry record with ${run_carry.claim_command(invocation)}, then ${found?.[1] ?? NEXT_DEFAULT}`
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
function handoff_material(
|
|
71
|
+
verdict: string,
|
|
72
|
+
resume: string,
|
|
73
|
+
invocation: string,
|
|
74
|
+
details: string,
|
|
75
|
+
): string {
|
|
41
76
|
const context = details === '' ? '' : `\nDetails: ${details_tail(details)}`
|
|
42
77
|
const epic = verdict.startsWith('epic #')
|
|
43
78
|
? '\nThe named item is an epic. Follow backlogrun-steps.md named epic procedure; do not launch the epic root as a fullrun child. After all children merge or park, record the root with pnpm josh run:carry --done <epic-number> --owner "$PPID" before continuing to the next named item.'
|
|
44
79
|
: ''
|
|
45
80
|
|
|
46
|
-
return `Driver result: ${verdict}\n${resume}${epic}${context}`
|
|
81
|
+
return `Driver result: ${verdict}\n${resume}${epic}${next_move(verdict, invocation)}${context}`
|
|
47
82
|
}
|
|
48
83
|
|
|
49
|
-
function driver_result(
|
|
84
|
+
function driver_result(
|
|
85
|
+
out: string,
|
|
86
|
+
read: CarryRead,
|
|
87
|
+
invocation: string,
|
|
88
|
+
details = '',
|
|
89
|
+
): DriveResult {
|
|
50
90
|
const lines = out.split('\n')
|
|
51
91
|
const verdict = lines[FIRST_LINE]
|
|
52
92
|
|
|
@@ -57,13 +97,27 @@ function driver_result(out: string, read: CarryRead, details = ''): DriveResult
|
|
|
57
97
|
return { kind: 'failed', note: `backlog:drive returned an incomplete result: ${out}` }
|
|
58
98
|
}
|
|
59
99
|
|
|
60
|
-
return { kind: 'judgment', material: handoff_material(verdict, resume, details) }
|
|
100
|
+
return { kind: 'judgment', material: handoff_material(verdict, resume, invocation, details) }
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
// **The cause is the driver's stdout `error …` line, so it leads the note** (joshuafolkken/kit#3242).
|
|
104
|
+
// `backlog:drive` prints an exception only to stdout, and its stderr is never empty — every command it
|
|
105
|
+
// ran is there — so reading `err ?? out` dropped the one line that said why. The stderr follows as its
|
|
106
|
+
// tail, for the same reason `details_tail` cuts a judgment's details.
|
|
107
|
+
function cause_of(out: string): string {
|
|
108
|
+
const errors = out.split('\n').filter((line) => line.startsWith(ERROR_PREFIX))
|
|
109
|
+
|
|
110
|
+
return errors.length > 0 ? errors.join('\n') : details_tail(out)
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
function failure_note(result: JoshResult): string {
|
|
114
|
+
return [cause_of(result.out), details_tail(result.err ?? '')].filter(Boolean).join('\n')
|
|
61
115
|
}
|
|
62
116
|
|
|
63
|
-
function read_result(result: JoshResult, target: string): DriveResult {
|
|
64
|
-
if (result.code !== SUCCESS) return { kind: 'failed', note: result
|
|
117
|
+
function read_result(result: JoshResult, target: string, invocation: string): DriveResult {
|
|
118
|
+
if (result.code !== SUCCESS) return { kind: 'failed', note: failure_note(result) }
|
|
65
119
|
|
|
66
|
-
return driver_result(result.out, run_carry.read_carry(target), result.err)
|
|
120
|
+
return driver_result(result.out, run_carry.read_carry(target), invocation, result.err)
|
|
67
121
|
}
|
|
68
122
|
|
|
69
123
|
function claim_record(target: string, invocation: string): boolean {
|
|
@@ -87,7 +141,7 @@ async function drive(target: string): Promise<DriveResult> {
|
|
|
87
141
|
|
|
88
142
|
const result = await josh_command.josh_run(args, true)
|
|
89
143
|
|
|
90
|
-
return read_result(result, target)
|
|
144
|
+
return read_result(result, target, read.carry.invocation)
|
|
91
145
|
}
|
|
92
146
|
|
|
93
147
|
export const run_wake_driver = { drive, driver_args, driver_result }
|
|
@@ -29,7 +29,8 @@ import { run_invocation } from './run-invocation'
|
|
|
29
29
|
//
|
|
30
30
|
// **The waker adds nothing to what may be run.** The argument vector is the constant command plus an
|
|
31
31
|
// invocation rebuilt to say exactly what the record said, and nothing here writes a label: `auto-ok`
|
|
32
|
-
//
|
|
32
|
+
// arrives only through `issue:file`'s default, so a woken session is offered by exactly the rules the
|
|
33
|
+
// first one was. A
|
|
33
34
|
// pool that grew across the seam is `backlogrun-steps.md` → "What one invocation approves", not the
|
|
34
35
|
// waker's doing (joshuafolkken/kit#1675).
|
|
35
36
|
const WAKE_COMMAND = claude_agent_argv.AGENT_COMMAND
|
|
@@ -12,7 +12,7 @@
|
|
|
12
12
|
// can catch a defect in it; this set keeps documentation *in* because a test could never have caught a
|
|
13
13
|
// prose defect either way. The two answer different questions, so sharing one list would be wrong, not
|
|
14
14
|
// a clone avoided. Here only paths that are mechanically certain to touch no runtime code are exempt —
|
|
15
|
-
// the non-executable config and cosmetic-asset arms of `
|
|
15
|
+
// the non-executable config and cosmetic-asset arms of `prompts/testing-guide.md`'s "Non-runtime updates" exception
|
|
16
16
|
// stay `required`, and a person declares that exception in the Step 0 work summary. Under-exempting
|
|
17
17
|
// costs one declared exception; over-exempting ships an untested runtime change. The one widening is
|
|
18
18
|
// the basic profile's manual check below, for files no runner kit has could test.
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { issue_bug_label } from '#scripts/issue/issue-bug-label'
|
|
2
|
+
import { observation_ledger } from '#scripts/observations/observation-ledger'
|
|
2
3
|
import { z } from 'zod'
|
|
3
4
|
import { test_declared_logic } from './test-declared-logic'
|
|
4
5
|
|
|
@@ -11,7 +12,7 @@ import { test_declared_logic } from './test-declared-logic'
|
|
|
11
12
|
// that test catches anything — the added and changed tests run against the merge-base, and at least one
|
|
12
13
|
// of them has to fail there.
|
|
13
14
|
|
|
14
|
-
type RedVerdict = 'red' | 'green' | 'no-test'
|
|
15
|
+
type RedVerdict = 'red' | 'green' | 'no-test' | 'test-only'
|
|
15
16
|
|
|
16
17
|
// The declaration a bug-fix Issue carries under its background heading, in the same slot as the
|
|
17
18
|
// behavior-change declaration (`behavior-change-lint.ts`). Read from the declaration, never inferred,
|
|
@@ -33,6 +34,18 @@ function unit_test_files(paths: ReadonlyArray<string>): Array<string> {
|
|
|
33
34
|
.filter((path) => test_declared_logic.is_test_file(path) && path.endsWith(UNIT_TEST_SUFFIX))
|
|
34
35
|
}
|
|
35
36
|
|
|
37
|
+
// **A change whose every path is a test file has no pre-fix code to run against**
|
|
38
|
+
// (joshuafolkken/kit#3214). The merge-base then runs the same code as HEAD, so its result equals HEAD's
|
|
39
|
+
// and says nothing about the fix. Exempt paths do not count: a document-rule test reads `*.md` and
|
|
40
|
+
// `prompts/` files, so a change to one of those is pre-fix content the test can be red against.
|
|
41
|
+
// Observation ledger paths do not count either way (joshuafolkken/kit#3230): a run commits its own
|
|
42
|
+
// ledger entry, and no test reads it as pre-fix content.
|
|
43
|
+
function is_test_only(paths: ReadonlyArray<string>): boolean {
|
|
44
|
+
const counted = paths.filter((path) => !observation_ledger.is_ledger_path(path))
|
|
45
|
+
|
|
46
|
+
return counted.length > 0 && counted.every((path) => test_declared_logic.is_test_file(path))
|
|
47
|
+
}
|
|
48
|
+
|
|
36
49
|
// Vitest's JSON report carries one `testResults` entry per file it loaded — a file that failed to load
|
|
37
50
|
// included — and none for a file its config excluded, so this is how many files actually ran. The
|
|
38
51
|
// changed-path count cannot stand in for it: a file the merge-base config does not include runs nothing
|
|
@@ -58,7 +71,8 @@ function verdict_for(test_count: number, is_failed: boolean): RedVerdict {
|
|
|
58
71
|
}
|
|
59
72
|
|
|
60
73
|
// Only `green` on a declared bug fix is refused. `no-test` is already `josh test:declared`'s refusal
|
|
61
|
-
// when a runtime file changed,
|
|
74
|
+
// when a runtime file changed, a declared fix that touched only docs has nothing to reproduce, and a
|
|
75
|
+
// `test-only` fix runs the same code on the merge-base as on HEAD.
|
|
62
76
|
function is_refused(body: string, verdict: RedVerdict): boolean {
|
|
63
77
|
return verdict === 'green' && is_bug_fix(body)
|
|
64
78
|
}
|
|
@@ -67,6 +81,7 @@ const test_red_logic = {
|
|
|
67
81
|
BUG_DECLARATION_LINE,
|
|
68
82
|
is_bug_fix,
|
|
69
83
|
is_refused,
|
|
84
|
+
is_test_only,
|
|
70
85
|
ran_file_count,
|
|
71
86
|
unit_test_files,
|
|
72
87
|
verdict_for,
|
package/scripts/test/test-red.ts
CHANGED
|
@@ -11,7 +11,7 @@ import { execa } from 'execa'
|
|
|
11
11
|
import { test_red_logic, type RedVerdict } from './test-red-logic'
|
|
12
12
|
|
|
13
13
|
// `josh test:red` — run the added and changed Vitest files against the pre-fix tree and print `red`,
|
|
14
|
-
// `green` or `
|
|
14
|
+
// `green`, `no-test` or `test-only` (joshuafolkken/kit#2448).
|
|
15
15
|
//
|
|
16
16
|
// **The pre-fix tree is a detached worktree at the merge-base, never the caller's checkout.** The tests
|
|
17
17
|
// are copied in from the working tree, so what runs is the new test against the old code, and the
|
|
@@ -26,7 +26,7 @@ const REPORT_NAME = 'report.json'
|
|
|
26
26
|
const NODE_MODULES = 'node_modules'
|
|
27
27
|
const VITEST_BIN = path.join(NODE_MODULES, '.bin', 'vitest')
|
|
28
28
|
const USAGE =
|
|
29
|
-
'Usage: josh test:red — prints red / green / no-test for the changed tests on the merge-base'
|
|
29
|
+
'Usage: josh test:red — prints red / green / no-test / test-only for the changed tests on the merge-base'
|
|
30
30
|
|
|
31
31
|
interface RedRun {
|
|
32
32
|
verdict: RedVerdict
|
|
@@ -34,9 +34,7 @@ interface RedRun {
|
|
|
34
34
|
}
|
|
35
35
|
|
|
36
36
|
// The changed Vitest files that still exist — a deleted test has nothing to run.
|
|
37
|
-
|
|
38
|
-
const paths = await changed_paths.read_changed_paths(false)
|
|
39
|
-
|
|
37
|
+
function changed_tests(paths: ReadonlyArray<string>): Array<string> {
|
|
40
38
|
return test_red_logic.unit_test_files(paths).filter((file) => existsSync(file))
|
|
41
39
|
}
|
|
42
40
|
|
|
@@ -126,12 +124,15 @@ async function verdict_on(base: string, files: ReadonlyArray<string>): Promise<R
|
|
|
126
124
|
}
|
|
127
125
|
|
|
128
126
|
// The verdict over the current checkout. No merge-base (not a repository, no default branch) reads as
|
|
129
|
-
// `no-test` — there is no pre-fix tree to run against, and a guard fails open.
|
|
127
|
+
// `no-test` — there is no pre-fix tree to run against, and a guard fails open. A change whose every path
|
|
128
|
+
// is a test file is `test-only` without a run: the merge-base runs the same code as HEAD.
|
|
130
129
|
async function run(): Promise<RedRun> {
|
|
131
130
|
const base = await change_base.resolved()
|
|
132
|
-
const
|
|
131
|
+
const paths = await changed_paths.read_changed_paths(false)
|
|
132
|
+
const files = changed_tests(paths)
|
|
133
133
|
|
|
134
134
|
if (base === undefined || files.length === 0) return { verdict: 'no-test', files }
|
|
135
|
+
if (test_red_logic.is_test_only(paths)) return { verdict: 'test-only', files }
|
|
135
136
|
|
|
136
137
|
return { verdict: await verdict_on(base, files), files }
|
|
137
138
|
}
|