@joshuafolkken/kit 1.978.0 → 1.983.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/skills/epic-commands/SKILL.md +6 -5
- package/.claude/skills/workflow-commands/SKILL.md +9 -9
- package/.claude/skills/workflow-commands/background-commands.md +1 -1
- package/.claude/skills/workflow-commands/fullrun-steps.md +8 -8
- package/.claude/skills/workflow-commands/halfrun.md +4 -5
- package/.claude/skills/workflow-commands/issue-comments.md +1 -1
- package/.claude/skills/workflow-commands/issue-scout.md +21 -22
- package/.claude/skills/workflow-commands/kickoff.md +8 -9
- package/.claude/skills/workflow-commands/observation-filing.md +9 -10
- package/.claude/skills/workflow-commands/prerequisite.md +7 -6
- package/.claude/skills/workflow-commands/retrospective.md +5 -5
- package/.claude/skills/workflow-commands/rule-residency.md +2 -2
- package/.claude/skills/workflow-commands/split-assessment.md +4 -3
- package/.claude/skills/workflow-commands/target-repository.md +7 -6
- package/dist/hooks/batch-guard.js +4 -4
- package/dist/hooks/{chunk-ZPLXLJ7O.js → chunk-47SPMGOD.js} +16 -1
- package/dist/hooks/{chunk-BVMSPG3M.js → chunk-5JX6HXDX.js} +3 -3
- package/dist/hooks/{chunk-KQG4776P.js → chunk-APVOACVT.js} +1 -1
- package/dist/hooks/{chunk-2K52BRWV.js → chunk-ASUZGZFT.js} +2 -2
- package/dist/hooks/{chunk-CRGJTD6E.js → chunk-ERXMJJOV.js} +1 -1
- package/dist/hooks/{chunk-Q7LE77CO.js → chunk-IM3KM25Y.js} +31 -29
- package/dist/hooks/{chunk-BNGGEY3Z.js → chunk-LBXQHXMC.js} +129 -342
- package/dist/hooks/{chunk-NXL3SBXY.js → chunk-P3UX7SFN.js} +2 -2
- package/dist/hooks/{chunk-F6OM6SIG.js → chunk-RFPHKU7C.js} +12 -12
- package/dist/hooks/{chunk-ZYYSBNR6.js → chunk-ZT65LR3U.js} +4 -4
- 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/investigation-guard.js +5 -5
- package/dist/hooks/pretool-guard.js +9 -9
- package/dist/hooks/stop-guard.js +4 -4
- package/dist/josh.js +16 -1
- package/package.json +1 -1
- package/prompts/collaboration-workflow/epic-bundle.md +8 -8
- package/prompts/collaboration-workflow/issue-template.md +5 -5
- package/prompts/collaboration-workflow/rule-delivery.md +43 -30
- package/prompts/collaboration-workflow/upstream-interrupt.md +1 -1
- package/prompts/collaboration-workflow/wip-cap.md +3 -3
- package/prompts/review.md +11 -9
- package/scripts/delegation/delegation-policy.ts +9 -9
- package/scripts/document/document-byte-budget.ts +1 -1
- package/scripts/issue/issue-file-cli.ts +188 -0
- package/scripts/issue/issue-file.ts +192 -0
- package/scripts/issue/issue-lint-cli.ts +1 -1
- package/scripts/issue/issue-scout-cli.ts +41 -97
- package/scripts/issue/issue-scout-closed.ts +102 -0
- package/scripts/josh/__snapshots__/josh-command-map.test.ts.snap +9 -1
- package/scripts/josh/josh-command-map.ts +1 -0
- package/scripts/josh/josh-commands-issue.ts +11 -0
- package/scripts/josh/josh-commands-project.ts +5 -1
- package/scripts/propagate/propagate-run.ts +1 -0
- package/scripts/propagate/propagate-select.ts +72 -0
- package/scripts/propagate/propagate-targets.ts +5 -3
- package/scripts/propagate/propagate.ts +86 -24
- package/scripts/rules/bash-triggers.ts +19 -6
- package/scripts/rules/delivered-rules.ts +28 -32
- package/scripts/rules/direct-filing.ts +38 -0
- package/scripts/rules/filing-cap.ts +11 -8
- package/scripts/rules/issue-fold.ts +4 -4
- package/scripts/rules/oracle-consulted.ts +4 -4
- package/scripts/rules/oracle-firing.ts +13 -16
- package/scripts/rules/rule-body-guard.ts +6 -9
- package/scripts/rules/stop-rules.ts +3 -2
- package/scripts/test/pilot-files.ts +0 -1
- package/scripts/version/fresh-metadata-cache.ts +36 -0
- package/scripts/version/latest-update.ts +2 -1
- package/scripts/rules/issue-bug-label.ts +0 -85
- package/scripts/rules/issue-filing-args.ts +0 -50
- package/scripts/rules/issue-scout.ts +0 -64
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
import { cost_blocks } from '#scripts/cost-runtime/cost-blocks'
|
|
2
2
|
import type { GuardedCall } from '#scripts/time-runtime/time-batch-guard'
|
|
3
3
|
import { time_shell } from '#scripts/time-runtime/time-shell'
|
|
4
|
+
import { shell_segments } from './shell-segments'
|
|
4
5
|
|
|
5
|
-
// The
|
|
6
|
+
// The trigger helpers the enumeration in `delivered-rules.ts` is built from, held here so a rule
|
|
6
7
|
// that lives in its own module can build its own row without importing the enumeration back
|
|
7
|
-
// (joshuafolkken/kit#2119). `is_issue_filing` is the one three rows
|
|
8
|
-
//
|
|
9
|
-
// their rows.
|
|
8
|
+
// (joshuafolkken/kit#2119). `is_issue_filing` is the one three rows share — the WIP cap, the per-run
|
|
9
|
+
// filing cap and the fold — so it could not stay a private function of the file that spreads their rows.
|
|
10
10
|
|
|
11
11
|
// **Only `Bash`, and the omission is deliberate** (joshuafolkken/kit#1390): Claude Code denies one
|
|
12
12
|
// call of a turn and runs the rest, so a refused `Edit` would leave its siblings applied and itself
|
|
@@ -33,12 +33,25 @@ const ISSUES_ENDPOINT = /repos\/[^\s'"]*\/issues(?=$|["'\s])/u
|
|
|
33
33
|
// that limit is recorded beside the non-`gh` one in `prompts/collaboration-workflow/rule-delivery.md`.
|
|
34
34
|
const TITLE_FIELD = /(?:-f|-F|--field|--raw-field)\s*'?title=/u
|
|
35
35
|
|
|
36
|
-
|
|
36
|
+
// A filing made by hand rather than through `josh issue:file` — the call the `direct-filing` row
|
|
37
|
+
// refuses on every occurrence (joshuafolkken/kit#2808).
|
|
38
|
+
function is_direct_filing(command: string): boolean {
|
|
37
39
|
if (ISSUE_CREATE_COMMAND.test(command)) return true
|
|
38
40
|
|
|
39
41
|
return ISSUES_ENDPOINT.test(command) && TITLE_FIELD.test(command)
|
|
40
42
|
}
|
|
41
43
|
|
|
42
|
-
|
|
44
|
+
// `pnpm josh issue:file`, in either spelling — **the** filing act once a direct one is refused, so the
|
|
45
|
+
// run-level filing rows (the WIP cap, the per-run cap, the fold) trigger on it. Segment-wise, so a
|
|
46
|
+
// spelling quoted inside another command's body is not read as a filing.
|
|
47
|
+
const FILING_NAMES: ReadonlySet<string> = new Set(['issue:file'])
|
|
48
|
+
|
|
49
|
+
function is_issue_filing(command: string): boolean {
|
|
50
|
+
return shell_segments
|
|
51
|
+
.segments_of(command)
|
|
52
|
+
.some((segment) => shell_segments.is_josh_command(segment, FILING_NAMES))
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
const bash_triggers = { is_direct_filing, is_issue_filing, on_bash_command }
|
|
43
56
|
|
|
44
57
|
export { bash_triggers }
|
|
@@ -3,15 +3,14 @@ import { hook_decision, type GuardRun, type TranscriptGuardSpec } from '#scripts
|
|
|
3
3
|
import { lane_guard_policy } from '#scripts/lane/lane-guard-policy'
|
|
4
4
|
import { time_batch_guard, type GuardedCall } from '#scripts/time-runtime/time-batch-guard'
|
|
5
5
|
import { bash_triggers } from './bash-triggers'
|
|
6
|
+
import { direct_filing } from './direct-filing'
|
|
6
7
|
import { early_heartbeat } from './early-heartbeat'
|
|
7
8
|
import { file_body } from './file-body'
|
|
8
9
|
import { filing_cap } from './filing-cap'
|
|
9
10
|
import { gh_api } from './gh-api'
|
|
10
11
|
import { git_force } from './git-force'
|
|
11
12
|
import { implementation_cut } from './implementation-cut'
|
|
12
|
-
import { issue_bug_label_rule } from './issue-bug-label'
|
|
13
13
|
import { issue_fold } from './issue-fold'
|
|
14
|
-
import { issue_scout } from './issue-scout'
|
|
15
14
|
import { josh_git_bare } from './josh-git-bare'
|
|
16
15
|
import { lane_background } from './lane-background'
|
|
17
16
|
import { lane_carry_conflict } from './lane-carry-conflict'
|
|
@@ -135,9 +134,9 @@ type MeasuredRule = Omit<DeliveredRule, 'is_trigger'> & {
|
|
|
135
134
|
const STAMP_PREFIX = 'josh-rule-guard-'
|
|
136
135
|
|
|
137
136
|
// **`is_issue_filing` and `on_bash_command` moved to `bash-triggers.ts`** (joshuafolkken/kit#2119):
|
|
138
|
-
// three rows
|
|
139
|
-
//
|
|
140
|
-
//
|
|
137
|
+
// three rows share the filing trigger — the WIP cap, the per-run filing cap and the fold — and the
|
|
138
|
+
// last two live in their own modules, which could not import the enumeration back to reach a private
|
|
139
|
+
// function of it. `on_bash_command` went with it because those modules build their own rows.
|
|
141
140
|
const { is_issue_filing, on_bash_command } = bash_triggers
|
|
142
141
|
|
|
143
142
|
// The whole of the WIP cap, in the shape a refusal can carry: the count, the refusal, the two
|
|
@@ -239,8 +238,8 @@ const ISSUE_COMMENTS_REASON =
|
|
|
239
238
|
"except for two answers that are not the run's to make: work a comment reassigns to another " +
|
|
240
239
|
'Issue is out of scope and is not implemented, and a comment saying the Issue no longer has a ' +
|
|
241
240
|
'reason to exist stops the run with a `confirmation` Telegram. The procedure is ' +
|
|
242
|
-
'`.claude/skills/workflow-commands/SKILL.md` → "An Issue\'s comments are part of the Issue".
|
|
243
|
-
'
|
|
241
|
+
'`.claude/skills/workflow-commands/SKILL.md` → "An Issue\'s comments are part of the Issue". ' +
|
|
242
|
+
'Every body-only read is refused until the comments are read.'
|
|
244
243
|
|
|
245
244
|
// The reading of the call itself — which spellings carry a body inline, and what the shell does to
|
|
246
245
|
// the value — is `shell-body-trigger.ts`, beside its own cases, as is the refusal text. A row states
|
|
@@ -295,26 +294,24 @@ const DELIVERED_RULES: ReadonlyArray<DeliveredRule> = [
|
|
|
295
294
|
// before any of those speak; on a first-party filing it is silent (the owners match), so `wip-cap`
|
|
296
295
|
// still speaks first, exactly as before. It fires on every occurrence (`git-force.ts`).
|
|
297
296
|
third_party_write.ROW,
|
|
297
|
+
// **A hand-built filing is refused on every occurrence** (joshuafolkken/kit#2808) and pointed at
|
|
298
|
+
// `josh issue:file`, which runs the scout, the lint, the labels, the `## Origin` check and
|
|
299
|
+
// `epic:bundle` itself. It claims the direct call only, so the rows below — which trigger on the
|
|
300
|
+
// `issue:file` call — never overlap it.
|
|
301
|
+
direct_filing.ROW,
|
|
298
302
|
{
|
|
299
303
|
id: 'wip-cap',
|
|
300
304
|
is_trigger: on_bash_command(is_issue_filing),
|
|
301
305
|
reason: WIP_CAP_REASON,
|
|
302
306
|
keeps: on_bash_command(counts_open_issues),
|
|
303
307
|
},
|
|
304
|
-
// **
|
|
305
|
-
// backlog count still speaks first: `
|
|
306
|
-
// run,
|
|
307
|
-
//
|
|
308
|
-
//
|
|
309
|
-
//
|
|
310
|
-
issue_scout.ROW,
|
|
308
|
+
// **Two more rows share the filing trigger** (joshuafolkken/kit#2119, joshuafolkken/kit#2213), listed
|
|
309
|
+
// after `wip-cap` so the backlog count still speaks first: `filing-cap` refuses every filing past
|
|
310
|
+
// the per-run ceiling, and `issue-fold` refuses a *second* filing the run has not folded. Each is a
|
|
311
|
+
// real instance of the admissible overlap the comment on `delivery` describes — the losing rule's
|
|
312
|
+
// delivery is still correct one reissue later — so an unfolded, over-cap second filing is delivered
|
|
313
|
+
// `wip-cap`, then `filing-cap`, then `issue-fold` across its reissues.
|
|
311
314
|
filing_cap.ROW,
|
|
312
|
-
// **`issue-fold` shares the same filing trigger** (joshuafolkken/kit#2213), listed after
|
|
313
|
-
// `filing-cap`: it refuses a *second* filing the run has not folded, once per run, stood down off
|
|
314
|
-
// the tail when the run has already folded or has filed nothing earlier. It is another instance of
|
|
315
|
-
// the admissible overlap — the losing rule's delivery is still correct one reissue later — so a
|
|
316
|
-
// scout-less, unfolded, over-cap second filing is delivered `wip-cap`, then `issue-scout`, then
|
|
317
|
-
// `filing-cap`, then `issue-fold` across its reissues.
|
|
318
315
|
issue_fold.ROW,
|
|
319
316
|
{
|
|
320
317
|
id: 'issue-comments',
|
|
@@ -541,13 +538,9 @@ const DELIVERED_RULES: ReadonlyArray<DeliveredRule> = [
|
|
|
541
538
|
rule_body_guard.ROW,
|
|
542
539
|
// **The generic oracle-consulted rows, one per oracle that declared a firing point**
|
|
543
540
|
// (joshuafolkken/kit#2324). Each refuses the action the oracle governs when the run has not run that
|
|
544
|
-
// oracle's command first, stood down once it has
|
|
545
|
-
//
|
|
546
|
-
// (`issue:lint`) delivers on the reissue, after `wip-cap` / `issue-scout` / `filing-cap` /
|
|
547
|
-
// `issue-fold` have each had their turn. The non-overlapping firing point (`pkg:scout` on a package
|
|
548
|
-
// add) claims a command no row above matches.
|
|
541
|
+
// oracle's command first, stood down once it has. Listed last; the one firing point left
|
|
542
|
+
// (`pkg:scout` on a package add) claims a command no row above matches.
|
|
549
543
|
...oracle_consulted.ROWS,
|
|
550
|
-
issue_bug_label_rule.ROW,
|
|
551
544
|
]
|
|
552
545
|
|
|
553
546
|
// A turn that issued more than this many calls is a turn that batched. The guard counts turns that
|
|
@@ -703,10 +696,13 @@ function is_first_delivery(
|
|
|
703
696
|
// **It is a trade, and the other side is named rather than hidden**: an arm allowed inside that window
|
|
704
697
|
// that really does run is not recorded either, so a second one before it fires goes uncaught. A missing
|
|
705
698
|
// record loses one detection; a wrong one blocks every arm until it expires.
|
|
706
|
-
// **Both extras
|
|
707
|
-
// recurring rule (joshuafolkken/kit#1570); `already_satisfied` is for a
|
|
708
|
-
//
|
|
709
|
-
//
|
|
699
|
+
// **Both extras replace `is_first_delivery`, and a row declares at most one.** `decide` is for a
|
|
700
|
+
// recurring rule (joshuafolkken/kit#1570); `already_satisfied` is for a precondition the run satisfies
|
|
701
|
+
// with an earlier act (joshuafolkken/kit#1905), and that act is the only way past it: the row refuses
|
|
702
|
+
// every candidate call until the act is on the tail (joshuafolkken/kit#2807). Once per run, the
|
|
703
|
+
// reissue went through with the precondition still unmet — a guard that passed precisely the run that
|
|
704
|
+
// skipped the procedure. Repeating cannot wedge a run that obeys, because the stand-down answers first.
|
|
705
|
+
// A row with neither takes `is_first_delivery` unchanged.
|
|
710
706
|
function delivery_decision(rule: DeliveredRule): TranscriptGuardSpec['should_block'] {
|
|
711
707
|
const { decide, already_satisfied } = rule
|
|
712
708
|
|
|
@@ -719,7 +715,7 @@ function delivery_decision(rule: DeliveredRule): TranscriptGuardSpec['should_blo
|
|
|
719
715
|
run: GuardRun,
|
|
720
716
|
): boolean {
|
|
721
717
|
if (already_satisfied?.(tail, call) === true) return false
|
|
722
|
-
if (decide === undefined) return
|
|
718
|
+
if (decide === undefined) return true
|
|
723
719
|
|
|
724
720
|
return decide(call, run, !will_batch_guard_refuse(tail, call, run), delivered_at_ms)
|
|
725
721
|
}
|
|
@@ -776,11 +772,11 @@ const delivered_rules = {
|
|
|
776
772
|
DELIVERED_RULES,
|
|
777
773
|
EARLY_HEARTBEAT_REASON: early_heartbeat.EARLY_HEARTBEAT_REASON,
|
|
778
774
|
FILE_BODY_REASON: file_body.FILE_BODY_REASON,
|
|
775
|
+
DIRECT_FILING_REASON: direct_filing.DIRECT_FILING_REASON,
|
|
779
776
|
FILING_CAP_REASON: filing_cap.FILING_CAP_REASON,
|
|
780
777
|
GIT_FORCE_REASON: git_force.GIT_FORCE_REASON,
|
|
781
778
|
ISSUE_COMMENTS_REASON,
|
|
782
779
|
ISSUE_FOLD_REASON: issue_fold.ISSUE_FOLD_REASON,
|
|
783
|
-
ISSUE_SCOUT_REASON: issue_scout.ISSUE_SCOUT_REASON,
|
|
784
780
|
LANE_INTERACTIVE_ASK_REASON: lane_interactive_ask.LANE_INTERACTIVE_ASK_REASON,
|
|
785
781
|
LANE_PARK_REASON: lane_park.LANE_PARK_REASON,
|
|
786
782
|
MEASURED_RULES,
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import { bash_triggers } from './bash-triggers'
|
|
2
|
+
|
|
3
|
+
// The `direct-filing` row of `delivered-rules.ts` (joshuafolkken/kit#2808). Filing an Issue owes a
|
|
4
|
+
// duplicate scout, a body lint, the classification / depth / `route:` labels, a `## Origin` on a
|
|
5
|
+
// cross-repository filing and an `epic:bundle` afterwards — steps spread over as many documents, each
|
|
6
|
+
// with its own guard, and joshuafolkken/kit#2805 ran two of them and skipped the rest. `josh issue:file`
|
|
7
|
+
// runs them all in one command, so a hand-built `gh api …/issues` / `gh issue create` filing is refused
|
|
8
|
+
// outright and pointed there; the step-by-step guards that used to sit on the direct call went with it.
|
|
9
|
+
|
|
10
|
+
const DIRECT_FILING_REASON =
|
|
11
|
+
'⛔ direct filing: an Issue is filed with `pnpm josh issue:file "<title>" --body-file <path> ' +
|
|
12
|
+
'--depth <0|1|2> [--route <route>] [--label <name>] [--repo <owner/repo>]`, never with `gh api …/issues` ' +
|
|
13
|
+
'or `gh issue create`. The command runs every filing step in order — the duplicate scout, the body ' +
|
|
14
|
+
'lint and its classification labels, the depth and `route:` labels, the `## Origin` check when filing ' +
|
|
15
|
+
'into another repository, and `epic:bundle` after the create — so none of them is a step to remember. ' +
|
|
16
|
+
'When the scout prints candidates it holds the filing: read each, then reissue with `--distinct <N,…>` ' +
|
|
17
|
+
'for the ones that are separate deliverables. Reference: `docs/josh-commands.md` → "`josh issue:file`". ' +
|
|
18
|
+
'**This rule fires on every occurrence, not once per run.**'
|
|
19
|
+
|
|
20
|
+
function is_any_filing(command: string): boolean {
|
|
21
|
+
return bash_triggers.is_direct_filing(command) || bash_triggers.is_issue_filing(command)
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
// `decide` returns true so it refuses every occurrence. Keeping the rule is filing through the command;
|
|
25
|
+
// the trigger is the violation itself, so the occasion it governs is a filing in either spelling
|
|
26
|
+
// (`reaches`, the `run-tail` shape).
|
|
27
|
+
const ROW = {
|
|
28
|
+
id: 'direct-filing',
|
|
29
|
+
is_trigger: bash_triggers.on_bash_command(bash_triggers.is_direct_filing),
|
|
30
|
+
reason: DIRECT_FILING_REASON,
|
|
31
|
+
decide: (): boolean => true,
|
|
32
|
+
keeps: bash_triggers.on_bash_command(bash_triggers.is_issue_filing),
|
|
33
|
+
reaches: bash_triggers.on_bash_command(is_any_filing),
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
const direct_filing = { DIRECT_FILING_REASON, ROW }
|
|
37
|
+
|
|
38
|
+
export { direct_filing }
|
|
@@ -21,7 +21,9 @@ import { tail_commands } from './tail-commands'
|
|
|
21
21
|
// **A guard-refused filing does not count.** Claude Code writes a denied call to the transcript as a
|
|
22
22
|
// `tool_use` block with an errored `tool_result`, so the WIP cap's own reissue would otherwise
|
|
23
23
|
// double-count one Issue — the same exclusion `investigation-reads.ts` makes for a refused read
|
|
24
|
-
// (joshuafolkken/kit#1764).
|
|
24
|
+
// (joshuafolkken/kit#1764). **Nor does one `issue:file` held itself** (joshuafolkken/kit#2808): a lint
|
|
25
|
+
// problem or an unacknowledged duplicate exits non-zero having filed nothing, and the reissue is the
|
|
26
|
+
// same Issue. The filing is counted only where its result did not fail.
|
|
25
27
|
|
|
26
28
|
const FILING_CAP = 10
|
|
27
29
|
// The current call is not yet on the tail, so the count read there is of filings *already* made. At
|
|
@@ -31,8 +33,9 @@ const CAP_REACHED = FILING_CAP
|
|
|
31
33
|
interface TailFilings {
|
|
32
34
|
// The `tool_use` id of every filing on the tail.
|
|
33
35
|
filing_ids: Array<string>
|
|
34
|
-
//
|
|
35
|
-
|
|
36
|
+
// The id of every result that came back as a failure — a guard refusal, or `issue:file` holding the
|
|
37
|
+
// filing itself (a lint problem, an unacknowledged duplicate), which files nothing either.
|
|
38
|
+
failures: Set<string>
|
|
36
39
|
}
|
|
37
40
|
|
|
38
41
|
function is_filing_use(block: Block): boolean {
|
|
@@ -42,7 +45,7 @@ function is_filing_use(block: Block): boolean {
|
|
|
42
45
|
}
|
|
43
46
|
|
|
44
47
|
function collect_block(block: Block, found: TailFilings): void {
|
|
45
|
-
if (block.result_id !== '') found.
|
|
48
|
+
if (block.result_id !== '' && block.is_error === true) found.failures.add(block.result_id)
|
|
46
49
|
if (is_filing_use(block)) found.filing_ids.push(block.id)
|
|
47
50
|
}
|
|
48
51
|
|
|
@@ -55,18 +58,18 @@ function collect_line(line: string, found: TailFilings): void {
|
|
|
55
58
|
}
|
|
56
59
|
|
|
57
60
|
function scan_tail(tail: string): TailFilings {
|
|
58
|
-
const found: TailFilings = { filing_ids: [],
|
|
61
|
+
const found: TailFilings = { filing_ids: [], failures: new Set<string>() }
|
|
59
62
|
|
|
60
63
|
for (const line of tail.split('\n')) collect_line(line, found)
|
|
61
64
|
|
|
62
65
|
return found
|
|
63
66
|
}
|
|
64
67
|
|
|
65
|
-
// How many Issues the run has already filed, counting only filings
|
|
68
|
+
// How many Issues the run has already filed, counting only filings whose result did not fail.
|
|
66
69
|
function prior_filing_count(tail: string): number {
|
|
67
|
-
const { filing_ids,
|
|
70
|
+
const { filing_ids, failures } = scan_tail(tail)
|
|
68
71
|
|
|
69
|
-
return filing_ids.filter((id) =>
|
|
72
|
+
return filing_ids.filter((id) => !failures.has(id)).length
|
|
70
73
|
}
|
|
71
74
|
|
|
72
75
|
// A person's prompt: a `user` line carrying no tool result. Tool results ride `user` lines too, so the
|
|
@@ -15,8 +15,8 @@ import { tail_commands } from './tail-commands'
|
|
|
15
15
|
// binds. The input is mechanical — has the run filed before, did it fold — so it is a delivered rule
|
|
16
16
|
// rather than resident prose.
|
|
17
17
|
//
|
|
18
|
-
// **It is `issue-scout`'s shape, not `filing-cap`'s.**
|
|
19
|
-
// stand-down read off the tail: a filing with no prior filing is stood down (nothing to fold), and so
|
|
18
|
+
// **It is `issue-scout`'s shape, not `filing-cap`'s.** Refused until an `already_satisfied`
|
|
19
|
+
// stand-down read off the tail answers: a filing with no prior filing is stood down (nothing to fold), and so
|
|
20
20
|
// is one the run has already folded. The refusal hands over the command rather than asking the run to
|
|
21
21
|
// "fold first". **No new predicate**: the filing trigger is `bash_triggers.is_issue_filing`, and the
|
|
22
22
|
// prior-filing count is `filing_cap.prior_filing_count`, both reused verbatim.
|
|
@@ -55,8 +55,8 @@ const ISSUE_FOLD_REASON =
|
|
|
55
55
|
'"<title>" …` — `fold` means file one Issue covering them, `separate` means file them apart ' +
|
|
56
56
|
'(separable findings whose combined size clears the split guide), and the default is `fold`. Add ' +
|
|
57
57
|
'`--not-separable` when they are really one deliverable. The procedure is ' +
|
|
58
|
-
'`.claude/skills/workflow-commands/split-assessment.md` → "The question".
|
|
59
|
-
'
|
|
58
|
+
'`.claude/skills/workflow-commands/split-assessment.md` → "The question". Every later filing is ' +
|
|
59
|
+
'refused until the fold is on the transcript.'
|
|
60
60
|
|
|
61
61
|
// The enumeration row itself, so `delivered-rules.ts` spreads one entry rather than restating the
|
|
62
62
|
// trigger and reason it already single-sources here. Once per run with the stand-down above.
|
|
@@ -11,8 +11,8 @@ import { tail_commands } from './tail-commands'
|
|
|
11
11
|
// `DeliveredRule` for each such oracle — refused when the run performs the governed action without
|
|
12
12
|
// having run the oracle's command earlier, stood down once it has.
|
|
13
13
|
//
|
|
14
|
-
// **It is `issue-scout`'s shape, generalized.** Each generated row
|
|
15
|
-
// `already_satisfied` stand-down
|
|
14
|
+
// **It is `issue-scout`'s shape, generalized.** Each generated row refuses until its
|
|
15
|
+
// `already_satisfied` stand-down reads the command off the tail — refuse the governed call, hand back the command,
|
|
16
16
|
// stand down once that command is on the tail. What `issue-scout` and `issue-fold` hand-wrote per
|
|
17
17
|
// oracle, this reads from the registry: the refusal is assembled from the oracle's decision, command,
|
|
18
18
|
// answer vocabulary and single source, so no oracle carries a hand-written refusal of its own. **No new
|
|
@@ -51,8 +51,8 @@ function reason_for(oracle: Oracle, firing_point: FiringPoint): string {
|
|
|
51
51
|
`⛔ decision oracle not consulted: this is ${firing_point.describes}, but the run has not run ` +
|
|
52
52
|
`\`${command_line(oracle)}\` first — the oracle that decides ${oracle.decision.toLowerCase()}. ` +
|
|
53
53
|
`It answers ${oracle.vocabulary.join(' | ')}; single source ${oracle.single_source}. Reissue ` +
|
|
54
|
-
`after running it and reading its verdict.
|
|
55
|
-
`
|
|
54
|
+
`after running it and reading its verdict. Every such call is refused until that command is on ` +
|
|
55
|
+
`the transcript.`
|
|
56
56
|
)
|
|
57
57
|
}
|
|
58
58
|
|
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
import { bash_triggers } from './bash-triggers'
|
|
2
1
|
import { decision_oracle } from './decision-oracle'
|
|
3
2
|
import { shell_segments } from './shell-segments'
|
|
4
3
|
|
|
@@ -32,9 +31,10 @@ function is_package_add(command: string): boolean {
|
|
|
32
31
|
return shell_segments.segments_of(command).some((segment) => PACKAGE_ADD.test(segment))
|
|
33
32
|
}
|
|
34
33
|
|
|
35
|
-
// The
|
|
36
|
-
//
|
|
37
|
-
//
|
|
34
|
+
// The firing point wired — the oracle whose governed call can be named without guessing: the package
|
|
35
|
+
// add. The Issue filing's `issue:lint` point left with joshuafolkken/kit#2808, when `josh issue:file`
|
|
36
|
+
// began running the lint itself. The rest declare why none can be named, so the unenforced oracles are
|
|
37
|
+
// visible rather than silently missing.
|
|
38
38
|
const FIRING: ReadonlyMap<string, FiringPoint> = new Map([
|
|
39
39
|
[
|
|
40
40
|
'pkg:scout',
|
|
@@ -43,13 +43,6 @@ const FIRING: ReadonlyMap<string, FiringPoint> = new Map([
|
|
|
43
43
|
governs: is_package_add,
|
|
44
44
|
},
|
|
45
45
|
],
|
|
46
|
-
[
|
|
47
|
-
'issue:lint',
|
|
48
|
-
{
|
|
49
|
-
describes: 'an Issue-filing call (`gh api …/issues`, `gh issue create`)',
|
|
50
|
-
governs: bash_triggers.is_issue_filing,
|
|
51
|
-
},
|
|
52
|
-
],
|
|
53
46
|
])
|
|
54
47
|
|
|
55
48
|
// The reason an oracle names no firing point, in three recurring shapes plus the one-off reasons for
|
|
@@ -66,9 +59,12 @@ const SELF = 'the command is itself the governed act; there is no earlier call t
|
|
|
66
59
|
const LATEST_SCOPE_REASON =
|
|
67
60
|
'the run it governs (`pnpm josh latest`) is already gated at its point of use by the latest-gate; ' +
|
|
68
61
|
'a firing here would double-gate it'
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
62
|
+
// `josh issue:file` runs the scout, the lint and `epic:bundle` itself, and a filing made any other way
|
|
63
|
+
// is refused by the `direct-filing` row (joshuafolkken/kit#2808) — so none of the three has a call left
|
|
64
|
+
// to gate before.
|
|
65
|
+
const FILING_STEP_REASON =
|
|
66
|
+
'`pnpm josh issue:file` runs it as one of its filing steps, and a direct filing is refused by the ' +
|
|
67
|
+
'`direct-filing` delivered rule; there is no separate call left to gate'
|
|
72
68
|
const RELEASE_SCOPE_REASON =
|
|
73
69
|
'it reads the release owed *after* `pnpm josh followup` has merged (`git_followup_pending`), so it ' +
|
|
74
70
|
'trails the merge rather than gating it; a firing on `followup` would gate a check that must run ' +
|
|
@@ -84,8 +80,9 @@ const NOT_NAMED: ReadonlyMap<string, string> = new Map([
|
|
|
84
80
|
['latest:scope', LATEST_SCOPE_REASON],
|
|
85
81
|
['run:hold', SELF],
|
|
86
82
|
['cost:cut', READ],
|
|
87
|
-
['epic:bundle',
|
|
88
|
-
['issue:scout',
|
|
83
|
+
['epic:bundle', FILING_STEP_REASON],
|
|
84
|
+
['issue:scout', FILING_STEP_REASON],
|
|
85
|
+
['issue:lint', FILING_STEP_REASON],
|
|
89
86
|
[
|
|
90
87
|
'issue:fold',
|
|
91
88
|
'the second filing it governs is already gated by the `issue-fold` delivered rule',
|
|
@@ -22,16 +22,13 @@ import { tail_commands } from './tail-commands'
|
|
|
22
22
|
// URL is stripped) all fail. What stays is the case the Issue names — writing rule prose into a rule
|
|
23
23
|
// document.
|
|
24
24
|
//
|
|
25
|
-
// **No `decide`, so once per run.** The refusal changes what the run *knows* — that a new rule owes
|
|
26
|
-
// question 0 and the ordering question — which is the shape `rule-delivery.md` says once-per-run is
|
|
27
|
-
// right for: told once, the run applies it to every later rule it writes.
|
|
28
|
-
//
|
|
29
25
|
// **The stand-down is what turned the reminder into a check** (joshuafolkken/kit#2324). Until now the
|
|
30
26
|
// row only *reminded* — it fired once and let the reissue through unconditionally, so "reissue and it
|
|
31
27
|
// goes" meant a computable rule could still be written into prose with nothing having answered the two
|
|
32
28
|
// questions. Now the pass condition is a *record*: the run has run `pnpm josh oracle:list` (question 0)
|
|
33
29
|
// and `pnpm josh run:step` (the ordering question) earlier in the transcript. Both present, the edit is
|
|
34
|
-
// stood down; either missing, it is refused
|
|
30
|
+
// stood down; either missing, it is refused — on the reissue too, since a row with a stand-down
|
|
31
|
+
// refuses until it is met (joshuafolkken/kit#2807). It passes on answering the questions, not on reading the
|
|
35
32
|
// reminder. Keeping the rule is therefore running those two commands, which `keeps` names.
|
|
36
33
|
|
|
37
34
|
// The two edit tools the `PreToolUse` matcher routes here. A `Write` carries the whole new file; an
|
|
@@ -133,7 +130,7 @@ function writes_rule_prose(
|
|
|
133
130
|
}
|
|
134
131
|
|
|
135
132
|
// The instruction in the shape a refusal can carry: what the edit is doing, the two questions it
|
|
136
|
-
// skips, the commands that answer them, and the reissue
|
|
133
|
+
// skips, the commands that answer them, and the reissue once they have run.
|
|
137
134
|
const RULE_BODY_REASON =
|
|
138
135
|
'⛔ rule written into prose: this edit adds rule text to CLAUDE.md, a `prompts/**` doc or a skill ' +
|
|
139
136
|
'body. Before it lands, run the residency questions the addition skips. A rule whose answer is ' +
|
|
@@ -144,9 +141,9 @@ const RULE_BODY_REASON =
|
|
|
144
141
|
'sequencing rule moves into. Only a rule that is neither — not computable, not ordering — stays ' +
|
|
145
142
|
'prose, and then as a trigger-plus-pointer line rather than a second copy of its procedure. The ' +
|
|
146
143
|
'criterion is `prompts/collaboration-workflow/residency.md` (question 0 and the ordering question); ' +
|
|
147
|
-
'the delivery enumeration is `prompts/collaboration-workflow/rule-delivery.md`.
|
|
148
|
-
'
|
|
149
|
-
'
|
|
144
|
+
'the delivery enumeration is `prompts/collaboration-workflow/rule-delivery.md`. Every such edit is ' +
|
|
145
|
+
'refused until both commands are on the transcript — run them, confirm the rule can be neither an ' +
|
|
146
|
+
'oracle nor an ordering step, then reissue this edit.'
|
|
150
147
|
|
|
151
148
|
// The two placement oracles the residency questions name: question 0 is `pnpm josh oracle:list`, the
|
|
152
149
|
// ordering question is `pnpm josh run:step` (its degenerate pre-implementation form `run:next` counts
|
|
@@ -136,8 +136,9 @@ const FILING_OFFER_REASON =
|
|
|
136
136
|
'⛔ filing offer: your reply offers to file an Issue instead of filing it. Filing into a ' +
|
|
137
137
|
'first-party repository is Tier A — the trigger is the judgement that it is worth filing, not the ' +
|
|
138
138
|
"run's progress (`.claude/skills/workflow-commands/SKILL.md` → §2i, `observation-filing.md`). Run " +
|
|
139
|
-
'`pnpm josh issue:
|
|
140
|
-
'one-line citation of what was filed
|
|
139
|
+
'`pnpm josh issue:file "<title>" --body-file <path> --depth <0|1|2>` — it scouts, lints, labels and ' +
|
|
140
|
+
'runs `epic:bundle` itself — then end with the one-line citation of what was filed; do not repeat ' +
|
|
141
|
+
'your previous reply. If it is not worth filing ' +
|
|
141
142
|
'after all, say so in one line instead.'
|
|
142
143
|
|
|
143
144
|
// **A headless parent's turn-end is its process's end** (joshuafolkken/kit#2437). Under `claude -p` the
|
|
@@ -320,7 +320,6 @@ const PILOT_FILES: ReadonlyArray<string> = [
|
|
|
320
320
|
'scripts/rules/issue-comments-rule.test.ts',
|
|
321
321
|
'scripts/rules/issue-depth-label.test.ts',
|
|
322
322
|
'scripts/rules/issue-fold.test.ts',
|
|
323
|
-
'scripts/rules/issue-scout.test.ts',
|
|
324
323
|
'scripts/rules/josh-git-bare.test.ts',
|
|
325
324
|
'scripts/rules/lane-interactive-ask-rule.test.ts',
|
|
326
325
|
'scripts/rules/lane-park-rule.test.ts',
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import { mkdtempSync, rmSync } from 'node:fs'
|
|
2
|
+
import { tmpdir } from 'node:os'
|
|
3
|
+
import path from 'node:path'
|
|
4
|
+
|
|
5
|
+
const CACHE_DIRECTORY_PREFIX = 'josh-latest-cache-'
|
|
6
|
+
const CACHE_DIRECTORY_FLAG = '--config.cache-dir='
|
|
7
|
+
|
|
8
|
+
// Points every stage at one metadata cache directory. pnpm keeps registry metadata there and the
|
|
9
|
+
// package store elsewhere, so an empty one costs a metadata refetch, never a tarball download.
|
|
10
|
+
function with_cache_directory(
|
|
11
|
+
stages: ReadonlyArray<ReadonlyArray<string>>,
|
|
12
|
+
cache_directory: string,
|
|
13
|
+
): Array<Array<string>> {
|
|
14
|
+
return stages.map((stage) => [...stage, `${CACHE_DIRECTORY_FLAG}${cache_directory}`])
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
// A cached metadata file written through safe-chain lacks every version that was under 24 hours old
|
|
18
|
+
// at the time, and with `minimumReleaseAge` set pnpm reuses a cache file younger than that age
|
|
19
|
+
// instead of refetching it — so a version that has since aged in stays invisible for up to a day
|
|
20
|
+
// (joshuafolkken/kit#2805). A fresh directory per run means nothing filtered earlier is ever read.
|
|
21
|
+
function run_in_fresh_cache<T>(
|
|
22
|
+
stages: ReadonlyArray<ReadonlyArray<string>>,
|
|
23
|
+
run_stages: (stages: Array<Array<string>>) => T,
|
|
24
|
+
): T {
|
|
25
|
+
const cache_directory = mkdtempSync(path.join(tmpdir(), CACHE_DIRECTORY_PREFIX))
|
|
26
|
+
|
|
27
|
+
try {
|
|
28
|
+
return run_stages(with_cache_directory(stages, cache_directory))
|
|
29
|
+
} finally {
|
|
30
|
+
rmSync(cache_directory, { recursive: true, force: true })
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
const fresh_metadata_cache = { with_cache_directory, run_in_fresh_cache }
|
|
35
|
+
|
|
36
|
+
export { fresh_metadata_cache }
|
|
@@ -11,6 +11,7 @@ import { lockfile_overrides } from '#scripts/overrides/lockfile-overrides'
|
|
|
11
11
|
import { overrides_files } from '#scripts/overrides/overrides-files'
|
|
12
12
|
import { overrides_check } from '#scripts/overrides/overrides-logic'
|
|
13
13
|
import { execaSync } from 'execa'
|
|
14
|
+
import { fresh_metadata_cache } from './fresh-metadata-cache'
|
|
14
15
|
import { latest_regression, type VersionRegression } from './latest-regression'
|
|
15
16
|
import { preinstall_version_update } from './preinstall-version-update'
|
|
16
17
|
|
|
@@ -99,7 +100,7 @@ function find_regressions_after_update(snapshot: TreeSnapshot): Array<VersionReg
|
|
|
99
100
|
// Exits zero: the tree is left exactly as it was found, nothing is broken, and every workflow that
|
|
100
101
|
// runs `josh latest` in its preamble would otherwise stop for a situation that resolves itself.
|
|
101
102
|
function update_without_downgrading(snapshot: TreeSnapshot): UpdateOutcome {
|
|
102
|
-
const status =
|
|
103
|
+
const status = fresh_metadata_cache.run_in_fresh_cache(build_update_commands(), run_update_stages)
|
|
103
104
|
if (status !== 0) return { status, is_rolled_back: false }
|
|
104
105
|
|
|
105
106
|
const regressions = find_regressions_after_update(snapshot)
|
|
@@ -1,85 +0,0 @@
|
|
|
1
|
-
import { readFileSync } from 'node:fs'
|
|
2
|
-
import path from 'node:path'
|
|
3
|
-
import { issue_classification } from '#scripts/issue/issue-classification'
|
|
4
|
-
import type { GuardRun } from '#scripts/josh/hook-decision'
|
|
5
|
-
import { time_density_hook } from '#scripts/time-runtime/time-density-hook'
|
|
6
|
-
import { time_shell } from '#scripts/time-runtime/time-shell'
|
|
7
|
-
import { bash_triggers } from './bash-triggers'
|
|
8
|
-
import { issue_filing_args } from './issue-filing-args'
|
|
9
|
-
import { shell_segments } from './shell-segments'
|
|
10
|
-
import { tail_commands } from './tail-commands'
|
|
11
|
-
|
|
12
|
-
const LINT_NAMES = new Set(['issue:lint'])
|
|
13
|
-
const LINT_PATH = /\b(?:issue:lint|iln)\s+(?:'([^']+)'|"([^"]+)"|(\S+))/u
|
|
14
|
-
|
|
15
|
-
function lint_argument(segment: string): string | undefined {
|
|
16
|
-
const [, single_quoted, double_quoted, bare] = LINT_PATH.exec(segment) ?? []
|
|
17
|
-
|
|
18
|
-
return single_quoted ?? double_quoted ?? bare
|
|
19
|
-
}
|
|
20
|
-
|
|
21
|
-
function latest_lint_path(tail: string): string | undefined {
|
|
22
|
-
const segments = tail_commands
|
|
23
|
-
.prior_bash_commands(tail)
|
|
24
|
-
.flatMap((command) => shell_segments.segments_of(command))
|
|
25
|
-
const lint = segments.findLast((segment) => shell_segments.is_josh_command(segment, LINT_NAMES))
|
|
26
|
-
|
|
27
|
-
return lint === undefined ? undefined : lint_argument(lint)
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
function linted_body(body_path: string): string | undefined {
|
|
31
|
-
try {
|
|
32
|
-
return readFileSync(body_path, 'utf8')
|
|
33
|
-
} catch {
|
|
34
|
-
return undefined
|
|
35
|
-
}
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
function matches_linted_file(command: string, lint_path: string): boolean {
|
|
39
|
-
const filing_path = issue_filing_args.body_file(command)
|
|
40
|
-
|
|
41
|
-
return filing_path !== undefined && path.resolve(filing_path) === path.resolve(lint_path)
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
function needs_classification_labels(command: string, tail: string): boolean {
|
|
45
|
-
const lint_path = latest_lint_path(tail)
|
|
46
|
-
|
|
47
|
-
if (lint_path === undefined) return false
|
|
48
|
-
const body = linted_body(lint_path)
|
|
49
|
-
|
|
50
|
-
if (body === undefined) return false
|
|
51
|
-
|
|
52
|
-
if (!matches_linted_file(command, lint_path) || issue_classification.problems(body).length > 0) {
|
|
53
|
-
return true
|
|
54
|
-
}
|
|
55
|
-
|
|
56
|
-
const expected = issue_classification.required_labels(body)
|
|
57
|
-
|
|
58
|
-
return issue_classification.CLASSIFICATION_LABELS.some(
|
|
59
|
-
(label) => expected.includes(label) !== issue_filing_args.has_label(command, label),
|
|
60
|
-
)
|
|
61
|
-
}
|
|
62
|
-
|
|
63
|
-
function decide(call: { input: unknown }, run: GuardRun): boolean {
|
|
64
|
-
const command = time_shell.bash_command(call.input)
|
|
65
|
-
const tail = time_density_hook.read_tail(run.transcript)
|
|
66
|
-
|
|
67
|
-
return needs_classification_labels(command, tail)
|
|
68
|
-
}
|
|
69
|
-
|
|
70
|
-
const ISSUE_CLASSIFICATION_REASON =
|
|
71
|
-
'⛔ issue classification mismatch: use the exact body file checked by `pnpm josh issue:lint` ' +
|
|
72
|
-
'in this creation call (`-F body=@<path>` or `gh issue create --body-file <path>`) and ' +
|
|
73
|
-
'apply exactly the `labels:` it reports (`bug`, `enhancement`, `breaking-change`). See ' +
|
|
74
|
-
'`prompts/collaboration-workflow/issue-template.md`.'
|
|
75
|
-
|
|
76
|
-
const ROW = {
|
|
77
|
-
id: 'issue-bug-label',
|
|
78
|
-
is_trigger: bash_triggers.on_bash_command(bash_triggers.is_issue_filing),
|
|
79
|
-
reason: ISSUE_CLASSIFICATION_REASON,
|
|
80
|
-
decide,
|
|
81
|
-
}
|
|
82
|
-
|
|
83
|
-
const issue_bug_label_rule = { ROW, latest_lint_path, needs_classification_labels }
|
|
84
|
-
|
|
85
|
-
export { issue_bug_label_rule }
|