@joshuafolkken/kit 1.1035.0 → 1.1050.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.
Files changed (118) hide show
  1. package/.claude/skills/verify-ui/SKILL.md +1 -1
  2. package/.claude/skills/workflow-commands/SKILL.md +2 -2
  3. package/.claude/skills/workflow-commands/background-commands.md +1 -1
  4. package/.claude/skills/workflow-commands/backlogrun-child.md +2 -2
  5. package/.claude/skills/workflow-commands/backlogrun-lanes.md +1 -1
  6. package/.claude/skills/workflow-commands/backlogrun-park.md +6 -0
  7. package/.claude/skills/workflow-commands/backlogrun-progress.md +30 -36
  8. package/.claude/skills/workflow-commands/backlogrun-steps.md +16 -16
  9. package/.claude/skills/workflow-commands/chain-rule.md +2 -2
  10. package/.claude/skills/workflow-commands/followup-reference.md +2 -2
  11. package/.claude/skills/workflow-commands/issue-comments.md +1 -1
  12. package/.claude/skills/workflow-commands/observation-filing.md +41 -86
  13. package/.claude/skills/workflow-commands/pre-gate-cut.md +33 -36
  14. package/.claude/skills/workflow-commands/working-tree-hold.md +2 -2
  15. package/.github/workflows/auto-tag.yml +21 -0
  16. package/.github/workflows/publish.yml +9 -10
  17. package/dist/hooks/batch-guard.js +4 -4
  18. package/dist/hooks/{chunk-HU66PWWL.js → chunk-5BK3K7V4.js} +89 -41
  19. package/dist/hooks/{chunk-GLBDBJOK.js → chunk-7VPDUBRN.js} +49 -151
  20. package/dist/hooks/{chunk-GJT4BNVE.js → chunk-DMQN45FN.js} +3 -3
  21. package/dist/hooks/{chunk-D67KVGGY.js → chunk-EJDX2TOB.js} +13 -3
  22. package/dist/hooks/{chunk-KG6PZD72.js → chunk-GQ7KP45M.js} +2 -2
  23. package/dist/hooks/{chunk-Y27M6A7X.js → chunk-HJIISEAC.js} +4 -4
  24. package/dist/hooks/{chunk-XPLP27WA.js → chunk-NAS4C7GP.js} +4 -26
  25. package/dist/hooks/{chunk-ZXNE6NY3.js → chunk-NJQOBE6L.js} +2 -2
  26. package/dist/hooks/{chunk-EIFCAJRB.js → chunk-RLDX5SOC.js} +1184 -946
  27. package/dist/hooks/{chunk-HOYJU5RM.js → chunk-WQ4OSBSS.js} +10 -3
  28. package/dist/hooks/codex-hook-adapter.js +10 -10
  29. package/dist/hooks/duplicate-read-guard.js +5 -5
  30. package/dist/hooks/format-edited.js +15 -34
  31. package/dist/hooks/investigation-guard.js +5 -5
  32. package/dist/hooks/pretool-guard.js +23 -16
  33. package/dist/hooks/stop-guard.js +13 -188
  34. package/dist/josh.js +4 -106
  35. package/dist/version/index.js +381 -9
  36. package/package.json +6 -6
  37. package/prompts/collaboration-workflow/file-edits.md +6 -53
  38. package/prompts/collaboration-workflow/residency.md +1 -1
  39. package/prompts/collaboration-workflow/rule-delivery.md +4 -35
  40. package/prompts/collaboration-workflow/shell-body.md +7 -9
  41. package/prompts/collaboration-workflow/turn-batching.md +21 -37
  42. package/prompts/collaboration-workflow/wip-cap.md +15 -18
  43. package/prompts/sonar-hotspot-handling.md +1 -1
  44. package/scripts/adopt/adopt-toolkits.ts +2 -0
  45. package/scripts/auto-ok/auto-ok-cli.ts +9 -9
  46. package/scripts/backlog/backlog-next.ts +24 -8
  47. package/scripts/backlog/backlog-plan-cli.ts +5 -1
  48. package/scripts/backlog/backlog-pool.ts +31 -11
  49. package/scripts/backlog/backlog-rank.ts +106 -0
  50. package/scripts/backlog/backlog-waves.ts +42 -29
  51. package/scripts/build/build-hooks.ts +7 -2
  52. package/scripts/delegation/delegation-policy.ts +11 -0
  53. package/scripts/document/document-byte-budget.ts +5 -5
  54. package/scripts/document/entry-read-budget.ts +5 -4
  55. package/scripts/epic/epic-lane-offer.ts +11 -22
  56. package/scripts/epic/epic-rank.ts +37 -0
  57. package/scripts/epic/epic-solo.ts +29 -28
  58. package/scripts/git/git-command.ts +1 -1
  59. package/scripts/git/git-execa-error.ts +6 -1
  60. package/scripts/git/git-next-issues.ts +34 -5
  61. package/scripts/git/git-preflight.ts +22 -4
  62. package/scripts/git/git-push-transport.ts +12 -69
  63. package/scripts/git/git-spawn.ts +79 -3
  64. package/scripts/git/git-ssh-keepalive.ts +74 -0
  65. package/scripts/git/gone-branch.ts +3 -2
  66. package/scripts/git/issue-labels.ts +12 -0
  67. package/scripts/git/issue-rank.ts +89 -0
  68. package/scripts/hooks/format-edited-cli.ts +15 -0
  69. package/scripts/hooks/format-edited-file.ts +2 -9
  70. package/scripts/hooks/pretool-guard-cli.ts +21 -0
  71. package/scripts/hooks/pretool-guard.ts +14 -15
  72. package/scripts/init/start-exec.ts +7 -1
  73. package/scripts/init/start-plan.ts +18 -5
  74. package/scripts/init/start-setup-pr.ts +46 -19
  75. package/scripts/init/start-steps.ts +41 -9
  76. package/scripts/init/start.ts +40 -10
  77. package/scripts/issue/issue-cite.ts +19 -1
  78. package/scripts/josh/josh-command-map.ts +0 -102
  79. package/scripts/josh/josh-commands-development.ts +2 -2
  80. package/scripts/josh/josh-commands-project.ts +1 -1
  81. package/scripts/lane/lane-child-marker.ts +16 -2
  82. package/scripts/lane/lane-cli.ts +32 -14
  83. package/scripts/lane/lane-report.ts +10 -4
  84. package/scripts/propagate/propagate-plan.ts +185 -0
  85. package/scripts/propagate/propagate-run.ts +5 -1
  86. package/scripts/propagate/propagate-steps.ts +16 -1
  87. package/scripts/propagate/propagate-targets.ts +13 -2
  88. package/scripts/propagate/propagate.ts +60 -42
  89. package/scripts/registry-migration/migrate-logic.ts +1 -0
  90. package/scripts/release/github-release.ts +1 -1
  91. package/scripts/review/review-attest.ts +44 -0
  92. package/scripts/review/review-brief-cli.ts +49 -2
  93. package/scripts/review/review-brief.ts +46 -1
  94. package/scripts/review/review-stamps.ts +7 -0
  95. package/scripts/rules/implementation-cut.ts +1 -1
  96. package/scripts/rules/issue-fold.ts +1 -2
  97. package/scripts/rules/lane-carry-conflict.ts +1 -2
  98. package/scripts/rules/lane-park.ts +1 -2
  99. package/scripts/rules/pre-gate-cut.ts +2 -2
  100. package/scripts/run/run-headless.ts +22 -6
  101. package/scripts/run/run-parent-cut-hook.ts +144 -0
  102. package/scripts/run/run-progress-read.ts +2 -1
  103. package/scripts/run/run-progress.ts +21 -8
  104. package/scripts/run/run-ship-cli.ts +6 -74
  105. package/scripts/run/run-ship-preflight.ts +87 -0
  106. package/scripts/run/run-ship-review-steps.ts +5 -26
  107. package/scripts/run/run-ship-review.ts +9 -0
  108. package/scripts/run/run-ship-scoped.ts +44 -0
  109. package/scripts/run/run-ship-stage.ts +5 -1
  110. package/scripts/run/run-ship-steps.ts +105 -0
  111. package/scripts/run/run-ship.ts +2 -0
  112. package/scripts/run/run-wake-cli.ts +2 -2
  113. package/scripts/run/run-wake-describe.ts +19 -1
  114. package/scripts/run/run-watcher-hook.ts +1 -1
  115. package/scripts/time-runtime/time-bundle-call.ts +2 -2
  116. package/scripts/version/npm-registry.ts +97 -0
  117. package/scripts/version/version-commands.ts +5 -4
  118. package/scripts/version/version-remote.ts +26 -12
