@joshuafolkken/kit 1.1136.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/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 +11 -60
- package/.claude/skills/workflow-commands/backlogrun-park.md +10 -8
- 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 +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 +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 +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/dist/CLAUDE.md +58 -120
- package/dist/hooks/batch-guard.js +4 -4
- package/dist/hooks/{chunk-NFZGFGOR.js → chunk-5KTCMCOT.js} +14 -1
- package/dist/hooks/{chunk-LJBLGHEH.js → chunk-A6P7PN7P.js} +4 -6
- package/dist/hooks/{chunk-FO2I7UT7.js → chunk-ANJ2WGFD.js} +1 -1
- package/dist/hooks/{chunk-GDU26K3J.js → chunk-DYAVAM6S.js} +563 -126
- package/dist/hooks/{chunk-RFWWUI5F.js → chunk-ENA7P5FB.js} +3 -3
- package/dist/hooks/{chunk-KBXZLMRH.js → chunk-GU5MATVE.js} +213 -65
- package/dist/hooks/chunk-GYNGP7HI.js +56 -0
- package/dist/hooks/{chunk-HBKZLITF.js → chunk-URJA6XY4.js} +4 -4
- package/dist/hooks/{chunk-KOKOAV6D.js → chunk-V2BTOZHT.js} +6 -4
- package/dist/hooks/{chunk-JKKTVDAG.js → chunk-ZDZ555YH.js} +100 -20
- 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 +193 -192
- 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 +25 -2
- 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-offer.ts +6 -6
- package/scripts/behavior/behavior-rules.ts +11 -4
- 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/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/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 +10 -0
- package/scripts/josh/josh-commands-issue.ts +2 -2
- package/scripts/lines/bytes-command.ts +29 -6
- 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/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-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 +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-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/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/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
|
@@ -114,6 +114,10 @@ interface GuardedCall {
|
|
|
114
114
|
input: unknown
|
|
115
115
|
}
|
|
116
116
|
|
|
117
|
+
// What naming a call reads off it — a closed turn's span or the call in hand alike, so the live density
|
|
118
|
+
// line can name the call that has just run beside the turns behind it (joshuafolkken/kit#3157).
|
|
119
|
+
type NamedCall = Pick<Span, 'is_bundleable' | 'is_writing' | 'label' | 'targets'>
|
|
120
|
+
|
|
117
121
|
// **A write is refusable, and the exclusion that said otherwise is gone** (joshuafolkken/kit#1762).
|
|
118
122
|
// Read over 19 recorded runs, 261 of 1,737 round trips (15.0%) were recoverable by batching and **184
|
|
119
123
|
// of them (70.5%) were writes** — 158 `Edit` alone. The refusal's own text has always said the rule
|
|
@@ -171,7 +175,7 @@ const REASON =
|
|
|
171
175
|
`the calls meant to follow it that do not need its result. **The criterion is whether this call's ` +
|
|
172
176
|
`input needs another call's result, not what kind of call it is** — edits are covered exactly as ` +
|
|
173
177
|
`reads are, and it never authorizes weakening a verification gate or a review: fewer turns, never ` +
|
|
174
|
-
`less work. The measured cost and
|
|
178
|
+
`less work. The rule, and where its measured cost and rejected mechanisms are, is in ` +
|
|
175
179
|
`\`prompts/collaboration-workflow/turn-batching.md\`. If this turn was already batching, or the call ` +
|
|
176
180
|
`genuinely has nothing to go beside it, reissue it as it was: this fires once per run of ` +
|
|
177
181
|
`single-call turns and cannot repeat on the call in hand.`
|
|
@@ -188,7 +192,7 @@ const NOTICE =
|
|
|
188
192
|
`write proceeds. Where the writes meant to follow it do not need its result, issue them in one turn ` +
|
|
189
193
|
`together. **The criterion is whether a call's input needs another call's result, not what kind of ` +
|
|
190
194
|
`call it is** — it never authorizes weakening a verification gate or a review: fewer turns, never ` +
|
|
191
|
-
`less work. The measured cost and
|
|
195
|
+
`less work. The rule, and where its measured cost and rejected mechanisms are, is in ` +
|
|
192
196
|
`\`prompts/collaboration-workflow/turn-batching.md\`. If this run was already batching, or the write ` +
|
|
193
197
|
`genuinely has nothing to go beside it, carry on: this recurs every ` +
|
|
194
198
|
`${String(NOTICE_REFIRE_EVERY)} further single-call turn(s).`
|
|
@@ -205,8 +209,8 @@ const LANE_NOTICE =
|
|
|
205
209
|
`proceeds, because a dispatched lane child ends its turn on a denial. Where the calls meant to follow ` +
|
|
206
210
|
`it do not need its result, issue them in one turn together. **The criterion is whether a call's ` +
|
|
207
211
|
`input needs another call's result, not what kind of call it is** — it never authorizes weakening a ` +
|
|
208
|
-
`verification gate or a review: fewer turns, never less work. The
|
|
209
|
-
`mechanisms are in \`prompts/collaboration-workflow/turn-batching.md\`. If this run was already ` +
|
|
212
|
+
`verification gate or a review: fewer turns, never less work. The rule, and where its measured cost ` +
|
|
213
|
+
`and rejected mechanisms are, is in \`prompts/collaboration-workflow/turn-batching.md\`. If this run was already ` +
|
|
210
214
|
`batching, or the call genuinely has nothing to go beside it, carry on: this recurs every ` +
|
|
211
215
|
`${String(NOTICE_REFIRE_EVERY)} further single-call turn(s).`
|
|
212
216
|
|
|
@@ -410,7 +414,7 @@ function should_notify(text: string, call: GuardedCall, notified_at_ms: number):
|
|
|
410
414
|
// touched where the label does not already carry it (joshuafolkken/kit#2276). `label` is the tool name
|
|
411
415
|
// for a non-shell call and the command for a shell one, so the target is appended only where it adds
|
|
412
416
|
// something the label has not already said.
|
|
413
|
-
function describe_span(span:
|
|
417
|
+
function describe_span(span: NamedCall): string {
|
|
414
418
|
const target = span.targets[0] ?? ''
|
|
415
419
|
if (target === '' || span.label.includes(target)) return span.label
|
|
416
420
|
|
|
@@ -428,7 +432,7 @@ const FOLD_MINIMUM = 2
|
|
|
428
432
|
// `read:files` cannot read — it would emit `Cannot read scripts/` and exit 1, so the paste-ready
|
|
429
433
|
// command must never carry it). The directory test is `has_extension` reused from the target scanner,
|
|
430
434
|
// not a second copy. Deduped, so a run that read one file twice folds it once and names it once.
|
|
431
|
-
function read_targets(sequence: ReadonlyArray<
|
|
435
|
+
function read_targets(sequence: ReadonlyArray<NamedCall>): ReadonlyArray<string> {
|
|
432
436
|
const paths = sequence
|
|
433
437
|
.filter((span) => span.is_bundleable && !span.is_writing)
|
|
434
438
|
.map((span) => span.targets[0] ?? '')
|
|
@@ -445,7 +449,7 @@ function read_targets(sequence: ReadonlyArray<Span>): ReadonlyArray<string> {
|
|
|
445
449
|
// kit#2202); so the guard now offers *that* command, the one the model can emit in a single turn,
|
|
446
450
|
// rather than another way to say "batch". Below two reads there is nothing to fold, so it is empty and
|
|
447
451
|
// the guidance stands alone.
|
|
448
|
-
function read_fold_directive(sequence: ReadonlyArray<
|
|
452
|
+
function read_fold_directive(sequence: ReadonlyArray<NamedCall>): string {
|
|
449
453
|
const paths = read_targets(sequence)
|
|
450
454
|
if (paths.length < FOLD_MINIMUM) return ''
|
|
451
455
|
|
|
@@ -459,7 +463,7 @@ function read_fold_directive(sequence: ReadonlyArray<Span>): string {
|
|
|
459
463
|
// can emit in a single turn where reissuing the edits as parallel `tool_use` blocks is the shape it
|
|
460
464
|
// resists. Only `Edit` counts: a whole-file `Write` creates content this command cannot content-address,
|
|
461
465
|
// and it earns its own notice already. Below two edits there is nothing to fold.
|
|
462
|
-
function write_targets(sequence: ReadonlyArray<
|
|
466
|
+
function write_targets(sequence: ReadonlyArray<NamedCall>): ReadonlyArray<string> {
|
|
463
467
|
const paths = sequence
|
|
464
468
|
.filter((span) => span.label === EDIT_TOOL)
|
|
465
469
|
.map((span) => span.targets[0] ?? '')
|
|
@@ -468,13 +472,24 @@ function write_targets(sequence: ReadonlyArray<Span>): ReadonlyArray<string> {
|
|
|
468
472
|
return [...new Set(paths)]
|
|
469
473
|
}
|
|
470
474
|
|
|
471
|
-
function write_fold_directive(sequence: ReadonlyArray<
|
|
475
|
+
function write_fold_directive(sequence: ReadonlyArray<NamedCall>): string {
|
|
472
476
|
const paths = write_targets(sequence)
|
|
473
477
|
if (paths.length < FOLD_MINIMUM) return ''
|
|
474
478
|
|
|
475
479
|
return ` Fold them into one \`pnpm josh edit:files\` call with a plan over: ${paths.join(' ')}.`
|
|
476
480
|
}
|
|
477
481
|
|
|
482
|
+
// The calls named, and their folds offered — the text both the refusable-call notice and the live
|
|
483
|
+
// density line carry, written once so the two cannot word the same calls differently.
|
|
484
|
+
function name_candidates(calls: ReadonlyArray<NamedCall>): string {
|
|
485
|
+
if (calls.length === NONE) return ''
|
|
486
|
+
|
|
487
|
+
const named = calls.map((call) => describe_span(call)).join(', ')
|
|
488
|
+
const folds = `${read_fold_directive(calls)}${write_fold_directive(calls)}`
|
|
489
|
+
|
|
490
|
+
return ` Just issued one per turn, so at least these could have shared a turn: ${named}.${folds}`
|
|
491
|
+
}
|
|
492
|
+
|
|
478
493
|
// **The concrete half of the notice** (joshuafolkken/kit#2276): the most recent single-call turns,
|
|
479
494
|
// named, so the model is shown the calls it should have batched rather than only told to batch — and,
|
|
480
495
|
// since kit#2311, the one `read:files` call that folds them where they were reads. Parsed from the tail
|
|
@@ -482,16 +497,36 @@ function write_fold_directive(sequence: ReadonlyArray<Span>): string {
|
|
|
482
497
|
// which is rare enough that the second parse costs less than carrying the spans through the hook's
|
|
483
498
|
// notice contract. An empty sequence names nothing and the notice falls back to its guidance alone.
|
|
484
499
|
function recent_candidates(tail: string): string {
|
|
485
|
-
|
|
486
|
-
.open_sequence(time_spans.parse_timeline(tail).spans)
|
|
487
|
-
|
|
500
|
+
return name_candidates(
|
|
501
|
+
time_bundles.open_sequence(time_spans.parse_timeline(tail).spans).slice(-NAMED_CANDIDATE_COUNT),
|
|
502
|
+
)
|
|
503
|
+
}
|
|
504
|
+
|
|
505
|
+
// **The candidates for a call that has already run** (joshuafolkken/kit#3157): what the `PostToolUse`
|
|
506
|
+
// density line names. A lane child never sees the notice above — kit#2405 turned it `off` there — so
|
|
507
|
+
// the density line is the one batching signal a lane receives, and until this it named nothing. The
|
|
508
|
+
// call in hand is not in the open sequence (its turn has not closed), so it is named after the turns
|
|
509
|
+
// behind it — and it is checked against them with the same shared-target test the refusal uses, so a
|
|
510
|
+
// call that read what the turn before it wrote is never offered as one that could have gone beside
|
|
511
|
+
// it. A call that is not bundleable names no targets, so the shared-target test cannot see what it
|
|
512
|
+
// needs — `pnpm josh gate` after a run of edits reads every one of them — and it is never offered
|
|
513
|
+
// either, exactly as `is_guarded_call` never refuses one. An empty answer means there is no concrete
|
|
514
|
+
// pair to name, and the caller says nothing at all. A call the payload does not name is judged on the
|
|
515
|
+
// closed turns alone.
|
|
516
|
+
function pairing_candidates(tail: string, call: GuardedCall | undefined): string {
|
|
517
|
+
const sequence = time_bundles.open_sequence(time_spans.parse_timeline(tail).spans)
|
|
518
|
+
|
|
519
|
+
if (call === undefined || sequence.length === NONE) {
|
|
520
|
+
return name_candidates(sequence.slice(-NAMED_CANDIDATE_COUNT))
|
|
521
|
+
}
|
|
488
522
|
|
|
489
|
-
|
|
523
|
+
const facts = time_bundle_call.call_facts(call.name, call.input)
|
|
490
524
|
|
|
491
|
-
|
|
492
|
-
const folds = `${read_fold_directive(recent)}${write_fold_directive(recent)}`
|
|
525
|
+
if (!facts.is_bundleable || depends_on_sequence(sequence, facts)) return ''
|
|
493
526
|
|
|
494
|
-
|
|
527
|
+
const current = time_spans.to_tool_call(call.name, call.input, time_spans.NO_MESSAGE_ID)
|
|
528
|
+
|
|
529
|
+
return name_candidates([...sequence.slice(ONE_TURN - NAMED_CANDIDATE_COUNT), current])
|
|
495
530
|
}
|
|
496
531
|
|
|
497
532
|
// **This guard's own name for its once-per-run record.** It lives beside the rule rather than in
|
|
@@ -519,6 +554,7 @@ const time_batch_guard = {
|
|
|
519
554
|
is_guarded_call,
|
|
520
555
|
is_notice_call,
|
|
521
556
|
is_read_only_call,
|
|
557
|
+
pairing_candidates,
|
|
522
558
|
recent_candidates,
|
|
523
559
|
should_block,
|
|
524
560
|
should_notify,
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { closeSync, fstatSync, openSync, readSync } from 'node:fs'
|
|
2
2
|
import { stamp_file } from '#scripts/josh/stamp-file'
|
|
3
3
|
import { z } from 'zod'
|
|
4
|
+
import { time_batch_guard, type GuardedCall } from './time-batch-guard'
|
|
4
5
|
import { time_density } from './time-density'
|
|
5
6
|
import { time_hook_transcript } from './time-hook-transcript'
|
|
6
7
|
|
|
@@ -61,6 +62,7 @@ const payload_schema = z.object({
|
|
|
61
62
|
agent_id: z.string().nullish(),
|
|
62
63
|
})
|
|
63
64
|
const notice_schema = z.object({ notified_at_ms: z.number() })
|
|
65
|
+
const call_schema = z.object({ tool_name: z.string().min(1), tool_input: z.unknown() })
|
|
64
66
|
|
|
65
67
|
function parse_transcript_path(raw_payload: string): string | undefined {
|
|
66
68
|
const parsed = payload_schema.safeParse(JSON.parse(raw_payload))
|
|
@@ -70,6 +72,17 @@ function parse_transcript_path(raw_payload: string): string | undefined {
|
|
|
70
72
|
return time_hook_transcript.transcript_of(parsed.data.transcript_path, parsed.data.agent_id)
|
|
71
73
|
}
|
|
72
74
|
|
|
75
|
+
// The call that has just run, as the payload names it (joshuafolkken/kit#3157) — what the line checks
|
|
76
|
+
// against the turns behind it, so a call that needed their result is never offered as one that could
|
|
77
|
+
// have shared their turn. A payload without a tool name answers `undefined`, judged on those turns alone.
|
|
78
|
+
function parse_call(raw_payload: string): GuardedCall | undefined {
|
|
79
|
+
const parsed = call_schema.safeParse(JSON.parse(raw_payload))
|
|
80
|
+
|
|
81
|
+
if (!parsed.success) return undefined
|
|
82
|
+
|
|
83
|
+
return { name: parsed.data.tool_name, input: parsed.data.tool_input }
|
|
84
|
+
}
|
|
85
|
+
|
|
73
86
|
// The last `max_bytes` of a file, as text. The leading line is usually cut mid-way, which every
|
|
74
87
|
// reader below already handles: an unparseable line is dropped rather than dated.
|
|
75
88
|
//
|
|
@@ -135,14 +148,19 @@ function notice_for(raw_payload: string, now_ms: number): string | undefined {
|
|
|
135
148
|
|
|
136
149
|
if (transcript_path === undefined) return undefined
|
|
137
150
|
|
|
138
|
-
const
|
|
151
|
+
const tail = read_tail(transcript_path)
|
|
152
|
+
const reading = time_density.read_window(tail)
|
|
139
153
|
const target = notice_path(transcript_path)
|
|
140
154
|
|
|
141
155
|
if (!time_density.is_due(reading, now_ms - last_notice_ms(target))) return undefined
|
|
142
156
|
|
|
157
|
+
const candidates = time_batch_guard.pairing_candidates(tail, parse_call(raw_payload))
|
|
158
|
+
|
|
159
|
+
if (candidates === '') return undefined
|
|
160
|
+
|
|
143
161
|
arm_throttle(target, now_ms)
|
|
144
162
|
|
|
145
|
-
return time_density.format_notice(reading)
|
|
163
|
+
return time_density.format_notice(reading, candidates)
|
|
146
164
|
}
|
|
147
165
|
|
|
148
166
|
// The whole call in one guard: every path above may throw on a file that moved or a payload that is
|
|
@@ -120,8 +120,9 @@ function is_due(reading: DensityReading, since_last_ms: number): boolean {
|
|
|
120
120
|
// One line, and it says three things: what the density is, what it is being measured against, and
|
|
121
121
|
// what to do differently. The instruction is last because it is the only part worth acting on, and it
|
|
122
122
|
// names the resident rule rather than restating it — the rule is already in the run's context, and a
|
|
123
|
-
// second wording of it would be a clone that can drift.
|
|
124
|
-
|
|
123
|
+
// second wording of it would be a clone that can drift. `candidates` is the concrete calls that could
|
|
124
|
+
// have shared a turn (joshuafolkken/kit#3157), appended last so the line ends on what to do next.
|
|
125
|
+
function format_notice(reading: DensityReading, candidates = ''): string {
|
|
125
126
|
const density = time_round_trips.format_density(reading.density)
|
|
126
127
|
const floor = time_round_trips.format_density(time_round_trips.CALLS_PER_ROUND_TRIP_FLOOR)
|
|
127
128
|
|
|
@@ -130,7 +131,7 @@ function format_notice(reading: DensityReading): string {
|
|
|
130
131
|
`under the ${floor} floor, and this turn issued one call. ` +
|
|
131
132
|
`Independent reads, greps, gh queries and edits go out together; the criterion is whether this ` +
|
|
132
133
|
`call's input needs another call's result, not what kind of call it is — ` +
|
|
133
|
-
`prompts/collaboration-workflow/turn-batching.md
|
|
134
|
+
`prompts/collaboration-workflow/turn-batching.md.${candidates}`
|
|
134
135
|
)
|
|
135
136
|
}
|
|
136
137
|
|
|
@@ -111,6 +111,12 @@ const BODY_FLAG_PATTERN = /--body(?:-file)?\b/u
|
|
|
111
111
|
const ISSUE_LABELS_PATTERN = /repos\/\S+\/issues\/(\d+)\/labels\b(?!\/)/u
|
|
112
112
|
const IN_PROGRESS_FIELD = 'labels[]=in-progress'
|
|
113
113
|
|
|
114
|
+
// **`run:entry` applies the label itself now** (joshuafolkken/kit#3182), so a run opened through it
|
|
115
|
+
// leaves no `gh api` add form in its transcript — the call that opens the run and names its issue is
|
|
116
|
+
// `pnpm josh run:entry <N>`, and it is read as the same declaration. `run:release` is not: like the
|
|
117
|
+
// removal form above, it ends a hold rather than opening one.
|
|
118
|
+
const RUN_ENTRY_PATTERN = /\bjosh\s+run:entry\s+(\d+)\b/u
|
|
119
|
+
|
|
114
120
|
// "This call declares no issue", which is the attribution's own sentinel rather than a second one:
|
|
115
121
|
// what `bash_issue` returns is read by `cost_attribute` and by nothing else, so two spellings of "no
|
|
116
122
|
// issue" could only drift apart.
|
|
@@ -161,6 +167,10 @@ function is_in_progress_label(command: string): boolean {
|
|
|
161
167
|
return ISSUE_LABELS_PATTERN.test(command) && command.includes(IN_PROGRESS_FIELD)
|
|
162
168
|
}
|
|
163
169
|
|
|
170
|
+
function is_run_start(command: string): boolean {
|
|
171
|
+
return is_in_progress_label(command) || RUN_ENTRY_PATTERN.test(command)
|
|
172
|
+
}
|
|
173
|
+
|
|
164
174
|
// Which issue a Bash call declares its run to be running, or `undefined` where it declares nothing.
|
|
165
175
|
//
|
|
166
176
|
// **Attribution's other evidence is the branch, and a lane run has none to give**
|
|
@@ -185,15 +195,16 @@ function is_in_progress_label(command: string): boolean {
|
|
|
185
195
|
// shape `sonarjs/super-linear-regex` refuses.
|
|
186
196
|
const COMMAND_SEPARATOR = /&&|\|\||;/u
|
|
187
197
|
|
|
188
|
-
function
|
|
189
|
-
const
|
|
198
|
+
function declared_issue(part: string): number {
|
|
199
|
+
const pattern = is_in_progress_label(part) ? ISSUE_LABELS_PATTERN : RUN_ENTRY_PATTERN
|
|
200
|
+
const matched = pattern.exec(part)
|
|
190
201
|
|
|
191
202
|
return matched?.[1] === undefined ? NO_ISSUE : Number(matched[1])
|
|
192
203
|
}
|
|
193
204
|
|
|
194
205
|
function bash_issue(command: string): number {
|
|
195
206
|
for (const part of command.split(COMMAND_SEPARATOR)) {
|
|
196
|
-
if (
|
|
207
|
+
if (is_run_start(part)) return declared_issue(part)
|
|
197
208
|
}
|
|
198
209
|
|
|
199
210
|
return NO_ISSUE
|
|
@@ -207,7 +218,7 @@ function bash_issue(command: string): number {
|
|
|
207
218
|
// auto-decision log take the same shape, so `time-phases.ts` accepts only a marker that closes
|
|
208
219
|
// before the first edit. The label carries no body, so the two tests cannot both match one command.
|
|
209
220
|
function bash_marker(command: string): PhaseMarker {
|
|
210
|
-
if (
|
|
221
|
+
if (is_run_start(command)) return WORKFLOW_MARKER
|
|
211
222
|
|
|
212
223
|
return is_api_body_write(command) || is_issue_comment(command) ? PLAN_MARKER : NO_MARKER
|
|
213
224
|
}
|
|
@@ -92,6 +92,16 @@ function read_release_times(package_name: string): Record<string, string> | unde
|
|
|
92
92
|
return parsed.success ? parsed.data.time : undefined
|
|
93
93
|
}
|
|
94
94
|
|
|
95
|
-
|
|
95
|
+
// Whether public npm already carries `<name>@<version>`. **Asked of public npm regardless of how this
|
|
96
|
+
// checkout routes the scope**, unlike the two readers above: `josh release` waits for `publish.yml`'s
|
|
97
|
+
// `publish-npm` job and links the npmjs.com page, so GitHub Packages answering would not be the
|
|
98
|
+
// question it asks (joshuafolkken/kit#3193).
|
|
99
|
+
function has_public_version(package_name: string, version: string): boolean {
|
|
100
|
+
const parsed = times_schema.safeParse(read_packument(package_name, FULL_ACCEPT))
|
|
101
|
+
|
|
102
|
+
return parsed.success && parsed.data.time[version] !== undefined
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
const npm_registry = { has_public_version, packument_url, read_latest, read_release_times }
|
|
96
106
|
|
|
97
107
|
export { npm_registry }
|
|
@@ -227,9 +227,17 @@ jobs:
|
|
|
227
227
|
if: steps.profile.outputs.is_basic == 'true'
|
|
228
228
|
run: pnpm josh gate --verbose
|
|
229
229
|
|
|
230
|
+
# size-limit is an opt-in bundle budget kit does not install, so a full project that never
|
|
231
|
+
# added it skips the check instead of failing on a missing command (joshuafolkken/kit#3217).
|
|
230
232
|
- name: Check bundle size
|
|
233
|
+
id: bundle-size
|
|
231
234
|
if: steps.profile.outputs.is_basic != 'true'
|
|
232
|
-
run:
|
|
235
|
+
run: |
|
|
236
|
+
if [ -x node_modules/.bin/size-limit ]; then
|
|
237
|
+
pnpm size-limit
|
|
238
|
+
else
|
|
239
|
+
echo 'size-limit is not installed — skipping the bundle-size check.'
|
|
240
|
+
fi
|
|
233
241
|
|
|
234
242
|
security:
|
|
235
243
|
name: Security Audit
|
|
@@ -1,118 +0,0 @@
|
|
|
1
|
-
# `pnpm josh followup` — the post-execution reference
|
|
2
|
-
|
|
3
|
-
The parts of the `followup` procedure a run reaches **after** it has issued `pnpm josh followup`: how
|
|
4
|
-
to read the stage-timing block the command prints, the AI-reviewer comment scan and the config-file
|
|
5
|
-
report it runs, and the release ask that closes the completion summary. `followup.md` carries what a
|
|
6
|
-
run needs **before** it issues the command and points here for the rest, so the read that lands in the
|
|
7
|
-
turn that issues `pnpm josh followup` stays small (joshuafolkken/kit#1905).
|
|
8
|
-
|
|
9
|
-
This file is the single source of the sections below; `followup.md`, `fullrun.md`, `backlogrun.md` and
|
|
10
|
-
`backlogrun.md` point here rather than restating them.
|
|
11
|
-
|
|
12
|
-
## Reading the stage-timing block
|
|
13
|
-
|
|
14
|
-
### It prints how long each of those stages took
|
|
15
|
-
|
|
16
|
-
Since joshuafolkken/kit#1349 the command closes with one row per stage and a total, so "where did
|
|
17
|
-
`followup` spend its time" is read rather than guessed. The shape, with illustrative durations:
|
|
18
|
-
|
|
19
|
-
```
|
|
20
|
-
followup stage: closes-and-context 2.1 s
|
|
21
|
-
followup stage: checks-wait 28.4 s
|
|
22
|
-
followup stage: coderabbit-comments 1.3 s
|
|
23
|
-
followup stage: ai-review-comments 2.0 s
|
|
24
|
-
followup stage: telegram 1.1 s
|
|
25
|
-
followup stage: merge 2.6 s
|
|
26
|
-
followup stage: completion-and-epic-close 1.4 s
|
|
27
|
-
followup stages total: 38.9 s
|
|
28
|
-
```
|
|
29
|
-
|
|
30
|
-
- **The block is printed on a failed run too**, up to and including the stage that threw, which is
|
|
31
|
-
named `interrupted`. A `followup` that exits non-zero on an AI-review blocker or a red check is the
|
|
32
|
-
invocation whose wait was longest, and one that printed nothing would leave the measurement blind to
|
|
33
|
-
exactly those. **A short block there is the run stopping early, not the printer breaking**: the
|
|
34
|
-
stages after the failure never ran, so they have no duration to report.
|
|
35
|
-
- **The `merge` row appears only on a run that merged**, so a `--no-merge` block is a row shorter.
|
|
36
|
-
- **Two rows say `and` because the requests behind them go out together** (joshuafolkken/kit#1446).
|
|
37
|
-
`closes-and-context` is the former `closes-check` plus `context` — the pull request body, the
|
|
38
|
-
repository name, the pull request URL and the issue title, four reads that need nothing from one
|
|
39
|
-
another; `completion-and-epic-close` is the former `completion-comment` plus `epic-close`. A lap
|
|
40
|
-
records an interval rather than a call, so a batch is one row, and the name is what says which
|
|
41
|
-
stages it holds. **The one dependency inside the batch is kept**: with no issue number on the
|
|
42
|
-
command line the title read waits for the body that names it, and only that read waits.
|
|
43
|
-
**`checks-wait` is untouched**, which is the point — what was overlapped was never a wait.
|
|
44
|
-
- **The total is the sum of the stages, not the command's whole wall clock.** What sits outside it is
|
|
45
|
-
the tail the workflow script runs afterwards — the review-record clears, the hold
|
|
46
|
-
release, the next-issue listing and the version line — so a run-timing reading of the same
|
|
47
|
-
span is longer, and that gap is the tail. It is normally a second or two.
|
|
48
|
-
- **What was measured is on joshuafolkken/kit#1349**, and cutting any of it is deliberately not this
|
|
49
|
-
block's business: the required-check wait and the AI-review scan are the merge gate, and narrowing
|
|
50
|
-
either to make a number smaller is the workaround `CLAUDE.md` prohibits.
|
|
51
|
-
|
|
52
|
-
A `failure` Telegram is sent **by hand, exactly once**, and only when the agent has finally given up
|
|
53
|
-
recovering — never once per retry:
|
|
54
|
-
|
|
55
|
-
```bash
|
|
56
|
-
pnpm josh notify --task-type failure --issue-url "<issue-url>" --body "<the reason and what is unresolved>"
|
|
57
|
-
```
|
|
58
|
-
|
|
59
|
-
## AI reviewer comment scan (automatic in `pnpm josh followup`)
|
|
60
|
-
|
|
61
|
-
`pnpm josh followup` scans top-level PR comments from AI reviewers (Claude Review, CodeRabbit summary comments) **independently of CI status**. This scan runs after CI is green and after the existing CodeRabbit line-comment check. The goal is to ensure substantive findings posted by AI reviewers _after_ CI goes green are not silently shipped.
|
|
62
|
-
|
|
63
|
-
**Temporary (kit#753)**: while CodeRabbit reviews are slow, CodeRabbit is non-blocking end to end — it is excluded from the default required checks (restore via `JOSH_REQUIRED_CHECKS`), `Actionable comments posted: N` is downgraded to an informational log, and unresolved CodeRabbit line comments no longer require an ignore reason. Every skip is printed to the console and appended to the completion Telegram body. Claude Review blockers are unchanged. Revert together with kit#752.
|
|
64
|
-
|
|
65
|
-
- Blocker heuristics (conservative, structural — not NLP):
|
|
66
|
-
- **Claude Review** (`author.login = claude`): body contains `### Issues`, `### Problem`, `#### Logic bug`, or a numbered finding heading like `### 1. ...`
|
|
67
|
-
- **CodeRabbit** (`author.login = coderabbitai` / `coderabbitai[bot]`): body contains `Actionable comments posted: N` with N > 0. Rate-limit notices (`rate limited by coderabbit.ai` / `Rate limit exceeded`) and "No actionable comments" summaries are ignored.
|
|
68
|
-
- If blockers exist and **no** ignore reason is supplied: `pnpm josh followup` sends a `confirmation` Telegram and exits non-zero. Fix the findings (or provide an ignore reason) and re-run.
|
|
69
|
-
- If blockers exist and `--ai-review-ignore-reason "<reason>"` is supplied: the workflow posts an ignore-reason comment to the PR (mirroring the CodeRabbit ignore-reason flow) and proceeds to completion.
|
|
70
|
-
- Acknowledgment-only Claude comments (`All issues resolved ✓`, `Everything else looks good`) do not match the blocker heuristics, so rounds where the AI reviewer explicitly signs off do not trigger a false positive.
|
|
71
|
-
- **A comment listing that could not be read is treated exactly like a standing blocker.** A rate limit, expired auth, a dropped connection — every one of them used to arrive as an empty listing, so the gate passed without having read anything (joshuafolkken/kit#973). `confirmation` Telegram, non-zero exit, re-run once the read succeeds. `--ai-review-ignore-reason` still gets past it, because what it means is unchanged: a person has looked — and the run then carries an audit note saying the scan was bypassed.
|
|
72
|
-
- **The CodeRabbit line-comment listing is the exception**: unreadable there is reported as an audit note rather than blocking, because kit#753 has CodeRabbit not blocking the merge at all. That reader also answers unreadable when the PR number itself would not resolve, which the top-level scan never has to do. Revert with kit#752.
|
|
73
|
-
|
|
74
|
-
## Config file reporting (inside `pnpm josh followup`)
|
|
75
|
-
|
|
76
|
-
**Nothing here asks you to compare anything by eye** (joshuafolkken/kit#1578). `pnpm josh followup` reads the tracked branch diff itself and matches every changed path against everything `josh sync` distributes — the three lists `AI_COPY_FILES`, `AI_COPY_FILE_MAPPINGS`, `AI_COPY_DIRECTORIES`, plus `SYNCED_PATHS`, the destinations `sync` writes directly (`playwright.config.ts`, `eslint.config.js`, the rest).
|
|
77
|
-
|
|
78
|
-
**It stops nothing** (joshuafolkken/kit#1592). What it produces is a report: the claimed paths and the list that claimed each one go into **the completion notification and the completion report on the Issue**, and the run continues to the CI wait and the merge exactly as a run that distributes nothing does. A run passing `--no-merge` is not reported on, because nothing has been distributed yet.
|
|
79
|
-
|
|
80
|
-
**It was a confirmation stop until joshuafolkken/kit#1592, and measurement is what removed it rather than preference.** In kit the condition is nearly always true, because kit is the distribution source: every change to `CLAUDE.md`, to `prompts/`, to `.claude/skills/` or to the distributed part of `docs/` is by definition a change to a distributed path. Measured on `epicrun #1413`, **two of three children stopped here** — and in both, the distributed file was the one that child's own acceptance criteria had ordered changed. The gate was not catching an unintended edit; it was stopping the work, and ending unattended execution until a person retyped one command.
|
|
81
|
-
|
|
82
|
-
**There is deliberately no branch on which repository this is.** Stopping in a consumer project — where editing a distributed file really is the mistake `CLAUDE.md` → "Route distributed-doc / config changes upstream to kit" names, since the next `josh sync` overwrites it silently — while staying quiet in kit was weighed and rejected on joshuafolkken/kit#1592: it needs a distribution-source test this package does not have, no case is recorded of the gate having saved a consumer's work, and the report below reaches a consumer's reader just as well.
|
|
83
|
-
|
|
84
|
-
**The matching itself is unchanged, and so is the reason it is mechanical.** The instruction the mechanical read replaced was skipped in two runs out of three on the day it was measured, and one of those two could not have succeeded by eye at all: `AI_COPY_DIRECTORIES` holds directories, so a distributed path such as `.claude/skills/workflow-commands/backlogrun.md` appears in no list textually.
|
|
85
|
-
|
|
86
|
-
```bash
|
|
87
|
-
pnpm josh sync:scope # managed | clean, naming which list claimed each path
|
|
88
|
-
```
|
|
89
|
-
|
|
90
|
-
- **There is no flag to pass and nothing to approve.** `--managed-config-ignore-reason` is gone with the stop it existed to get past.
|
|
91
|
-
- This report is produced independently of AI reviewer comment scanning, which still stops the run on a standing blocker
|
|
92
|
-
|
|
93
|
-
## When `pnpm josh release` runs
|
|
94
|
-
|
|
95
|
-
**The release point is a position plus a command's answer, never a judgement** (joshuafolkken/kit#1582). joshuafolkken/kit#1169 took the version off the branch and put it behind one command a person types; nothing said *when* to type it, and because nothing fails when nobody does — CI green, every pull request merged, every Issue closed — 53 merges reached main unreleased and no consumer of this package saw one of them.
|
|
96
|
-
|
|
97
|
-
**The position: once per invocation, after the last merge.** Ask when `pnpm josh followup` has merged the last pull request *this invocation* authorized — a lone `fullrun`'s only one, a `backlogrun`'s **last** child, never once per child. In a lane the parent asks it, in the primary checkout, after the last lane is closed. Asked per child it would cut a release in the middle of a batch whose remaining children are still moving main.
|
|
98
|
-
|
|
99
|
-
**The answer: `pnpm josh release:scope`.**
|
|
100
|
-
|
|
101
|
-
```bash
|
|
102
|
-
pnpm josh release:scope # → required | skip | unknown
|
|
103
|
-
pnpm josh release:scope --json # the same answer as one JSON object
|
|
104
|
-
```
|
|
105
|
-
|
|
106
|
-
| It answers | What it means | What the run does |
|
|
107
|
-
| ---------- | ---------------------------------------------------------------- | --------------------------------------------------------------------------------- |
|
|
108
|
-
| `required` | main has taken at least one merge since the version last changed | Close the completion report with the release request, naming `pnpm josh release` |
|
|
109
|
-
| `skip` | the count is zero — nothing is waiting to ship | Say so in one line and finish |
|
|
110
|
-
| `unknown` | the count could not be read | **Report it as `unknown`** — it is never read as `skip` |
|
|
111
|
-
|
|
112
|
-
**The threshold is one, and that is deliberate.** joshuafolkken/kit#1582's own complaint is that accumulating makes a single version larger and its contents harder to trace afterwards, so a release per invocation is the cadence rather than a compromise — and it is the cadence that already held while every child bumped, before joshuafolkken/kit#1486 removed that. What keeps it from firing per child is the position above, not a larger number.
|
|
113
|
-
|
|
114
|
-
**The run never types `pnpm josh release` itself.** That command opens a pull request of its own, merges it, and starts the tag → publish → `production` chain: outward-facing and effectively irreversible, so it is Tier C (`CLAUDE.md` → "Decision autonomy"). Typing `fullrun` authorizes merging *this Issue's* pull request and nothing past it. On `required` the run reports the request as its closing line and stops there; a person types the command in the primary checkout, on the default branch, with a clean tree.
|
|
115
|
-
|
|
116
|
-
**`pnpm josh release --dry-run` was checked first and does not answer this.** It refuses off the default branch and on a dirty working tree, and it counts against `HEAD` rather than `origin/<default>` — and every position above is a feature branch or a lane, which is exactly where it throws. So `release:scope` adds **no counting of its own**: it reads `git_followup_pending.read_pending`, the same fetch-then-count `pnpm josh followup` already uses for the Telegram line, and the two therefore cannot disagree.
|
|
117
|
-
|
|
118
|
-
**This section is the single source.** `fullrun.md`, `backlogrun.md` and `backlogrun.md` point here rather than restating it, and `docs/josh-commands-automation.md` → "`josh release:scope`" documents the command itself.
|