@joshuafolkken/kit 1.1387.0 → 1.1435.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 +1 -1
- package/.claude/skills/epic-commands/epic-bundle.md +5 -0
- package/.claude/skills/workflow-commands/backlogrun-lanes.md +5 -3
- package/.claude/skills/workflow-commands/backlogrun-steps.md +4 -3
- package/.claude/skills/workflow-commands/backlogrun.md +1 -0
- package/.claude/skills/workflow-commands/chain-rule.md +4 -2
- package/.claude/skills/workflow-commands/issue-comments.md +6 -0
- package/.claude/skills/workflow-commands/observation-ledger.md +3 -2
- package/.claude/skills/workflow-commands/pre-gate-cut.md +5 -3
- package/README.md +25 -0
- package/dist/hooks/audit-provision.js +2 -2
- package/dist/hooks/batch-guard.js +7 -7
- package/dist/hooks/{chunk-IYVOLN6A.js → chunk-2G5F2YCD.js} +3 -3
- package/dist/hooks/{chunk-2YSW3CXH.js → chunk-3LE6VLSR.js} +17 -6
- package/dist/hooks/{chunk-J3KSS7II.js → chunk-4BHUPFKF.js} +1 -1
- package/dist/hooks/{chunk-EAQEGME7.js → chunk-5SJSSCJG.js} +1 -1
- package/dist/hooks/{chunk-FZ6FUSBM.js → chunk-723HKZO5.js} +1 -1
- package/dist/hooks/{chunk-LMWBRJIP.js → chunk-7KMN5QB5.js} +3 -3
- package/dist/hooks/{chunk-NEC4AT4Z.js → chunk-DGT54IH5.js} +4 -0
- package/dist/hooks/{chunk-STDSEPWU.js → chunk-EMB55BDF.js} +4 -4
- package/dist/hooks/{chunk-JBBMAPC4.js → chunk-IX6TTTI6.js} +4 -0
- package/dist/hooks/{chunk-GJ7JPIHN.js → chunk-JXQBK323.js} +2674 -187
- package/dist/hooks/{chunk-DCY2GWHC.js → chunk-MSYSUSY6.js} +5 -5
- package/dist/hooks/{chunk-EBSHN25W.js → chunk-MU4LJFIV.js} +4 -4
- package/dist/hooks/{chunk-WNFJLBXI.js → chunk-QL6QTPWN.js} +3 -3
- package/dist/hooks/{chunk-SL4GQ2XP.js → chunk-XER6SSIB.js} +91 -18
- package/dist/hooks/{chunk-U32VSRNG.js → chunk-YW452ZAX.js} +89 -34
- package/dist/hooks/{chunk-GNDJ7NJ7.js → chunk-YZXYOAZ3.js} +202 -55
- package/dist/hooks/codex-hook-adapter.js +15 -15
- package/dist/hooks/duplicate-read-guard.js +8 -8
- package/dist/hooks/format-edited.js +5 -5
- package/dist/hooks/inputs.json +288 -229
- package/dist/hooks/investigation-guard.js +8 -8
- package/dist/hooks/pretool-guard.js +14 -14
- package/dist/hooks/session-lang.js +69 -16
- package/dist/hooks/stop-guard.js +13 -38
- package/dist/josh.js +62 -9
- package/package.json +4 -3
- package/prompts/review.md +22 -0
- package/scripts/agent/claude-agent-argv.ts +13 -1
- package/scripts/auth/auth-setup-cli.ts +101 -0
- package/scripts/auth/claude-token-output.ts +78 -0
- package/scripts/auth/claude-token.ts +68 -0
- package/scripts/backlog/backlog-drive-epic.ts +24 -8
- package/scripts/backlog/backlog-drive-launch.ts +3 -0
- package/scripts/backlog/backlog-drive-named-offer.ts +7 -2
- package/scripts/backlog/backlog-drive-named.ts +4 -2
- package/scripts/backlog/backlog-drive-unread.ts +56 -0
- package/scripts/backlog/backlog-drive.ts +16 -8
- package/scripts/backlog/backlog-overlap.ts +22 -5
- package/scripts/backlog/backlog-ready.ts +1 -3
- package/scripts/backlog/backlogrun-args.ts +13 -2
- package/scripts/backlog/backlogrun-cli.ts +91 -19
- package/scripts/backlog/backlogrun-launch.ts +24 -0
- package/scripts/backlog/backlogrun-replace.ts +166 -0
- package/scripts/backlog/backlogrun-stop.ts +178 -0
- package/scripts/behavior/behavior-cli.ts +6 -1
- package/scripts/doctor/doctor-io.ts +60 -9
- package/scripts/doctor/doctor.ts +20 -1
- package/scripts/document/document-byte-budget.ts +3 -3
- package/scripts/epic/epic-bundle-cli.ts +8 -8
- package/scripts/epic/epic-bundle-referenced.ts +2 -5
- package/scripts/epic/epic-bundle.ts +39 -4
- package/scripts/exports/unused-members-cli.ts +2 -1
- package/scripts/followup/followup-checks-event.ts +82 -0
- package/scripts/followup/git-followup-finish.ts +16 -2
- package/scripts/followup/git-followup-workflow.ts +5 -2
- package/scripts/followup/git-pr-followup-checks.ts +18 -4
- package/scripts/followup/git-pr-followup-wrapup.ts +5 -0
- package/scripts/followup/git-pr-followup.ts +4 -1
- package/scripts/fresh-machine/fresh-machine-cli.ts +204 -0
- package/scripts/fresh-machine/fresh-machine-docker.ts +65 -0
- package/scripts/fresh-machine/fresh-machine-image.ts +62 -0
- package/scripts/fresh-machine/fresh-machine-log.ts +61 -0
- package/scripts/fresh-machine/fresh-machine-patterns.ts +130 -0
- package/scripts/fresh-machine/fresh-machine-run.ts +215 -0
- package/scripts/fresh-machine/fresh-machine-script.ts +45 -0
- package/scripts/fresh-machine/fresh-machine-spec.ts +57 -0
- package/scripts/fresh-machine/specs/kit-test-backlogrun-python.json +34 -0
- package/scripts/fresh-machine/specs/kit-test-backlogrun.json +31 -0
- package/scripts/gate/gate-report.ts +70 -15
- package/scripts/gate/project-checks.ts +13 -2
- package/scripts/gate/scoped-green.ts +2 -1
- package/scripts/gate/verification-gate.ts +4 -3
- package/scripts/gh/gh-author-association.ts +14 -0
- package/scripts/gh/git-gh-conditional.ts +66 -0
- package/scripts/gh/git-gh-exec.ts +1 -1
- package/scripts/gh/git-gh-issue-read.ts +6 -3
- package/scripts/gh/git-gh-issue-write.ts +5 -1
- package/scripts/gh/git-gh-issue.ts +4 -1
- package/scripts/gh/git-pr-checks-eval.ts +13 -0
- package/scripts/gh/git-pr-checks-parse.ts +8 -1
- package/scripts/gh/git-pr-checks-watch.ts +11 -4
- package/scripts/gh/git-pr-checks.ts +36 -2
- package/scripts/git/git-command.ts +16 -4
- package/scripts/git/git-schemas.ts +1 -0
- package/scripts/git/git-spawn-sync.ts +1 -1
- package/scripts/hooks/pretool-guard.ts +3 -1
- package/scripts/init/hook-command-rewrite.ts +31 -0
- package/scripts/init/init-actions.ts +4 -1
- package/scripts/init/init-ai-copy.ts +33 -10
- package/scripts/init/init-basic.ts +8 -3
- package/scripts/init/init-logic-toolchain.ts +76 -0
- package/scripts/init/init-logic.ts +2 -0
- package/scripts/init/init.ts +5 -23
- package/scripts/init/project-profile.ts +36 -7
- package/scripts/init/start-exec.ts +27 -1
- package/scripts/init/start-plan.ts +15 -0
- package/scripts/init/start-steps.ts +46 -0
- package/scripts/init/start.ts +1 -0
- package/scripts/init/workflow-mark.ts +35 -0
- package/scripts/issue/issue-ack-cli.ts +84 -0
- package/scripts/issue/issue-labels.ts +40 -8
- package/scripts/issue/issue-state-cli.ts +14 -4
- package/scripts/issue/issue-watch-diff.ts +152 -0
- package/scripts/issue/issue-watch-gate.ts +81 -0
- package/scripts/issue/issue-watch-hook.ts +54 -0
- package/scripts/issue/issue-watch.ts +240 -0
- package/scripts/issue/measure-rerun-cli.ts +3 -4
- package/scripts/josh/josh-command-map.ts +4 -0
- package/scripts/josh/josh-commands-backlog.ts +2 -2
- package/scripts/josh/josh-commands-development.ts +1 -1
- package/scripts/josh/josh-commands-issue.ts +7 -0
- package/scripts/josh/josh-commands-lane.ts +11 -2
- package/scripts/josh/josh-commands-project.ts +24 -0
- package/scripts/josh/josh-verdict.ts +33 -4
- package/scripts/lane/claude-lane-broker.ts +142 -0
- package/scripts/lane/lane-await.ts +6 -2
- package/scripts/lane/lane-cache.ts +37 -7
- package/scripts/lane/lane-capacity.ts +8 -6
- package/scripts/lane/lane-child-invocation.ts +1 -0
- package/scripts/lane/lane-cli.ts +15 -1
- package/scripts/lane/lane-close.ts +15 -6
- package/scripts/lane/lane-dispatch.ts +36 -10
- package/scripts/lane/lane-environment.ts +1 -1
- package/scripts/lane/lane-launch-cli.ts +13 -4
- package/scripts/lane/lane-ledger.ts +21 -3
- package/scripts/lane/lane-measure-cli.ts +10 -2
- package/scripts/lane/lane-open.ts +9 -1
- package/scripts/lane/lane-preserve.ts +49 -0
- package/scripts/lane/lane-reap.ts +20 -5
- package/scripts/lane/lane-registry.ts +6 -0
- package/scripts/lane/lane-restart-cli.ts +51 -0
- package/scripts/lane/lane-restart.ts +175 -0
- package/scripts/lane/{openai-review-broker.ts → lane-session-broker.ts} +106 -28
- package/scripts/lane/lane-spent.ts +195 -0
- package/scripts/lane/lane-stage-stats.ts +27 -3
- package/scripts/lane/lane-stop-stats.ts +96 -0
- package/scripts/lane/openai-lane-supervisor.ts +2 -2
- package/scripts/lane/process-tools.ts +31 -0
- package/scripts/lib/buffered-process.ts +36 -2
- package/scripts/lib/pnpm-reach.ts +27 -0
- package/scripts/lib/status-icons.ts +5 -1
- package/scripts/lint/lint-parallel.ts +19 -2
- package/scripts/observations/observation-ledger-home.ts +1 -0
- package/scripts/review/review-attest-cli.ts +21 -6
- package/scripts/review/review-brief-cli.ts +2 -2
- package/scripts/review/review-comment-round.ts +114 -0
- package/scripts/review/review-record-cli.ts +39 -17
- package/scripts/rules/implementation-cut.ts +3 -3
- package/scripts/rules/pre-gate-cut.ts +1 -1
- package/scripts/run/board/run-board-checks.ts +42 -0
- package/scripts/run/board/run-board-cli.ts +2 -0
- package/scripts/run/board/run-board-every.ts +3 -1
- package/scripts/run/board/run-board-fit.ts +16 -5
- package/scripts/run/board/run-board-fresh.ts +64 -0
- package/scripts/run/board/run-board-header.ts +129 -53
- package/scripts/run/board/run-board-labels.ts +47 -4
- package/scripts/run/board/run-board-lane-event.ts +66 -0
- package/scripts/run/board/run-board-layout.ts +57 -9
- package/scripts/run/board/run-board-now.ts +234 -0
- package/scripts/run/board/run-board-operation-read.ts +75 -0
- package/scripts/run/board/run-board-operation.ts +150 -0
- package/scripts/run/board/run-board-phase.ts +26 -1
- package/scripts/run/board/run-board-plan.ts +4 -6
- package/scripts/run/board/run-board-read.ts +49 -3
- package/scripts/run/board/run-board-reason.ts +124 -0
- package/scripts/run/board/run-board-render-checks.ts +55 -0
- package/scripts/run/board/run-board-render-legend.ts +12 -3
- package/scripts/run/board/run-board-render-slot.ts +2 -2
- package/scripts/run/board/run-board-render-tail.ts +128 -0
- package/scripts/run/board/run-board-render-thanks.ts +50 -0
- package/scripts/run/board/run-board-render-title.ts +58 -0
- package/scripts/run/board/run-board-render.ts +96 -80
- package/scripts/run/board/run-board-row-yield.ts +32 -0
- package/scripts/run/board/run-board-setup.ts +61 -13
- package/scripts/run/board/run-board-state.ts +10 -0
- package/scripts/run/board/run-board-status.ts +66 -7
- package/scripts/run/board/run-board-thanks-csv.ts +71 -0
- package/scripts/run/board/run-board-thanks-read.ts +112 -0
- package/scripts/run/board/run-board-thanks.ts +53 -0
- package/scripts/run/board/run-board-tick.ts +50 -21
- package/scripts/run/board/run-board-usual.ts +44 -0
- package/scripts/run/carry/run-carry-args.ts +9 -1
- package/scripts/run/carry/run-carry-cli.ts +2 -45
- package/scripts/run/carry/run-carry-end.ts +73 -0
- package/scripts/run/carry/run-carry.ts +8 -1
- package/scripts/run/entry/run-entry-cli.ts +5 -0
- package/scripts/run/event/run-event-checks.ts +54 -0
- package/scripts/run/event/run-event-render.ts +2 -0
- package/scripts/run/event/run-event-scope.ts +1 -0
- package/scripts/run/event/run-event-setup.ts +55 -11
- package/scripts/run/event/run-event-stream.ts +27 -11
- package/scripts/run/merge/run-merge-cli.ts +7 -4
- package/scripts/run/merge/run-merge-ship.ts +61 -0
- package/scripts/run/merge/run-merge-steps.ts +7 -14
- package/scripts/run/merge/run-merge-token.ts +16 -2
- package/scripts/run/run-headless.ts +1 -0
- package/scripts/run/run-step.ts +2 -0
- package/scripts/run/ship/run-ship-cli.ts +22 -2
- package/scripts/run/ship/run-ship-return.ts +31 -13
- package/scripts/run/ship/run-ship-review-steps.ts +17 -4
- package/scripts/run/ship/run-ship-stage.ts +1 -0
- package/scripts/run/wake/run-wake-await.ts +130 -0
- package/scripts/run/wake/run-wake-cli.ts +60 -71
- package/scripts/run/wake/run-wake-describe.ts +34 -2
- package/scripts/run/wake/run-wake-driver.ts +11 -4
- package/scripts/run/wake/run-wake-session.ts +20 -3
- package/scripts/run/wake/run-wake-stop.ts +37 -0
- package/scripts/run/wake/run-wake-supervise.ts +44 -0
- package/scripts/sync/claude-settings-merge.ts +144 -0
- package/scripts/sync/sync-ai-files.ts +28 -3
- package/scripts/sync/sync.ts +15 -0
- package/scripts/test/project-test-script.ts +73 -0
- package/scripts/test/skip-marker.ts +14 -1
- package/scripts/test/test-related.ts +3 -2
- package/scripts/test/test-unit-guard.ts +78 -14
- package/scripts/time-runtime/time-reported-failure.ts +11 -3
- package/scripts/uninstall/distributed-files.ts +92 -0
- package/scripts/uninstall/uninstall-plan.ts +119 -0
- package/scripts/uninstall/uninstall-report.ts +61 -0
- package/scripts/uninstall/uninstall.ts +206 -0
- package/templates/AGENTS.basic.md +4 -0
- package/templates/GEMINI.basic.md +6 -0
- package/dist/hooks/chunk-6V5OPIUU.js +0 -13
- package/scripts/run/board/run-board-track.ts +0 -22
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
import { git_gh_exec, has_stdout_field } from './git-gh-exec'
|
|
2
|
+
|
|
3
|
+
// A `gh api` GET that sends the ETag of the previous answer, so a resource polled every few minutes
|
|
4
|
+
// costs nothing when it has not changed: GitHub answers `304 Not Modified` to a matching
|
|
5
|
+
// `If-None-Match`, and a conditional request answered 304 does not count against the rate limit.
|
|
6
|
+
//
|
|
7
|
+
// **`gh` exits non-zero on a 304** (it prints `gh: HTTP 304`), so the "unchanged" answer arrives as
|
|
8
|
+
// a thrown failure. The status is read off the `--include` status line the failure still carries in
|
|
9
|
+
// its stdout — the same reading `exec_gh_api_status` makes — never off the stderr wording.
|
|
10
|
+
|
|
11
|
+
const NOT_MODIFIED_STATUS = 304
|
|
12
|
+
const OK_STATUS = 200
|
|
13
|
+
const INCLUDE_FLAG = '--include'
|
|
14
|
+
const HEADER_FLAG = '-H'
|
|
15
|
+
const IF_NONE_MATCH = 'If-None-Match: '
|
|
16
|
+
const HEADER_BODY_SEPARATOR = /\r?\n\r?\n/u
|
|
17
|
+
const ETAG_HEADER = /^etag: *(?<etag>[^\r\n]+)/imu
|
|
18
|
+
|
|
19
|
+
type ConditionalRead =
|
|
20
|
+
| { kind: 'unchanged' }
|
|
21
|
+
| { kind: 'changed'; etag: string | undefined; body: string }
|
|
22
|
+
| { kind: 'unreadable' }
|
|
23
|
+
|
|
24
|
+
const UNCHANGED: ConditionalRead = { kind: 'unchanged' }
|
|
25
|
+
const UNREADABLE: ConditionalRead = { kind: 'unreadable' }
|
|
26
|
+
|
|
27
|
+
function to_args(path: string, etag: string | undefined): Array<string> {
|
|
28
|
+
const header = etag === undefined ? [] : [HEADER_FLAG, `${IF_NONE_MATCH}${etag}`]
|
|
29
|
+
|
|
30
|
+
return ['api', INCLUDE_FLAG, ...header, path]
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
// The headers and the body of an `--include` answer, split at the first blank line.
|
|
34
|
+
function to_changed(output: string): ConditionalRead {
|
|
35
|
+
const [headers = '', ...rest] = output.split(HEADER_BODY_SEPARATOR)
|
|
36
|
+
const etag = ETAG_HEADER.exec(headers)?.groups?.['etag']
|
|
37
|
+
|
|
38
|
+
return { kind: 'changed', etag, body: rest.join('\n\n') }
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function to_read(output: string): ConditionalRead {
|
|
42
|
+
return git_gh_exec.parse_status_line(output) === OK_STATUS ? to_changed(output) : UNREADABLE
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function failure_status(error: unknown): number | undefined {
|
|
46
|
+
const cause = error instanceof Error ? error.cause : undefined
|
|
47
|
+
|
|
48
|
+
return has_stdout_field(cause) ? git_gh_exec.parse_status_line(cause.stdout) : undefined
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
async function read_conditional(
|
|
52
|
+
path: string,
|
|
53
|
+
etag: string | undefined,
|
|
54
|
+
timeout_ms?: number,
|
|
55
|
+
): Promise<ConditionalRead> {
|
|
56
|
+
try {
|
|
57
|
+
return to_read(await git_gh_exec.exec_gh_command(to_args(path, etag), timeout_ms))
|
|
58
|
+
} catch (error) {
|
|
59
|
+
return failure_status(error) === NOT_MODIFIED_STATUS ? UNCHANGED : UNREADABLE
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
const git_gh_conditional = { read_conditional, to_args }
|
|
64
|
+
|
|
65
|
+
export type { ConditionalRead }
|
|
66
|
+
export { git_gh_conditional }
|
|
@@ -288,7 +288,7 @@ interface GhApiRequest {
|
|
|
288
288
|
// second argument for the reason recorded beside that constant. `0` is
|
|
289
289
|
// execa's own "no budget", kept for the one reader whose caller's writes depend on the answer
|
|
290
290
|
// (`gh-spawn.ts`).
|
|
291
|
-
timeout_ms?: number
|
|
291
|
+
timeout_ms?: number | undefined
|
|
292
292
|
// The checkout gh runs in, which is what `{owner}/{repo}` in `path` resolves against. Absent means
|
|
293
293
|
// the process's own directory.
|
|
294
294
|
cwd?: string
|
|
@@ -350,11 +350,14 @@ async function issue_get_labels_and_body(issue_number: string): Promise<string |
|
|
|
350
350
|
// No `repo` parameter, unlike the reads above: the auto-close lists its candidate epics from the
|
|
351
351
|
// repository it runs in, so the epic whose comments are read is always a local one. The path builder
|
|
352
352
|
// takes one when a caller that needs it appears.
|
|
353
|
-
async function issue_list_comments(
|
|
353
|
+
async function issue_list_comments(
|
|
354
|
+
issue_number: string,
|
|
355
|
+
timeout_ms?: number,
|
|
356
|
+
): Promise<string | undefined> {
|
|
354
357
|
const path = `${git_gh_api_path.issue_comments_api_path(issue_number)}${git_gh_api_path.FULL_PAGE_QUERY}`
|
|
355
358
|
|
|
356
359
|
try {
|
|
357
|
-
return await git_gh_exec.exec_gh_api({ path, should_paginate: true })
|
|
360
|
+
return await git_gh_exec.exec_gh_api({ path, should_paginate: true, timeout_ms })
|
|
358
361
|
} catch {
|
|
359
362
|
return undefined
|
|
360
363
|
}
|
|
@@ -436,5 +439,5 @@ const git_gh_issue_read = {
|
|
|
436
439
|
issue_list_comments,
|
|
437
440
|
}
|
|
438
441
|
|
|
439
|
-
export type { IssueRead, IssueReadFailure }
|
|
442
|
+
export type { IssueRead, IssueReadFailure, IssueReadReason }
|
|
440
443
|
export { git_gh_issue_read, read_blocked_by, NOT_FOUND_STATUS }
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { issue_watch_diff } from '#scripts/issue/issue-watch-diff'
|
|
1
2
|
import { session_cite } from '#scripts/issue/session-cite'
|
|
2
3
|
import { error_text } from '#scripts/lib/error-message'
|
|
3
4
|
import { git_gh_api_path } from './git-gh-api-path'
|
|
@@ -64,10 +65,13 @@ async function issue_edit_body(issue_number: string, body: string): Promise<stri
|
|
|
64
65
|
})
|
|
65
66
|
}
|
|
66
67
|
|
|
68
|
+
// **Every comment josh posts carries `RUN_MARKER`**, an HTML comment GitHub does not render. A run
|
|
69
|
+
// posts under the person's own `gh` login, so the marker is the only thing that tells the issue watch
|
|
70
|
+
// (`issue-watch-diff.ts`) a run's comment from a person's update.
|
|
67
71
|
async function issue_comment(issue_number: string, body: string): Promise<string> {
|
|
68
72
|
return await git_gh_exec.exec_gh_api({
|
|
69
73
|
path: `${git_gh_api_path.issue_api_path(issue_number)}${COMMENTS_SEGMENT}`,
|
|
70
|
-
body: JSON.stringify({ body }),
|
|
74
|
+
body: JSON.stringify({ body: `${body}\n\n${issue_watch_diff.RUN_MARKER}` }),
|
|
71
75
|
jq_filter: HTML_URL_FILTER,
|
|
72
76
|
})
|
|
73
77
|
}
|
|
@@ -14,7 +14,10 @@ const NUMBER_AND_BODY_FIELDS = 'number,body'
|
|
|
14
14
|
// tell a row with no blockers from one with some **before** deciding whether that row is worth a
|
|
15
15
|
// per-issue read, which would otherwise be made for every open issue in the repository. Absent on a
|
|
16
16
|
// row whose summary is missing, which must not be read as zero; `git-gh-issue-rest.ts` states why.
|
|
17
|
-
|
|
17
|
+
//
|
|
18
|
+
// `labels` rides along on the same response too: `epic:bundle` reads the release label off it,
|
|
19
|
+
// because the release issue's blockers are recorded mechanically and signal no shared work.
|
|
20
|
+
const BACKLOG_SCAN_FIELDS = `number,title,body,labels,${BLOCKED_BY_COUNT_FIELD}`
|
|
18
21
|
// The recently-closed scan's fields. It compares titles like the scan above
|
|
19
22
|
// and never reads a body, and it needs the labels to tell an epic from a deliverable — the open half
|
|
20
23
|
// gets that from the epic listing, which by construction holds no closed epic.
|
|
@@ -119,6 +119,18 @@ function is_coderabbit_check(check_name: string): boolean {
|
|
|
119
119
|
return is_required_match(check_name, CODERABBIT_CHECK_NAME)
|
|
120
120
|
}
|
|
121
121
|
|
|
122
|
+
// CodeRabbit reports a rate-limited review as a *passing* status whose description says so
|
|
123
|
+
// (`Review rate limited`), so only the description tells it from a finished review
|
|
124
|
+
// (joshuafolkken/kit#3770).
|
|
125
|
+
const CODERABBIT_RATE_LIMIT_DESCRIPTION = /rate limit/iu
|
|
126
|
+
|
|
127
|
+
function is_coderabbit_rate_limited(check: RollupCheck): boolean {
|
|
128
|
+
return (
|
|
129
|
+
is_coderabbit_check(check.name) &&
|
|
130
|
+
CODERABBIT_RATE_LIMIT_DESCRIPTION.test(check.description ?? '')
|
|
131
|
+
)
|
|
132
|
+
}
|
|
133
|
+
|
|
122
134
|
// Temporary: a pending or failing CodeRabbit check makes GitHub report UNSTABLE even
|
|
123
135
|
// though CodeRabbit is not required. Accept that state only when CodeRabbit checks are the
|
|
124
136
|
// sole non-passing ones — any other non-passing check keeps the gate strict.
|
|
@@ -249,6 +261,7 @@ export {
|
|
|
249
261
|
is_review_decision_decisive,
|
|
250
262
|
read_required_statuses,
|
|
251
263
|
is_coderabbit_check,
|
|
264
|
+
is_coderabbit_rate_limited,
|
|
252
265
|
collect_blocking_failures,
|
|
253
266
|
describe_pr_failure,
|
|
254
267
|
REQUIRED_CHECKS,
|
|
@@ -31,10 +31,14 @@ const KEY_STATUS = 'status'
|
|
|
31
31
|
const KEY_CONCLUSION = 'conclusion'
|
|
32
32
|
const KEY_NAME = 'name'
|
|
33
33
|
const KEY_CONTEXT = 'context'
|
|
34
|
+
const KEY_DESCRIPTION = 'description'
|
|
34
35
|
|
|
35
36
|
interface RollupCheck {
|
|
36
37
|
name: string
|
|
37
38
|
status: string
|
|
39
|
+
// A status context's own words — CodeRabbit says `Review rate limited` under a `success` state
|
|
40
|
+
// (joshuafolkken/kit#3770). Absent where the check carries none.
|
|
41
|
+
description?: string
|
|
38
42
|
}
|
|
39
43
|
|
|
40
44
|
interface PrStateSnapshot {
|
|
@@ -95,7 +99,10 @@ function parse_rollup_item(item: RollupItemData): RollupCheck | undefined {
|
|
|
95
99
|
const name = read_string(item[KEY_NAME]) ?? read_string(item[KEY_CONTEXT])
|
|
96
100
|
if (name === undefined) return undefined
|
|
97
101
|
|
|
98
|
-
|
|
102
|
+
const check = { name, status: parse_rollup_status(item) }
|
|
103
|
+
const description = read_string(item[KEY_DESCRIPTION])
|
|
104
|
+
|
|
105
|
+
return description === undefined ? check : { ...check, description }
|
|
99
106
|
}
|
|
100
107
|
|
|
101
108
|
function parse_rollup_checks(raw_json: string): Array<RollupCheck> {
|
|
@@ -4,9 +4,11 @@ import {
|
|
|
4
4
|
default_fetch_pr_state,
|
|
5
5
|
DEFAULT_STABLE_READS,
|
|
6
6
|
is_pr_checks_timeout,
|
|
7
|
+
observed_fetcher,
|
|
7
8
|
SECONDS_TO_MS,
|
|
8
9
|
wait_for_pr_success,
|
|
9
10
|
type PrStateEvaluator,
|
|
11
|
+
type SnapshotObserver,
|
|
10
12
|
} from './git-pr-checks'
|
|
11
13
|
import { is_coderabbit_check, is_required_check, type PrEvaluation } from './git-pr-checks-eval'
|
|
12
14
|
import {
|
|
@@ -125,10 +127,12 @@ const CHECKS_SETTLED_EVALUATOR: PrStateEvaluator = {
|
|
|
125
127
|
should_read_review_decision: should_never_read_review_decision,
|
|
126
128
|
}
|
|
127
129
|
|
|
128
|
-
|
|
130
|
+
// `observe` sees the watch's polls too, so a red wait's checks line is replaced while the new CI is
|
|
131
|
+
// still pending rather than only after the watch (joshuafolkken/kit#3770).
|
|
132
|
+
async function watch_until_settled(branch_name: string, observe?: SnapshotObserver): Promise<void> {
|
|
129
133
|
await wait_for_pr_success({
|
|
130
134
|
branch_name,
|
|
131
|
-
fetcher:
|
|
135
|
+
fetcher: observed_fetcher(observe),
|
|
132
136
|
interval_ms: CHECK_WAIT_INTERVAL_MS,
|
|
133
137
|
max_attempts: compute_max_attempts(
|
|
134
138
|
PR_CHECKS_WATCH_TIMEOUT_MS / SECONDS_TO_MS,
|
|
@@ -154,9 +158,12 @@ async function fail_when_no_checks(branch_name: string): Promise<void> {
|
|
|
154
158
|
// Everything else — a failing check, an unreadable read, a branch with no checks — is rethrown, which
|
|
155
159
|
// is what the caller already handles: `git-pr-followup.ts` swallows it and falls through to its own
|
|
156
160
|
// poll.
|
|
157
|
-
async function pr_checks_watch(
|
|
161
|
+
async function pr_checks_watch(
|
|
162
|
+
branch_name: string,
|
|
163
|
+
observe?: SnapshotObserver,
|
|
164
|
+
): Promise<WatchResult> {
|
|
158
165
|
try {
|
|
159
|
-
await watch_until_settled(branch_name)
|
|
166
|
+
await watch_until_settled(branch_name, observe)
|
|
160
167
|
|
|
161
168
|
return { timed_out: false }
|
|
162
169
|
} catch (error) {
|
|
@@ -128,6 +128,9 @@ interface WaitForPrSuccessOptions {
|
|
|
128
128
|
max_attempts: number
|
|
129
129
|
required_stable_reads: number
|
|
130
130
|
evaluator?: PrStateEvaluator
|
|
131
|
+
// Asked before every poll; a throw ends the wait with that error. `josh followup` stops here on a
|
|
132
|
+
// person's unread issue update (`issue-watch-gate.ts`).
|
|
133
|
+
before_poll?: (() => Promise<void>) | undefined
|
|
131
134
|
}
|
|
132
135
|
|
|
133
136
|
function parse_repo_name_from_package(package_json_content: string): string {
|
|
@@ -272,6 +275,8 @@ async function wait_for_pr_success(options: WaitForPrSuccessOptions): Promise<Pr
|
|
|
272
275
|
|
|
273
276
|
for (let attempt = 0; attempt < options.max_attempts; attempt += 1) {
|
|
274
277
|
console.info(`Checking PR status… (${String(attempt + 1)}/${String(options.max_attempts)})`)
|
|
278
|
+
// eslint-disable-next-line no-await-in-loop -- polling: the check belongs to this poll, not to all of them at once
|
|
279
|
+
await options.before_poll?.()
|
|
275
280
|
// eslint-disable-next-line no-await-in-loop -- polling: each read waits on the state the previous one saw
|
|
276
281
|
carried = await attempt_pr_success_poll({
|
|
277
282
|
options,
|
|
@@ -311,16 +316,38 @@ async function default_fetch_pr_state(
|
|
|
311
316
|
// **`required_stable_reads` is a parameter, not a constant, so the caller can lower it** — `run_checks`
|
|
312
317
|
// passes `WATCH_CONFIRMED_STABLE_READS` when the watch already confirmed completion. It defaults to
|
|
313
318
|
// `DEFAULT_STABLE_READS`, so every caller that names only the branch is exactly where it was.
|
|
319
|
+
// What a caller is shown of each poll it waits on — `followup` reports the checks to the run's event
|
|
320
|
+
// stream from it (joshuafolkken/kit#3770). It decides nothing: the wait reads the same snapshot.
|
|
321
|
+
type SnapshotObserver = (snapshot: PrStateSnapshot) => Promise<void>
|
|
322
|
+
|
|
323
|
+
function observed_fetcher(observe: SnapshotObserver | undefined): PrStateFetcher {
|
|
324
|
+
if (observe === undefined) return default_fetch_pr_state
|
|
325
|
+
|
|
326
|
+
return async function fetch_observed(
|
|
327
|
+
branch_name: string,
|
|
328
|
+
should_read_review_decision: ReviewDecisionPredicate,
|
|
329
|
+
): Promise<PrStateSnapshot> {
|
|
330
|
+
const snapshot = await default_fetch_pr_state(branch_name, should_read_review_decision)
|
|
331
|
+
|
|
332
|
+
await observe(snapshot)
|
|
333
|
+
|
|
334
|
+
return snapshot
|
|
335
|
+
}
|
|
336
|
+
}
|
|
337
|
+
|
|
314
338
|
async function wait_for_pr_success_default(
|
|
315
339
|
branch_name: string,
|
|
316
340
|
required_stable_reads: number = DEFAULT_STABLE_READS,
|
|
341
|
+
before_poll?: () => Promise<void>,
|
|
342
|
+
observe?: SnapshotObserver,
|
|
317
343
|
): Promise<PrStateSnapshot> {
|
|
318
344
|
return await wait_for_pr_success({
|
|
319
345
|
branch_name,
|
|
320
|
-
fetcher:
|
|
346
|
+
fetcher: observed_fetcher(observe),
|
|
321
347
|
interval_ms: CHECK_WAIT_INTERVAL_MS,
|
|
322
348
|
max_attempts: CHECK_MAX_ATTEMPTS,
|
|
323
349
|
required_stable_reads,
|
|
350
|
+
before_poll,
|
|
324
351
|
})
|
|
325
352
|
}
|
|
326
353
|
|
|
@@ -383,10 +410,17 @@ export {
|
|
|
383
410
|
SECONDS_TO_MS,
|
|
384
411
|
is_pr_checks_timeout,
|
|
385
412
|
default_fetch_pr_state,
|
|
413
|
+
observed_fetcher,
|
|
386
414
|
MERGE_GATE_EVALUATOR,
|
|
387
415
|
should_always_read_review_decision,
|
|
388
416
|
}
|
|
389
|
-
export type {
|
|
417
|
+
export type {
|
|
418
|
+
MergeProgress,
|
|
419
|
+
PrStateFetcher,
|
|
420
|
+
PrStateEvaluator,
|
|
421
|
+
ReviewDecisionPredicate,
|
|
422
|
+
SnapshotObserver,
|
|
423
|
+
}
|
|
390
424
|
export {
|
|
391
425
|
collect_blocking_failures,
|
|
392
426
|
describe_pr_failure,
|
|
@@ -166,14 +166,25 @@ async function delete_branch(branch_name: string): Promise<void> {
|
|
|
166
166
|
// report "nothing is stranded" about a branch nobody measured, which is the one wrong answer that
|
|
167
167
|
// loses work. `count_merges` guards the same way and falls back to `0` instead, because there a
|
|
168
168
|
// missing count means an empty range rather than a claim about somebody's commits.
|
|
169
|
-
async function
|
|
170
|
-
const
|
|
171
|
-
const output = await git_spawn.read(['rev-list', '--count', range])
|
|
169
|
+
async function read_commit_count(revisions: ReadonlyArray<string>): Promise<number> {
|
|
170
|
+
const output = await git_spawn.read(['rev-list', '--count', ...revisions])
|
|
172
171
|
const parsed = Number(output)
|
|
173
172
|
|
|
174
173
|
if (output.length > 0 && Number.isFinite(parsed)) return parsed
|
|
175
174
|
|
|
176
|
-
throw new Error(
|
|
175
|
+
throw new Error(
|
|
176
|
+
`\`git rev-list --count ${revisions.join(' ')}\` printed \`${output}\` rather than a count`,
|
|
177
|
+
)
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
async function commit_count_beyond(base: string, tip: string): Promise<number> {
|
|
181
|
+
return await read_commit_count([`${base}..${tip}`])
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
// How many commits `branch_name` holds that no remote-tracking ref reaches — what deleting the branch
|
|
185
|
+
// would lose. An unparseable count throws for the reason `commit_count_beyond`'s does.
|
|
186
|
+
async function commit_count_unpushed(branch_name: string): Promise<number> {
|
|
187
|
+
return await read_commit_count([branch_name, '--not', '--remotes'])
|
|
177
188
|
}
|
|
178
189
|
|
|
179
190
|
function is_exit_code_128(cause: unknown): boolean {
|
|
@@ -397,6 +408,7 @@ const git_command = {
|
|
|
397
408
|
checkout,
|
|
398
409
|
commit,
|
|
399
410
|
commit_count_beyond,
|
|
411
|
+
commit_count_unpushed,
|
|
400
412
|
delete_branch,
|
|
401
413
|
push,
|
|
402
414
|
pull_fast_forward,
|
|
@@ -72,4 +72,4 @@ function toplevel(timeout: number = GIT_TIMEOUT_MS): GitSyncResult {
|
|
|
72
72
|
const git_spawn_sync = { GIT_TIMEOUT_MS, C_LOCALE, run, read, origin_url, toplevel }
|
|
73
73
|
|
|
74
74
|
export { git_spawn_sync }
|
|
75
|
-
export type { GitSyncResult }
|
|
75
|
+
export type { GitSyncOptions, GitSyncResult }
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { duplicate_read_outcome } from '#scripts/delegation/duplicate-read-guard'
|
|
2
2
|
import { investigation_refusal } from '#scripts/delegation/investigation-guard'
|
|
3
|
+
import { issue_watch_hook } from '#scripts/issue/issue-watch-hook'
|
|
3
4
|
import { hook_decision, type GuardOutcome } from '#scripts/josh/hook-decision'
|
|
4
5
|
import { lane_phase } from '#scripts/lane/lane-phase'
|
|
5
6
|
import { json_value } from '#scripts/lib/json-value'
|
|
@@ -104,8 +105,9 @@ async function mark_phase(outcome: GuardOutcome): Promise<void> {
|
|
|
104
105
|
// surfaced first.
|
|
105
106
|
async function async_reason(raw_payload: string): Promise<string | undefined> {
|
|
106
107
|
const watcher = await run_watcher_hook.watcher_hook_reason(raw_payload)
|
|
108
|
+
const cut = watcher ?? (await run_parent_cut_hook.parent_cut_reason(raw_payload))
|
|
107
109
|
|
|
108
|
-
return
|
|
110
|
+
return cut ?? (await issue_watch_hook.watch_reason())
|
|
109
111
|
}
|
|
110
112
|
|
|
111
113
|
async function pretool_outcome_async(raw_payload: string): Promise<GuardOutcome> {
|
|
@@ -25,6 +25,30 @@ const CONSUMER_PACKAGE_MANIFEST = `${claude_plugin_config.MARKETPLACE_PATH}/pack
|
|
|
25
25
|
const MISSING_INSTALL_NOTICE = 'kit hooks inactive: run pnpm install, then reread CLAUDE.md'
|
|
26
26
|
const MISSING_INSTALL_GUARD = `if [ ! -f ${CONSUMER_PACKAGE_MANIFEST} ]; then echo '${MISSING_INSTALL_NOTICE}' >&2; exit 0; fi; `
|
|
27
27
|
const CODEX_HOOKS_DESTINATION = '.codex/hooks.json'
|
|
28
|
+
// Every shape a hook command kit wrote can carry: the launcher as kit's own file names it, the
|
|
29
|
+
// launcher rebased onto the installed package, and the installed dispatcher the releases from
|
|
30
|
+
// 2026-09-13 (joshuafolkken/kit#1930) to the launcher rebased `pnpm josh` onto. The settings merge
|
|
31
|
+
// tells kit's hooks from a project's own by these and the list below, so the shapes stay beside the
|
|
32
|
+
// rewrite that produces them.
|
|
33
|
+
const KIT_COMMAND_MARKERS: ReadonlyArray<string> = [
|
|
34
|
+
RUN_HOOK_INVOCATION,
|
|
35
|
+
CONSUMER_RUN_HOOK_INVOCATION,
|
|
36
|
+
BUNDLE_INVOCATION,
|
|
37
|
+
]
|
|
38
|
+
// Before joshuafolkken/kit#1930 nothing was rebased: a consumer received these six as the whole hook
|
|
39
|
+
// command. A plain `pnpm josh <command>` is also how a project writes a hook of its own, so only the
|
|
40
|
+
// exact commands count — and kit still delivers each of them through the launcher, so one left in
|
|
41
|
+
// place would run its guard or formatter a second time. The list is history and never grows.
|
|
42
|
+
const PLAIN_KIT_COMMANDS: ReadonlySet<string> = new Set(
|
|
43
|
+
[
|
|
44
|
+
'audit:provision',
|
|
45
|
+
'batch:guard',
|
|
46
|
+
'format:edited',
|
|
47
|
+
'investigation:guard',
|
|
48
|
+
'rule:guard',
|
|
49
|
+
'session:lang',
|
|
50
|
+
].map((command) => `${PNPM_JOSH_PREFIX}${command}`),
|
|
51
|
+
)
|
|
28
52
|
// The values below are matched inside raw JSON text, so the root prefix is taken in its escaped form.
|
|
29
53
|
const PROJECT_ROOT_PREFIX = JSON.stringify(hook_launch.PROJECT_ROOT_PREFIX).slice(1, -1)
|
|
30
54
|
// Capture the value of every `"command"` field, escapes and all, so the rewrites below touch command
|
|
@@ -80,8 +104,15 @@ function apply_hook_command_rewrite_for_destination(
|
|
|
80
104
|
return content
|
|
81
105
|
}
|
|
82
106
|
|
|
107
|
+
function is_kit_hook_command(command: string): boolean {
|
|
108
|
+
if (PLAIN_KIT_COMMANDS.has(command)) return true
|
|
109
|
+
|
|
110
|
+
return KIT_COMMAND_MARKERS.some((marker) => command.includes(marker))
|
|
111
|
+
}
|
|
112
|
+
|
|
83
113
|
const hook_command_rewrite = {
|
|
84
114
|
apply_hook_command_rewrite_for_destination,
|
|
115
|
+
is_kit_hook_command,
|
|
85
116
|
rewrite_hook_commands,
|
|
86
117
|
}
|
|
87
118
|
|
|
@@ -221,6 +221,9 @@ function build_basic_actions(shape: ProjectShape): ReadonlyArray<FileAction> {
|
|
|
221
221
|
}
|
|
222
222
|
|
|
223
223
|
actions.push(...build_basic_vscode_actions(shape))
|
|
224
|
+
// The `.claude/settings.json` a `josh start` project receives wires the `session:lang` hook, whose
|
|
225
|
+
// unset default is `ja`, so the language is seeded from the locale as on `full`.
|
|
226
|
+
if (shape.has_workflow) actions.push(...session_language_environment.build_session_lang_actions())
|
|
224
227
|
|
|
225
228
|
return actions
|
|
226
229
|
}
|
|
@@ -248,4 +251,4 @@ function build_file_actions(shape?: ProjectShape): ReadonlyArray<FileAction> {
|
|
|
248
251
|
const init_actions = { build_file_actions, read_package_json }
|
|
249
252
|
|
|
250
253
|
export type { FileAction }
|
|
251
|
-
export { init_actions, PRETTIER_CONFIG_JS, BASIC_PRETTIER_CONFIG }
|
|
254
|
+
export { init_actions, PRETTIER_CONFIG_JS, BASIC_PRETTIER_CONFIG, LEFTHOOK_PATH }
|
|
@@ -25,9 +25,11 @@ const PRETTIER_IGNORE = '.prettierignore'
|
|
|
25
25
|
const GIT_ATTRIBUTES = '.gitattributes'
|
|
26
26
|
const CODE_OF_CONDUCT = 'CODE_OF_CONDUCT.md'
|
|
27
27
|
const { SECURITY_MD } = security_policy
|
|
28
|
+
const AGENTS_MD = 'AGENTS.md'
|
|
29
|
+
const GEMINI_MD = 'GEMINI.md'
|
|
28
30
|
const BASIC_AI_FILES = [
|
|
29
|
-
|
|
30
|
-
|
|
31
|
+
AGENTS_MD,
|
|
32
|
+
GEMINI_MD,
|
|
31
33
|
PRETTIER_IGNORE,
|
|
32
34
|
GIT_ATTRIBUTES,
|
|
33
35
|
CODE_OF_CONDUCT,
|
|
@@ -36,6 +38,8 @@ const BASIC_AI_FILES = [
|
|
|
36
38
|
]
|
|
37
39
|
// A basic project gets its own template for these, not kit's development copy.
|
|
38
40
|
const BASIC_SOURCES: Readonly<Record<string, string>> = {
|
|
41
|
+
[AGENTS_MD]: 'templates/AGENTS.basic.md',
|
|
42
|
+
[GEMINI_MD]: 'templates/GEMINI.basic.md',
|
|
39
43
|
[PRETTIER_IGNORE]: 'templates/prettierignore.basic',
|
|
40
44
|
[WORKSPACE_YAML]: 'templates/pnpm-workspace.basic.yaml',
|
|
41
45
|
}
|
|
@@ -43,6 +47,9 @@ const BASIC_SOURCES: Readonly<Record<string, string>> = {
|
|
|
43
47
|
const CONSUMER_SOURCES: Readonly<Record<string, string>> = {
|
|
44
48
|
[SECURITY_MD]: security_policy.SECURITY_TEMPLATE,
|
|
45
49
|
}
|
|
50
|
+
// A basic project `josh start` set up for the GitHub Issue workflow runs its agents unattended, on the
|
|
51
|
+
// permissions, hooks and plugin this file declares (#3796).
|
|
52
|
+
const BASIC_WORKFLOW_AI_FILES = [...BASIC_AI_FILES, '.claude/settings.json']
|
|
46
53
|
const GITHUB_FILES = new Set([CODE_OF_CONDUCT, SECURITY_MD])
|
|
47
54
|
const BASIC_POINTER_MAPPING = { src: 'templates/cursorrules.basic', dest: '.cursorrules' }
|
|
48
55
|
const PULL_REQUEST_TEMPLATE = '.github/pull_request_template.md'
|
|
@@ -62,11 +69,15 @@ function did_read_unstamped(destination_path: string): boolean {
|
|
|
62
69
|
}
|
|
63
70
|
}
|
|
64
71
|
|
|
65
|
-
|
|
66
|
-
|
|
72
|
+
// What kit writes at a destination it copies to — the content `josh uninstall` compares a project's
|
|
73
|
+
// file against, so it is read from the one place the copy itself reads it.
|
|
74
|
+
function copied_content(source_path: string, destination_path: string): string {
|
|
75
|
+
return transform_copied_content(destination_path, readFileSync(source_path, 'utf8'))
|
|
76
|
+
}
|
|
67
77
|
|
|
78
|
+
function copy_ai_file(source_path: string, destination_path: string): void {
|
|
68
79
|
mkdirSync(path.dirname(destination_path), { recursive: true })
|
|
69
|
-
writeFileSync(destination_path,
|
|
80
|
+
writeFileSync(destination_path, copied_content(source_path, destination_path))
|
|
70
81
|
}
|
|
71
82
|
|
|
72
83
|
// `init` declines to modify a file that already exists, and that includes not stamping it: the
|
|
@@ -132,6 +143,14 @@ function merge_existing_workspace_yaml(source_path: string, destination_path: st
|
|
|
132
143
|
console.info(` ✔ updated ${WORKSPACE_YAML}`)
|
|
133
144
|
}
|
|
134
145
|
|
|
146
|
+
// The workspace file kit writes where the project has none.
|
|
147
|
+
function workspace_content(source_path: string, destination_path: string): string {
|
|
148
|
+
return transform_copied_content(
|
|
149
|
+
destination_path,
|
|
150
|
+
read_workspace_template(source_path, destination_path),
|
|
151
|
+
)
|
|
152
|
+
}
|
|
153
|
+
|
|
135
154
|
function did_skip_workspace_yaml_copy(source_path: string, destination_path: string): boolean {
|
|
136
155
|
if (existsSync(destination_path)) {
|
|
137
156
|
merge_existing_workspace_yaml(source_path, destination_path)
|
|
@@ -139,9 +158,7 @@ function did_skip_workspace_yaml_copy(source_path: string, destination_path: str
|
|
|
139
158
|
return false
|
|
140
159
|
}
|
|
141
160
|
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
writeFileSync(destination_path, transform_copied_content(destination_path, template))
|
|
161
|
+
writeFileSync(destination_path, workspace_content(source_path, destination_path))
|
|
145
162
|
console.info(` ✔ created ${WORKSPACE_YAML}`)
|
|
146
163
|
|
|
147
164
|
return false
|
|
@@ -342,8 +359,12 @@ function should_copy_ai_file(filename: string, shape?: ProjectShape): boolean {
|
|
|
342
359
|
)
|
|
343
360
|
}
|
|
344
361
|
|
|
362
|
+
function basic_ai_files(shape: ProjectShape): ReadonlyArray<string> {
|
|
363
|
+
return shape.has_workflow ? BASIC_WORKFLOW_AI_FILES : BASIC_AI_FILES
|
|
364
|
+
}
|
|
365
|
+
|
|
345
366
|
function ai_files(shape?: ProjectShape): ReadonlyArray<string> {
|
|
346
|
-
const files = shape?.profile === 'basic' ?
|
|
367
|
+
const files = shape?.profile === 'basic' ? basic_ai_files(shape) : init_logic.get_ai_copy_files()
|
|
347
368
|
|
|
348
369
|
return files.filter((filename) => should_copy_ai_file(filename, shape))
|
|
349
370
|
}
|
|
@@ -397,6 +418,8 @@ function run_ai_copies(shape?: ProjectShape): string | undefined {
|
|
|
397
418
|
|
|
398
419
|
const init_ai_copy = {
|
|
399
420
|
copy_ai_file,
|
|
421
|
+
copied_content,
|
|
422
|
+
workspace_content,
|
|
400
423
|
run_ai_copies,
|
|
401
424
|
ai_files,
|
|
402
425
|
ai_file_source,
|
|
@@ -405,4 +428,4 @@ const init_ai_copy = {
|
|
|
405
428
|
read_workspace_template,
|
|
406
429
|
}
|
|
407
430
|
|
|
408
|
-
export { init_ai_copy, CLAUDE_MD_FILENAME }
|
|
431
|
+
export { init_ai_copy, CLAUDE_MD_FILENAME, WORKSPACE_YAML }
|
|
@@ -3,6 +3,7 @@ import { json_value } from '#scripts/lib/json-value'
|
|
|
3
3
|
import { z } from 'zod'
|
|
4
4
|
import { init_logic } from './init-logic'
|
|
5
5
|
import type { ProjectShape } from './project-profile'
|
|
6
|
+
import { workflow_mark } from './workflow-mark'
|
|
6
7
|
|
|
7
8
|
const KIT_PACKAGE_NAME = '@joshuafolkken/kit'
|
|
8
9
|
const record_schema = z.record(z.string(), z.unknown())
|
|
@@ -16,13 +17,17 @@ function initial_manifest(): string {
|
|
|
16
17
|
return json_format.format_json({ private: true })
|
|
17
18
|
}
|
|
18
19
|
|
|
19
|
-
function with_recorded_profile(
|
|
20
|
+
function with_recorded_profile(
|
|
21
|
+
content: string,
|
|
22
|
+
profile: ProjectShape['profile'],
|
|
23
|
+
has_workflow = false,
|
|
24
|
+
): string {
|
|
20
25
|
const parsed: unknown = JSON.parse(content)
|
|
21
26
|
const manifest = record_schema.parse(parsed)
|
|
22
27
|
const existing = manifest['josh']
|
|
23
28
|
const josh = json_value.is_record(existing) ? existing : {}
|
|
24
29
|
|
|
25
|
-
manifest['josh'] = { ...josh, profile }
|
|
30
|
+
manifest['josh'] = { ...josh, profile, ...workflow_mark.recorded_fields(has_workflow) }
|
|
26
31
|
|
|
27
32
|
return json_format.format_json(manifest)
|
|
28
33
|
}
|
|
@@ -32,7 +37,7 @@ function merge_basic_manifest(
|
|
|
32
37
|
shape: ProjectShape,
|
|
33
38
|
versions: BasicVersions,
|
|
34
39
|
): string {
|
|
35
|
-
const with_profile = with_recorded_profile(content, shape.profile)
|
|
40
|
+
const with_profile = with_recorded_profile(content, shape.profile, shape.has_workflow)
|
|
36
41
|
const with_script = init_logic.merge_package_scripts(with_profile, {
|
|
37
42
|
preinstall: init_logic.SAFE_CHAIN_CMD,
|
|
38
43
|
josh: 'josh',
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
import { parse_jsonc } from '#scripts/config-merge/parse-jsonc'
|
|
2
|
+
import { json_value } from '#scripts/lib/json-value'
|
|
3
|
+
import { init_logic_json_merge } from './init-logic-json-merge'
|
|
4
|
+
import { kit_development_versions } from './kit-development-versions'
|
|
5
|
+
|
|
6
|
+
// The tools a consumer has to resolve itself, as opposed to the ones only kit's own ESLint config
|
|
7
|
+
// imports. Those are optional kit peers, which pnpm does not install unlisted, so a consumer still
|
|
8
|
+
// needs them — `josh init` writes them, `josh sync` leaves them to it (#3711). A tool belongs here
|
|
9
|
+
// when the consumer runs its bin from the project root (eslint, prettier, tsc, playwright, cspell,
|
|
10
|
+
// lefthook), when a config the consumer owns imports it (`playwright.config.ts` → @playwright/test,
|
|
11
|
+
// @types/node), or when it is named by a string Prettier resolves from the consumer (the preset's
|
|
12
|
+
// plugins). `josh init` and `josh sync` both read this one list, so a tool added here reaches a fresh
|
|
13
|
+
// project and an already-initialized one alike.
|
|
14
|
+
const CONSUMER_RESOLVED_TOOLS = [
|
|
15
|
+
'eslint',
|
|
16
|
+
'prettier',
|
|
17
|
+
'typescript',
|
|
18
|
+
'@playwright/test',
|
|
19
|
+
'@ianvs/prettier-plugin-sort-imports',
|
|
20
|
+
'prettier-plugin-svelte',
|
|
21
|
+
'prettier-plugin-tailwindcss',
|
|
22
|
+
'cspell',
|
|
23
|
+
'@types/node',
|
|
24
|
+
]
|
|
25
|
+
// Only a project with Git gets `lefthook.yml`, so only that one needs the bin.
|
|
26
|
+
const LEFTHOOK_DEVELOPMENT_DEPENDENCY = 'lefthook'
|
|
27
|
+
const DECLARING_FIELDS = [
|
|
28
|
+
'dependencies',
|
|
29
|
+
'devDependencies',
|
|
30
|
+
'peerDependencies',
|
|
31
|
+
'optionalDependencies',
|
|
32
|
+
]
|
|
33
|
+
|
|
34
|
+
function consumer_resolved_names(has_git: boolean): ReadonlyArray<string> {
|
|
35
|
+
return has_git
|
|
36
|
+
? [...CONSUMER_RESOLVED_TOOLS, LEFTHOOK_DEVELOPMENT_DEPENDENCY]
|
|
37
|
+
: CONSUMER_RESOLVED_TOOLS
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
// What `josh init` adds: every kit peer — the ones only kit's own configs import included — and the
|
|
41
|
+
// tools above. Versions mirror kit's own devDependencies, so the consumer runs the toolchain kit is
|
|
42
|
+
// verified with.
|
|
43
|
+
function get_toolchain_development_dependencies(has_git: boolean): Record<string, string> {
|
|
44
|
+
return kit_development_versions.versions_of([
|
|
45
|
+
...kit_development_versions.peer_names(),
|
|
46
|
+
...consumer_resolved_names(has_git),
|
|
47
|
+
])
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function is_declared(manifest: Record<string, unknown>, name: string): boolean {
|
|
51
|
+
return DECLARING_FIELDS.some((field) => {
|
|
52
|
+
const dependencies = manifest[field]
|
|
53
|
+
|
|
54
|
+
return json_value.is_record(dependencies) && Object.hasOwn(dependencies, name)
|
|
55
|
+
})
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
// What `josh sync` adds to an already-initialized consumer: only the tools above, and only the ones
|
|
59
|
+
// the manifest declares nowhere — a tool kept under `dependencies` is present, not missing.
|
|
60
|
+
function merge_consumer_resolved_development_deps(content: string, has_git: boolean): string {
|
|
61
|
+
const manifest = parse_jsonc(content)
|
|
62
|
+
const missing = consumer_resolved_names(has_git).filter((name) => !is_declared(manifest, name))
|
|
63
|
+
|
|
64
|
+
return init_logic_json_merge.merge_development_dependencies(
|
|
65
|
+
content,
|
|
66
|
+
kit_development_versions.versions_of(missing),
|
|
67
|
+
)
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
const init_logic_toolchain = {
|
|
71
|
+
consumer_resolved_names,
|
|
72
|
+
get_toolchain_development_dependencies,
|
|
73
|
+
merge_consumer_resolved_development_deps,
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
export { init_logic_toolchain }
|