@@ -1,10 +1,8 @@
1
- #!/usr/bin/env tsx
2
- import { text } from 'node:stream/consumers'
3
- import { fileURLToPath } from 'node:url'
4
1
  import { duplicate_read_outcome } from '#scripts/delegation/duplicate-read-guard'
5
2
  import { investigation_refusal } from '#scripts/delegation/investigation-guard'
6
- import { hook_decision, type GuardOutcome } from '#scripts/josh/hook-decision'
3
+ import type { GuardOutcome } from '#scripts/josh/hook-decision'
7
4
  import { delivered_rules } from '#scripts/rules/delivered-rules'
5
+ import { run_parent_cut_hook } from '#scripts/run/run-parent-cut-hook'
8
6
  import { run_watcher_hook } from '#scripts/run/run-watcher-hook'
9
7
  import { batch_outcome } from './batch-guard'
10
8
 
@@ -71,26 +69,27 @@ function pretool_outcome(raw_payload: string): GuardOutcome {
71
69
  // as `combine_outcomes` orders them, and the watcher's refusal fills a clear verdict rather than
72
70
  // overriding one. It fires once per run itself (`run-watcher-hook.ts`), so a stale watcher refuses the
73
71
  // first call but not the `pnpm josh run:progress --wait` that fixes it.
72
+ //
73
+ // **The parent hand-off guard is the second asynchronous rule, asked after the watcher's**
74
+ // (joshuafolkken/kit#2947). It reads the carry record to tell the `backlogrun` parent apart, so it sits
75
+ // beside the watcher rather than among the synchronous rows; a stale watcher is the cheaper fix and is
76
+ // surfaced first.
77
+ async function async_reason(raw_payload: string): Promise<string | undefined> {
78
+ const watcher = await run_watcher_hook.watcher_hook_reason(raw_payload)
79
+
80
+ return watcher ?? (await run_parent_cut_hook.parent_cut_reason(raw_payload))
81
+ }
82
+
74
83
  async function pretool_outcome_async(raw_payload: string): Promise<GuardOutcome> {
75
84
  const base = pretool_outcome(raw_payload)
76
85
 
77
86
  if (!is_clear(base)) return base
78
87
 
79
- const reason = await run_watcher_hook.watcher_hook_reason(raw_payload)
88
+ const reason = await async_reason(raw_payload)
80
89
 
81
90
  return reason === undefined ? base : { reason, notice: undefined, fault: undefined }
82
91
  }
83
92
 
84
93
  const pretool_guard = { combine_outcomes, pretool_outcome, pretool_outcome_async }
85
94
 
86
- if (process.argv[1] === fileURLToPath(import.meta.url)) {
87
- if (process.stdin.isTTY) hook_decision.report_no_payload('pretool:guard')
88
- else {
89
- const raw_payload = await text(process.stdin)
90
-
91
- hook_decision.load_environment_file()
92
- hook_decision.emit_outcome(await pretool_outcome_async(raw_payload))
93
- }
94
- }
95
-
96
95
  export { pretool_guard, pretool_outcome, pretool_outcome_async }
@@ -23,6 +23,12 @@ function run(command: string, args: ReadonlyArray<string>, root: string): void {
23
23
  execaSync(command, args, { ...git_gh_exec.direct_environment(), cwd: root, stdio: 'inherit' })
24
24
  }
25
25
 
26
- const start_exec = { succeeds, read_output, run }
26
+ // A caller's own initialize command stands in for the in-process `josh init`, so it keeps the
27
+ // environment that setup would have had — its install goes through a scanner's proxy as usual.
28
+ function run_local(command: string, args: ReadonlyArray<string>, root: string): void {
29
+ execaSync(command, args, { cwd: root, stdio: 'inherit' })
30
+ }
31
+
32
+ const start_exec = { succeeds, read_output, run, run_local }
27
33
 
28
34
  export { start_exec }
@@ -8,6 +8,7 @@ interface StartOptions {
8
8
  is_yes: boolean
9
9
  is_github: boolean
10
10
  visibility: Visibility
11
+ init_command: string | undefined
11
12
  }
12
13
 
13
14
  type StartStep = 'git_init' | 'initialize' | 'commit' | 'repository' | 'labels' | 'setup_pr'
@@ -137,20 +138,31 @@ function github_consent(
137
138
  }
138
139
  }
