@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
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
import type { BusyRead } from '#scripts/epic/epic-busy'
|
|
2
|
+
import type { EpicChild } from '#scripts/epic/epic-graph'
|
|
3
|
+
import type { SoloSelection } from '#scripts/epic/epic-solo'
|
|
4
|
+
|
|
5
|
+
// Which candidates may not run side by side because both restructure the same file
|
|
6
|
+
// (joshuafolkken/kit#3221). The condition is deliberately narrow: plain overlap is not serialized,
|
|
7
|
+
// because every resume record of the 2026-10-05 backlog run was resolved mechanically by merging
|
|
8
|
+
// main. What git cannot resolve on its own is a rename or modify/delete conflict — two lanes that
|
|
9
|
+
// each move, split, rename or delete the same file — so only a path both bodies name on a line with
|
|
10
|
+
// such a verb holds the second one back. `remove` is left out: issue bodies use it for lines and
|
|
11
|
+
// options far more often than for files.
|
|
12
|
+
const RESTRUCTURE_WORDS: ReadonlySet<string> = new Set([
|
|
13
|
+
'move',
|
|
14
|
+
'moved',
|
|
15
|
+
'moves',
|
|
16
|
+
'moving',
|
|
17
|
+
'rename',
|
|
18
|
+
'renamed',
|
|
19
|
+
'renames',
|
|
20
|
+
'renaming',
|
|
21
|
+
'split',
|
|
22
|
+
'splits',
|
|
23
|
+
'splitting',
|
|
24
|
+
'delete',
|
|
25
|
+
'deleted',
|
|
26
|
+
'deletes',
|
|
27
|
+
'deleting',
|
|
28
|
+
])
|
|
29
|
+
const RESTRUCTURE_PHRASES: ReadonlyArray<string> = ['移動', '分割', '削除', 'リネーム', '改名']
|
|
30
|
+
const WORD = /[a-z]+/giu
|
|
31
|
+
const CODE_SPAN = /`([^`\s]+)`/gu
|
|
32
|
+
const LINE_SUFFIX = /:\d+(?::\d+)?$/u
|
|
33
|
+
const FILE_EXTENSION = /\.[a-z]\w*$/iu
|
|
34
|
+
|
|
35
|
+
// The restructured paths each issue declares, by issue number.
|
|
36
|
+
type Declared = ReadonlyMap<number, ReadonlySet<string>>
|
|
37
|
+
|
|
38
|
+
interface DeclaringIssue {
|
|
39
|
+
number: number
|
|
40
|
+
body?: string | undefined
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function is_path(token: string): boolean {
|
|
44
|
+
return token.includes('/') || FILE_EXTENSION.test(token)
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function paths_on(line: string): Array<string> {
|
|
48
|
+
return [...line.matchAll(CODE_SPAN)]
|
|
49
|
+
.map((match) => (match[1] ?? '').replace(LINE_SUFFIX, ''))
|
|
50
|
+
.filter((token) => is_path(token))
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function is_restructuring(line: string): boolean {
|
|
54
|
+
const words = line.match(WORD) ?? []
|
|
55
|
+
|
|
56
|
+
if (words.some((word) => RESTRUCTURE_WORDS.has(word.toLowerCase()))) return true
|
|
57
|
+
|
|
58
|
+
return RESTRUCTURE_PHRASES.some((phrase) => line.includes(phrase))
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
// The backticked paths on the lines of `body` that carry a restructuring verb.
|
|
62
|
+
function restructured_paths(body: string): ReadonlySet<string> {
|
|
63
|
+
const lines = body.split('\n').filter((line) => is_restructuring(line))
|
|
64
|
+
|
|
65
|
+
return new Set(lines.flatMap((line) => paths_on(line)))
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
function declared_of(issues: ReadonlyArray<DeclaringIssue>): Declared {
|
|
69
|
+
return new Map(issues.map((issue) => [issue.number, restructured_paths(issue.body ?? '')]))
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
interface SeparateContext {
|
|
73
|
+
read: BusyRead
|
|
74
|
+
repo: string
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
// The path each running lane restructures, mapped to the issue that holds it.
|
|
78
|
+
function running_claims(declared: Declared, read: BusyRead): Map<string, number> {
|
|
79
|
+
const holders = read.kind === 'busy' ? read.issues.map((issue) => issue.number) : []
|
|
80
|
+
const pairs = holders.flatMap((number) =>
|
|
81
|
+
[...(declared.get(number) ?? [])].map((path): [string, number] => [path, number]),
|
|
82
|
+
)
|
|
83
|
+
|
|
84
|
+
return new Map(pairs)
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
function paths_of(child: EpicChild, declared: Declared, repo: string): ReadonlySet<string> {
|
|
88
|
+
if (child.repo !== repo) return new Set()
|
|
89
|
+
|
|
90
|
+
return declared.get(child.number) ?? new Set()
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
function clash_note(child: EpicChild, path: string, holder: number): string {
|
|
94
|
+
return `#${String(child.number)} waits: it and #${String(holder)} both restructure \`${path}\`.`
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
interface Walk {
|
|
98
|
+
claims: Map<string, number>
|
|
99
|
+
offered: Array<EpicChild>
|
|
100
|
+
withheld: Array<EpicChild>
|
|
101
|
+
notes: Array<string>
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
function visit(walk: Walk, child: EpicChild, paths: ReadonlySet<string>): void {
|
|
105
|
+
const clash = [...paths].find((path) => walk.claims.has(path))
|
|
106
|
+
|
|
107
|
+
if (clash === undefined) {
|
|
108
|
+
walk.offered.push(child)
|
|
109
|
+
for (const path of paths) walk.claims.set(path, child.number)
|
|
110
|
+
|
|
111
|
+
return
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
walk.withheld.push(child)
|
|
115
|
+
walk.notes.push(clash_note(child, clash, walk.claims.get(clash) ?? child.number))
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
function merged_notice(
|
|
119
|
+
notice: string | undefined,
|
|
120
|
+
notes: ReadonlyArray<string>,
|
|
121
|
+
): string | undefined {
|
|
122
|
+
const parts = notice === undefined ? notes : [notice, ...notes]
|
|
123
|
+
|
|
124
|
+
return parts.length === 0 ? undefined : parts.join('\n')
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
// `selection` with every offered candidate that restructures a path a running lane or an earlier
|
|
128
|
+
// candidate already restructures moved to `withheld`, in offer order.
|
|
129
|
+
function separate(
|
|
130
|
+
selection: SoloSelection,
|
|
131
|
+
declared: Declared,
|
|
132
|
+
context: SeparateContext,
|
|
133
|
+
): SoloSelection {
|
|
134
|
+
const walk: Walk = {
|
|
135
|
+
claims: running_claims(declared, context.read),
|
|
136
|
+
offered: [],
|
|
137
|
+
withheld: [],
|
|
138
|
+
notes: [],
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
for (const child of selection.offered) visit(walk, child, paths_of(child, declared, context.repo))
|
|
142
|
+
|
|
143
|
+
const notice = merged_notice(selection.notice, walk.notes)
|
|
144
|
+
const withheld = [...walk.withheld, ...selection.withheld]
|
|
145
|
+
|
|
146
|
+
return { offered: walk.offered, withheld, ...(notice !== undefined && { notice }) }
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
const backlog_restructure = { declared_of, restructured_paths, separate }
|
|
150
|
+
|
|
151
|
+
export { backlog_restructure }
|
|
152
|
+
export type { Declared }
|
|
@@ -10,7 +10,7 @@ import {
|
|
|
10
10
|
NEEDS_DECISION_LABEL,
|
|
11
11
|
} from '#scripts/issue/issue-labels'
|
|
12
12
|
import { backlog_plan, type PlanContext } from './backlog-plan'
|
|
13
|
-
import { backlog_rank } from './backlog-rank'
|
|
13
|
+
import { backlog_rank, type GateScope } from './backlog-rank'
|
|
14
14
|
|
|
15
15
|
// The order a `backlogrun` takes, wave by wave, before it starts (joshuafolkken/kit#2778).
|
|
16
16
|
//
|
|
@@ -18,7 +18,8 @@ import { backlog_rank } from './backlog-rank'
|
|
|
18
18
|
// made from, so the order past the first answer was left for a person to work out by hand. This plays
|
|
19
19
|
// the asks forward under one assumption: **every issue of a wave merges before the next wave starts**.
|
|
20
20
|
//
|
|
21
|
-
// **No rule is written twice.** The first wave is `backlog_rank.select` — rank, `run:solo` gate,
|
|
21
|
+
// **No rule is written twice.** The first wave is `backlog_rank.select` — rank, `run:solo` gate,
|
|
22
|
+
// restructure separation, cap —
|
|
22
23
|
// over the very candidates `backlog:next` resolves, asked of an idle repository — so it is what
|
|
23
24
|
// `backlog:next` prints when nothing is running (joshuafolkken/kit#2928). Each later wave marks the earlier waves closed and re-classifies the same pool
|
|
24
25
|
// through `epic_classify.classify_children` with the resolver `epic:next` sorted it with the first
|
|
@@ -28,6 +29,7 @@ import { backlog_rank } from './backlog-rank'
|
|
|
28
29
|
|
|
29
30
|
const IDLE: BusyRead = { kind: 'idle' }
|
|
30
31
|
const CLOSED = 'CLOSED'
|
|
32
|
+
const EMPTY_SCOPE: GateScope = { standalone: new Set(), declared: new Map() }
|
|
31
33
|
|
|
32
34
|
const HEADING_ASSUMPTION =
|
|
33
35
|
'Assumes every issue in a wave merges before the next wave starts. Issues a run already has are left out, and only issues of this repository are planned.'
|
|
@@ -59,17 +61,17 @@ interface WavePool {
|
|
|
59
61
|
children: ReadonlyArray<EpicChild>
|
|
60
62
|
running: ReadonlySet<string>
|
|
61
63
|
repo: string
|
|
62
|
-
|
|
64
|
+
scope: GateScope
|
|
63
65
|
}
|
|
64
66
|
|
|
65
|
-
function pool_of(result: EpicNextResult, repo: string,
|
|
67
|
+
function pool_of(result: EpicNextResult, repo: string, scope: GateScope): WavePool {
|
|
66
68
|
const all = backlog_rank.everything_in(result)
|
|
67
69
|
|
|
68
70
|
return {
|
|
69
71
|
children: all.filter((child) => !is_running(child)),
|
|
70
72
|
running: epic_outside_blocker.running_keys(all),
|
|
71
73
|
repo,
|
|
72
|
-
|
|
74
|
+
scope,
|
|
73
75
|
}
|
|
74
76
|
}
|
|
75
77
|
|
|
@@ -78,9 +80,9 @@ function offer(
|
|
|
78
80
|
children: ReadonlyArray<EpicChild>,
|
|
79
81
|
pool: WavePool,
|
|
80
82
|
): ReadonlyArray<EpicChild> {
|
|
81
|
-
const { repo,
|
|
83
|
+
const { repo, scope } = pool
|
|
82
84
|
|
|
83
|
-
return backlog_rank.select({ candidates, pool: children, read: IDLE, repo,
|
|
85
|
+
return backlog_rank.select({ candidates, pool: children, read: IDLE, repo, ...scope }).offered
|
|
84
86
|
}
|
|
85
87
|
|
|
86
88
|
// Ranked against the whole result, exactly as `backlog:next` ranks it, so the two agree.
|
|
@@ -106,14 +108,10 @@ function next_wave(pool: WavePool, done: ReadonlySet<string>): ReadonlyArray<Epi
|
|
|
106
108
|
}
|
|
107
109
|
|
|
108
110
|
// Each wave is closed before the next is asked for; a wave that offers nothing ends the plan, which
|
|
109
|
-
// the pool's size bounds because every wave closes at least one child. `
|
|
110
|
-
// offer's cap bounds; left empty, no wave is capped.
|
|
111
|
-
function build(
|
|
112
|
-
result
|
|
113
|
-
repo: string,
|
|
114
|
-
standalone: ReadonlySet<string> = new Set(),
|
|
115
|
-
): WavePlan {
|
|
116
|
-
const pool = pool_of(result, repo, standalone)
|
|
111
|
+
// the pool's size bounds because every wave closes at least one child. `scope` names the rows the
|
|
112
|
+
// offer's cap bounds and the paths each issue restructures; left empty, no wave is capped or separated.
|
|
113
|
+
function build(result: EpicNextResult, repo: string, scope: GateScope = EMPTY_SCOPE): WavePlan {
|
|
114
|
+
const pool = pool_of(result, repo, scope)
|
|
117
115
|
const done = new Set<string>()
|
|
118
116
|
const waves: Array<ReadonlyArray<EpicChild>> = []
|
|
119
117
|
|
|
@@ -155,11 +153,11 @@ function unreached_lines(plan: WavePlan, context: PlanContext): Array<string> {
|
|
|
155
153
|
function format_waves(
|
|
156
154
|
result: EpicNextResult,
|
|
157
155
|
context: PlanContext,
|
|
158
|
-
|
|
156
|
+
scope: GateScope = EMPTY_SCOPE,
|
|
159
157
|
): string {
|
|
160
158
|
if (result.verdict === 'error') return backlog_plan.format_unusable(result)
|
|
161
159
|
|
|
162
|
-
const plan = build(result, context.repo,
|
|
160
|
+
const plan = build(result, context.repo, scope)
|
|
163
161
|
const waves = plan.waves.map((wave, index) => wave_line(wave, index, context.repo))
|
|
164
162
|
|
|
165
163
|
return [
|
|
@@ -13,7 +13,13 @@ const TOOL_USE = 'tool_use'
|
|
|
13
13
|
const COMMAND_KEY = 'command'
|
|
14
14
|
const FIRST_POSITION_OFFSET = 1
|
|
15
15
|
const TOOL_RESULT = 'tool_result'
|
|
16
|
-
|
|
16
|
+
// The openings of the two results that mean a Bash call never ran: the permission layer's refusal,
|
|
17
|
+
// and a PreToolUse hook's block (joshuafolkken/kit#3197). A PostToolUse hook error is deliberately
|
|
18
|
+
// absent — that hook fires after the command has already run.
|
|
19
|
+
const DENIED_BASH_PREFIXES = [
|
|
20
|
+
'Permission to use Bash with command ',
|
|
21
|
+
'PreToolUse:Bash hook error: ',
|
|
22
|
+
]
|
|
17
23
|
|
|
18
24
|
// The git subcommands that stage or rewrite the index — the ones `CLAUDE.md` reserves for `pnpm josh
|
|
19
25
|
// git` under "Never stage or mutate the git index on your own". An agent that runs one directly has
|
|
@@ -71,14 +77,15 @@ function is_index_mutation(command: string): boolean {
|
|
|
71
77
|
.some((segment) => INDEX_MUTATION_HEAD.test(segment) && !is_dry_run(segment))
|
|
72
78
|
}
|
|
73
79
|
|
|
74
|
-
// An errored result alone may mean a command ran and failed. Only
|
|
75
|
-
// identifies a Bash call that never ran; the parser keeps
|
|
80
|
+
// An errored result alone may mean a command ran and failed. Only a refusal before execution — the
|
|
81
|
+
// permission layer's or a PreToolUse hook's — identifies a Bash call that never ran; the parser keeps
|
|
82
|
+
// the opening of that result's text.
|
|
76
83
|
function is_denied_result(block: Block): boolean {
|
|
77
84
|
return (
|
|
78
85
|
block.type === TOOL_RESULT &&
|
|
79
86
|
block.result_id !== '' &&
|
|
80
87
|
block.is_error === true &&
|
|
81
|
-
block.error_text.startsWith(
|
|
88
|
+
DENIED_BASH_PREFIXES.some((prefix) => block.error_text.startsWith(prefix))
|
|
82
89
|
)
|
|
83
90
|
}
|
|
84
91
|
|
|
@@ -10,12 +10,4 @@ import { context_cut_payback } from './context-cut-payback'
|
|
|
10
10
|
// from the cache-price multipliers and the horizon rather than restated, so it moves with the model.
|
|
11
11
|
const CONTEXT_CUT_THRESHOLD = context_cut_payback.break_even_context()
|
|
12
12
|
|
|
13
|
-
|
|
14
|
-
// (joshuafolkken/kit#2295). A long parent session's whole-session average lags its current context by
|
|
15
|
-
// dozens of requests — measured at 85 requests (~4 hours) behind on the 2026-09-21 parent — so it
|
|
16
|
-
// keeps supervising an already-oversized context long after a cut was due. Ten requests is enough to
|
|
17
|
-
// smooth a single outlier without reaching back into the cheap early context; it sits beside the
|
|
18
|
-
// threshold so the window and the limit are read from one place and cannot drift.
|
|
19
|
-
const RECENT_REQUEST_WINDOW = 10
|
|
20
|
-
|
|
21
|
-
export { CONTEXT_CUT_THRESHOLD, RECENT_REQUEST_WINDOW }
|
|
13
|
+
export { CONTEXT_CUT_THRESHOLD }
|
|
@@ -3,42 +3,31 @@
|
|
|
3
3
|
// of a session will cost, and takes only the figures its verdict needs — an over measurement — never
|
|
4
4
|
// a whole report. The `--cap` counterfactual it once sat beside was retired in #2016 as readerless.
|
|
5
5
|
|
|
6
|
-
import { RECENT_REQUEST_WINDOW } from './context-cut-threshold'
|
|
7
|
-
|
|
8
6
|
const FAILURE_EXIT_CODE = 1
|
|
9
7
|
const OVER_VERDICT = 'over'
|
|
10
8
|
const UNDER_VERDICT = 'under'
|
|
11
9
|
|
|
12
|
-
// What `--over` reads of a session: the billed input each request paid, oldest first. The
|
|
13
|
-
//
|
|
14
|
-
//
|
|
15
|
-
// context by dozens of requests, so a sum could not answer what the recent window actually cost.
|
|
10
|
+
// What `--over` reads of a session: the billed input each request paid, oldest first. The sequence
|
|
11
|
+
// is carried rather than a single sum because the hand-off prices the newest request alone
|
|
12
|
+
// (joshuafolkken/kit#3224) — a sum could not answer what the session's current context costs.
|
|
16
13
|
interface OverMeasurement {
|
|
17
14
|
billed_input_per_request: ReadonlyArray<number>
|
|
18
15
|
}
|
|
19
16
|
|
|
20
|
-
// The most recent requests the hand-off prices, oldest first — the whole sequence when it is shorter
|
|
21
|
-
// than the window, so a session with fewer than `RECENT_REQUEST_WINDOW` requests is priced on what it
|
|
22
|
-
// has rather than padded.
|
|
23
|
-
function recent_requests(measurement: OverMeasurement): ReadonlyArray<number> {
|
|
24
|
-
return measurement.billed_input_per_request.slice(-RECENT_REQUEST_WINDOW)
|
|
25
|
-
}
|
|
26
|
-
|
|
27
17
|
// What a turn costs is decided by the accumulated preamble, not by what the turn does, so the
|
|
28
|
-
// marginal cost of a session is the billed input its
|
|
18
|
+
// marginal cost of a session is the billed input its newest request paid. Measured across one
|
|
29
19
|
// `epicrun` that ran six children in one context: 222k per request during the first child, 645k
|
|
30
20
|
// during the sixth — the same work at 2.9x the price (joshuafolkken/kit#968).
|
|
31
21
|
//
|
|
32
|
-
// The
|
|
33
|
-
//
|
|
34
|
-
//
|
|
22
|
+
// **The newest request, not an average.** joshuafolkken/kit#2295 replaced the whole-session average,
|
|
23
|
+
// which lagged a long parent by dozens of requests, with the average of the last ten; that still
|
|
24
|
+
// lagged. A session's billed input only grows between compactions (a unit writes its own transcript),
|
|
25
|
+
// so an average smooths no outlier — it only trails the context it averages. On 2026-10-05 wake
|
|
26
|
+
// session 03367124 crossed the threshold at request 65 and its ten-request average at request 70
|
|
27
|
+
// (joshuafolkken/kit#3224). The threshold is a break-even on the *current* context
|
|
28
|
+
// (`context-cut-payback.ts`), so the current context is what it is compared against.
|
|
35
29
|
function per_request_cost(measurement: OverMeasurement): number {
|
|
36
|
-
|
|
37
|
-
if (window.length === 0) return 0
|
|
38
|
-
|
|
39
|
-
const total = window.reduce((sum, billed) => sum + billed, 0)
|
|
40
|
-
|
|
41
|
-
return Math.round(total / window.length)
|
|
30
|
+
return measurement.billed_input_per_request.at(-1) ?? 0
|
|
42
31
|
}
|
|
43
32
|
|
|
44
33
|
// The bare over/under decision, without a report: `run:status` reads this same verdict read-only
|
|
@@ -51,14 +40,12 @@ function classify(
|
|
|
51
40
|
return per_request_cost(measurement) > limit ? OVER_VERDICT : UNDER_VERDICT
|
|
52
41
|
}
|
|
53
42
|
|
|
54
|
-
// The stderr line names
|
|
55
|
-
// from
|
|
56
|
-
// session is longer than the window and the whole count when it is shorter.
|
|
43
|
+
// The stderr line names which request was priced, so the reader can tell the newest request's cost
|
|
44
|
+
// from an average over the session (joshuafolkken/kit#3224).
|
|
57
45
|
function over_summary(measurement: OverMeasurement, limit: number): string {
|
|
58
46
|
const total = measurement.billed_input_per_request.length
|
|
59
|
-
const window = recent_requests(measurement).length
|
|
60
47
|
|
|
61
|
-
return `${String(per_request_cost(measurement))} billed input tokens per request
|
|
48
|
+
return `${String(per_request_cost(measurement))} billed input tokens per request, on the newest of ${String(total)} request(s); limit ${String(limit)}`
|
|
62
49
|
}
|
|
63
50
|
|
|
64
51
|
// A verdict, not a table: the point of the flag is that the hand-off is decided by a number rather
|
|
@@ -71,7 +71,7 @@ const INVESTIGATOR_AGENT = 'investigator'
|
|
|
71
71
|
// What the refusal tells the model. It has to name the count, the command and the return shape,
|
|
72
72
|
// because the deny reason is the only text that reaches the model — a run that is refused and told
|
|
73
73
|
// nothing reads it as a broken tool.
|
|
74
|
-
const REASON = `⛔ investigation: ${String(delegation_policy.INVESTIGATION_FILE_THRESHOLD)} files read and not edited, or ${String(delegation_policy.INVESTIGATION_SEARCH_TURN_THRESHOLD)} search turns, since the last delegated unit, so the reading from here goes to a unit of its own (a successful write also starts the search count over). Ask \`pnpm josh delegate investigation\`, then dispatch the \`${INVESTIGATOR_AGENT}\` agent (\`kit:${INVESTIGATOR_AGENT}\` in a consumer) with a brief of what the main line has already concluded and what is left to find out; it returns the conclusion plus its \`file:line\` citations, never the file text. Keep a read in the main line only where this run will edit that file — an \`Edit\` cannot be issued against text you do not hold. This run's own instructions and the harness's own session files — a backgrounded call's output, a persisted tool result, this session's scratchpad — never count toward the files, so they are not what took the file count here; every read-only search turn counts, whatever it names. The rule is \`.claude/skills/workflow-commands/
|
|
74
|
+
const REASON = `⛔ investigation: ${String(delegation_policy.INVESTIGATION_FILE_THRESHOLD)} files read and not edited, or ${String(delegation_policy.INVESTIGATION_SEARCH_TURN_THRESHOLD)} search turns, since the last delegated unit, so the reading from here goes to a unit of its own (a successful write also starts the search count over). Ask \`pnpm josh delegate investigation\`, then dispatch the \`${INVESTIGATOR_AGENT}\` agent (\`kit:${INVESTIGATOR_AGENT}\` in a consumer) with a brief of what the main line has already concluded and what is left to find out; it returns the conclusion plus its \`file:line\` citations, never the file text. Keep a read in the main line only where this run will edit that file — an \`Edit\` cannot be issued against text you do not hold. This run's own instructions and the harness's own session files — a backgrounded call's output, a persisted tool result, this session's scratchpad — never count toward the files, so they are not what took the file count here; every read-only search turn counts, whatever it names. The rule is \`.claude/skills/workflow-commands/delegation.md\` → "The pre-implementation reading".`
|
|
75
75
|
|
|
76
76
|
interface ReadTally {
|
|
77
77
|
// The files read and not since edited, resolved so the two ways a target reaches here compare.
|
|
@@ -92,13 +92,13 @@ interface ReadTally {
|
|
|
92
92
|
searches: number
|
|
93
93
|
searches_since: number
|
|
94
94
|
// The files this run has successfully edited within the window, resolved (joshuafolkken/kit#1840).
|
|
95
|
-
// A re-read of one of them is a read of a file the run edited, which
|
|
95
|
+
// A re-read of one of them is a read of a file the run edited, which `delegation.md` keeps in the main line — so
|
|
96
96
|
// it is excluded from the count and from a call's projected growth rather than counted as fresh
|
|
97
97
|
// investigation.
|
|
98
98
|
edited: ReadonlySet<string>
|
|
99
99
|
}
|
|
100
100
|
|
|
101
|
-
// **The run's own instructions are not the Issue's subject, so reading them is not what
|
|
101
|
+
// **The run's own instructions are not the Issue's subject, so reading them is not what `delegation.md` delegates.**
|
|
102
102
|
// #1426's rule is about reading to find out how the subject works; `CLAUDE.md`, a `prompts/` topic and
|
|
103
103
|
// a skill file are read to find out what to *do*, they are read by nearly every run, and a unit cannot
|
|
104
104
|
// be sent to read them on the main line's behalf. #1441's own measurement excluded 5 of them by hand
|
|
@@ -264,7 +264,7 @@ function is_failed_outcome(span: Span): boolean {
|
|
|
264
264
|
|
|
265
265
|
// **Every file the run successfully wrote within the window, resolved** (joshuafolkken/kit#1840). It
|
|
266
266
|
// is the one source of "this run edited that file", shared by the live tally and the replay: a read of
|
|
267
|
-
// a file the run edits is reading
|
|
267
|
+
// a file the run edits is reading `delegation.md` keeps in the main line, so it is never counted — whether the read
|
|
268
268
|
// came before the edit or after it. A failed edit wrote nothing and so exempts nothing, and a file
|
|
269
269
|
// edited outside the window is unknown rather than assumed: the set is only ever as wide as the tail
|
|
270
270
|
// the hook read, which is the bound joshuafolkken/kit#1764 already holds the accumulation to.
|
|
@@ -444,7 +444,7 @@ function is_at_search_threshold(search_count: number): boolean {
|
|
|
444
444
|
//
|
|
445
445
|
// **A delegated unit is not re-armed here because it is not refused at all** (joshuafolkken/kit#1840).
|
|
446
446
|
// The hook counts a unit's reading against the unit (joshuafolkken/kit#1424), and the refusal asks for
|
|
447
|
-
// a dispatch — but a unit is already where
|
|
447
|
+
// a dispatch — but a unit is already where `delegation.md` sends the reading, and a read-only one (`Explore`,
|
|
448
448
|
// `Plan`) has no `Agent` tool to dispatch with. It was formerly degraded to a single first refusal,
|
|
449
449
|
// which still asked for an action the unit could not take; now the unit short circuit in `should_block`
|
|
450
450
|
// turns it off entirely, so this function is the parent's alone and needs no delegation condition.
|
|
@@ -59,22 +59,35 @@ function resolve_git_directory(repository_path: string): string | undefined {
|
|
|
59
59
|
}
|
|
60
60
|
}
|
|
61
61
|
|
|
62
|
-
// The
|
|
63
|
-
|
|
64
|
-
// fail a command whose contract is to report what it could find.
|
|
65
|
-
function read_origin_url(repository_path: string): string | undefined {
|
|
62
|
+
// The work tree's git config text, or nothing when it is unreadable or not a work tree.
|
|
63
|
+
function read_config(repository_path: string): string | undefined {
|
|
66
64
|
const git_directory = resolve_git_directory(repository_path)
|
|
67
65
|
if (git_directory === undefined) return undefined
|
|
68
66
|
|
|
69
67
|
try {
|
|
70
|
-
return
|
|
71
|
-
readFileSync(path.join(git_directory, GIT_CONFIG), 'utf8'),
|
|
72
|
-
)
|
|
68
|
+
return readFileSync(path.join(git_directory, GIT_CONFIG), 'utf8')
|
|
73
69
|
} catch {
|
|
74
70
|
return undefined
|
|
75
71
|
}
|
|
76
72
|
}
|
|
77
73
|
|
|
74
|
+
// The `origin` URL a work tree declares, or nothing when it has no `origin`, no readable config, or
|
|
75
|
+
// is not a work tree at all. Every failure reads as "no remote" so one unreadable sibling cannot
|
|
76
|
+
// fail a command whose contract is to report what it could find.
|
|
77
|
+
function read_origin_url(repository_path: string): string | undefined {
|
|
78
|
+
const config = read_config(repository_path)
|
|
79
|
+
|
|
80
|
+
return config === undefined ? undefined : repo_origin.parse_origin_from_config(config)
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
// The `url` of every remote the work tree declares, with the same "unreadable reads as none" contract
|
|
84
|
+
// as `read_origin_url`.
|
|
85
|
+
function read_remote_urls(repository_path: string): Array<string> {
|
|
86
|
+
const config = read_config(repository_path)
|
|
87
|
+
|
|
88
|
+
return config === undefined ? [] : repo_origin.parse_remote_urls_from_config(config)
|
|
89
|
+
}
|
|
90
|
+
|
|
78
91
|
// Every immediate child directory of `parent`, in name order. An unreadable parent yields nothing
|
|
79
92
|
// rather than throwing: `josh doctor` reports what it could find, and a permission error on the
|
|
80
93
|
// parent is not a broken install.
|
|
@@ -158,6 +171,7 @@ const repo_discovery = {
|
|
|
158
171
|
OVERRIDE_ENV_KEY,
|
|
159
172
|
main_worktree,
|
|
160
173
|
read_origin_url,
|
|
174
|
+
read_remote_urls,
|
|
161
175
|
resolve_current_owner,
|
|
162
176
|
scan_siblings,
|
|
163
177
|
discover_repositories,
|
|
@@ -26,6 +26,7 @@ const REPO_PATH_SEGMENTS = 2
|
|
|
26
26
|
const NOT_FOUND = -1
|
|
27
27
|
const SECTION_PREFIX = '['
|
|
28
28
|
const ORIGIN_SECTION = /^\[remote\s+"origin"\]$/u
|
|
29
|
+
const REMOTE_SECTION = /^\[remote\s+"[^"]+"\]$/u
|
|
29
30
|
const URL_KEY = 'url'
|
|
30
31
|
|
|
31
32
|
// Whether a URL's hostname is GitHub. Exact, with no alias spelling accepted: an alias is a name
|
|
@@ -132,6 +133,30 @@ function parse_origin_from_config(content: string): string | undefined {
|
|
|
132
133
|
.find((url) => url !== undefined)
|
|
133
134
|
}
|
|
134
135
|
|
|
136
|
+
// The config's lines grouped by section, each group led by its header. Lines before the first header
|
|
137
|
+
// belong to no section and are dropped.
|
|
138
|
+
function section_groups(lines: ReadonlyArray<string>): Array<Array<string>> {
|
|
139
|
+
const groups: Array<Array<string>> = []
|
|
140
|
+
|
|
141
|
+
for (const line of lines) {
|
|
142
|
+
if (is_section_header(line)) groups.push([line])
|
|
143
|
+
else groups.at(-1)?.push(line)
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
return groups
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
// The `url` of every `[remote "…"]` section, in config order — the candidates `gh` picks its base
|
|
150
|
+
// repository from when it expands the `{owner}` placeholder (joshuafolkken/kit#3188).
|
|
151
|
+
function parse_remote_urls_from_config(content: string): Array<string> {
|
|
152
|
+
const lines = content.split('\n').map((line) => line.trim())
|
|
153
|
+
|
|
154
|
+
return section_groups(lines)
|
|
155
|
+
.filter((group) => REMOTE_SECTION.test(group[0] ?? ''))
|
|
156
|
+
.flatMap((group) => group.map((line) => entry_value(line, URL_KEY)))
|
|
157
|
+
.filter((url) => url !== undefined)
|
|
158
|
+
}
|
|
159
|
+
|
|
135
160
|
// The `owner/repo` key a discovery map is keyed by. Lowercased, because GitHub resolves owner and
|
|
136
161
|
// repository names case-insensitively: two remotes spelling the same repository differently are the
|
|
137
162
|
// same repository, and keying by the spelling would put both in the map — where an override, which
|
|
@@ -146,6 +171,7 @@ const repo_origin = {
|
|
|
146
171
|
is_github_ssh_host,
|
|
147
172
|
parse_origin_from_config,
|
|
148
173
|
parse_origin_url,
|
|
174
|
+
parse_remote_urls_from_config,
|
|
149
175
|
format_identity,
|
|
150
176
|
}
|
|
151
177
|
|
|
@@ -16,6 +16,8 @@ type Party = 'first-party' | 'third-party' | 'unknown'
|
|
|
16
16
|
const FIRST_PARTY: Party = 'first-party'
|
|
17
17
|
const THIRD_PARTY: Party = 'third-party'
|
|
18
18
|
const UNKNOWN: Party = 'unknown'
|
|
19
|
+
const GH_REPO_ENV_KEY = 'GH_REPO'
|
|
20
|
+
const GH_REPO_PATH_SEGMENTS = 2
|
|
19
21
|
|
|
20
22
|
// `unknown` is not `third-party`. An owner that cannot be read — no `origin`, an unreadable config —
|
|
21
23
|
// is a "cannot decide", and reading it as third-party would refuse first-party writes whenever the
|
|
@@ -40,7 +42,46 @@ function current_owner(repository_path: string = process.cwd()): string | undefi
|
|
|
40
42
|
return repo_discovery.resolve_current_owner(repository_path)
|
|
41
43
|
}
|
|
42
44
|
|
|
43
|
-
|
|
45
|
+
// The owner `GH_REPO` names, read from its last two segments so an optional host prefix is skipped.
|
|
46
|
+
function gh_repo_owners(gh_repo: string): Array<string | undefined> {
|
|
47
|
+
return [target_owner(gh_repo.split('/').slice(-GH_REPO_PATH_SEGMENTS).join('/'))]
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
// The owner of every remote the work tree declares. A remote whose owner cannot be read stays in the
|
|
51
|
+
// set as `undefined` rather than being dropped: an SSH host alias (`git@github-work:o/r.git`) is one
|
|
52
|
+
// `gh` translates to github.com and may pick, so dropping it would hide the owner `gh` writes to.
|
|
53
|
+
function remote_owners(repository_path: string): Array<string | undefined> {
|
|
54
|
+
return repo_discovery
|
|
55
|
+
.read_remote_urls(repository_path)
|
|
56
|
+
.map((url) => repo_origin.parse_origin_url(url)?.owner)
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
// Every owner `gh` could expand the `{owner}` placeholder to (joshuafolkken/kit#3188). `GH_REPO`
|
|
60
|
+
// (`[HOST/]OWNER/REPO`) overrides the remotes outright; otherwise `gh` picks its base repository among
|
|
61
|
+
// the GitHub remotes — `upstream` ahead of `origin`, or a `gh repo set-default` choice — so in a fork
|
|
62
|
+
// checkout it can expand to the upstream owner. Answering the whole candidate set rather than
|
|
63
|
+
// re-implementing that pick keeps the caller safe whichever remote `gh` chooses. An `undefined` entry
|
|
64
|
+
// is a candidate whose owner could not be read, which the caller must not assume is first-party.
|
|
65
|
+
function placeholder_owners(
|
|
66
|
+
repository_path: string = process.cwd(),
|
|
67
|
+
environment: NodeJS.ProcessEnv = process.env,
|
|
68
|
+
): Array<string | undefined> {
|
|
69
|
+
const gh_repo = environment[GH_REPO_ENV_KEY]
|
|
70
|
+
|
|
71
|
+
return gh_repo === undefined || gh_repo === ''
|
|
72
|
+
? remote_owners(repository_path)
|
|
73
|
+
: gh_repo_owners(gh_repo)
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
const repo_party = {
|
|
77
|
+
FIRST_PARTY,
|
|
78
|
+
THIRD_PARTY,
|
|
79
|
+
UNKNOWN,
|
|
80
|
+
classify,
|
|
81
|
+
current_owner,
|
|
82
|
+
placeholder_owners,
|
|
83
|
+
target_owner,
|
|
84
|
+
}
|
|
44
85
|
|
|
45
86
|
export type { Party }
|
|
46
87
|
export { repo_party }
|
|
@@ -8,25 +8,19 @@
|
|
|
8
8
|
// (`lane-child-read-set.ts`) — through the shared `read-set-trim`.
|
|
9
9
|
//
|
|
10
10
|
// **This is a different set from the worker's.** The parent keeps §0 (its session-cut / resume
|
|
11
|
-
// paragraph is the parent's own),
|
|
12
|
-
//
|
|
13
|
-
// parent files prerequisites and observations for the batch) — all of which the worker drops. What the
|
|
14
|
-
// parent drops is the implementer-only set below. **The parent owns every point-of-use document**, so
|
|
15
|
-
// unlike the worker it drops none of them: the trim is the `SKILL.md` sections alone.
|
|
11
|
+
// paragraph is the parent's own), which the worker drops. **The parent owns every point-of-use
|
|
12
|
+
// document**, so unlike the worker it drops none of them: the trim is the `SKILL.md` sections alone.
|
|
16
13
|
|
|
17
14
|
import type { ReadSetCost } from './entry-read-set'
|
|
18
15
|
import { read_set_trim } from './read-set-trim'
|
|
19
16
|
|
|
20
17
|
const BACKLOGRUN = 'backlogrun'
|
|
21
18
|
|
|
22
|
-
// **The `SKILL.md` sections the `backlogrun` parent never uses.**
|
|
23
|
-
//
|
|
24
|
-
//
|
|
25
|
-
//
|
|
19
|
+
// **The `SKILL.md` sections the `backlogrun` parent never uses.** It edits no rule mid-run (§3, read
|
|
20
|
+
// only on an editing turn). The implementer-only rules it used to skip — the `into` target, the
|
|
21
|
+
// working-tree hold, the Issue's comments — are single rows of §2's table since joshuafolkken/kit#3174,
|
|
22
|
+
// so there is no section of them left to trim.
|
|
26
23
|
const UNUSED_SKILL_SECTIONS: ReadonlyArray<string> = [
|
|
27
|
-
'2a. The `into <target>` suffix — where the new Issue lands',
|
|
28
|
-
'2f. The working-tree hold — one run per tree',
|
|
29
|
-
"2g. An Issue's comments are part of the Issue",
|
|
30
24
|
'3. What stays resident, and what is read from here',
|
|
31
25
|
]
|
|
32
26
|
|
|
@@ -34,10 +28,15 @@ const UNUSED_SKILL_SECTIONS: ReadonlyArray<string> = [
|
|
|
34
28
|
// the park — so it drops none. Named empty so the shared trim reads the same shape as the worker's.
|
|
35
29
|
const SKIPPED_POINT_OF_USE: ReadonlySet<string> = new Set<string>()
|
|
36
30
|
|
|
37
|
-
// **The parent runs the end-of-run retrospective** (`
|
|
31
|
+
// **The parent runs the end-of-run retrospective** (`retrospective.md`) when `run:step` prints it, but its
|
|
38
32
|
// manifest names `retrospective.md` only through `backlogrun-steps.md` — one hop further than the
|
|
39
|
-
// derivation follows — so the parent's reach names it (joshuafolkken/kit#3078).
|
|
40
|
-
|
|
33
|
+
// derivation follows — so the parent's reach names it (joshuafolkken/kit#3078). **It polls a
|
|
34
|
+
// delegated unit that went silent** through `backlogrun-recovery.md`, named only from
|
|
35
|
+
// `backlogrun-child.md`, so the reach names that too (joshuafolkken/kit#3175).
|
|
36
|
+
const REACHED_POINT_OF_USE: ReadonlySet<string> = new Set([
|
|
37
|
+
'retrospective.md',
|
|
38
|
+
'backlogrun-recovery.md',
|
|
39
|
+
])
|
|
41
40
|
|
|
42
41
|
function costed(root: string): ReadSetCost {
|
|
43
42
|
return read_set_trim.costed(root, {
|