@joshuafolkken/kit 1.1094.0 → 1.1128.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/workflow-commands/SKILL.md +1 -1
- package/.claude/skills/workflow-commands/background-commands.md +36 -85
- package/.claude/skills/workflow-commands/backlogrun-progress.md +29 -32
- package/.claude/skills/workflow-commands/backlogrun-steps.md +2 -2
- package/.claude/skills/workflow-commands/fullrun.md +5 -4
- package/.claude/skills/workflow-commands/halfrun.md +5 -4
- package/.claude/skills/workflow-commands/latest-gate.md +7 -15
- package/.claude/skills/workflow-commands/pre-gate-cut.md +4 -8
- package/.claude/skills/workflow-commands/split-assessment.md +4 -18
- package/.github/actions/setup-node/action.yml +33 -0
- package/.github/actions/setup-pnpm/action.yml +4 -4
- package/.github/workflows/auto-tag.yml +2 -3
- package/.github/workflows/ci.yml +7 -8
- package/.github/workflows/dependabot-auto-merge.yml +1 -0
- package/.github/workflows/pr-classification.yml +1 -3
- package/.github/workflows/production.yml +1 -0
- package/.github/workflows/publish.yml +10 -9
- package/.github/workflows/sonar-qube.yml +17 -13
- package/.pnpmfile.mjs +29 -0
- package/AGENTS.md +4 -11
- package/CLAUDE.md +24 -17
- package/GEMINI.md +4 -13
- package/README.md +8 -3
- package/SECURITY.md +1 -0
- package/cspell/index.yaml +4 -0
- package/dist/CLAUDE.basic.md +6 -6
- package/dist/CLAUDE.md +24 -17
- package/dist/CLAUDE.static.md +6 -6
- package/dist/config-merge/index.js +33 -9
- package/dist/hooks/batch-guard.js +4 -4
- package/dist/hooks/{chunk-NKOG3XAV.js → chunk-4JZDP5X5.js} +2 -2
- package/dist/hooks/{chunk-3C4YCCEF.js → chunk-5SD2YM7Z.js} +4 -4
- package/dist/hooks/{chunk-OIZUHPJI.js → chunk-77J7IEQM.js} +1 -1
- package/dist/hooks/{chunk-KHWHKJOM.js → chunk-7HVB7SZG.js} +3 -3
- package/dist/hooks/{chunk-UD24EAIL.js → chunk-AIOIOMPX.js} +1875 -104
- package/dist/hooks/{chunk-M2XDCUKD.js → chunk-M7QKEKWO.js} +88 -1783
- package/dist/hooks/{chunk-E54KGFEQ.js → chunk-MKVGBQPA.js} +1 -1
- package/dist/hooks/{chunk-7ZPFAVR7.js → chunk-NXYMNPUQ.js} +3 -3
- package/dist/hooks/{chunk-O4LL4HTI.js → chunk-QVLT52OV.js} +2 -2
- package/dist/hooks/{chunk-IL27YTPH.js → chunk-XYUIQOW3.js} +2 -2
- package/dist/hooks/codex-hook-adapter.js +12 -47
- package/dist/hooks/duplicate-read-guard.js +5 -5
- package/dist/hooks/format-edited.js +2 -2
- package/dist/hooks/inputs.json +180 -179
- package/dist/hooks/investigation-guard.js +5 -5
- package/dist/hooks/pretool-guard.js +9 -9
- package/dist/hooks/stop-guard.js +17 -12
- package/dist/josh.js +79 -30
- package/dist/version/index.js +36 -20
- package/eslint/rules/unicorn.js +3 -2
- package/package.json +18 -2
- package/pnpm-workspace.yaml +5 -7
- package/prompts/collaboration-workflow/glossary.md +17 -0
- package/prompts/collaboration-workflow/issue-template.md +1 -1
- package/prompts/collaboration-workflow/operating-rules.md +1 -1
- package/prompts/collaboration-workflow/principles.md +9 -0
- package/prompts/collaboration-workflow.md +1 -0
- package/scripts/backlog/backlog-arrival.ts +30 -10
- package/scripts/backlog/backlog-budget-cli.ts +15 -16
- package/scripts/backlog/backlog-offer-cli.ts +6 -14
- package/scripts/backlog/backlog-plan.ts +29 -18
- package/scripts/backlog/backlog-ready.ts +20 -7
- package/scripts/config-merge/parse-jsonc.ts +33 -3
- package/scripts/config-merge/patch-json-key.ts +13 -6
- package/scripts/cost-runtime/cost-cli.ts +3 -7
- package/scripts/doctor/doctor-consumer.ts +3 -5
- package/scripts/doctor/doctor-io.ts +9 -13
- package/scripts/document/backlogrun-parent-read-set.ts +12 -1
- package/scripts/document/document-byte-budget.ts +1 -0
- package/scripts/document/entry-read-budget.ts +13 -7
- package/scripts/document/entry-read-set.ts +32 -8
- package/scripts/document/lane-child-read-set.ts +7 -0
- package/scripts/document/read-set-point-of-use.ts +8 -3
- package/scripts/document/read-set-trim.ts +3 -1
- package/scripts/epic/epic-audit.ts +3 -18
- package/scripts/gh/git-gh-issue-list.ts +12 -11
- package/scripts/gh/git-gh-issue-write.ts +4 -1
- package/scripts/gh/git-gh-pr-auto-merge.ts +4 -1
- package/scripts/git/git-command.ts +6 -1
- package/scripts/git/git-spawn-sync.ts +65 -0
- package/scripts/git/git-spawn.ts +5 -4
- package/scripts/git/stash/stash-pop-args.ts +1 -6
- package/scripts/init/init-file-action.ts +54 -0
- package/scripts/init/init-logic-migrate.ts +35 -17
- package/scripts/init/init-logic-secretlint.ts +1 -1
- package/scripts/init/init-logic.ts +6 -22
- package/scripts/init/init.ts +11 -68
- package/scripts/init/kit-written-paths.ts +2 -0
- package/scripts/init/project-profile.ts +3 -6
- package/scripts/init/start-steps.ts +2 -1
- package/scripts/issue/defect-rate-cli.ts +61 -20
- package/scripts/issue/issue-backlinks.ts +7 -14
- package/scripts/issue/issue-cite.ts +12 -6
- package/scripts/issue/issue-file.ts +5 -12
- package/scripts/issue/markdown-section.ts +21 -6
- package/scripts/issue/measure-rerun-cli.ts +73 -14
- package/scripts/josh/josh-commands-project.ts +1 -1
- package/scripts/josh/josh-commands-workflow.ts +1 -1
- package/scripts/josh/run-github-prerequisite.ts +3 -6
- package/scripts/lane/lane-await.ts +43 -13
- package/scripts/lane/lane-close.ts +4 -1
- package/scripts/lane/lane-launch-cli.ts +10 -16
- package/scripts/lane/openai-review-broker.ts +4 -1
- package/scripts/lib/cli-flags.ts +31 -2
- package/scripts/lib/error-message.ts +12 -1
- package/scripts/lines/line-targets.ts +0 -11
- package/scripts/lines/lines-command.ts +5 -3
- package/scripts/propagate/propagate-git.ts +3 -8
- package/scripts/refactor/refactor-scan.ts +2 -1
- package/scripts/registry-migration/migrate-logic.ts +28 -19
- package/scripts/registry-migration/migrate.ts +6 -5
- package/scripts/release/github-release-cli.ts +14 -3
- package/scripts/release/release-publish.ts +36 -2
- package/scripts/release/release-worktree.ts +97 -2
- package/scripts/review/review-checkout.ts +3 -2
- package/scripts/review/review-record-cli.ts +9 -16
- package/scripts/rules/early-heartbeat.ts +4 -3
- package/scripts/rules/lane-park.ts +9 -0
- package/scripts/rules/stop-rules.ts +7 -4
- package/scripts/run/run-carry-args.ts +8 -12
- package/scripts/run/run-cut-args.ts +1 -6
- package/scripts/run/run-ending-cli.ts +1 -5
- package/scripts/run/run-entry-cli.ts +40 -9
- package/scripts/run/run-entry-stop.ts +104 -0
- package/scripts/run/run-event-stream-emit.ts +7 -2
- package/scripts/run/run-hold.ts +4 -1
- package/scripts/run/run-liveness-cli.ts +1 -5
- package/scripts/run/run-merge-cli.ts +2 -10
- package/scripts/run/run-parent-cut-hook.ts +4 -1
- package/scripts/run/run-prep-cli.ts +4 -1
- package/scripts/run/run-progress-args.ts +20 -4
- package/scripts/run/run-progress-cli.ts +31 -29
- package/scripts/run/run-progress-clock.ts +4 -1
- package/scripts/run/run-ship-cli.ts +8 -16
- package/scripts/run/run-ship-probe.ts +4 -1
- package/scripts/run/run-status-cli.ts +2 -15
- package/scripts/run/run-tidy-lanes.ts +4 -1
- package/scripts/run/run-wake-cli.ts +2 -10
- package/scripts/run/run-watcher-guard.ts +9 -5
- package/scripts/safe-chain/pack-hook.ts +92 -0
- package/scripts/sync/sync-ai-files.ts +4 -0
- package/scripts/test/test-declared-changed.ts +13 -16
- package/scripts/test/test-declared.ts +3 -7
- package/scripts/version/version-commands.ts +11 -1
- package/templates/CLAUDE.basic.md +6 -6
- package/templates/workflows/ci.yml +16 -82
- package/templates/workflows/dependabot-auto-merge.yml +1 -0
- package/templates/workflows/github-release.yml +1 -3
- package/tsconfig.sonar.json +4 -22
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
#!/usr/bin/env tsx
|
|
2
2
|
import { fileURLToPath } from 'node:url'
|
|
3
|
-
import { parseArgs } from 'node:util'
|
|
4
3
|
import { api_outage } from '#scripts/agent/api-outage'
|
|
5
4
|
import { CONTEXT_CUT_THRESHOLD } from '#scripts/cost-runtime/context-cut-threshold'
|
|
6
5
|
import { issue_closing_pr } from '#scripts/issue/issue-closing-pr'
|
|
7
6
|
import { issue_state_cli } from '#scripts/issue/issue-state-cli'
|
|
8
7
|
import { lane_handoff } from '#scripts/lane/lane-handoff'
|
|
8
|
+
import { cli_flags } from '#scripts/lib/cli-flags'
|
|
9
9
|
import { run_carry, type CarryOwner, type RunCarry } from './run-carry'
|
|
10
10
|
import { run_ending } from './run-ending'
|
|
11
11
|
import { run_event_stream } from './run-event-stream'
|
|
@@ -69,14 +69,6 @@ interface ParsedArguments {
|
|
|
69
69
|
values: Partial<Record<'epic' | 'output' | 'over' | 'owner' | 'repo', string>>
|
|
70
70
|
}
|
|
71
71
|
|
|
72
|
-
function read_args(argv: ReadonlyArray<string>): ParsedArguments | undefined {
|
|
73
|
-
try {
|
|
74
|
-
return parseArgs({ args: [...argv], options: OPTIONS, allowPositionals: true, strict: true })
|
|
75
|
-
} catch {
|
|
76
|
-
return undefined
|
|
77
|
-
}
|
|
78
|
-
}
|
|
79
|
-
|
|
80
72
|
// Absent owner is "no owner declared", which the carry guard reads as not provably foreign; a present
|
|
81
73
|
// but malformed pid invalidates the invocation rather than silently counting unguarded.
|
|
82
74
|
function to_owner(raw: string | undefined): CarryOwner | undefined {
|
|
@@ -170,7 +162,7 @@ function to_context(parsed: ParsedArguments): MergeContext | undefined {
|
|
|
170
162
|
}
|
|
171
163
|
|
|
172
164
|
function parse(argv: ReadonlyArray<string>): MergeContext | undefined {
|
|
173
|
-
const parsed =
|
|
165
|
+
const parsed = cli_flags.arguments_of(argv, OPTIONS)
|
|
174
166
|
|
|
175
167
|
return parsed === undefined ? undefined : to_context(parsed)
|
|
176
168
|
}
|
|
@@ -3,6 +3,7 @@ import { cost_cli, type CostVerdict } from '#scripts/cost-runtime/cost-cli'
|
|
|
3
3
|
import { cost_format } from '#scripts/cost-runtime/cost-format'
|
|
4
4
|
import { cost_verdict } from '#scripts/cost-runtime/cost-verdict'
|
|
5
5
|
import { hook_decision } from '#scripts/josh/hook-decision'
|
|
6
|
+
import { error_text } from '#scripts/lib/error-message'
|
|
6
7
|
import { implementation_cut_verdict } from '#scripts/rules/implementation-cut-verdict'
|
|
7
8
|
import { run_carry, type RunCarry } from './run-carry'
|
|
8
9
|
import { run_headless } from './run-headless'
|
|
@@ -104,7 +105,9 @@ async function refusal_for(
|
|
|
104
105
|
function is_unit_call(raw_payload: string): boolean {
|
|
105
106
|
try {
|
|
106
107
|
return (hook_decision.parse_hook_payload(raw_payload)?.agent_id ?? '') !== ''
|
|
107
|
-
} catch {
|
|
108
|
+
} catch (error) {
|
|
109
|
+
error_text.trace_swallowed('run_parent_cut_hook.is_unit_call', error)
|
|
110
|
+
|
|
108
111
|
return false
|
|
109
112
|
}
|
|
110
113
|
}
|
|
@@ -4,6 +4,7 @@ import { git_stash } from '#scripts/git/stash/git-stash'
|
|
|
4
4
|
import { issue_read_cli, type BlockRead } from '#scripts/issue/issue-read-cli'
|
|
5
5
|
import { issue_state_cli, type StateRead } from '#scripts/issue/issue-state-cli'
|
|
6
6
|
import { lane_child_marker } from '#scripts/lane/lane-child-marker'
|
|
7
|
+
import { error_text } from '#scripts/lib/error-message'
|
|
7
8
|
import { latest_scope_cli } from '#scripts/version/latest-scope-cli'
|
|
8
9
|
import { run_prep, type PrepParts } from './run-prep'
|
|
9
10
|
import { run_prep_locate } from './run-prep-locate'
|
|
@@ -82,7 +83,9 @@ async function lane_has_changes(): Promise<boolean> {
|
|
|
82
83
|
|
|
83
84
|
try {
|
|
84
85
|
return await git_stash.has_changes(process.cwd())
|
|
85
|
-
} catch {
|
|
86
|
+
} catch (error) {
|
|
87
|
+
error_text.trace_swallowed('run_prep_cli.lane_has_changes', error)
|
|
88
|
+
|
|
86
89
|
return false
|
|
87
90
|
}
|
|
88
91
|
}
|
|
@@ -12,6 +12,12 @@ import { run_progress_config } from './run-progress-config'
|
|
|
12
12
|
// only a heartbeat the caller restarts on the next interval, so the two guard different things and
|
|
13
13
|
// only one has anything to lose by being short.
|
|
14
14
|
const DEFAULT_MAX_HOURS = 1
|
|
15
|
+
// **`--wait` is bounded by a run, not by three intervals** (joshuafolkken/kit#3102). It no longer exits
|
|
16
|
+
// at its first report — the reports go to the event stream — so its one-hour bound would be the last
|
|
17
|
+
// wake that asks the parent for no judgement, a restart an hour. Eight hours is `run:hold`'s expiry;
|
|
18
|
+
// what ends a finished run's watcher sooner is `josh followup` removing its life record, and a stop
|
|
19
|
+
// tearing it down in the same turn.
|
|
20
|
+
const DEFAULT_WAIT_MAX_HOURS = 8
|
|
15
21
|
const MS_PER_HOUR = 3_600_000
|
|
16
22
|
|
|
17
23
|
const OPTIONS = {
|
|
@@ -37,7 +43,7 @@ interface ParsedValues {
|
|
|
37
43
|
}
|
|
38
44
|
|
|
39
45
|
function read_arguments(argv: ReadonlyArray<string>): ParsedValues | undefined {
|
|
40
|
-
return cli_flags.
|
|
46
|
+
return cli_flags.values_of(argv, OPTIONS)
|
|
41
47
|
}
|
|
42
48
|
|
|
43
49
|
// A hand-typed `--interval` outranks the environment, the environment outranks the interval the
|
|
@@ -48,14 +54,24 @@ function to_interval_ms(raw: string | undefined): number {
|
|
|
48
54
|
return run_progress_config.resolve_interval_ms(raw)
|
|
49
55
|
}
|
|
50
56
|
|
|
51
|
-
function
|
|
57
|
+
function default_hours(is_wait: boolean): number {
|
|
58
|
+
return is_wait ? DEFAULT_WAIT_MAX_HOURS : DEFAULT_MAX_HOURS
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
function to_max_ms(raw: string | undefined, is_wait = false): number {
|
|
52
62
|
const hours = Number(raw)
|
|
53
63
|
const is_usable = raw !== undefined && Number.isFinite(hours) && hours > 0
|
|
54
64
|
|
|
55
|
-
return (is_usable ? hours :
|
|
65
|
+
return (is_usable ? hours : default_hours(is_wait)) * MS_PER_HOUR
|
|
56
66
|
}
|
|
57
67
|
|
|
58
|
-
const run_progress_args = {
|
|
68
|
+
const run_progress_args = {
|
|
69
|
+
DEFAULT_MAX_HOURS,
|
|
70
|
+
DEFAULT_WAIT_MAX_HOURS,
|
|
71
|
+
read_arguments,
|
|
72
|
+
to_interval_ms,
|
|
73
|
+
to_max_ms,
|
|
74
|
+
}
|
|
59
75
|
|
|
60
76
|
export type { ParsedValues }
|
|
61
77
|
export { run_progress_args }
|
|
@@ -51,7 +51,7 @@ const TICK_SECONDS = 30
|
|
|
51
51
|
const DECLINE_RETRY_SECONDS = 120
|
|
52
52
|
// The clock a fresh loop starts with: nothing has declined yet, so nothing is being waited out.
|
|
53
53
|
const NO_RETRY = 0
|
|
54
|
-
const {
|
|
54
|
+
const { DEFAULT_WAIT_MAX_HOURS } = run_progress_args
|
|
55
55
|
const ENVIRONMENT_KEY = 'JOSH_PROGRESS'
|
|
56
56
|
const DISABLED_VALUE = '0'
|
|
57
57
|
|
|
@@ -74,7 +74,7 @@ const UNREADABLE_NOTICE =
|
|
|
74
74
|
'The `in-progress` listing could not be read, so nothing is reported. That is not "nothing is running" — check `gh auth status` and ask again.'
|
|
75
75
|
const FAILED_TICK_PREFIX =
|
|
76
76
|
'A progress reading failed, so nothing is reported for it. The watcher is still running, and will read again after the cooldown:'
|
|
77
|
-
const WAIT_EXPIRED_NOTICE = `The watch bound (\`--hours\`, ${String(
|
|
77
|
+
const WAIT_EXPIRED_NOTICE = `The watch bound (\`--hours\`, ${String(DEFAULT_WAIT_MAX_HOURS)} by default for \`--wait\`) ran out with no newly runnable work arriving. Every report it made is already on the run's event stream (\`pnpm josh run:event --watch\`), so there is nothing to relay. Starting another \`--wait\` resumes the same clock.`
|
|
78
78
|
// The repository name is only ever printed, never written against, so the bounded lookup is the right
|
|
79
79
|
// one: a `gh` call that hangs would otherwise block the synchronous read at startup and leave the
|
|
80
80
|
// watcher neither running nor saying so.
|
|
@@ -104,6 +104,10 @@ interface WatchOptions {
|
|
|
104
104
|
output_paths: ReadonlyArray<string>
|
|
105
105
|
repo: string
|
|
106
106
|
tick_ms: number
|
|
107
|
+
// `--wait` alone: the line goes to the run's event stream and the ambient log, never to standard
|
|
108
|
+
// output, because a background command's output reaches the session only as a wake
|
|
109
|
+
// (joshuafolkken/kit#3102).
|
|
110
|
+
is_stream_only?: boolean
|
|
107
111
|
}
|
|
108
112
|
|
|
109
113
|
interface EmitContext {
|
|
@@ -202,7 +206,7 @@ async function emit(options: WatchOptions, context: EmitContext): Promise<EmitRe
|
|
|
202
206
|
unchanged_since_ms: state.unchanged_since_ms,
|
|
203
207
|
})
|
|
204
208
|
|
|
205
|
-
console.info(line)
|
|
209
|
+
if (options.is_stream_only !== true) console.info(line)
|
|
206
210
|
|
|
207
211
|
return { kind: 'observed', state, line }
|
|
208
212
|
}
|
|
@@ -287,8 +291,8 @@ async function step(options: WatchOptions, target: string, loop: WatchLoop): Pro
|
|
|
287
291
|
/**
|
|
288
292
|
* The loop both reporting forms are, with one difference between them.
|
|
289
293
|
*
|
|
290
|
-
* `watch` runs until `--hours` expires; `--wait` stops at the first
|
|
291
|
-
* (joshuafolkken/kit#
|
|
294
|
+
* `watch` runs until `--hours` expires; `--wait` stops at the first arrival and keeps its lines off
|
|
295
|
+
* standard output (joshuafolkken/kit#3102). Everything else — where the clock is seeded from, the tick, the decline
|
|
292
296
|
* cooldown `step` keeps — is one implementation on purpose: two copies would let a fix to the bound or
|
|
293
297
|
* to the initial silence reach one form and not the other, and `--wait` is the form a run starts.
|
|
294
298
|
*/
|
|
@@ -301,17 +305,14 @@ function seed_loop(target: string, started_ms: number): WatchLoop {
|
|
|
301
305
|
// Why the loop stopped, so `drive` prints the bound-expired notice only when the bound is what
|
|
302
306
|
// expired — not when `josh followup` ended the watcher, which is a clean stop with nothing to report.
|
|
303
307
|
// `arrived` is a `--wait` ended early by newly runnable work (joshuafolkken/kit#2503).
|
|
304
|
-
type WatchExit = '
|
|
308
|
+
type WatchExit = 'arrived' | 'ended' | 'bound'
|
|
305
309
|
|
|
306
|
-
// `wait` is present for `--wait` alone, which ends at the first
|
|
307
|
-
//
|
|
308
|
-
//
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
wait: ArrivalProbe | undefined,
|
|
312
|
-
): Promise<WatchExit | undefined> {
|
|
310
|
+
// `wait` is present for `--wait` alone, which ends at the first arrival its probe sees. **A report is
|
|
311
|
+
// not an exit** (joshuafolkken/kit#3102): every exit wakes the session that started it, and a wake
|
|
312
|
+
// spent relaying a line the event stream already carries is the most expensive turn a run takes, so
|
|
313
|
+
// only what the parent has to act on ends the wait.
|
|
314
|
+
async function wait_exit(wait: ArrivalProbe | undefined): Promise<WatchExit | undefined> {
|
|
313
315
|
if (wait === undefined) return undefined
|
|
314
|
-
if (loop.state !== undefined) return 'reported'
|
|
315
316
|
|
|
316
317
|
return (await wait.has_arrived(Date.now())) ? 'arrived' : undefined
|
|
317
318
|
}
|
|
@@ -335,8 +336,8 @@ function final_exit(life: string): WatchExit {
|
|
|
335
336
|
// The loop both reporting forms share, reading its own liveness record at the top of each pass — the
|
|
336
337
|
// `run-wake-loop.ts` `run_loop` shape (joshuafolkken/kit#1727). A record gone means `josh followup`
|
|
337
338
|
// removed it at the merge, so the watcher stops at once instead of waiting out its bound
|
|
338
|
-
// (joshuafolkken/kit#1821); `--wait` also stops at the first
|
|
339
|
-
//
|
|
339
|
+
// (joshuafolkken/kit#1821); `--wait` also stops at the first arrival, and otherwise the loop runs
|
|
340
|
+
// until `--hours` expires.
|
|
340
341
|
async function run_ticks(
|
|
341
342
|
options: WatchOptions,
|
|
342
343
|
target: string,
|
|
@@ -354,7 +355,7 @@ async function run_ticks(
|
|
|
354
355
|
loop = await step(options, target, loop)
|
|
355
356
|
|
|
356
357
|
// eslint-disable-next-line no-await-in-loop -- polling: each read waits on the state the previous one saw
|
|
357
|
-
const exit = await wait_exit(
|
|
358
|
+
const exit = await wait_exit(wait)
|
|
358
359
|
|
|
359
360
|
if (exit !== undefined) return exit
|
|
360
361
|
}
|
|
@@ -397,14 +398,16 @@ async function once(options: WatchOptions): Promise<number> {
|
|
|
397
398
|
}
|
|
398
399
|
|
|
399
400
|
/**
|
|
400
|
-
*
|
|
401
|
-
*
|
|
401
|
+
* The form a run starts in the background: it reports on its own and wakes the caller only for work
|
|
402
|
+
* (joshuafolkken/kit#3102).
|
|
402
403
|
*
|
|
403
|
-
* **
|
|
404
|
-
* output
|
|
405
|
-
*
|
|
406
|
-
*
|
|
407
|
-
*
|
|
404
|
+
* **A report is not a wake.** It once ended at its first line so a harness that delivers a background
|
|
405
|
+
* command's output only on exit would relay it (joshuafolkken/kit#1576) — which cost the orchestrating
|
|
406
|
+
* session one turn per heartbeat, re-reading a hundred thousand tokens of context to pass on a line it
|
|
407
|
+
* was forbidden to change. Every line already reaches the run's event stream as a `heartbeat` and the
|
|
408
|
+
* ambient log, which a person's `run:event --watch` pane reads with no model in between, so this form
|
|
409
|
+
* keeps its lines off standard output and exits only on an arrival, on `josh followup` ending its life
|
|
410
|
+
* record, or on its bound. The caller starts it once and restarts it only after one of those.
|
|
408
411
|
*
|
|
409
412
|
* **The clock is still this command's, not the caller's.** It is the watch loop itself, a tick at a
|
|
410
413
|
* time, rather than a second reading of the same record — so a real report elsewhere pushes the next
|
|
@@ -419,10 +422,10 @@ async function once(options: WatchOptions): Promise<number> {
|
|
|
419
422
|
* carried budget, no lane, and no `in-progress` child — so a genuinely idle repository keeps the loop
|
|
420
423
|
* waiting instead of ending it; returning there would hand the caller an instant answer to restart,
|
|
421
424
|
* and the documented restart makes that a poll rather than a heartbeat. Once a run has started the
|
|
422
|
-
* loop reports
|
|
425
|
+
* loop reports even before the first child carries `in-progress` (joshuafolkken/kit#1900).
|
|
423
426
|
*/
|
|
424
427
|
async function wait_once(options: WatchOptions): Promise<number> {
|
|
425
|
-
return await drive(options, true)
|
|
428
|
+
return await drive({ ...options, is_stream_only: true }, true)
|
|
426
429
|
}
|
|
427
430
|
|
|
428
431
|
async function mark_now(): Promise<number> {
|
|
@@ -448,7 +451,7 @@ function to_options(values: ParsedValues): WatchOptions | undefined {
|
|
|
448
451
|
|
|
449
452
|
return {
|
|
450
453
|
interval_ms: run_progress_args.to_interval_ms(values.interval),
|
|
451
|
-
max_ms: run_progress_args.to_max_ms(values.hours),
|
|
454
|
+
max_ms: run_progress_args.to_max_ms(values.hours, values.wait === true),
|
|
452
455
|
output_paths: values.output ?? [],
|
|
453
456
|
repo,
|
|
454
457
|
tick_ms: TICK_SECONDS * run_progress.MS_PER_SECOND,
|
|
@@ -494,7 +497,7 @@ async function main(argv: ReadonlyArray<string>): Promise<void> {
|
|
|
494
497
|
}
|
|
495
498
|
|
|
496
499
|
const run_progress_cli = {
|
|
497
|
-
|
|
500
|
+
DEFAULT_WAIT_MAX_HOURS,
|
|
498
501
|
DISABLED_NOTICE,
|
|
499
502
|
FAILED_TICK_PREFIX,
|
|
500
503
|
FRESH_LOOP,
|
|
@@ -508,7 +511,6 @@ const run_progress_cli = {
|
|
|
508
511
|
run,
|
|
509
512
|
step,
|
|
510
513
|
to_interval_ms: run_progress_args.to_interval_ms,
|
|
511
|
-
to_max_ms: run_progress_args.to_max_ms,
|
|
512
514
|
wait_once,
|
|
513
515
|
}
|
|
514
516
|
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { git_common_directory } from '#scripts/git/git-common-directory'
|
|
2
2
|
import { stamp_file } from '#scripts/josh/stamp-file'
|
|
3
|
+
import { error_text } from '#scripts/lib/error-message'
|
|
3
4
|
import { z } from 'zod'
|
|
4
5
|
|
|
5
6
|
// The report clock `josh run:progress` keeps, in the one form both of its readers can use
|
|
@@ -232,7 +233,9 @@ function is_life_fresh(target: string, threshold_ms: number): boolean {
|
|
|
232
233
|
const pinged_ms = parse_pinged_at(raw)
|
|
233
234
|
|
|
234
235
|
return pinged_ms !== undefined && Date.now() - pinged_ms < threshold_ms
|
|
235
|
-
} catch {
|
|
236
|
+
} catch (error) {
|
|
237
|
+
error_text.trace_swallowed('run_progress_clock.is_life_fresh', error)
|
|
238
|
+
|
|
236
239
|
return false
|
|
237
240
|
}
|
|
238
241
|
}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
#!/usr/bin/env tsx
|
|
2
2
|
import { fileURLToPath } from 'node:url'
|
|
3
|
-
import { parseArgs } from 'node:util'
|
|
4
3
|
import { lane_child_marker } from '#scripts/lane/lane-child-marker'
|
|
4
|
+
import { cli_flags } from '#scripts/lib/cli-flags'
|
|
5
5
|
import { run_event_stream } from './run-event-stream'
|
|
6
6
|
import { run_event_stream_emit } from './run-event-stream-emit'
|
|
7
7
|
import { run_ship, type ShipSection } from './run-ship'
|
|
@@ -121,13 +121,6 @@ interface ParsedValues extends NotifyValues {
|
|
|
121
121
|
cite?: Array<string>
|
|
122
122
|
}
|
|
123
123
|
|
|
124
|
-
function parse_values(argv: ReadonlyArray<string>): {
|
|
125
|
-
values: ParsedValues
|
|
126
|
-
positionals: Array<string>
|
|
127
|
-
} {
|
|
128
|
-
return parseArgs({ args: [...argv], options: OPTIONS, allowPositionals: true, strict: true })
|
|
129
|
-
}
|
|
130
|
-
|
|
131
124
|
function ship_args(
|
|
132
125
|
title: string,
|
|
133
126
|
rest: ReadonlyArray<string>,
|
|
@@ -155,9 +148,10 @@ function log_command(number: string, positionals: ReadonlyArray<string>): ShipCo
|
|
|
155
148
|
return CITE_PATTERN.test(number) && positionals.length === 0 ? { kind: 'log', number } : undefined
|
|
156
149
|
}
|
|
157
150
|
|
|
158
|
-
function
|
|
159
|
-
|
|
160
|
-
|
|
151
|
+
function command_of(
|
|
152
|
+
values: ParsedValues,
|
|
153
|
+
positionals: ReadonlyArray<string>,
|
|
154
|
+
): ShipCommand | undefined {
|
|
161
155
|
if (values.log !== undefined) return log_command(values.log, positionals)
|
|
162
156
|
|
|
163
157
|
const title = positionals[FIRST]
|
|
@@ -170,11 +164,9 @@ function read_args(argv: ReadonlyArray<string>): ShipCommand | undefined {
|
|
|
170
164
|
// Numbers-only tail and a strict parse, so a title with no `#<N>` or a stray flag is refused rather
|
|
171
165
|
// than shipped past the wrong step.
|
|
172
166
|
function parse(argv: ReadonlyArray<string>): ShipCommand | undefined {
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
return undefined
|
|
177
|
-
}
|
|
167
|
+
const parsed = cli_flags.arguments_of(argv, OPTIONS)
|
|
168
|
+
|
|
169
|
+
return parsed === undefined ? undefined : command_of(parsed.values, parsed.positionals)
|
|
178
170
|
}
|
|
179
171
|
|
|
180
172
|
async function run_step(step: Step, args: ShipArguments, state: ShipState): Promise<ShipSection> {
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { git_command } from '#scripts/git/git-command'
|
|
2
2
|
import { git_worktree } from '#scripts/git/git-worktree'
|
|
3
|
+
import { error_text } from '#scripts/lib/error-message'
|
|
3
4
|
import { run_hold } from './run-hold'
|
|
4
5
|
import { run_preflight } from './run-preflight'
|
|
5
6
|
import { run_ship_stage, type ShipState } from './run-ship-stage'
|
|
@@ -20,7 +21,9 @@ const HEAD_REFERENCE = 'HEAD'
|
|
|
20
21
|
async function or_false(is_true: () => Promise<boolean>): Promise<boolean> {
|
|
21
22
|
try {
|
|
22
23
|
return await is_true()
|
|
23
|
-
} catch {
|
|
24
|
+
} catch (error) {
|
|
25
|
+
error_text.trace_swallowed('run_ship_probe.or_false', error)
|
|
26
|
+
|
|
24
27
|
return false
|
|
25
28
|
}
|
|
26
29
|
}
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
#!/usr/bin/env tsx
|
|
2
2
|
import { fileURLToPath } from 'node:url'
|
|
3
|
-
import { parseArgs } from 'node:util'
|
|
4
3
|
import { cost_cli, type CostVerdict } from '#scripts/cost-runtime/cost-cli'
|
|
5
4
|
import { issue_state_cli, type StateRead } from '#scripts/issue/issue-state-cli'
|
|
5
|
+
import { cli_flags } from '#scripts/lib/cli-flags'
|
|
6
6
|
import { run_carry, type CarryRead } from './run-carry'
|
|
7
7
|
import { run_status, type StatusParts } from './run-status'
|
|
8
8
|
|
|
@@ -41,19 +41,6 @@ interface StatusReads {
|
|
|
41
41
|
cost: CostVerdict
|
|
42
42
|
}
|
|
43
43
|
|
|
44
|
-
interface ParsedArguments {
|
|
45
|
-
positionals: ReadonlyArray<string>
|
|
46
|
-
values: { repo?: string }
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
function read_args(argv: ReadonlyArray<string>): ParsedArguments | undefined {
|
|
50
|
-
try {
|
|
51
|
-
return parseArgs({ args: [...argv], options: OPTIONS, allowPositionals: true, strict: true })
|
|
52
|
-
} catch {
|
|
53
|
-
return undefined
|
|
54
|
-
}
|
|
55
|
-
}
|
|
56
|
-
|
|
57
44
|
// Exactly one issue number, or the call is refused: `run:status` reports one run, and a second number
|
|
58
45
|
// would print a second issue's state a caller reads as this run's own.
|
|
59
46
|
function valid_issue(positionals: ReadonlyArray<string>): string | undefined {
|
|
@@ -65,7 +52,7 @@ function valid_issue(positionals: ReadonlyArray<string>): string | undefined {
|
|
|
65
52
|
}
|
|
66
53
|
|
|
67
54
|
function parse(argv: ReadonlyArray<string>): StatusRequest | undefined {
|
|
68
|
-
const parsed =
|
|
55
|
+
const parsed = cli_flags.arguments_of(argv, OPTIONS)
|
|
69
56
|
|
|
70
57
|
if (parsed === undefined) return undefined
|
|
71
58
|
|
|
@@ -2,6 +2,7 @@ import { git_spawn } from '#scripts/git/git-spawn'
|
|
|
2
2
|
import { git_stash } from '#scripts/git/stash/git-stash'
|
|
3
3
|
import { lane_close } from '#scripts/lane/lane-close'
|
|
4
4
|
import { lane_registry, type LaneInfo } from '#scripts/lane/lane-registry'
|
|
5
|
+
import { error_text } from '#scripts/lib/error-message'
|
|
5
6
|
import { run_hold } from './run-hold'
|
|
6
7
|
import { run_tidy, type Outcome, type Verdict } from './run-tidy'
|
|
7
8
|
|
|
@@ -31,7 +32,9 @@ async function has_unpushed(directory: string): Promise<boolean> {
|
|
|
31
32
|
])
|
|
32
33
|
|
|
33
34
|
return count !== '0'
|
|
34
|
-
} catch {
|
|
35
|
+
} catch (error) {
|
|
36
|
+
error_text.trace_swallowed('run_tidy_lanes.has_unpushed', error)
|
|
37
|
+
|
|
35
38
|
return true
|
|
36
39
|
}
|
|
37
40
|
}
|
|
@@ -119,14 +119,6 @@ function note_to_stderr(note: string): void {
|
|
|
119
119
|
console.error(`wake: ${note}`)
|
|
120
120
|
}
|
|
121
121
|
|
|
122
|
-
function read_arguments(argv: ReadonlyArray<string>): ParsedValues | undefined {
|
|
123
|
-
return cli_flags.parse_or_undefined({ args: [...argv], options: OPTIONS, strict: true })?.values
|
|
124
|
-
}
|
|
125
|
-
|
|
126
|
-
function text_of(value: string | boolean | undefined): string | undefined {
|
|
127
|
-
return typeof value === 'string' ? value : undefined
|
|
128
|
-
}
|
|
129
|
-
|
|
130
122
|
function to_interval_ms(value: string | boolean | undefined): number | undefined {
|
|
131
123
|
if (typeof value !== 'string') return DEFAULT_INTERVAL_SECONDS * MS_PER_SECOND
|
|
132
124
|
if (!INTERVAL_PATTERN.test(value)) return undefined
|
|
@@ -379,14 +371,14 @@ async function dispatch(
|
|
|
379
371
|
interval_ms: number,
|
|
380
372
|
): Promise<number> {
|
|
381
373
|
if (values.loop === true) return await loop(context, interval_ms)
|
|
382
|
-
if (values.start === true) return start(context,
|
|
374
|
+
if (values.start === true) return start(context, cli_flags.string_of(values.interval))
|
|
383
375
|
if (values.stop === true) return stop_supervisor(context)
|
|
384
376
|
|
|
385
377
|
return list(context)
|
|
386
378
|
}
|
|
387
379
|
|
|
388
380
|
async function run(argv: ReadonlyArray<string>): Promise<number> {
|
|
389
|
-
const values =
|
|
381
|
+
const values = cli_flags.values_of(argv, OPTIONS)
|
|
390
382
|
|
|
391
383
|
if (values === undefined || group_count(values) !== ONE_GROUP) return usage()
|
|
392
384
|
|
|
@@ -2,13 +2,15 @@ import { lane_registry } from '#scripts/lane/lane-registry'
|
|
|
2
2
|
import { run_headless } from './run-headless'
|
|
3
3
|
import { run_progress_clock } from './run-progress-clock'
|
|
4
4
|
|
|
5
|
-
// A watcher that stopped while children are still in-flight leaves the
|
|
6
|
-
//
|
|
7
|
-
// calls that follow a missed restart (joshuafolkken/kit#2113).
|
|
5
|
+
// A watcher that stopped while children are still in-flight leaves the run's event stream without a
|
|
6
|
+
// heartbeat and the parent without the wake an arrival would deliver. This guard detects that state so
|
|
7
|
+
// a hook can refuse the calls that follow a missed restart (joshuafolkken/kit#2113).
|
|
8
8
|
//
|
|
9
9
|
// **Staleness threshold is `TICK_MULTIPLIER` watcher ticks.** The watcher pings its life record on
|
|
10
10
|
// every tick (every 30 s), so a gap wider than three ticks means the process has almost certainly
|
|
11
|
-
// stopped:
|
|
11
|
+
// stopped: it exited on an arrival, its bound expired, or the session was cut without a restart. A
|
|
12
|
+
// report is no longer one of those (joshuafolkken/kit#3102) — `--wait` streams its reports and keeps
|
|
13
|
+
// running — so a restart is owed only after an exit the parent was woken for. Three gives one missed
|
|
12
14
|
// tick of slack before the guard fires.
|
|
13
15
|
//
|
|
14
16
|
// **It watches no relay** (joshuafolkken/kit#2492). A session following the run's event stream after a
|
|
@@ -24,7 +26,9 @@ const STALE_THRESHOLD_S = STALE_THRESHOLD_MS / MS_PER_SECOND
|
|
|
24
26
|
|
|
25
27
|
const STALE_NOTE =
|
|
26
28
|
`Lane children are in-flight but the progress watcher has not pinged in the last ` +
|
|
27
|
-
`${String(STALE_THRESHOLD_S)}s — run \`pnpm josh run:progress --wait\` in the background before proceeding`
|
|
29
|
+
`${String(STALE_THRESHOLD_S)}s — run \`pnpm josh run:progress --wait\` in the background before proceeding. ` +
|
|
30
|
+
'It reports to the run event stream on its own and exits only when newly runnable work arrives, so ' +
|
|
31
|
+
'start it once and relay nothing'
|
|
28
32
|
|
|
29
33
|
type GuardResult = { kind: 'ok' } | { kind: 'stale'; note: string }
|
|
30
34
|
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
import { existsSync, readFileSync, rmSync } from 'node:fs'
|
|
2
|
+
import path from 'node:path'
|
|
3
|
+
import { file_reader } from '#scripts/lib/read-file'
|
|
4
|
+
import { file_content } from '#scripts/sync/file-content'
|
|
5
|
+
import { z } from 'zod'
|
|
6
|
+
|
|
7
|
+
// kit writes the Safe Chain `preinstall` into every project it sets up, and a package that publishes
|
|
8
|
+
// would ship it: the consumer's pnpm then refuses it as an unapproved build script and `pnpm add`
|
|
9
|
+
// exits 1 (joshuafolkken/kit#3110). kit's own `.pnpmfile.mjs` strips it at pack time
|
|
10
|
+
// (joshuafolkken/kit#2693), so that one file is what a publishing project receives — never a copy of
|
|
11
|
+
// its logic.
|
|
12
|
+
//
|
|
13
|
+
// A private package is left alone: it never packs, and a new pnpmfile changes the lockfile's
|
|
14
|
+
// `pnpmfileChecksum`, which fails a frozen install until the lockfile is refreshed.
|
|
15
|
+
const PACK_HOOK_FILE = '.pnpmfile.mjs'
|
|
16
|
+
const MANAGED_LINE = '// josh-managed-pnpmfile: @joshuafolkken/kit'
|
|
17
|
+
const MANIFEST_FILE = 'package.json'
|
|
18
|
+
// pnpm loads `.pnpmfile.mjs` in preference to `.pnpmfile.cjs`, and neither once a `pnpmfile` setting
|
|
19
|
+
// names another file — so writing ours beside either would silently drop the project's hooks, or
|
|
20
|
+
// report a hook pnpm never runs.
|
|
21
|
+
const LEGACY_HOOK_FILE = '.pnpmfile.cjs'
|
|
22
|
+
const CONFIG_FILES: ReadonlyArray<string> = ['pnpm-workspace.yaml', '.npmrc']
|
|
23
|
+
const PNPMFILE_SETTINGS: ReadonlySet<string> = new Set(['pnpmfile', 'pnpmfiles'])
|
|
24
|
+
const SETTING_KEY_END = /[\s:=]/u
|
|
25
|
+
const manifest_schema = z.looseObject({ private: z.boolean().optional() })
|
|
26
|
+
|
|
27
|
+
type PackHookResult = 'written' | 'unchanged' | 'private' | 'owned' | 'withdrawn'
|
|
28
|
+
|
|
29
|
+
function is_published(manifest_text: string): boolean {
|
|
30
|
+
return manifest_schema.parse(JSON.parse(manifest_text)).private !== true
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function sets_pnpmfile(config_text: string): boolean {
|
|
34
|
+
return config_text
|
|
35
|
+
.split('\n')
|
|
36
|
+
.some((line) => PNPMFILE_SETTINGS.has(line.trimStart().split(SETTING_KEY_END, 1)[0] ?? ''))
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function has_own_hook_source(project_root: string): boolean {
|
|
40
|
+
if (existsSync(path.join(project_root, LEGACY_HOOK_FILE))) return true
|
|
41
|
+
|
|
42
|
+
return CONFIG_FILES.some((name) =>
|
|
43
|
+
sets_pnpmfile(file_reader.read_file_or_empty(path.join(project_root, name))),
|
|
44
|
+
)
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function write_hook(destination: string, package_directory: string): PackHookResult {
|
|
48
|
+
const source = readFileSync(path.join(package_directory, PACK_HOOK_FILE), 'utf8')
|
|
49
|
+
|
|
50
|
+
return file_content.write_text_if_changed(destination, source) ? 'written' : 'unchanged'
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
// A copy kit wrote before the project added its own pnpmfile would still shadow that one.
|
|
54
|
+
function withdraw(destination: string, existing: string | undefined): PackHookResult {
|
|
55
|
+
if (existing === undefined) return 'owned'
|
|
56
|
+
rmSync(destination)
|
|
57
|
+
|
|
58
|
+
return 'withdrawn'
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
// A pnpmfile without the managed line is the project's own; overwriting it would drop its hooks.
|
|
62
|
+
function apply(project_root: string, package_directory: string): PackHookResult {
|
|
63
|
+
const destination = path.join(project_root, PACK_HOOK_FILE)
|
|
64
|
+
const existing = file_reader.read_optional(destination)
|
|
65
|
+
if (existing !== undefined && !existing.startsWith(MANAGED_LINE)) return 'owned'
|
|
66
|
+
if (has_own_hook_source(project_root)) return withdraw(destination, existing)
|
|
67
|
+
|
|
68
|
+
return write_hook(destination, package_directory)
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
const MESSAGES: Readonly<Record<PackHookResult, string | undefined>> = {
|
|
72
|
+
written: ` ✔ synced ${PACK_HOOK_FILE} (run \`pnpm install\` to refresh pnpmfileChecksum)`,
|
|
73
|
+
unchanged: ` ✔ unchanged ${PACK_HOOK_FILE}`,
|
|
74
|
+
private: undefined,
|
|
75
|
+
owned: ` ⚠ skipped ${PACK_HOOK_FILE} — the project has its own pnpmfile; strip the safe-chain preinstall at pack time there`,
|
|
76
|
+
withdrawn: ` ⚠ removed ${PACK_HOOK_FILE} — it shadowed the project's own pnpmfile; strip the safe-chain preinstall at pack time there`,
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
function sync_pack_hook(project_root: string, package_directory: string): PackHookResult {
|
|
80
|
+
const manifest_text = file_reader.read_optional(path.join(project_root, MANIFEST_FILE))
|
|
81
|
+
const is_private = manifest_text === undefined || !is_published(manifest_text)
|
|
82
|
+
const result = is_private ? 'private' : apply(project_root, package_directory)
|
|
83
|
+
const message = MESSAGES[result]
|
|
84
|
+
if (message !== undefined) console.info(message)
|
|
85
|
+
|
|
86
|
+
return result
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
const pack_hook = { MANAGED_LINE, sync_pack_hook }
|
|
90
|
+
|
|
91
|
+
export { pack_hook, PACK_HOOK_FILE }
|
|
92
|
+
export type { PackHookResult }
|
|
@@ -6,6 +6,7 @@ import { init_logic } from '#scripts/init/init-logic'
|
|
|
6
6
|
import { PACKAGE_DIR, PROJECT_ROOT } from '#scripts/init/init-paths'
|
|
7
7
|
import type { ProjectShape } from '#scripts/init/project-profile'
|
|
8
8
|
import { file_reader } from '#scripts/lib/read-file'
|
|
9
|
+
import { pack_hook } from '#scripts/safe-chain/pack-hook'
|
|
9
10
|
import { copy_directory_failure, directory_copy_blocker } from './directory-copy-guard'
|
|
10
11
|
import { file_content } from './file-content'
|
|
11
12
|
import { REMOVED_SKILL_MANIFEST } from './removed-skill-manifest'
|
|
@@ -189,6 +190,9 @@ function sync_ai_copy_all(is_force: boolean, shape?: ProjectShape): void {
|
|
|
189
190
|
}
|
|
190
191
|
|
|
191
192
|
migrate_removed_skills()
|
|
193
|
+
// Both profiles write the Safe Chain `preinstall`, so both get the hook that keeps it out of a
|
|
194
|
+
// published package (joshuafolkken/kit#3110).
|
|
195
|
+
pack_hook.sync_pack_hook(PROJECT_ROOT, PACKAGE_DIR)
|
|
192
196
|
}
|
|
193
197
|
|
|
194
198
|
const sync_ai_files = {
|
|
@@ -1,5 +1,6 @@
|
|
|
1
|
-
import { execFileSync } from 'node:child_process'
|
|
2
1
|
import { project_checks } from '#scripts/gate/project-checks'
|
|
2
|
+
import { PORCELAIN_FLAG } from '#scripts/git/constants'
|
|
3
|
+
import { git_spawn_sync } from '#scripts/git/git-spawn-sync'
|
|
3
4
|
import { test_declared_logic, type Verdict } from './test-declared-logic'
|
|
4
5
|
|
|
5
6
|
// The working-tree change set and the verdict over it, read **synchronously** for the `PreToolUse`
|
|
@@ -30,21 +31,17 @@ function path_of(line: string): string {
|
|
|
30
31
|
}
|
|
31
32
|
|
|
32
33
|
function read_changed_paths_sync(): Array<string> {
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
.map((line) => path_of(line))
|
|
45
|
-
} catch {
|
|
46
|
-
return []
|
|
47
|
-
}
|
|
34
|
+
// Outside a repository git fails with `fatal: not a git repository`, which reads as an empty change
|
|
35
|
+
// set; its stderr is captured rather than surfaced, since it would only be noise.
|
|
36
|
+
// Unbounded (`timeout: 0`) on purpose: a timed-out status would read as an empty change set, and so
|
|
37
|
+
// as `exempt`, letting an untested runtime change through on a slow checkout.
|
|
38
|
+
const output = git_spawn_sync.run(['status', PORCELAIN_FLAG], { timeout: 0 })
|
|
39
|
+
if (output.exit_code !== 0) return []
|
|
40
|
+
|
|
41
|
+
return output.stdout
|
|
42
|
+
.split('\n')
|
|
43
|
+
.filter((line) => line.length > STATUS_PREFIX_LENGTH)
|
|
44
|
+
.map((line) => path_of(line))
|
|
48
45
|
}
|
|
49
46
|
|
|
50
47
|
// The working-tree read behind `current_verdict`, injectable so the delivery path can be exercised
|