139
140
 
141
+ // A run whose initialize step was handed to a caller's command names that command wherever the step
142
+ // is printed, so the plan, the progress and a failure all say what actually runs (#2872).
143
+ function step_labels(init_command: string | undefined): Readonly<Record<StartStep, string>> {
144
+ if (init_command === undefined) return STEP_LABELS
145
+
146
+ return { ...STEP_LABELS, initialize: `Initialize with ${init_command}` }
147
+ }
148
+
140
149
  // What a failed run prints, so the user can tell how far it got before deciding how to resume.
141
150
  function progress_report(
142
151
  failed: StartStep,
143
152
  completed: ReadonlyArray<StartStep>,
144
153
  cause: string,
154
+ labels: Readonly<Record<StartStep, string>> = STEP_LABELS,
145
155
  ): string {
146
- const done =
147
- completed.length === 0 ? 'nothing' : completed.map((step) => STEP_LABELS[step]).join('; ')
156
+ const done = completed.length === 0 ? 'nothing' : completed.map((step) => labels[step]).join('; ')
148
157
 
149
- return `josh start stopped at: ${STEP_LABELS[failed]}\nCompleted: ${done}\nCause: ${cause}`
158
+ return `josh start stopped at: ${labels[failed]}\nCompleted: ${done}\nCause: ${cause}`
150
159
  }
151
160
 
