@joshuafolkken/kit 1.1387.0 → 1.1453.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-park.md +2 -0
- package/.claude/skills/workflow-commands/backlogrun-steps.md +13 -10
- package/.claude/skills/workflow-commands/backlogrun.md +3 -2
- package/.claude/skills/workflow-commands/chain-rule.md +4 -2
- package/.claude/skills/workflow-commands/fullrun-steps.md +1 -1
- package/.claude/skills/workflow-commands/halfrun.md +1 -1
- package/.claude/skills/workflow-commands/issue-comments.md +6 -0
- package/.claude/skills/workflow-commands/kickoff.md +12 -13
- package/.claude/skills/workflow-commands/observation-filing.md +1 -1
- package/.claude/skills/workflow-commands/observation-ledger.md +3 -2
- package/.claude/skills/workflow-commands/pre-gate-cut.md +5 -3
- package/.claude/skills/workflow-commands/prerequisite.md +6 -0
- package/.claude/skills/workflow-commands/retrospective.md +5 -5
- package/.claude/skills/workflow-commands/split-assessment.md +2 -2
- package/README.md +27 -4
- package/dist/hooks/audit-provision.js +2 -2
- package/dist/hooks/batch-guard.js +7 -7
- package/dist/hooks/{chunk-2YSW3CXH.js → chunk-3C4XRYG6.js} +35 -11
- package/dist/hooks/{chunk-GJ7JPIHN.js → chunk-5C7L6FRC.js} +2704 -190
- package/dist/hooks/{chunk-FZ6FUSBM.js → chunk-723HKZO5.js} +1 -1
- package/dist/hooks/{chunk-STDSEPWU.js → chunk-D3NJXN5M.js} +4 -4
- package/dist/hooks/{chunk-NEC4AT4Z.js → chunk-DGT54IH5.js} +4 -0
- package/dist/hooks/{chunk-LMWBRJIP.js → chunk-EF5GQ6WS.js} +3 -3
- package/dist/hooks/{chunk-JBBMAPC4.js → chunk-IX6TTTI6.js} +4 -0
- package/dist/hooks/{chunk-IYVOLN6A.js → chunk-LBFYEOXJ.js} +3 -3
- package/dist/hooks/{chunk-SL4GQ2XP.js → chunk-MBJYQPEK.js} +106 -23
- package/dist/hooks/{chunk-WNFJLBXI.js → chunk-NO4AALP5.js} +3 -3
- package/dist/hooks/{chunk-J3KSS7II.js → chunk-OLMO72ON.js} +1 -1
- package/dist/hooks/{chunk-DCY2GWHC.js → chunk-SHYUQOIS.js} +5 -5
- package/dist/hooks/{chunk-EAQEGME7.js → chunk-UT7YC3N2.js} +1 -1
- package/dist/hooks/{chunk-EBSHN25W.js → chunk-W5ZKGTSY.js} +4 -4
- package/dist/hooks/{chunk-GNDJ7NJ7.js → chunk-Y3GKIKGW.js} +209 -56
- package/dist/hooks/{chunk-U32VSRNG.js → chunk-Z4XI7CZS.js} +89 -34
- 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 +261 -201
- 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 +95 -19
- package/package.json +4 -3
- package/prompts/collaboration-workflow/issue-template.md +10 -8
- package/prompts/review.md +30 -4
- 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/auto-ok/auto-ok-judge-cli.ts +125 -0
- package/scripts/auto-ok/auto-ok-judge-read.ts +95 -0
- package/scripts/auto-ok/auto-ok-judge-write.ts +109 -0
- package/scripts/auto-ok/auto-ok-judge.ts +67 -0
- package/scripts/backlog/backlog-drive-cli.ts +14 -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 +38 -11
- 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 +175 -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 +18 -4
- 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-rest.ts +1 -0
- package/scripts/gh/git-gh-issue-write.ts +5 -1
- package/scripts/gh/git-gh-issue.ts +13 -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 +7 -4
- package/scripts/init/init-ai-copy.ts +34 -11
- package/scripts/init/init-basic.ts +9 -4
- package/scripts/init/init-bootstrap.ts +6 -2
- package/scripts/init/init-dry-run-report.ts +150 -0
- package/scripts/init/init-dry-run.ts +189 -0
- package/scripts/init/init-install.ts +8 -1
- package/scripts/init/init-logic-toolchain.ts +76 -0
- package/scripts/init/init-logic.ts +2 -0
- package/scripts/init/init.ts +62 -42
- package/scripts/init/kit-setup-state.ts +11 -17
- package/scripts/init/own-formatter.ts +21 -0
- package/scripts/init/project-profile.ts +60 -12
- package/scripts/init/start-dry-run.ts +70 -0
- package/scripts/init/start-exec.ts +27 -1
- package/scripts/init/start-plan.ts +23 -3
- package/scripts/init/start-setup-pr.ts +13 -24
- package/scripts/init/start-steps.ts +62 -1
- package/scripts/init/start.ts +38 -17
- package/scripts/init/workflow-mark.ts +35 -0
- package/scripts/init/working-tree.ts +116 -0
- package/scripts/issue/issue-ack-cli.ts +84 -0
- package/scripts/issue/issue-auto-ok.ts +68 -99
- package/scripts/issue/issue-file-cli.ts +17 -17
- package/scripts/issue/issue-file.ts +1 -6
- package/scripts/issue/issue-labels.ts +46 -10
- package/scripts/issue/issue-release-cli.ts +69 -23
- package/scripts/issue/issue-release.ts +3 -1
- 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-ai.ts +11 -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 +10 -3
- package/scripts/josh/josh-commands-lane.ts +11 -2
- package/scripts/josh/josh-commands-project.ts +26 -2
- package/scripts/josh/josh-verdict.ts +33 -4
- package/scripts/josh/process-identity.ts +32 -6
- package/scripts/lane/claude-lane-broker.ts +142 -0
- package/scripts/lane/lane-await.ts +6 -2
- package/scripts/lane/lane-broker-server.ts +69 -0
- 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/lane-session-broker.ts +363 -0
- 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/observations/observation-ledger-migrate.ts +3 -15
- package/scripts/release/release-issue-close.ts +133 -0
- package/scripts/release/release-publish.ts +12 -1
- package/scripts/repo/repository-labels.ts +11 -1
- package/scripts/review/comment-ranges.ts +199 -0
- package/scripts/review/review-attest-cli.ts +21 -6
- package/scripts/review/review-brief-cli.ts +24 -7
- package/scripts/review/review-code-tree.ts +119 -0
- package/scripts/review/review-comment-round.ts +114 -0
- package/scripts/review/review-record-cli.ts +39 -17
- package/scripts/review/review-round2-cli.ts +16 -5
- package/scripts/review/review-round2.ts +30 -10
- package/scripts/review/review-stamps.ts +22 -1
- 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/lane/openai-review-broker.ts +0 -262
- package/scripts/run/board/run-board-track.ts +0 -22
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
import { json_format } from '#scripts/config-merge/json-format'
|
|
2
|
+
import { json_value } from '#scripts/lib/json-value'
|
|
3
|
+
import { KIT_PACKAGE_NAME } from '#scripts/version/kit-descriptor'
|
|
4
|
+
|
|
5
|
+
// One file kit writes into a project, with the content it writes there. `distributed` is absent when
|
|
6
|
+
// that content cannot be worked out here — a file whose text depends on the repository it lands in,
|
|
7
|
+
// read where there is no such repository — and such a file is never one this command may delete.
|
|
8
|
+
interface DistributedFile {
|
|
9
|
+
dest: string
|
|
10
|
+
distributed: string | undefined
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
interface FilePlan {
|
|
14
|
+
remove: Array<string>
|
|
15
|
+
keep: Array<string>
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
type ReadFile = (destination: string) => string | undefined
|
|
19
|
+
|
|
20
|
+
// Never deleted, even while it is still what kit wrote: without it the `.env` and `node_modules` it
|
|
21
|
+
// kept out of Git turn into untracked files, and the next `git add -A` stages them.
|
|
22
|
+
const IGNORE_FILE = '.gitignore'
|
|
23
|
+
const DEPENDENCY_FIELDS = ['dependencies', 'devDependencies']
|
|
24
|
+
const JOSH_FIELD = 'josh'
|
|
25
|
+
const PROFILE_FIELD = 'profile'
|
|
26
|
+
// `strip_kit` takes kit's own entry back out of this field, so it is not left for a person to review.
|
|
27
|
+
const HANDLED_FIELDS: ReadonlySet<string> = new Set([JOSH_FIELD])
|
|
28
|
+
|
|
29
|
+
// A file is deleted only while it is still, byte for byte, what kit writes: anything else holds the
|
|
30
|
+
// project's own lines — an edit, or a file kit merged its entries into — and kit kept no record of
|
|
31
|
+
// which lines those are.
|
|
32
|
+
function classify_files(files: ReadonlyArray<DistributedFile>, read_file: ReadFile): FilePlan {
|
|
33
|
+
const present = files
|
|
34
|
+
.map((file) => ({ ...file, existing: read_file(file.dest) }))
|
|
35
|
+
.filter((file) => file.existing !== undefined)
|
|
36
|
+
const untouched = new Set(
|
|
37
|
+
present.filter((file) => file.existing === file.distributed).map((file) => file.dest),
|
|
38
|
+
)
|
|
39
|
+
const destinations = [...new Set(present.map((file) => file.dest))]
|
|
40
|
+
|
|
41
|
+
return {
|
|
42
|
+
remove: destinations.filter((destination) => untouched.has(destination)),
|
|
43
|
+
keep: destinations.filter((destination) => !untouched.has(destination)),
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function parse_manifest(manifest_text: string): Record<string, unknown> {
|
|
48
|
+
const parsed: unknown = JSON.parse(manifest_text)
|
|
49
|
+
|
|
50
|
+
if (!json_value.is_record(parsed)) throw new Error('package.json must contain an object')
|
|
51
|
+
|
|
52
|
+
return parsed
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function without_key(record: Record<string, unknown>, key: string): Record<string, unknown> {
|
|
56
|
+
return Object.fromEntries(Object.entries(record).filter(([name]) => name !== key))
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
// The field with one key taken out of the object it holds — and the field itself once that leaves
|
|
60
|
+
// the object empty, so a manifest kit created is not left carrying `"devDependencies": {}`.
|
|
61
|
+
function without_nested_key(
|
|
62
|
+
manifest: Record<string, unknown>,
|
|
63
|
+
field: string,
|
|
64
|
+
key: string,
|
|
65
|
+
): Record<string, unknown> {
|
|
66
|
+
const value = manifest[field]
|
|
67
|
+
if (!json_value.is_record(value) || !Object.hasOwn(value, key)) return manifest
|
|
68
|
+
const rest = without_key(value, key)
|
|
69
|
+
|
|
70
|
+
return Object.keys(rest).length === 0
|
|
71
|
+
? without_key(manifest, field)
|
|
72
|
+
: { ...manifest, [field]: rest }
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
// The manifest without kit's dependency entry and its profile record — the two entries that are
|
|
76
|
+
// kit's alone, so removing them cannot take anything of the project's.
|
|
77
|
+
function strip_kit(manifest_text: string): string {
|
|
78
|
+
let manifest = without_nested_key(parse_manifest(manifest_text), JOSH_FIELD, PROFILE_FIELD)
|
|
79
|
+
|
|
80
|
+
for (const field of DEPENDENCY_FIELDS) {
|
|
81
|
+
manifest = without_nested_key(manifest, field, KIT_PACKAGE_NAME)
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
return json_format.format_package_json(manifest)
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
// Whether the manifest still holds an entry `strip_kit` takes out — asked of the parsed manifest, so
|
|
88
|
+
// a file indented another way is not mistaken for one that has something to remove.
|
|
89
|
+
function has_kit_entries(manifest_text: string): boolean {
|
|
90
|
+
const unchanged = json_format.format_package_json(parse_manifest(manifest_text))
|
|
91
|
+
|
|
92
|
+
return strip_kit(manifest_text) !== unchanged
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
function shared_entries(field: string, kit_value: unknown, project_value: unknown): Array<string> {
|
|
96
|
+
if (project_value === undefined) return []
|
|
97
|
+
if (!json_value.is_record(kit_value) || !json_value.is_record(project_value)) return [field]
|
|
98
|
+
|
|
99
|
+
return Object.keys(kit_value)
|
|
100
|
+
.filter((key) => key !== KIT_PACKAGE_NAME && Object.hasOwn(project_value, key))
|
|
101
|
+
.map((key) => `${field}.${key}`)
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
// The manifest entries kit merges in that the project still holds, named `scripts.preinstall` style.
|
|
105
|
+
// `kit_manifest_text` is what kit's own merge writes into an empty manifest, so the names come from
|
|
106
|
+
// the merge and not from a second list of them. They are reported, never removed: the merge kept no
|
|
107
|
+
// record of the value it found, so a script named `josh` may be kit's or the project's own.
|
|
108
|
+
function manifest_review(manifest_text: string, kit_manifest_text: string): Array<string> {
|
|
109
|
+
const manifest = parse_manifest(manifest_text)
|
|
110
|
+
|
|
111
|
+
return Object.entries(parse_manifest(kit_manifest_text))
|
|
112
|
+
.filter(([field]) => !HANDLED_FIELDS.has(field))
|
|
113
|
+
.flatMap(([field, kit_value]) => shared_entries(field, kit_value, manifest[field]))
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
const uninstall_plan = { classify_files, strip_kit, has_kit_entries, manifest_review }
|
|
117
|
+
|
|
118
|
+
export { uninstall_plan, IGNORE_FILE }
|
|
119
|
+
export type { DistributedFile, FilePlan }
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import type { FilePlan } from './uninstall-plan'
|
|
2
|
+
|
|
3
|
+
interface UninstallPlan {
|
|
4
|
+
files: FilePlan
|
|
5
|
+
// Whether kit's dependency entry or profile record is still in `package.json` to take out.
|
|
6
|
+
has_manifest_entries: boolean
|
|
7
|
+
// Whether lefthook is installed here with a config kit wrote or merged into.
|
|
8
|
+
has_git_hooks: boolean
|
|
9
|
+
review: Array<string>
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
const NOTHING = ' (nothing)'
|
|
13
|
+
// The review comes first: the install runs whatever `scripts.prepare` is still in the manifest, and
|
|
14
|
+
// the one kit merged in installs lefthook's hooks again.
|
|
15
|
+
const NEXT_STEPS = [
|
|
16
|
+
'First clear "Review by hand" — a `scripts.prepare` left there reinstalls the git hooks.',
|
|
17
|
+
'Run `pnpm install` — it drops the package from node_modules and pnpm-lock.yaml.',
|
|
18
|
+
'Delete node_modules and pnpm-lock.yaml yourself if kit was the only dependency.',
|
|
19
|
+
'Labels, Issues and pull requests `josh start` created on GitHub are not touched.',
|
|
20
|
+
]
|
|
21
|
+
|
|
22
|
+
function section(title: string, lines: ReadonlyArray<string>): string {
|
|
23
|
+
const body = lines.length === 0 ? [NOTHING] : lines.map((line) => ` ${line}`)
|
|
24
|
+
|
|
25
|
+
return [`${title}:`, ...body].join('\n')
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function removal_lines(plan: UninstallPlan): Array<string> {
|
|
29
|
+
const lines = [...plan.files.remove]
|
|
30
|
+
|
|
31
|
+
if (plan.has_manifest_entries) {
|
|
32
|
+
lines.push('package.json — the @joshuafolkken/kit dependency and the josh.profile record')
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
if (plan.has_git_hooks) lines.push('git hooks installed by lefthook (lefthook uninstall)')
|
|
36
|
+
|
|
37
|
+
return lines
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
// Printed before anything is deleted, so the three lists are the whole of what the command will do
|
|
41
|
+
// and of what it leaves for a person: nothing outside the first one is ever written to.
|
|
42
|
+
function describe(plan: UninstallPlan): string {
|
|
43
|
+
return [
|
|
44
|
+
section('Remove (still exactly what kit wrote)', removal_lines(plan)),
|
|
45
|
+
section('Keep (differs from what kit writes — remove kit lines by hand)', plan.files.keep),
|
|
46
|
+
section('Review by hand (kit cannot tell these from your own)', plan.review),
|
|
47
|
+
].join('\n\n')
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function next_steps(): string {
|
|
51
|
+
return section('Next', NEXT_STEPS)
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function is_empty(plan: UninstallPlan): boolean {
|
|
55
|
+
return removal_lines(plan).length === 0
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
const uninstall_report = { describe, next_steps, is_empty }
|
|
59
|
+
|
|
60
|
+
export { uninstall_report }
|
|
61
|
+
export type { UninstallPlan }
|
|
@@ -0,0 +1,206 @@
|
|
|
1
|
+
#!/usr/bin/env tsx
|
|
2
|
+
import { existsSync, readdirSync, rmdirSync, rmSync, writeFileSync } from 'node:fs'
|
|
3
|
+
import path from 'node:path'
|
|
4
|
+
import { stdin as input, stdout as output } from 'node:process'
|
|
5
|
+
import { createInterface } from 'node:readline/promises'
|
|
6
|
+
import { fileURLToPath } from 'node:url'
|
|
7
|
+
import { resolve_local_bin } from '#scripts/build/local-bin'
|
|
8
|
+
import { ask_yes_no_simple } from '#scripts/git/git-prompt'
|
|
9
|
+
import { LEFTHOOK_PATH } from '#scripts/init/init-actions'
|
|
10
|
+
import { init_logic_sonar } from '#scripts/init/init-logic-sonar'
|
|
11
|
+
import { PACKAGE_DIR, PROJECT_ROOT } from '#scripts/init/init-paths'
|
|
12
|
+
import { project_profile, type ProjectProfile } from '#scripts/init/project-profile'
|
|
13
|
+
import { error_text } from '#scripts/lib/error-message'
|
|
14
|
+
import { file_reader } from '#scripts/lib/read-file'
|
|
15
|
+
import { PROJECT_CONFIG_FILE } from '#scripts/safe-chain/project-config'
|
|
16
|
+
import { did_refuse_self_run } from '#scripts/self-sync-guard/self-sync-refusal'
|
|
17
|
+
import { KIT_PACKAGE_NAME } from '#scripts/version/kit-descriptor'
|
|
18
|
+
import { execaSync } from 'execa'
|
|
19
|
+
import { distributed_files } from './distributed-files'
|
|
20
|
+
import { IGNORE_FILE, uninstall_plan, type FilePlan } from './uninstall-plan'
|
|
21
|
+
import { uninstall_report, type UninstallPlan } from './uninstall-report'
|
|
22
|
+
|
|
23
|
+
const PACKAGE_JSON = 'package.json'
|
|
24
|
+
const LEFTHOOK_BIN = 'lefthook'
|
|
25
|
+
const ARGUMENT_START_INDEX = 2
|
|
26
|
+
const YES_FLAG = '--yes'
|
|
27
|
+
const UNINSTALL_USAGE = 'josh uninstall [--profile basic|full] [--yes]'
|
|
28
|
+
const QUESTION = 'Remove the files and entries listed under "Remove"? (y/n): '
|
|
29
|
+
const NOT_CONFIRMED = 'Nothing was removed.'
|
|
30
|
+
const NO_TERMINAL = `${NOT_CONFIRMED} This is a preview: there is no terminal to confirm on, so pass --yes to remove what is listed.`
|
|
31
|
+
// Written from the repository's own name and settings, so there is no distributed content to compare
|
|
32
|
+
// them with: they are named for review and never deleted.
|
|
33
|
+
const REVIEW_ONLY_FILES = [init_logic_sonar.get_sonar_template_destination(), PROJECT_CONFIG_FILE]
|
|
34
|
+
const IGNORE_FILE_REVIEW = `${IGNORE_FILE} — delete it only once nothing it ignores (.env, node_modules) is left`
|
|
35
|
+
|
|
36
|
+
interface Options {
|
|
37
|
+
is_yes: boolean
|
|
38
|
+
profile: ProjectProfile
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function project_path(relative_path: string): string {
|
|
42
|
+
return path.join(PROJECT_ROOT, relative_path)
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function read_project_file(relative_path: string): string | undefined {
|
|
46
|
+
return file_reader.read_if_readable(project_path(relative_path))
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function parse_options(args: ReadonlyArray<string>): Options {
|
|
50
|
+
const rest = args.filter((argument) => argument !== YES_FLAG)
|
|
51
|
+
const requested = project_profile.requested_profile(rest, UNINSTALL_USAGE)
|
|
52
|
+
|
|
53
|
+
return {
|
|
54
|
+
is_yes: args.includes(YES_FLAG),
|
|
55
|
+
profile: project_profile.resolve_profile(PROJECT_ROOT, requested).profile,
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
// A lefthook config the profile distributes that still names the package — the one kit wrote, or the
|
|
60
|
+
// project's own with kit's preset merged in. One that names it nowhere is the project's alone.
|
|
61
|
+
function has_kit_lefthook_config(files: FilePlan): boolean {
|
|
62
|
+
if (![...files.remove, ...files.keep].includes(LEFTHOOK_PATH)) return false
|
|
63
|
+
|
|
64
|
+
return read_project_file(LEFTHOOK_PATH)?.includes(KIT_PACKAGE_NAME) === true
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
// Hooks are taken out only where the lefthook config is one kit wrote or merged into: once kit is
|
|
68
|
+
// gone that config extends a preset that no longer exists, and a hook left in `.git/hooks` would
|
|
69
|
+
// fail every commit on it.
|
|
70
|
+
function has_git_hooks(files: FilePlan): boolean {
|
|
71
|
+
return (
|
|
72
|
+
has_kit_lefthook_config(files) &&
|
|
73
|
+
existsSync(project_path('.git')) &&
|
|
74
|
+
existsSync(resolve_local_bin(PROJECT_ROOT, LEFTHOOK_BIN))
|
|
75
|
+
)
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
function review_entries(manifest: string | undefined, profile: ProjectProfile): Array<string> {
|
|
79
|
+
const merged =
|
|
80
|
+
manifest === undefined
|
|
81
|
+
? []
|
|
82
|
+
: uninstall_plan.manifest_review(manifest, distributed_files.kit_manifest(profile))
|
|
83
|
+
|
|
84
|
+
return [
|
|
85
|
+
...merged.map((entry) => `${PACKAGE_JSON} ${entry}`),
|
|
86
|
+
...REVIEW_ONLY_FILES.filter((file) => existsSync(project_path(file))),
|
|
87
|
+
...(existsSync(project_path(IGNORE_FILE)) ? [IGNORE_FILE_REVIEW] : []),
|
|
88
|
+
]
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
function build_plan(profile: ProjectProfile): UninstallPlan {
|
|
92
|
+
const files = uninstall_plan.classify_files(distributed_files.list(profile), read_project_file)
|
|
93
|
+
const manifest = read_project_file(PACKAGE_JSON)
|
|
94
|
+
|
|
95
|
+
return {
|
|
96
|
+
files,
|
|
97
|
+
has_manifest_entries: manifest !== undefined && uninstall_plan.has_kit_entries(manifest),
|
|
98
|
+
has_git_hooks: has_git_hooks(files),
|
|
99
|
+
review: review_entries(manifest, profile),
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
async function is_confirmed(is_yes: boolean): Promise<boolean> {
|
|
104
|
+
if (is_yes) return true
|
|
105
|
+
|
|
106
|
+
if (!process.stdin.isTTY) {
|
|
107
|
+
console.info(`\n${NO_TERMINAL}`)
|
|
108
|
+
|
|
109
|
+
return false
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
const prompt = createInterface({ input, output })
|
|
113
|
+
|
|
114
|
+
try {
|
|
115
|
+
const is_accepted = await ask_yes_no_simple(prompt, `\n${QUESTION}`)
|
|
116
|
+
if (!is_accepted) console.info(NOT_CONFIRMED)
|
|
117
|
+
|
|
118
|
+
return is_accepted
|
|
119
|
+
} finally {
|
|
120
|
+
prompt.close()
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
// Run before the config is deleted, while lefthook can still read the project it is leaving.
|
|
125
|
+
function remove_git_hooks(): void {
|
|
126
|
+
const bin = resolve_local_bin(PROJECT_ROOT, LEFTHOOK_BIN)
|
|
127
|
+
const result = execaSync(bin, ['uninstall'], {
|
|
128
|
+
cwd: PROJECT_ROOT,
|
|
129
|
+
stdio: 'inherit',
|
|
130
|
+
reject: false,
|
|
131
|
+
})
|
|
132
|
+
|
|
133
|
+
if (result.exitCode === 0) console.info(' ✔ removed git hooks')
|
|
134
|
+
else console.warn(' ⚠ lefthook uninstall failed — run it manually: lefthook uninstall')
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
function is_empty_directory(directory: string): boolean {
|
|
138
|
+
return existsSync(directory) && readdirSync(directory).length === 0
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
// A directory kit created for its own file — `.vscode`, `.github` — goes with the last file in it,
|
|
142
|
+
// and one that still holds anything of the project's stays.
|
|
143
|
+
function remove_empty_parents(relative_path: string): void {
|
|
144
|
+
let directory = path.dirname(relative_path)
|
|
145
|
+
|
|
146
|
+
while (directory !== '.' && is_empty_directory(project_path(directory))) {
|
|
147
|
+
rmdirSync(project_path(directory))
|
|
148
|
+
directory = path.dirname(directory)
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
function remove_file(relative_path: string): void {
|
|
153
|
+
rmSync(project_path(relative_path))
|
|
154
|
+
remove_empty_parents(relative_path)
|
|
155
|
+
console.info(` ✔ removed ${relative_path}`)
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
function strip_manifest(): void {
|
|
159
|
+
const manifest = read_project_file(PACKAGE_JSON)
|
|
160
|
+
if (manifest === undefined) return
|
|
161
|
+
|
|
162
|
+
writeFileSync(project_path(PACKAGE_JSON), uninstall_plan.strip_kit(manifest))
|
|
163
|
+
console.info(` ✔ updated ${PACKAGE_JSON}`)
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
function execute(plan: UninstallPlan): void {
|
|
167
|
+
console.info('\nRemoving:')
|
|
168
|
+
if (plan.has_git_hooks) remove_git_hooks()
|
|
169
|
+
for (const file of plan.files.remove) remove_file(file)
|
|
170
|
+
if (plan.has_manifest_entries) strip_manifest()
|
|
171
|
+
console.info(`\n${uninstall_report.next_steps()}\n\n✅ Done.\n`)
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
// The refusal `josh init` and `josh sync` make, for the opposite damage: in kit's own repository
|
|
175
|
+
// every distributed file is the source, and each of them equals what kit distributes.
|
|
176
|
+
async function main(args: ReadonlyArray<string> = []): Promise<void> {
|
|
177
|
+
if (did_refuse_self_run(PACKAGE_DIR, PROJECT_ROOT)) return
|
|
178
|
+
const options = parse_options(args)
|
|
179
|
+
const plan = build_plan(options.profile)
|
|
180
|
+
|
|
181
|
+
console.info(`\n🧹 Uninstalling @joshuafolkken/kit (profile: ${options.profile})\n`)
|
|
182
|
+
console.info(uninstall_report.describe(plan))
|
|
183
|
+
|
|
184
|
+
if (uninstall_report.is_empty(plan)) {
|
|
185
|
+
console.info('\nNothing of kit is left to remove.\n')
|
|
186
|
+
|
|
187
|
+
return
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
if (await is_confirmed(options.is_yes)) execute(plan)
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
async function run_cli(args: ReadonlyArray<string>): Promise<void> {
|
|
194
|
+
try {
|
|
195
|
+
await main(args)
|
|
196
|
+
} catch (error) {
|
|
197
|
+
console.error(`\n✖ ${error_text.message_of(error)}\n`)
|
|
198
|
+
process.exitCode = 1
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
if (process.argv[1] === fileURLToPath(import.meta.url)) {
|
|
203
|
+
await run_cli(process.argv.slice(ARGUMENT_START_INDEX))
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
export { main }
|
|
@@ -1,13 +0,0 @@
|
|
|
1
|
-
// scripts/josh/session-language.ts
|
|
2
|
-
var ENV_KEY = "JOSH_SESSION_LANG";
|
|
3
|
-
var DEFAULT_SESSION_LANG = "ja";
|
|
4
|
-
function resolve_session_lang() {
|
|
5
|
-
const value = process.env[ENV_KEY]?.trim();
|
|
6
|
-
if (value) return { lang: value, is_default: false };
|
|
7
|
-
return { lang: DEFAULT_SESSION_LANG, is_default: true };
|
|
8
|
-
}
|
|
9
|
-
var session_language = { DEFAULT_SESSION_LANG, ENV_KEY, resolve_session_lang };
|
|
10
|
-
|
|
11
|
-
export {
|
|
12
|
-
session_language
|
|
13
|
-
};
|
|
@@ -1,262 +0,0 @@
|
|
|
1
|
-
import { randomUUID } from 'node:crypto'
|
|
2
|
-
import { mkdirSync, readFileSync, renameSync, unlinkSync, writeFileSync } from 'node:fs'
|
|
3
|
-
import path from 'node:path'
|
|
4
|
-
import { setTimeout as sleep } from 'node:timers/promises'
|
|
5
|
-
import { agent_argv } from '#scripts/agent/agent-argv'
|
|
6
|
-
import { agent_role_profile } from '#scripts/agent/agent-role-profile'
|
|
7
|
-
import { git_common_directory } from '#scripts/git/git-common-directory'
|
|
8
|
-
import { stamp_file } from '#scripts/josh/stamp-file'
|
|
9
|
-
import { error_text } from '#scripts/lib/error-message'
|
|
10
|
-
import { json_value } from '#scripts/lib/json-value'
|
|
11
|
-
import { detached_launch } from '#scripts/run/detached-launch'
|
|
12
|
-
import { run_ship_review } from '#scripts/run/ship/run-ship-review'
|
|
13
|
-
import { z } from 'zod'
|
|
14
|
-
import type { LaneInfo } from './lane-registry'
|
|
15
|
-
|
|
16
|
-
const REQUEST_PREFIX = '.josh-review-request-'
|
|
17
|
-
const RESPONSE_PREFIX = '.josh-review-response-'
|
|
18
|
-
const BRIEF_PREFIX = 'josh-ship-review-brief-'
|
|
19
|
-
const FINDINGS_PREFIX = 'josh-ship-review-findings-'
|
|
20
|
-
const LOG_PREFIX = 'josh-ship-review-log-'
|
|
21
|
-
const CACHE_DIRECTORY = ['node_modules', '.cache']
|
|
22
|
-
const POLL_MS = 100
|
|
23
|
-
const HEARTBEAT_MS = 500
|
|
24
|
-
const HEARTBEAT_LIMIT_MS = 3000
|
|
25
|
-
const MINUTE_MS = 60_000
|
|
26
|
-
const REQUEST_LIMIT_MINUTES = 30
|
|
27
|
-
const REQUEST_LIMIT_MS = REQUEST_LIMIT_MINUTES * MINUTE_MS
|
|
28
|
-
const request_schema = z
|
|
29
|
-
.object({ session: z.uuid(), nonce: z.uuid(), round: z.enum(['1', '2']) })
|
|
30
|
-
.strict()
|
|
31
|
-
const response_schema = z
|
|
32
|
-
.object({
|
|
33
|
-
session: z.uuid(),
|
|
34
|
-
nonce: z.string(),
|
|
35
|
-
is_success: z.boolean(),
|
|
36
|
-
heartbeat_at: z.number().int().positive(),
|
|
37
|
-
})
|
|
38
|
-
.strict()
|
|
39
|
-
type ReviewRequest = z.infer<typeof request_schema>
|
|
40
|
-
type ReviewResponse = z.infer<typeof response_schema>
|
|
41
|
-
type ReviewRound = ReviewRequest['round']
|
|
42
|
-
|
|
43
|
-
function review_paths(directory: string): { brief: string; findings: string; log: string } {
|
|
44
|
-
return {
|
|
45
|
-
brief: stamp_file.stamp_path(BRIEF_PREFIX, directory, '.md'),
|
|
46
|
-
findings: stamp_file.stamp_path(FINDINGS_PREFIX, directory, '.txt'),
|
|
47
|
-
log: stamp_file.stamp_path(LOG_PREFIX, directory, '.log'),
|
|
48
|
-
}
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
function request_path(directory: string, issue: string): string {
|
|
52
|
-
return path.join(directory, ...CACHE_DIRECTORY, `${REQUEST_PREFIX}${issue}.json`)
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
function response_path(directory: string, issue: string): string | undefined {
|
|
56
|
-
const common = git_common_directory.resolve(directory)
|
|
57
|
-
if (common === undefined || path.basename(common) !== '.git') return undefined
|
|
58
|
-
|
|
59
|
-
return path.join(path.dirname(common), ...CACHE_DIRECTORY, `${RESPONSE_PREFIX}${issue}.json`)
|
|
60
|
-
}
|
|
61
|
-
|
|
62
|
-
function read_record<T>(target: string, schema: z.ZodType<T>): T | undefined {
|
|
63
|
-
try {
|
|
64
|
-
return json_value.parse_with(readFileSync(target, 'utf8'), schema)
|
|
65
|
-
} catch {
|
|
66
|
-
return undefined
|
|
67
|
-
}
|
|
68
|
-
}
|
|
69
|
-
|
|
70
|
-
async function launch_review(lane: LaneInfo, round: ReviewRound): Promise<boolean> {
|
|
71
|
-
const target = review_paths(lane.directory)
|
|
72
|
-
const prompt_of =
|
|
73
|
-
round === '1' ? run_ship_review.reviewer_prompt : run_ship_review.verification_prompt
|
|
74
|
-
const built = agent_argv.resolve_in(
|
|
75
|
-
prompt_of(target.brief, target.findings),
|
|
76
|
-
agent_role_profile.REVIEWER,
|
|
77
|
-
lane.directory,
|
|
78
|
-
)
|
|
79
|
-
if (built.kind === 'rejected' || built.profile.provider !== 'openai') return false
|
|
80
|
-
|
|
81
|
-
const result = await detached_launch.launch_attached(
|
|
82
|
-
{
|
|
83
|
-
argv: built.argv,
|
|
84
|
-
cwd: lane.directory,
|
|
85
|
-
log_path: target.log,
|
|
86
|
-
profile: built.profile,
|
|
87
|
-
},
|
|
88
|
-
console.error,
|
|
89
|
-
)
|
|
90
|
-
|
|
91
|
-
return result.kind === 'completed' && result.exit_code === 0
|
|
92
|
-
}
|
|
93
|
-
|
|
94
|
-
function write_response(target: string, body: z.infer<typeof response_schema>): void {
|
|
95
|
-
const temporary = `${target}.${String(process.pid)}.tmp`
|
|
96
|
-
|
|
97
|
-
writeFileSync(temporary, JSON.stringify(body))
|
|
98
|
-
renameSync(temporary, target)
|
|
99
|
-
}
|
|
100
|
-
|
|
101
|
-
async function launched(lane: LaneInfo, round: ReviewRound): Promise<boolean> {
|
|
102
|
-
try {
|
|
103
|
-
return await launch_review(lane, round)
|
|
104
|
-
} catch (error) {
|
|
105
|
-
error_text.trace_swallowed('openai_review_broker.launched', error)
|
|
106
|
-
|
|
107
|
-
return false
|
|
108
|
-
}
|
|
109
|
-
}
|
|
110
|
-
|
|
111
|
-
async function process_request(
|
|
112
|
-
lane: LaneInfo,
|
|
113
|
-
response: string,
|
|
114
|
-
received: ReviewRequest,
|
|
115
|
-
state: { current: ReviewResponse },
|
|
116
|
-
): Promise<void> {
|
|
117
|
-
const is_success = await launched(lane, received.round)
|
|
118
|
-
|
|
119
|
-
state.current = {
|
|
120
|
-
session: received.session,
|
|
121
|
-
nonce: received.nonce,
|
|
122
|
-
is_success,
|
|
123
|
-
heartbeat_at: Date.now(),
|
|
124
|
-
}
|
|
125
|
-
write_response(response, state.current)
|
|
126
|
-
}
|
|
127
|
-
|
|
128
|
-
async function serve(
|
|
129
|
-
lane: LaneInfo,
|
|
130
|
-
session: string,
|
|
131
|
-
response: string,
|
|
132
|
-
state: { is_running: boolean; current: ReviewResponse },
|
|
133
|
-
): Promise<void> {
|
|
134
|
-
let last_nonce = ''
|
|
135
|
-
const target = request_path(lane.directory, lane.issue)
|
|
136
|
-
|
|
137
|
-
while (state.is_running) {
|
|
138
|
-
const received = read_record(target, request_schema)
|
|
139
|
-
|
|
140
|
-
if (received?.session === session && received.nonce !== last_nonce) {
|
|
141
|
-
last_nonce = received.nonce
|
|
142
|
-
// eslint-disable-next-line no-await-in-loop -- requests are served one at a time, in arrival order
|
|
143
|
-
await process_request(lane, response, received, state)
|
|
144
|
-
}
|
|
145
|
-
|
|
146
|
-
// eslint-disable-next-line no-await-in-loop -- polling: each read waits on the state the previous one saw
|
|
147
|
-
await sleep(POLL_MS)
|
|
148
|
-
}
|
|
149
|
-
}
|
|
150
|
-
|
|
151
|
-
function prepare(lane: LaneInfo): { response: string; current: ReviewResponse } {
|
|
152
|
-
const response = response_path(lane.directory, lane.issue)
|
|
153
|
-
if (response === undefined) throw new Error('review broker has no trusted response location')
|
|
154
|
-
|
|
155
|
-
const session = randomUUID()
|
|
156
|
-
|
|
157
|
-
mkdirSync(path.dirname(response), { recursive: true })
|
|
158
|
-
mkdirSync(path.dirname(request_path(lane.directory, lane.issue)), { recursive: true })
|
|
159
|
-
const current: ReviewResponse = {
|
|
160
|
-
session,
|
|
161
|
-
nonce: '',
|
|
162
|
-
is_success: false,
|
|
163
|
-
heartbeat_at: Date.now(),
|
|
164
|
-
}
|
|
165
|
-
|
|
166
|
-
write_response(response, current)
|
|
167
|
-
|
|
168
|
-
return { response, current }
|
|
169
|
-
}
|
|
170
|
-
|
|
171
|
-
function start_heartbeat(state: { current: ReviewResponse }, response: string): NodeJS.Timeout {
|
|
172
|
-
const heartbeat = setInterval(() => {
|
|
173
|
-
state.current = { ...state.current, heartbeat_at: Date.now() }
|
|
174
|
-
write_response(response, state.current)
|
|
175
|
-
}, HEARTBEAT_MS)
|
|
176
|
-
|
|
177
|
-
heartbeat.unref()
|
|
178
|
-
|
|
179
|
-
return heartbeat
|
|
180
|
-
}
|
|
181
|
-
|
|
182
|
-
async function stop_server(
|
|
183
|
-
state: { is_running: boolean },
|
|
184
|
-
heartbeat: NodeJS.Timeout,
|
|
185
|
-
serving: Promise<void>,
|
|
186
|
-
response: string,
|
|
187
|
-
): Promise<void> {
|
|
188
|
-
clearInterval(heartbeat)
|
|
189
|
-
state.is_running = false
|
|
190
|
-
await serving
|
|
191
|
-
unlinkSync(response)
|
|
192
|
-
}
|
|
193
|
-
|
|
194
|
-
async function with_server<T>(lane: LaneInfo, run: () => Promise<T>): Promise<T> {
|
|
195
|
-
const { response, current } = prepare(lane)
|
|
196
|
-
const state = { is_running: true, current }
|
|
197
|
-
const heartbeat = start_heartbeat(state, response)
|
|
198
|
-
const serving = serve(lane, current.session, response, state)
|
|
199
|
-
|
|
200
|
-
try {
|
|
201
|
-
return await run()
|
|
202
|
-
} finally {
|
|
203
|
-
await stop_server(state, heartbeat, serving, response)
|
|
204
|
-
}
|
|
205
|
-
}
|
|
206
|
-
|
|
207
|
-
function is_live_response(
|
|
208
|
-
response: ReviewResponse | undefined,
|
|
209
|
-
session: string,
|
|
210
|
-
): response is ReviewResponse {
|
|
211
|
-
const now = Date.now()
|
|
212
|
-
|
|
213
|
-
return (
|
|
214
|
-
response?.session === session &&
|
|
215
|
-
response.heartbeat_at <= now &&
|
|
216
|
-
response.heartbeat_at >= now - HEARTBEAT_LIMIT_MS
|
|
217
|
-
)
|
|
218
|
-
}
|
|
219
|
-
|
|
220
|
-
function response_verdict(
|
|
221
|
-
response: ReviewResponse | undefined,
|
|
222
|
-
session: string,
|
|
223
|
-
nonce: string,
|
|
224
|
-
): 'wait' | 'success' | 'failure' {
|
|
225
|
-
if (!is_live_response(response, session)) return 'failure'
|
|
226
|
-
if (response.nonce !== nonce) return 'wait'
|
|
227
|
-
|
|
228
|
-
return response.is_success ? 'success' : 'failure'
|
|
229
|
-
}
|
|
230
|
-
|
|
231
|
-
async function wait_response(target: string, session: string, nonce: string): Promise<boolean> {
|
|
232
|
-
const deadline = Date.now() + REQUEST_LIMIT_MS
|
|
233
|
-
|
|
234
|
-
while (Date.now() < deadline) {
|
|
235
|
-
const verdict = response_verdict(read_record(target, response_schema), session, nonce)
|
|
236
|
-
if (verdict !== 'wait') return verdict === 'success'
|
|
237
|
-
|
|
238
|
-
// eslint-disable-next-line no-await-in-loop -- polling: each read waits on the state the previous one saw
|
|
239
|
-
await sleep(POLL_MS)
|
|
240
|
-
}
|
|
241
|
-
|
|
242
|
-
return false
|
|
243
|
-
}
|
|
244
|
-
|
|
245
|
-
async function request(directory: string, issue: string, round: ReviewRound): Promise<boolean> {
|
|
246
|
-
const target = response_path(directory, issue)
|
|
247
|
-
if (target === undefined) return false
|
|
248
|
-
const response = read_record(target, response_schema)
|
|
249
|
-
|
|
250
|
-
if (response === undefined || !is_live_response(response, response.session)) return false
|
|
251
|
-
|
|
252
|
-
const nonce = randomUUID()
|
|
253
|
-
const body: ReviewRequest = { session: response.session, nonce, round }
|
|
254
|
-
|
|
255
|
-
stamp_file.replace_stamp(request_path(directory, issue), body)
|
|
256
|
-
|
|
257
|
-
return await wait_response(target, response.session, nonce)
|
|
258
|
-
}
|
|
259
|
-
|
|
260
|
-
const openai_review_broker = { request, request_path, response_path, review_paths, with_server }
|
|
261
|
-
|
|
262
|
-
export { openai_review_broker }
|