@joshuafolkken/kit 1.1128.0 → 1.1166.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/agents/investigator.md +17 -0
- package/.claude/settings.json +5 -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 +11 -60
- package/.claude/skills/workflow-commands/backlogrun-park.md +10 -8
- package/.claude/skills/workflow-commands/backlogrun-progress.md +19 -175
- package/.claude/skills/workflow-commands/backlogrun-recovery.md +155 -0
- package/.claude/skills/workflow-commands/backlogrun-steps.md +64 -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 +7 -3
- 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 +33 -93
- 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/AGENTS.md +1 -6
- package/CLAUDE.md +58 -120
- package/GEMINI.md +1 -6
- package/SECURITY.md +1 -0
- package/dist/CLAUDE.md +58 -120
- package/dist/hooks/batch-guard.js +4 -4
- package/dist/hooks/{chunk-MKVGBQPA.js → chunk-5KTCMCOT.js} +14 -1
- package/dist/hooks/{chunk-NXYMNPUQ.js → chunk-A6P7PN7P.js} +59 -21
- package/dist/hooks/{chunk-77J7IEQM.js → chunk-ANJ2WGFD.js} +1 -1
- package/dist/hooks/{chunk-4JZDP5X5.js → chunk-DYAVAM6S.js} +572 -127
- package/dist/hooks/{chunk-7HVB7SZG.js → chunk-ENA7P5FB.js} +4 -4
- package/dist/hooks/{chunk-M7QKEKWO.js → chunk-GU5MATVE.js} +214 -85
- package/dist/hooks/chunk-GYNGP7HI.js +56 -0
- package/dist/hooks/{chunk-5SD2YM7Z.js → chunk-URJA6XY4.js} +4 -4
- package/dist/hooks/{chunk-XYUIQOW3.js → chunk-V2BTOZHT.js} +6 -4
- package/dist/hooks/{chunk-AIOIOMPX.js → chunk-ZDZ555YH.js} +225 -78
- 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 +186 -184
- package/dist/hooks/investigation-guard.js +5 -5
- package/dist/hooks/pretool-guard.js +11 -10
- package/dist/hooks/stop-guard.js +5 -5
- package/dist/josh.js +33 -3
- package/dist/version/index.js +5 -1
- package/package.json +7 -4
- 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-offer.ts +6 -6
- package/scripts/behavior/behavior-rules.ts +11 -4
- package/scripts/delegation/delegation-policy.ts +9 -1
- package/scripts/delegation/investigation-guard.ts +1 -1
- package/scripts/delegation/investigation-reads.ts +109 -23
- 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/capped-print.ts +74 -0
- package/scripts/document/document-byte-budget.ts +34 -20
- package/scripts/document/document-section-cli.ts +4 -3
- 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/gate/scoped-green.ts +25 -14
- package/scripts/gh/git-gh-api-path.ts +7 -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/claude-kit-only-settings.ts +29 -0
- package/scripts/init/hook-command-rewrite.ts +18 -35
- package/scripts/init/hook-launch.ts +21 -32
- package/scripts/init/init-copy-content.ts +8 -2
- package/scripts/init/initial-commit.ts +24 -0
- package/scripts/init/start-exec.ts +16 -3
- package/scripts/init/start-steps.ts +8 -12
- 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 +4 -3
- package/scripts/issue/issue-read.ts +2 -2
- package/scripts/issue/issue-report-failures.ts +6 -2
- package/scripts/issue/issue-scout-cli.ts +2 -2
- package/scripts/issue/issue-state-cli.ts +2 -2
- 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 +10 -0
- package/scripts/josh/josh-commands-issue.ts +2 -2
- package/scripts/josh/josh-commands-lane.ts +1 -1
- package/scripts/josh/josh-commands-project.ts +7 -0
- package/scripts/lane/lane-await.ts +46 -2
- package/scripts/lane/lane-cli.ts +11 -13
- package/scripts/lines/bytes-command.ts +29 -6
- package/scripts/observations/observation-ledger-line.ts +1 -1
- package/scripts/release/github-release-environment.ts +1 -0
- package/scripts/release/github-release.ts +103 -17
- 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/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-cli.ts +3 -1
- package/scripts/run/run-carry-conversation.ts +94 -0
- package/scripts/run/run-carry-reclaim.ts +37 -0
- package/scripts/run/run-carry.ts +21 -5
- package/scripts/run/run-cut-handoff.ts +21 -1
- 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 +12 -4
- 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-preflight.ts +34 -3
- package/scripts/run/run-ship-review-steps.ts +1 -1
- package/scripts/run/run-ship-review.ts +26 -5
- package/scripts/run/run-step-cli.ts +20 -7
- package/scripts/run/run-wake-driver.ts +24 -1
- package/scripts/run/run-wake-session.ts +2 -1
- package/scripts/test/test-declared-logic.ts +1 -1
- package/scripts/test/test-related.ts +1 -6
- package/scripts/test/test-unit-guard.ts +21 -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-spans.ts +4 -0
- package/scripts/version/npm-registry.ts +11 -1
- package/templates/workflows/ci.yml +9 -1
- package/templates/workflows/github-release.yml +13 -25
- package/.claude/skills/workflow-commands/followup-reference.md +0 -118
- package/dist/hooks/chunk-QVLT52OV.js +0 -464
|
@@ -55,10 +55,23 @@ const CONTENT_READ_LABELS: ReadonlySet<string> = new Set(
|
|
|
55
55
|
CONTENT_READ_COMMANDS.map((command) => time_shell.bash_label(command)),
|
|
56
56
|
)
|
|
57
57
|
|
|
58
|
+
// **The searches carry no file text, so they are counted in turns rather than files**
|
|
59
|
+
// (joshuafolkken/kit#3139). The file count above never saw a `grep`, yet searches were the commonest
|
|
60
|
+
// exploration turn a lane took — 457 of them in one day, each re-reading the main line's whole context.
|
|
61
|
+
// What a search costs is the turn, so a turn is the unit: parallel searches in one turn count once.
|
|
62
|
+
const SEARCH_COMMANDS: ReadonlyArray<string> = ['fd', 'find', 'grep', 'rg']
|
|
63
|
+
const SEARCH_LABELS: ReadonlySet<string> = new Set(
|
|
64
|
+
SEARCH_COMMANDS.map((command) => time_shell.bash_label(command)),
|
|
65
|
+
)
|
|
66
|
+
|
|
67
|
+
// The unit the refusal sends the reading to (joshuafolkken/kit#3139): `.claude/agents/investigator.md`,
|
|
68
|
+
// the parent's model at a lowered effort. A consumer receives it through the plugin as `kit:investigator`.
|
|
69
|
+
const INVESTIGATOR_AGENT = 'investigator'
|
|
70
|
+
|
|
58
71
|
// What the refusal tells the model. It has to name the count, the command and the return shape,
|
|
59
72
|
// because the deny reason is the only text that reaches the model — a run that is refused and told
|
|
60
73
|
// nothing reads it as a broken tool.
|
|
61
|
-
const REASON = `⛔ investigation: ${String(delegation_policy.INVESTIGATION_FILE_THRESHOLD)} files read and not edited since the last delegated unit, so the reading from here goes to a unit of its own. Ask \`pnpm josh delegate investigation\`, then
|
|
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".`
|
|
62
75
|
|
|
63
76
|
interface ReadTally {
|
|
64
77
|
// The files read and not since edited, resolved so the two ways a target reaches here compare.
|
|
@@ -74,14 +87,18 @@ interface ReadTally {
|
|
|
74
87
|
// The first instant the window covers. A refusal recorded before it cannot be checked against a
|
|
75
88
|
// delegation any more, because the delegation that would re-arm it has scrolled out.
|
|
76
89
|
window_start_ms: number
|
|
90
|
+
// The search turns since the last delegation or successful write, and how many of them came after
|
|
91
|
+
// the instant asked about — the search-side `pending` / `pending_since` (joshuafolkken/kit#3139).
|
|
92
|
+
searches: number
|
|
93
|
+
searches_since: number
|
|
77
94
|
// The files this run has successfully edited within the window, resolved (joshuafolkken/kit#1840).
|
|
78
|
-
// 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
|
|
79
96
|
// it is excluded from the count and from a call's projected growth rather than counted as fresh
|
|
80
97
|
// investigation.
|
|
81
98
|
edited: ReadonlySet<string>
|
|
82
99
|
}
|
|
83
100
|
|
|
84
|
-
// **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.**
|
|
85
102
|
// #1426's rule is about reading to find out how the subject works; `CLAUDE.md`, a `prompts/` topic and
|
|
86
103
|
// a skill file are read to find out what to *do*, they are read by nearly every run, and a unit cannot
|
|
87
104
|
// be sent to read them on the main line's behalf. #1441's own measurement excluded 5 of them by hand
|
|
@@ -247,7 +264,7 @@ function is_failed_outcome(span: Span): boolean {
|
|
|
247
264
|
|
|
248
265
|
// **Every file the run successfully wrote within the window, resolved** (joshuafolkken/kit#1840). It
|
|
249
266
|
// is the one source of "this run edited that file", shared by the live tally and the replay: a read of
|
|
250
|
-
// 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
|
|
251
268
|
// came before the edit or after it. A failed edit wrote nothing and so exempts nothing, and a file
|
|
252
269
|
// edited outside the window is unknown rather than assumed: the set is only ever as wide as the tail
|
|
253
270
|
// the hook read, which is the bound joshuafolkken/kit#1764 already holds the accumulation to.
|
|
@@ -279,18 +296,65 @@ function add_all(
|
|
|
279
296
|
// the edit's own span, so a read issued *after* the edit was added and stayed. `sed -i`, `MultiEdit`
|
|
280
297
|
// and `NotebookEdit` need no special case any more — each is a write, so its target is in `edited` and
|
|
281
298
|
// never enters the read count.
|
|
282
|
-
|
|
299
|
+
interface Accumulation {
|
|
300
|
+
// Read files, keyed by resolved path, valued by the instant each entered.
|
|
301
|
+
pending: Map<string, number>
|
|
302
|
+
// Search turns, keyed by message id so a turn's parallel searches count once, valued the same way.
|
|
303
|
+
searches: Map<string, number>
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
function is_read_only_search(span: Span): boolean {
|
|
307
|
+
return SEARCH_LABELS.has(span.label) && span.is_bundleable && !span.may_write
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
// **A span with no message id belongs to no known turn, so it is a turn of its own** — the reading every
|
|
311
|
+
// other turn counter gives `NO_MESSAGE_ID` (`time-round-trips.ts`, `time-bundles.ts`). Keyed by the
|
|
312
|
+
// id, every untagged search would collapse into one entry and the count would never reach its threshold.
|
|
313
|
+
function search_turn_key(searches: ReadonlyMap<string, number>, span: Span): string {
|
|
314
|
+
if (span.message_id !== time_spans.NO_MESSAGE_ID) return span.message_id
|
|
315
|
+
|
|
316
|
+
return `${time_spans.NO_MESSAGE_ID}#${String(searches.size)}`
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
// **A successful write ends the streak.** The measurement behind the threshold counted searches since
|
|
320
|
+
// the last edit or delegation: a run that has started writing is implementing, and the searches that
|
|
321
|
+
// follow locate the next edit rather than continue an investigation.
|
|
322
|
+
//
|
|
323
|
+
// **Every read-only search counts, whatever it names** (joshuafolkken/kit#3139). The file count can
|
|
324
|
+
// exempt the run's own instructions because a read names each file it reads; a search does not — the
|
|
325
|
+
// targets drop a bare directory, so `grep -rn foo CLAUDE.md scripts` looked like a search of
|
|
326
|
+
// `CLAUDE.md` alone and an exemption let the whole sweep through. `is_bundleable` without `may_write`
|
|
327
|
+
// is the read-only test `is_search_call` asks at the call, so the transcript never counts a search the
|
|
328
|
+
// guard could not have refused.
|
|
329
|
+
function apply_search(searches: Map<string, number>, span: Span): void {
|
|
330
|
+
if (span.is_writing) {
|
|
331
|
+
searches.clear()
|
|
332
|
+
|
|
333
|
+
return
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
const turn = search_turn_key(searches, span)
|
|
337
|
+
|
|
338
|
+
if (is_read_only_search(span) && !searches.has(turn)) searches.set(turn, span.ended_ms)
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
function apply_span(accumulation: Accumulation, span: Span, edited: ReadonlySet<string>): void {
|
|
283
342
|
if (DELEGATION_TOOLS.has(span.label)) {
|
|
284
|
-
pending.clear()
|
|
343
|
+
accumulation.pending.clear()
|
|
344
|
+
accumulation.searches.clear()
|
|
285
345
|
|
|
286
346
|
return
|
|
287
347
|
}
|
|
288
348
|
|
|
289
|
-
if (
|
|
349
|
+
if (is_failed_outcome(span)) return
|
|
350
|
+
|
|
351
|
+
apply_search(accumulation.searches, span)
|
|
352
|
+
|
|
353
|
+
if (!is_content_read(span.label)) return
|
|
290
354
|
|
|
291
355
|
const fresh = subject_targets(span.targets).filter((target) => !edited.has(target))
|
|
292
356
|
|
|
293
|
-
add_all(pending, fresh, span.ended_ms)
|
|
357
|
+
add_all(accumulation.pending, fresh, span.ended_ms)
|
|
294
358
|
}
|
|
295
359
|
|
|
296
360
|
function last_delegation_ms(spans: ReadonlyArray<Span>): number {
|
|
@@ -307,15 +371,17 @@ function pending_after(pending: ReadonlyMap<string, number>, since_ms: number):
|
|
|
307
371
|
function tally_of(text: string, since_ms: number = hook_decision.NEVER_MS): ReadTally {
|
|
308
372
|
const { spans, started_ms } = time_spans.parse_timeline(text)
|
|
309
373
|
const edited = edited_files(spans)
|
|
310
|
-
const
|
|
374
|
+
const accumulation: Accumulation = { pending: new Map(), searches: new Map() }
|
|
311
375
|
|
|
312
|
-
for (const span of spans) apply_span(
|
|
376
|
+
for (const span of spans) apply_span(accumulation, span, edited)
|
|
313
377
|
|
|
314
378
|
return {
|
|
315
|
-
pending: [...pending.keys()],
|
|
316
|
-
pending_since: pending_after(pending, since_ms),
|
|
379
|
+
pending: [...accumulation.pending.keys()],
|
|
380
|
+
pending_since: pending_after(accumulation.pending, since_ms),
|
|
317
381
|
reset_ms: last_delegation_ms(spans),
|
|
318
382
|
window_start_ms: started_ms,
|
|
383
|
+
searches: accumulation.searches.size,
|
|
384
|
+
searches_since: pending_after(accumulation.searches, since_ms),
|
|
319
385
|
edited,
|
|
320
386
|
}
|
|
321
387
|
}
|
|
@@ -347,6 +413,11 @@ function is_at_threshold(pending_count: number): boolean {
|
|
|
347
413
|
return pending_count + 1 >= delegation_policy.INVESTIGATION_FILE_THRESHOLD
|
|
348
414
|
}
|
|
349
415
|
|
|
416
|
+
// The search turn that takes the count up to its threshold is the boundary, as above.
|
|
417
|
+
function is_at_search_threshold(search_count: number): boolean {
|
|
418
|
+
return search_count + 1 >= delegation_policy.INVESTIGATION_SEARCH_TURN_THRESHOLD
|
|
419
|
+
}
|
|
420
|
+
|
|
350
421
|
// **One refusal per accumulation, re-armed by a delegation and by nothing else.** A run that reads on
|
|
351
422
|
// regardless is not refused again, so a false positive — three files the run was about to edit — costs
|
|
352
423
|
// one round trip rather than wedging the run. And a run that *does* delegate is refused again as soon
|
|
@@ -373,7 +444,7 @@ function is_at_threshold(pending_count: number): boolean {
|
|
|
373
444
|
//
|
|
374
445
|
// **A delegated unit is not re-armed here because it is not refused at all** (joshuafolkken/kit#1840).
|
|
375
446
|
// The hook counts a unit's reading against the unit (joshuafolkken/kit#1424), and the refusal asks for
|
|
376
|
-
// 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`,
|
|
377
448
|
// `Plan`) has no `Agent` tool to dispatch with. It was formerly degraded to a single first refusal,
|
|
378
449
|
// which still asked for an action the unit could not take; now the unit short circuit in `should_block`
|
|
379
450
|
// turns it off entirely, so this function is the parent's alone and needs no delegation condition.
|
|
@@ -386,7 +457,7 @@ function is_rearmed(tally: ReadTally, refused_at_ms: number): boolean {
|
|
|
386
457
|
return true
|
|
387
458
|
}
|
|
388
459
|
|
|
389
|
-
return is_at_threshold(tally.pending_since)
|
|
460
|
+
return is_at_threshold(tally.pending_since) || is_at_search_threshold(tally.searches_since)
|
|
390
461
|
}
|
|
391
462
|
|
|
392
463
|
// A transcript under a session's `subagents/` directory is a delegated unit's. The segment is
|
|
@@ -403,10 +474,19 @@ function is_unit_run(run: GuardRun | undefined): boolean {
|
|
|
403
474
|
return run !== undefined && is_unit_transcript(run.transcript)
|
|
404
475
|
}
|
|
405
476
|
|
|
406
|
-
function
|
|
477
|
+
function is_read_only_command(call: GuardedCall, labels: ReadonlySet<string>): boolean {
|
|
478
|
+
if (call.name !== cost_blocks.BASH_TOOL) return false
|
|
479
|
+
|
|
407
480
|
const label = time_shell.bash_label(time_shell.bash_command(call.input))
|
|
408
481
|
|
|
409
|
-
return
|
|
482
|
+
return labels.has(label) && time_batch_guard.is_read_only_call(call)
|
|
483
|
+
}
|
|
484
|
+
|
|
485
|
+
// **A search is refusable without a subject target**, unlike a read: `grep -rn foo scripts` names a
|
|
486
|
+
// bare directory, which `call_facts` deliberately does not take as a target, and such a sweep is exactly
|
|
487
|
+
// the search this count exists for.
|
|
488
|
+
function is_search_call(call: GuardedCall): boolean {
|
|
489
|
+
return is_read_only_command(call, SEARCH_LABELS)
|
|
410
490
|
}
|
|
411
491
|
|
|
412
492
|
// **A `Read` is always safe to refuse; a `Bash` line is refused only where it writes nothing.** This
|
|
@@ -427,10 +507,20 @@ function is_refusable_command(call: GuardedCall): boolean {
|
|
|
427
507
|
// rule being enforced — and a call whose targets could not be read at all, which cannot move the count
|
|
428
508
|
// and so has nothing to be refused for.
|
|
429
509
|
function is_refusable_call(call: GuardedCall): boolean {
|
|
510
|
+
if (is_search_call(call)) return true
|
|
430
511
|
if (call_targets(call).length === 0) return false
|
|
431
512
|
if (READ_TOOLS.has(call.name)) return true
|
|
432
513
|
|
|
433
|
-
return call
|
|
514
|
+
return is_read_only_command(call, CONTENT_READ_LABELS)
|
|
515
|
+
}
|
|
516
|
+
|
|
517
|
+
// A search call grows the search count by its own turn; a read grows the file count only by a file the
|
|
518
|
+
// count does not already hold.
|
|
519
|
+
function is_growing_past_threshold(tally: ReadTally, call: GuardedCall): boolean {
|
|
520
|
+
if (is_search_call(call)) return is_at_search_threshold(tally.searches)
|
|
521
|
+
if (!is_at_threshold(tally.pending.length)) return false
|
|
522
|
+
|
|
523
|
+
return projected_count(tally.pending, call, tally.edited) > tally.pending.length
|
|
434
524
|
}
|
|
435
525
|
|
|
436
526
|
function should_block(
|
|
@@ -446,15 +536,11 @@ function should_block(
|
|
|
446
536
|
|
|
447
537
|
const tally = tally_of(text, refused_at_ms)
|
|
448
538
|
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
return (
|
|
452
|
-
projected_count(tally.pending, call, tally.edited) > tally.pending.length &&
|
|
453
|
-
is_rearmed(tally, refused_at_ms)
|
|
454
|
-
)
|
|
539
|
+
return is_growing_past_threshold(tally, call) && is_rearmed(tally, refused_at_ms)
|
|
455
540
|
}
|
|
456
541
|
|
|
457
542
|
const investigation_reads = {
|
|
543
|
+
INVESTIGATOR_AGENT,
|
|
458
544
|
READ_TOOLS,
|
|
459
545
|
REASON,
|
|
460
546
|
is_at_threshold,
|
|
@@ -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, {
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
import { mkdtempSync, writeFileSync } from 'node:fs'
|
|
2
|
+
import path from 'node:path'
|
|
3
|
+
import { PLATFORM_TEMP_ROOT } from '#scripts/josh/platform-temporary'
|
|
4
|
+
import { review_diff_parts } from '#scripts/review/review-diff-parts'
|
|
5
|
+
import { bash_output_cap_reader } from './bash-output-cap'
|
|
6
|
+
|
|
7
|
+
// Prints a command's output whole when it fits under the Bash output cap, and as part files when it
|
|
8
|
+
// does not (joshuafolkken/kit#3143). Past `BASH_MAX_OUTPUT_LENGTH` Claude Code saves the output and
|
|
9
|
+
// hands back a 2 KB preview, and the run spends a turn reading the saved file — 224 overflows and 132
|
|
10
|
+
// re-reads over one day's sessions, `doc:section` and `issue:read` the two most frequent `josh` sources.
|
|
11
|
+
//
|
|
12
|
+
// **The cut is #2963's, not a second one.** The parts come from `review_diff_parts.part_texts`, so each
|
|
13
|
+
// file — header included — fits under the cap and the bodies concatenate back to exactly the output.
|
|
14
|
+
// A reader opens them all with the Read tool in one turn, which is the turn the preview used to cost.
|
|
15
|
+
//
|
|
16
|
+
// **A fresh directory per call**, unlike the review's per-checkout one: a run fetches several sections
|
|
17
|
+
// in one turn, and a shared directory replaced on every call would delete one call's parts while
|
|
18
|
+
// another call's listing still names them.
|
|
19
|
+
|
|
20
|
+
const DIRECTORY_PREFIX = 'josh-capped-print-'
|
|
21
|
+
const FILE_MODE = 0o600
|
|
22
|
+
const PART_SUFFIX = '.md'
|
|
23
|
+
const LINE_BREAK = '\n'
|
|
24
|
+
// What else shares the Bash result with this output: the launcher line, a script banner, a few
|
|
25
|
+
// stderr lines. Measured at about 80 characters for `pnpm josh doc:section` in kit, so 512 holds the
|
|
26
|
+
// longest heading or issue list with room for a consumer's banner.
|
|
27
|
+
const HEADROOM_CHARS = 512
|
|
28
|
+
|
|
29
|
+
function write_parts(label: string, text: string, cap: number): Array<string> {
|
|
30
|
+
const directory = mkdtempSync(path.join(PLATFORM_TEMP_ROOT, DIRECTORY_PREFIX))
|
|
31
|
+
|
|
32
|
+
return review_diff_parts.part_texts(label, text, cap).map((part, index) => {
|
|
33
|
+
const file = path.join(directory, `part-${String(index + 1)}${PART_SUFFIX}`)
|
|
34
|
+
|
|
35
|
+
writeFileSync(file, part, { flag: 'wx', mode: FILE_MODE })
|
|
36
|
+
|
|
37
|
+
return file
|
|
38
|
+
})
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function part_count(count: number): string {
|
|
42
|
+
return `written as ${String(count)} part file${count === 1 ? '' : 's'} under it`
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function listing(text: string, cap: number, files: ReadonlyArray<string>): string {
|
|
46
|
+
const figures = `${text.length.toLocaleString('en-US')} > ${cap.toLocaleString('en-US')} characters`
|
|
47
|
+
const lead = `Over the Bash cap (${figures}) — ${part_count(files.length)}, together exactly the output. Read every part with the Read tool, in one turn, not cat:`
|
|
48
|
+
|
|
49
|
+
return [lead, ...files.map((file) => ` ${file}`)].join(LINE_BREAK)
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
// **The budget is the cap less a headroom, not the cap.** The Bash result carries more than this
|
|
53
|
+
// output — `pnpm josh`'s own launcher line, a consumer's script banner, the stderr lines naming a
|
|
54
|
+
// number that did not resolve — so a text measured against the bare cap would overflow by exactly
|
|
55
|
+
// those lines, and cost the re-read this module exists to remove.
|
|
56
|
+
function output_budget(root: string): number {
|
|
57
|
+
return Math.max(1, bash_output_cap_reader.bash_output_cap(root) - HEADROOM_CHARS)
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
function capped_print(text: string, label: string, root: string = process.cwd()): void {
|
|
61
|
+
const budget = output_budget(root)
|
|
62
|
+
|
|
63
|
+
if (text.length < budget) {
|
|
64
|
+
console.info(text)
|
|
65
|
+
|
|
66
|
+
return
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
console.info(listing(text, budget, write_parts(label, text, budget)))
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
const capped_output = { HEADROOM_CHARS, capped_print }
|
|
73
|
+
|
|
74
|
+
export { capped_output }
|
|
@@ -65,15 +65,19 @@ function block_ceiling(size: number): number {
|
|
|
65
65
|
// unreached file, so the definition cannot rot as documents move on or off the execution path.
|
|
66
66
|
const DOCUMENT_BYTE_BUDGET: ReadonlyArray<DocumentBudget> = [
|
|
67
67
|
{ path: '.claude/skills/dependency-update/SKILL.md', bytes: 8192 },
|
|
68
|
-
|
|
68
|
+
// joshuafolkken/kit#3173 split the waves and the `epic:bundle` detail into lazily read references.
|
|
69
|
+
{ path: '.claude/skills/epic-commands/SKILL.md', bytes: 20_480 },
|
|
70
|
+
{ path: '.claude/skills/epic-commands/epic-bundle.md', bytes: 12_288 },
|
|
71
|
+
{ path: '.claude/skills/epic-commands/execution-waves.md', bytes: 12_288 },
|
|
69
72
|
{ path: '.claude/skills/verify-ui/SKILL.md', bytes: 8192 },
|
|
70
|
-
{ path: '.claude/skills/workflow-commands/followup-reference.md', bytes: 16_384 },
|
|
71
73
|
{ path: '.claude/skills/workflow-commands/fullrun-steps.md', bytes: 8192 },
|
|
72
74
|
{ path: '.claude/skills/workflow-commands/into-target.md', bytes: 4096 },
|
|
73
75
|
{ path: '.claude/skills/workflow-commands/issue-comments.md', bytes: 8192 },
|
|
74
76
|
{ path: '.claude/skills/workflow-commands/issue-fold-existing.md', bytes: 4096 },
|
|
75
77
|
{ path: '.claude/skills/workflow-commands/needs-human-review.md', bytes: 8192 },
|
|
76
|
-
{ path: '.claude/skills/workflow-commands/observation-filing.md', bytes:
|
|
78
|
+
{ path: '.claude/skills/workflow-commands/observation-filing.md', bytes: 12_288 },
|
|
79
|
+
// joshuafolkken/kit#3176 moved the ledger out of `observation-filing.md`, read only when it appends.
|
|
80
|
+
{ path: '.claude/skills/workflow-commands/observation-ledger.md', bytes: 12_288 },
|
|
77
81
|
// `pre-gate-cut.md` left this per-document budget in joshuafolkken/kit#2289: it became a point-of-use
|
|
78
82
|
// document, so its bytes are now held by every entry's total read (`entry-read-budget.ts`) and the
|
|
79
83
|
// two budgets must not hold it twice — the reachability line moved it from `unreached` to
|
|
@@ -82,34 +86,44 @@ const DOCUMENT_BYTE_BUDGET: ReadonlyArray<DocumentBudget> = [
|
|
|
82
86
|
{ path: '.claude/skills/workflow-commands/target-repository.md', bytes: 8192 },
|
|
83
87
|
{ path: '.claude/skills/workflow-commands/working-tree-hold.md', bytes: 8192 },
|
|
84
88
|
// joshuafolkken/kit#2998 split the reference: the developer commands stay, the rest moved out.
|
|
85
|
-
|
|
89
|
+
// joshuafolkken/kit#3175 raised it: the supervisor, provider-table and answer-to-budget prose moved
|
|
90
|
+
// here out of `backlogrun-steps.md`, so a run reads it only when it asks about those commands.
|
|
91
|
+
{ path: 'docs/josh-commands-automation.md', bytes: 159_744 },
|
|
86
92
|
{ path: 'docs/josh-commands.md', bytes: 36_864 },
|
|
87
93
|
{ path: 'prompts/coding-standards.md', bytes: 8192 },
|
|
88
94
|
{ path: 'prompts/collaboration-workflow.md', bytes: 8192 },
|
|
89
95
|
{ path: 'prompts/collaboration-workflow/file-edits.md', bytes: 12_288 },
|
|
90
96
|
{ path: 'prompts/collaboration-workflow/gh-rest.md', bytes: 8192 },
|
|
91
97
|
{ path: 'prompts/collaboration-workflow/glossary.md', bytes: 4096 },
|
|
92
|
-
{ path: 'prompts/collaboration-workflow/issue-citation.md', bytes:
|
|
93
|
-
// #
|
|
94
|
-
{ path: 'prompts/collaboration-workflow/issue-template.md', bytes:
|
|
95
|
-
{ path: 'prompts/collaboration-workflow/operating-rules.md', bytes:
|
|
96
|
-
|
|
97
|
-
{ path: 'prompts/collaboration-workflow/
|
|
98
|
-
{ path: 'prompts/collaboration-workflow/
|
|
98
|
+
{ path: 'prompts/collaboration-workflow/issue-citation.md', bytes: 4096 },
|
|
99
|
+
// #3178 left the template and the judgement tables; the epic commands moved to `epic-commands`, the by-hand fallback stayed.
|
|
100
|
+
{ path: 'prompts/collaboration-workflow/issue-template.md', bytes: 8192 },
|
|
101
|
+
{ path: 'prompts/collaboration-workflow/operating-rules.md', bytes: 12_288 },
|
|
102
|
+
// #3177 moved the rationale to `docs/maintainers/output-bounds-rationale.md`.
|
|
103
|
+
{ path: 'prompts/collaboration-workflow/output-bounds.md', bytes: 4096 },
|
|
104
|
+
{ path: 'prompts/collaboration-workflow/overview.md', bytes: 4096 },
|
|
105
|
+
// #3178 replaced the review-chain restatement with a pointer to `chain-rule.md`.
|
|
106
|
+
{ path: 'prompts/collaboration-workflow/plan-comment.md', bytes: 8192 },
|
|
99
107
|
{ path: 'prompts/collaboration-workflow/principles.md', bytes: 12_288 },
|
|
100
108
|
{ path: 'prompts/collaboration-workflow/proposal-request.md', bytes: 4096 },
|
|
101
|
-
|
|
102
|
-
{ path: 'prompts/collaboration-workflow/
|
|
103
|
-
|
|
104
|
-
{ path: 'prompts/collaboration-workflow/
|
|
105
|
-
|
|
109
|
+
// #3178 dropped the restatements `report:lint` and `chain-rule.md` already carry.
|
|
110
|
+
{ path: 'prompts/collaboration-workflow/report-format.md', bytes: 16_384 },
|
|
111
|
+
// #3177 cut it to the four questions; the history and lists moved to `docs/maintainers/residency-rationale.md`.
|
|
112
|
+
{ path: 'prompts/collaboration-workflow/residency.md', bytes: 8192 },
|
|
113
|
+
// #3186 moved the hook wiring and history to `docs/maintainers/rule-delivery-rationale.md`;
|
|
114
|
+
// #3179 adds the `index-mutation` / `destructive-command` / `protected-file` entries.
|
|
115
|
+
{ path: 'prompts/collaboration-workflow/rule-delivery.md', bytes: 20_480 },
|
|
116
|
+
{ path: 'prompts/collaboration-workflow/shell-body.md', bytes: 8192 },
|
|
117
|
+
// #3177 moved the measurements and rejected mechanisms to `docs/maintainers/turn-batching-rationale.md`.
|
|
118
|
+
{ path: 'prompts/collaboration-workflow/turn-batching.md', bytes: 8192 },
|
|
106
119
|
{ path: 'prompts/collaboration-workflow/upstream-interrupt.md', bytes: 12_288 },
|
|
107
|
-
{ path: 'prompts/collaboration-workflow/wip-cap.md', bytes:
|
|
120
|
+
{ path: 'prompts/collaboration-workflow/wip-cap.md', bytes: 12_288 },
|
|
108
121
|
{ path: 'prompts/refactoring.md', bytes: 8192 },
|
|
109
|
-
|
|
110
|
-
{ path: 'prompts/review.md', bytes:
|
|
122
|
+
// #3180 moved the history and the lint-settled proofs to `docs/maintainers/`.
|
|
123
|
+
{ path: 'prompts/review-rubric.md', bytes: 16_384 },
|
|
124
|
+
{ path: 'prompts/review.md', bytes: 16_384 },
|
|
111
125
|
{ path: 'prompts/sonar-hotspot-handling.md', bytes: 8192 },
|
|
112
|
-
{ path: 'prompts/testing-guide.md', bytes:
|
|
126
|
+
{ path: 'prompts/testing-guide.md', bytes: 20_480 },
|
|
113
127
|
]
|
|
114
128
|
|
|
115
129
|
// The recorded ceiling for one path, or undefined when it carries no budget entry.
|
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
import { existsSync } from 'node:fs'
|
|
3
3
|
import path from 'node:path'
|
|
4
4
|
import { fileURLToPath } from 'node:url'
|
|
5
|
+
import { capped_output } from './capped-print'
|
|
5
6
|
import { document_section } from './document-section'
|
|
6
7
|
import { entry_read_set } from './entry-read-set'
|
|
7
8
|
|
|
@@ -56,12 +57,12 @@ function report_missing(markdown: string, heading: string): number {
|
|
|
56
57
|
return FAILURE_EXIT_CODE
|
|
57
58
|
}
|
|
58
59
|
|
|
59
|
-
function print_section(markdown: string, heading: string): number {
|
|
60
|
+
function print_section(markdown: string, heading: string, root: string): number {
|
|
60
61
|
const found = document_section.section(markdown, heading)
|
|
61
62
|
|
|
62
63
|
if (found === undefined) return report_missing(markdown, heading)
|
|
63
64
|
|
|
64
|
-
|
|
65
|
+
capped_output.capped_print(found.text, heading, root)
|
|
65
66
|
|
|
66
67
|
return 0
|
|
67
68
|
}
|
|
@@ -87,7 +88,7 @@ function run(argv: ReadonlyArray<string>, root: string = process.cwd()): number
|
|
|
87
88
|
|
|
88
89
|
const markdown = read_named(name, root)
|
|
89
90
|
|
|
90
|
-
return markdown === undefined ? FAILURE_EXIT_CODE : print_section(markdown, heading)
|
|
91
|
+
return markdown === undefined ? FAILURE_EXIT_CODE : print_section(markdown, heading, root)
|
|
91
92
|
}
|
|
92
93
|
|
|
93
94
|
function main(argv: ReadonlyArray<string>): void {
|