152
- function plan_summary(steps: ReadonlyArray<StartStep>): string {
153
- const lines = steps.map((step, index) => ` ${String(index + 1)}. ${STEP_LABELS[step]}`)
161
+ function plan_summary(
162
+ steps: ReadonlyArray<StartStep>,
163
+ labels: Readonly<Record<StartStep, string>> = STEP_LABELS,
164
+ ): string {
165
+ const lines = steps.map((step, index) => ` ${String(index + 1)}. ${labels[step]}`)
154
166
 
155
167
  return ['josh start will:', ...lines].join('\n')
156
168
  }
@@ -161,6 +173,7 @@ const start_plan = {
161
173
  github_consent,
162
174
  progress_report,
163
175
  plan_summary,
176
+ step_labels,
164
177
  STEP_LABELS,
165
178
  }
166
179
  export { start_plan }
@@ -12,15 +12,22 @@ import { start_exec } from './start-exec'
12
12
  const SETUP_TITLE = `Set up ${KIT_PACKAGE_NAME}`
13
13
  const SETUP_BODY = [
14
14
  `Adds the files \`josh start\` wrote while setting up ${KIT_PACKAGE_NAME}, so main carries the`,
15
- 'rules, settings and workflows every later Issue is run with. Only the files kit wrote are in the',
16
- 'pull request; anything else in the working tree is left as it was.',
15
+ 'rules, settings and workflows every later Issue is run with. Only the files the setup wrote are in',
16
+ 'the pull request; anything else in the working tree is left as it was.',
17
17
  ].join('\n')
18
+ const NAME_ONLY = '--name-only'
18
19
  const NOTHING_TO_COMMIT = 'The kit setup has no changes to commit, so no pull request was opened.'
19
20
  const OPEN_SETUP_ISSUES_QUERY = `?state=open&labels=${IGNORE_FOR_RELEASE_LABEL}&per_page=100`
20
21
  const FIRST_SETUP_ISSUE_FILTER = `[.[] | select(.title == "${SETUP_TITLE}") | .number][0] // empty`
21
22
 
22
- function lines_of(output: string | undefined): Array<string> {
23
- return (output ?? '').split('\n').filter((line) => line.length > 0)
23
+ // Listings are read NUL-separated (`-z`), so a path is never quoted — a name a caller's initialize
24
+ // command wrote need not be ASCII the way kit's own are (#2872).
25
+ function paths_of(output: string | undefined): Array<string> {
26
+ return (output ?? '').split('\0').filter((listed) => listed.length > 0)
27
+ }
28
+
29
+ function listing(args: ReadonlyArray<string>, root: string): Array<string> {
30
+ return paths_of(start_exec.read_output('git', [...args, '-z'], root))
24
31
  }
25
32
 
26
33
  function setup_issue(issue_number: string): IssueInfo {
@@ -42,17 +49,18 @@ function is_setup_branch(branch: string | undefined): boolean {
42
49
  }
43
50
 
44
51
  // Untracked files the ignore rules let through, plus tracked files the setup changed — two plain
45
- // listings, so nothing has to be parsed out of `git status`. Every kit path is ASCII, so git's
46
- // quoting of other names never hides one.
52
+ // listings, so nothing has to be parsed out of `git status`.
47
53
  function changed_paths(root: string): Array<string> {
48
- const untracked = start_exec.read_output(
49
- 'git',
50
- ['ls-files', '--others', '--exclude-standard'],
51
- root,
52
- )
53
- const modified = start_exec.read_output('git', ['diff', '--name-only', 'HEAD'], root)
54
-
55
- return [...lines_of(untracked), ...lines_of(modified)]
54
+ const untracked = listing(['ls-files', '--others', '--exclude-standard'], root)
55
+ const modified = listing(['diff', NAME_ONLY, 'HEAD'], root)
56
+
57
+ return [...untracked, ...modified]
58
+ }
59
+
60
+ // On the setup branch a failed commit left, that run's `git add` already staged its files, so they
61
+ // read as changed before this run's command ran — the staged ones count as setup output (#2872).
62
+ function carried_paths(root: string, current: string | undefined): Array<string> {
63
+ return is_setup_branch(current) ? listing(['diff', NAME_ONLY, '--cached'], root) : []
56
64
  }
57
65
 
58
66
  // The Issue carries the release classification the pull request is opened with, so the PR step finds
@@ -113,10 +121,30 @@ function commit_kit_paths(paths: ReadonlyArray<string>, issue: IssueInfo, root:
113
121
  start_exec.run('git', ['commit', '--message', issue.commit_message, '--', ...paths], root)
114
122
  }
115
123
 
116
- // Issue → branch → only kit's files committed → push → pull request. The merge is left to a person:
117
- // it is the one step that changes main.
118
- async function open(root: string): Promise<void> {
119
- const paths = kit_written_paths.select(changed_paths(root))
124
+ // A caller's initialize command writes files kit cannot name, so with a `baseline` — what was already
125
+ // changed before that command ran — everything changed since joins kit's own files, while a change
126
+ // the user had made before the run still stays out (#2872).
127
+ function setup_paths(
128
+ root: string,
129
+ baseline: ReadonlyArray<string> | undefined,
130
+ current: string | undefined,
131
+ ): Array<string> {
132
+ const changed = changed_paths(root)
133
+ const kit_paths = kit_written_paths.select(changed)
134
+
135
+ if (baseline === undefined) return kit_paths
136
+
137
+ const carried = carried_paths(root, current)
138
+ const written = changed.filter((path) => !baseline.includes(path) || carried.includes(path))
139
+
140
+ return [...new Set([...kit_paths, ...written])]
141
+ }
142
+
143
+ // Issue → branch → only the setup's files committed → push → pull request. The merge is left to a
144
+ // person: it is the one step that changes main.
145
+ async function open(root: string, baseline?: ReadonlyArray<string>): Promise<void> {
146
+ const current = start_exec.read_output('git', ['symbolic-ref', '--short', 'HEAD'], root)
147
+ const paths = setup_paths(root, baseline, current)
120
148
 
121
149
  if (paths.length === 0) {
122
150
  console.info(NOTHING_TO_COMMIT)
@@ -124,7 +152,6 @@ async function open(root: string): Promise<void> {
124
152
  return
125
153
  }
126
154
 
127
- const current = start_exec.read_output('git', ['symbolic-ref', '--short', 'HEAD'], root)
128
155
  const issue = resolve_issue(current)
129
156
 
130
157
  switch_to(issue.branch_name, current, root)
@@ -2,6 +2,7 @@ import path from 'node:path'
2
2
  import { error_text } from '#scripts/lib/error-message'
3
3
  import { repository_labels } from '#scripts/repo/repository-labels'
4
4
  import { self_sync_guard } from '#scripts/self-sync-guard/self-sync-guard-logic'
5
+ import { parseCommandString } from 'execa'
5
6
  import { main as init_main } from './init'
6
7
  import { PACKAGE_DIR } from './init-paths'
7
8
  import { kit_setup_state } from './kit-setup-state'
@@ -14,6 +15,11 @@ interface StepContext {
14
15
  root: string
15
16
  profile: ProjectProfile
16
17
  visibility: Visibility
18
+ init_command: string | undefined
19
+ }
20
+
21
+ interface RunContext extends StepContext {
22
+ baseline: ReadonlyArray<string> | undefined
17
23
  }
18
24
 
19
25
  const DEFAULT_BRANCH = 'main'
@@ -22,7 +28,7 @@ const GH_INSTALL_HINT =
22
28
  const GH_AUTH_HINT =
23
29
  'GitHub CLI is not signed in. Run gh auth login, then run josh start again. Nothing was changed.'
24
30
 
25
- const { succeeds, read_output, run } = start_exec
31
+ const { succeeds, read_output, run, run_local } = start_exec
26
32
 
27
33
  function read_git_state(root: string, shape: ProjectShape): GitState {
28
34
  const { has_git, has_github } = shape
@@ -67,8 +73,19 @@ function create_git_repository(context: StepContext): void {
67
73
  }
68
74
 
69
75
  // The same setup `josh init` runs, called rather than copied, with the profile the user confirmed.
76
+ // A toolkit that layers its files over kit's (`josh-app init`) names its own setup instead, so every
77
+ // other step and guard stays here rather than in a copy of this command (#2872). That command runs
78
+ // without a shell, is given the same profile, and a non-zero exit stops the run before the commit.
70
79
  function initialize(context: StepContext): void {
71
- init_main(['--profile', context.profile])
80
+ if (context.init_command === undefined) {
81
+ init_main(['--profile', context.profile])
82
+
83
+ return
84
+ }
85
+
86
+ const [command = '', ...args] = parseCommandString(context.init_command)
87
+
88
+ run_local(command, [...args, '--profile', context.profile], context.root)
72
89
  }
73
90
 
74
91
  function commit_all(context: StepContext): void {
@@ -97,11 +114,22 @@ function add_missing_labels(): void {
97
114
 
98
115
  // Main already has a history here, so the setup reaches it the way every other change does: through
99
116
  // an Issue and a pull request a person merges (joshuafolkken/kit#2816).
100
- async function open_setup_pull_request(context: StepContext): Promise<void> {
101
- await start_setup_pr.open(context.root)
117
+ async function open_setup_pull_request(context: RunContext): Promise<void> {
118
+ await start_setup_pr.open(context.root, context.baseline)
102
119
  }
103
120
 
104
- type StepAction = (context: StepContext) => void | Promise<void>
121
+ // What was already changed before a caller's initialize command ran, read only when the setup pull
122
+ // request will need to tell that command's files from the user's own (#2872).
123
+ function baseline_of(
124
+ steps: ReadonlyArray<StartStep>,
125
+ context: StepContext,
126
+ ): ReadonlyArray<string> | undefined {
127
+ if (context.init_command === undefined || !steps.includes('setup_pr')) return undefined
128
+
129
+ return start_setup_pr.changed_paths(context.root)
130
+ }
131
+
132
+ type StepAction = (context: RunContext) => void | Promise<void>
105
133
 
106
134
  const STEP_ACTIONS: Readonly<Record<StartStep, StepAction>> = {
107
135
  git_init: create_git_repository,
@@ -114,24 +142,28 @@ const STEP_ACTIONS: Readonly<Record<StartStep, StepAction>> = {
114
142
 
115
143
  async function run_step(
116
144
  step: StartStep,
117
- context: StepContext,
145
+ context: RunContext,
118
146
  completed: ReadonlyArray<StartStep>,
119
147
  ): Promise<void> {
120
148
  try {
121
149
  await STEP_ACTIONS[step](context)
122
150
  } catch (error) {
123
151
  const cause = error_text.message_of(error)
152
+ const labels = start_plan.step_labels(context.init_command)
124
153
 
125
- throw new Error(start_plan.progress_report(step, completed, cause), { cause: error })
154
+ throw new Error(start_plan.progress_report(step, completed, cause, labels), { cause: error })
126
155
  }
127
156
  }
128
157
 
129
158
  async function run_steps(steps: ReadonlyArray<StartStep>, context: StepContext): Promise<void> {
159
+ const labels = start_plan.step_labels(context.init_command)
160
+ const run_context: RunContext = { ...context, baseline: baseline_of(steps, context) }
161
+
130
162
  for (const [index, step] of steps.entries()) {
131
163
  const position = `${String(index + 1)}/${String(steps.length)}`
132
164
 
133
- console.info(`\n[${position}] ${start_plan.STEP_LABELS[step]}`)
134
- await run_step(step, context, steps.slice(0, index))
165
+ console.info(`\n[${position}] ${labels[step]}`)
166
+ await run_step(step, run_context, steps.slice(0, index))
135
167
  }
136
168
  }
137
169
 
@@ -14,7 +14,9 @@ import { start_steps } from './start-steps'
14
14
 
15
15
  const ARGUMENT_START_INDEX = 2
16
16
  const READY_MESSAGE = 'GitHub workflow ready. Use kickoff new in your assistant to plan an Issue.'
17
- const START_USAGE = 'josh start [--profile basic|full] [--yes] [--github] [--public]'
17
+ const START_USAGE =
18
+ 'josh start [--profile basic|full] [--yes] [--github] [--public] [--init-command <command>]'
19
+ const INIT_COMMAND_FLAG = '--init-command'
18
20
  const YES_FLAG = '--yes'
19
21
  const GITHUB_FLAG = '--github'
20
22
  const PUBLIC_FLAG = '--public'
@@ -26,16 +28,42 @@ interface Prepared {
26
28
  choices: Choices
27
29
  }
28
30
 
29
- // The switches are peeled off first, so what remains is exactly the `--profile` pair `josh init`
30
- // accepts — one parser for the profile in both commands.
31
+ interface InitCommandSplit {
32
+ init_command: string | undefined
33
+ rest: ReadonlyArray<string>
34
+ }
35
+
36
+ // A value that is blank or another flag means the command itself was left out, which would otherwise
37
+ // swallow the next switch or hand the initialize step nothing to run.
38
+ function is_command_value(value: string | undefined): value is string {
39
+ return value !== undefined && value.trim() !== '' && !value.startsWith('-')
40
+ }
41
+
42
+ // The command arrives as the one argument after its flag and is removed together with it.
43
+ function split_init_command(args: ReadonlyArray<string>): InitCommandSplit {
44
+ const index = args.indexOf(INIT_COMMAND_FLAG)
45
+
46
+ if (index === -1) return { init_command: undefined, rest: args }
47
+
48
+ const value = args[index + 1]
49
+
50
+ if (!is_command_value(value)) throw new Error(`Usage: ${START_USAGE}`)
51
+
52
+ return { init_command: value, rest: args.filter((_, at) => at !== index && at !== index + 1) }
53
+ }
54
+
55
+ // The switches and the initialize command are peeled off first, so what remains is exactly the
56
+ // `--profile` pair `josh init` accepts — one parser for the profile in both commands.
31
57
  function parse_start_options(args: ReadonlyArray<string>): StartOptions {
32
- const profile_args = args.filter((argument) => !SWITCHES.has(argument))
58
+ const { init_command, rest } = split_init_command(args)
59
+ const profile_args = rest.filter((argument) => !SWITCHES.has(argument))
33
60
 
34
61
  return {
35
62
  profile: project_profile.requested_profile(profile_args, START_USAGE),
36
- is_yes: args.includes(YES_FLAG),
37
- is_github: args.includes(GITHUB_FLAG),
38
- visibility: args.includes(PUBLIC_FLAG) ? 'public' : 'private',
63
+ is_yes: rest.includes(YES_FLAG),
64
+ is_github: rest.includes(GITHUB_FLAG),
65
+ visibility: rest.includes(PUBLIC_FLAG) ? 'public' : 'private',
66
+ init_command,
39
67
  }
40
68
  }
41
69
 
@@ -83,11 +111,13 @@ function prepare(args: ReadonlyArray<string>, is_tty: boolean, root: string): Pr
83
111
  async function main(args: ReadonlyArray<string>, is_tty: boolean): Promise<void> {
84
112
  const { steps, interaction, choices } = prepare(args, is_tty, process.cwd())
85
113
 
86
- console.info(start_plan.plan_summary(steps))
87
- const profile = await resolve_profile(choices, interaction)
88
114
  const { root, options } = choices
115
+ const { visibility, init_command } = options
116
+
117
+ console.info(start_plan.plan_summary(steps, start_plan.step_labels(init_command)))
118
+ const profile = await resolve_profile(choices, interaction)
89
119
 
90
- await start_steps.run_steps(steps, { root, profile, visibility: options.visibility })
120
+ await start_steps.run_steps(steps, { root, profile, visibility, init_command })
91
121
  console.info(`\n${READY_MESSAGE}`)
92
122
  }
93
123
 
@@ -40,6 +40,23 @@ function citation_line(slug: string, number: string, summary: string): string {
40
40
  return `[#${number}](${issue_url(slug, number)})${SUMMARY_SEPARATOR}${summary}`
41
41
  }
42
42
 
43
+ // How a listing names one issue, given what it already holds (joshuafolkken/kit#2943). A progress
44
+ // command's output is copied into a report verbatim, so the line it prints is the citation the report
45
+ // carries: the full form when the title is in hand, the bare number-link when only the repository is,
46
+ // and the plain `#N` only when the repository could not be read at all.
47
+ type IssueCiter = (number: string) => string
48
+
49
+ function citer(slug: string | undefined, titles: ReadonlyMap<string, string>): IssueCiter {
50
+ return function cite(number: string): string {
51
+ if (slug === undefined) return `#${number}`
52
+ const title = titles.get(number)
53
+
54
+ return title === undefined
55
+ ? `[#${number}](${issue_url(slug, number)})`
56
+ : citation_line(slug, number, title)
57
+ }
58
+ }
59
+
43
60
  function qualified_target(token: string): CiteTarget | undefined {
44
61
  const match = REPO_QUALIFIED.exec(token)
45
62
  if (match === null) return undefined
@@ -90,6 +107,7 @@ function no_repo_line(target: CiteTarget): string {
90
107
  const issue_cite = {
91
108
  SUMMARY_SEPARATOR,
92
109
  citation_line,
110
+ citer,
93
111
  issue_url,
94
112
  label,
95
113
  missing_line,
@@ -98,5 +116,5 @@ const issue_cite = {
98
116
  unreadable_line,
99
117
  }
100
118
 
101
- export type { CiteTarget }
119
+ export type { CiteTarget, IssueCiter }
102
120
  export { issue_cite }
@@ -38,8 +38,6 @@ const ALIASES: Record<string, string> = {
38
38
  ln: 'lines',
39
39
  by: 'bytes',
40
40
  f: 'format',
41
- fd: 'format:edited',
42
- bg: 'batch:guard',
43
41
  sd: 'cspell:dot',
44
42
  bh: 'behavior',
45
43
  t: 'test',
@@ -48,8 +46,6 @@ const ALIASES: Record<string, string> = {
48
46
  td: 'test:declared',
49
47
  trd: 'test:red',
50
48
  te: 'test:e2e',
51
- er: 'e2e:retry-check',
52
- sl: 'session:lang',
53
49
  c: 'check',
54
50
  pt: 'port',
55
51
  i: 'init',
@@ -57,19 +53,12 @@ const ALIASES: Record<string, string> = {
57
53
  st: 'start',
58
54
  sy: 'sync',
59
55
  rmi: 'registry:migrate',
60
- sys: 'sync:scope',
61
56
  g: 'git',
62
57
  gp: 'pr',
63
- prc: 'pr:classification',
64
- fu: 'followup',
65
- nf: 'notify',
66
- obf: 'observations:flush',
67
- mrr: 'measure:rerun',
68
58
  ms: 'main:sync',
69
59
  mm: 'main:merge',
70
60
  bp: 'bump',
71
61
  re: 'release',
72
- res: 'release:scope',
73
62
  v: 'version',
74
63
  r: 'ranges',
75
64
  dr: 'doctor',
@@ -77,107 +66,16 @@ const ALIASES: Record<string, string> = {
77
66
  ad: 'adopt',
78
67
  ov: 'overrides',
79
68
  a: 'audit',
80
- ap: 'audit:provision',
81
69
  rt: 'reconcile-templates',
82
70
  u: 'latest',
83
- lg: 'latest:guard',
84
71
  lc: 'latest:corepack',
85
72
  lu: 'latest:update',
86
- ls: 'latest:scope',
87
- pm: 'prevent-main-commit',
88
- cm: 'check-commit-message',
89
- ss: 'secretlint-scan',
90
- ppu: 'pre-push-unit',
91
- rer: 'reserved-run',
92
- ptc: 'pre-commit-type-check',
93
- ird: 'issue:read',
94
- ist: 'issue:state',
95
- isc: 'issue:scout',
96
- ifl: 'issue:file',
97
- isf: 'issue:fold',
98
- isfe: 'issue:fold-existing',
99
- dfr: 'defect:rate',
100
- ici: 'issue:cite',
101
- icm: 'issue:comment',
102
- pks: 'pkg:scout',
103
- iln: 'issue:lint',
104
- ibl: 'issue:backlinks',
105
- rl: 'report:lint',
106
- ep: 'epic',
107
- ec: 'epic:check',
108
- en: 'epic:next',
109
- ea: 'epic:audit',
110
- eb: 'epic:bundle',
111
- ao: 'auto-ok:next',
112
- bl: 'backlog:next',
113
- blo: 'backlog:offer',
114
- bld: 'backlog:drive',
115
- blp: 'backlog:plan',
116
- bls: 'backlog:stalled',
117
- bb: 'backlog:budget',
118
- co: 'cost',
119
- ds: 'doc:section',
120
- rs: 'read:set',
121
- dcr: 'doc:read',
122
- rf: 'read:files',
123
- ef: 'edit:files',
124
- rfs: 'refactor:scan',
125
73
  tm: 'time',
126
74
  tmd: 'time:density',
127
75
  rtr: 'retrospective',
128
- rb: 'review:brief',
129
- r2: 'review:round2',
130
- ra: 'review:attest',
131
- rvr: 'review:record',
132
76
  rvf: 'review:findings',
133
- dp: 'disposition',
134
- dg: 'delegate',
135
- fo: 'fanout',
136
- rh: 'run:hold',
137
- rr: 'run:release',
138
- rc: 'run:carry',
139
- rtd: 'run:tidy',
140
- rw: 'run:wake',
141
- rct: 'run:cut',
142
- rv: 'run:liveness',
143
- red: 'run:ending',
144
- rg: 'run:progress',
145
- rp: 'run:prep',
146
- ren: 'run:entry',
147
- rtl: 'run:tail',
148
- shp: 'ship',
149
- rst: 'run:status',
150
- rn: 'run:next',
151
- rsp: 'run:step',
152
- rpy: 'repo:party',
153
- rmg: 'run:merge',
154
- rrp: 'run:report',
155
- rrv: 'run:review',
156
- rev: 'run:event',
157
- sp: 'stash:pop',
158
- lno: 'lane:open',
159
- lnc: 'lane:close',
160
- lnl: 'lane:list',
161
- lnp: 'lane:prune',
162
- lnv: 'lane:output',
163
- lnd: 'lane:dispatch',
164
- lna: 'lane:await',
165
- lnla: 'lane:launch',
166
- rwg: 'run:watcher:guard',
167
- rsd: 'run:stranded',
168
- ig: 'investigation:guard',
169
- drg: 'duplicate-read:guard',
170
- rug: 'rule:guard',
171
77
  ruv: 'rule:value',
172
- ptg: 'pretool:guard',
173
- sg: 'stop:guard',
174
78
  ev: 'eval',
175
- ol: 'oracle:list',
176
- sa: 'split:assess',
177
- shs: 'sonar:hotspots',
178
- uir: 'ui:routes',
179
- cs: 'clone:scan',
180
- ca: 'cases',
181
79
  }
182
80
 
183
81
  // The canonical name of a `josh` subcommand: an alias expands, and anything else passes through
@@ -66,7 +66,7 @@ const DEV_COMMANDS: Record<string, CommandEntry> = {
66
66
  reference: ['', 'developer', ['files', 'processes']],
67
67
  },
68
68
  'format:edited': {
69
- script: 'scripts/hooks/format-edited-file.ts',
69
+ script: 'scripts/hooks/format-edited-cli.ts',
70
70
  description: 'Claude Code hook: format the file just edited (reads the tool call on stdin)',
71
71
  category: 'Development',
72
72
  reference: ['', 'automation', ['files', 'processes']],
@@ -85,7 +85,7 @@ const DEV_COMMANDS: Record<string, CommandEntry> = {
85
85
  // node's own `--env-file` parser with node's own precedence.
86
86
  },
87
87
  'pretool:guard': {
88
- script: 'scripts/hooks/pretool-guard.ts',
88
+ script: 'scripts/hooks/pretool-guard-cli.ts',
89
89
  description:
90
90
  'Claude Code hook: the batch, investigation and rule guards in one process (reads the tool call on stdin)',
91
91
  category: 'Development',
@@ -17,7 +17,7 @@ const PROJECT_COMMANDS: Record<string, CommandEntry> = {
17
17
  description: 'Set a project up for the GitHub Issue workflow, from git init to the setup PR',
18
18
  category: 'Project',
19
19
  reference: [
20
- `${PROFILE_ARGUMENTS} [--yes] [--github] [--public]`,
20
+ `${PROFILE_ARGUMENTS} [--yes] [--github] [--public] [--init-command <command>]`,
21
21
  'developer',
22
22
  ['files', 'git', 'network', 'processes'],
23
23
  ],
@@ -19,6 +19,15 @@ import { lane_paths } from './lane-paths'
19
19
  // `.claude/skills/workflow-commands/pre-gate-cut.md`.
20
20
  const KEY = 'JOSH_LANE_CHILD'
21
21
 
22
+ // **A dispatched child also asks for the five-minute prompt-cache TTL** (joshuafolkken/kit#2944). A
23
+ // headless `claude -p` session on a subscription defaults to the one-hour TTL, whose writes bill at 2x
24
+ // the input rate against 1.25x for five minutes. A replay of 1,953 lane transcripts (about 57,000
25
+ // requests) found 0.3% of request gaps over five minutes, so the shorter TTL — re-writing the prefix on
26
+ // those few gaps — still costs 12-13% less. Every launcher of a child composes its environment here,
27
+ // so this is the one place the choice is made.
28
+ const CACHE_TTL_KEY = 'CLAUDE_CODE_PROMPT_CACHE_TTL'
29
+ const CACHE_TTL = '5m'
30
+
22
31
  type MarkerSource = Readonly<Record<string, string | undefined>>
23
32
 
24
33
  // The environment fragment that marks a child dispatched for `#<issue>`. Composed from a digits-only
@@ -27,7 +36,7 @@ type MarkerSource = Readonly<Record<string, string | undefined>>
27
36
  function environment_for(issue: string): Record<string, string> {
28
37
  run_issue_number.require_issue_number(issue)
29
38
 
30
- return { [KEY]: issue }
39
+ return { [KEY]: issue, [CACHE_TTL_KEY]: CACHE_TTL }
31
40
  }
32
41
 
33
42
  // The issue a child was dispatched for, or `undefined` when the mark is absent or malformed. A blank
@@ -52,7 +61,12 @@ function is_child_of(directory: string, source: MarkerSource = process.env): boo
52
61
  return marked !== undefined && marked === lane_paths.lane_issue_of(directory, { ...source })
53
62
  }
54
63
 
55
- const lane_child_marker = { KEY, env_for: environment_for, is_child_of, marked_issue }
64
+ const lane_child_marker = {
65
+ KEY,
66
+ env_for: environment_for,
67
+ is_child_of,
68
+ marked_issue,
69
+ }
56
70
 
57
71
  export type { MarkerSource }
58
72
  export { lane_child_marker }