@joshuafolkken/kit 1.887.0 → 1.921.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude/skills/workflow-commands/fullrun-steps.md +4 -2
- package/.claude/skills/workflow-commands/halfrun.md +3 -1
- package/.claude/skills/workflow-commands/kickoff.md +5 -3
- package/.claude/skills/workflow-commands/observation-filing.md +3 -1
- package/.claude/skills/workflow-commands/prerequisite.md +3 -1
- package/.claude/skills/workflow-commands/target-repository.md +1 -1
- package/.github/pull_request_template.md +12 -0
- package/.github/release.yml +3 -0
- package/.github/workflows/ci.yml +8 -0
- package/.github/workflows/production.yml +10 -1
- package/.github/workflows/publish.yml +72 -2
- package/CLAUDE.md +2 -0
- package/dist/CLAUDE.md +2 -0
- package/dist/CLAUDE.static.md +8 -0
- package/dist/hooks/batch-guard.js +4 -4
- package/dist/hooks/{chunk-36UE53LS.js → chunk-3HZ5MJIW.js} +2 -2
- package/dist/hooks/chunk-3V37IID6.js +82 -0
- package/dist/hooks/{chunk-FRJKVSK3.js → chunk-77RLKZ5V.js} +2 -2
- package/dist/hooks/{chunk-VGYSA2NT.js → chunk-LC765SUA.js} +16 -2
- package/dist/hooks/{chunk-ZBK4XRAS.js → chunk-NWD47MW3.js} +3 -3
- package/dist/hooks/{chunk-4OXFU3MK.js → chunk-O2FBZPSW.js} +619 -156
- package/dist/hooks/{chunk-6MQCXEEA.js → chunk-OLTAGGHH.js} +25 -88
- package/dist/hooks/{chunk-FNFOLFFV.js → chunk-OYU6KPRA.js} +1 -1
- package/dist/hooks/{chunk-PRDCNGA6.js → chunk-UANRLZV7.js} +4 -4
- package/dist/hooks/{chunk-264QY6KS.js → chunk-UULARQ5Q.js} +23 -1
- package/dist/hooks/{chunk-Y5OYCU2F.js → chunk-XMR3BS35.js} +3 -3
- package/dist/hooks/codex-hook-adapter.js +11 -10
- package/dist/hooks/duplicate-read-guard.js +5 -5
- package/dist/hooks/format-edited.js +3 -2
- package/dist/hooks/investigation-guard.js +5 -5
- package/dist/hooks/pretool-guard.js +10 -9
- package/dist/hooks/stop-guard.js +3 -3
- package/dist/josh.js +155 -12
- package/package.json +68 -17
- package/prettier/index.js +3 -8
- package/prettier/static.d.ts +3 -0
- package/prettier/static.js +10 -0
- package/prompts/collaboration-workflow/issue-template.md +11 -4
- package/prompts/collaboration-workflow/upstream-interrupt.md +1 -1
- package/prompts/collaboration-workflow/wip-cap.md +2 -2
- package/prompts/review.md +2 -2
- package/prompts/testing-guide.md +1 -0
- package/scripts/build/build-claude-md.ts +6 -1
- package/scripts/ci/pr-classification.ts +114 -0
- package/scripts/document/document-byte-budget.ts +3 -2
- package/scripts/gate/gate-report.ts +2 -0
- package/scripts/gate/gate-steps.ts +50 -0
- package/scripts/gate/project-checks.ts +121 -0
- package/scripts/gate/verification-gate.ts +14 -43
- package/scripts/git/git-common-directory.ts +9 -1
- package/scripts/git/git-gh-pr-rollup.ts +29 -1
- package/scripts/git/git-gh-pr.ts +61 -1
- package/scripts/git/git-pr.ts +62 -13
- package/scripts/git/issue-labels.ts +9 -0
- package/scripts/init/distributed-paths.ts +9 -5
- package/scripts/init/hook-command-bootstrap.ts +40 -8
- package/scripts/init/hook-command-rewrite.ts +7 -3
- package/scripts/init/init-actions.ts +94 -7
- package/scripts/init/init-ai-copy.ts +104 -19
- package/scripts/init/init-static.ts +52 -0
- package/scripts/init/init.ts +97 -30
- package/scripts/init/project-profile-cli.ts +14 -0
- package/scripts/init/project-profile.ts +149 -0
- package/scripts/init/start.ts +30 -0
- package/scripts/issue/issue-bug-label.ts +13 -0
- package/scripts/issue/issue-classification.ts +74 -0
- package/scripts/issue/issue-lint-cli.ts +10 -3
- package/scripts/issue/markdown-section.ts +38 -4
- package/scripts/josh/__snapshots__/josh-command-map.test.ts.snap +27 -1
- package/scripts/josh/josh-command-map.ts +3 -0
- package/scripts/josh/josh-commands-development.ts +6 -0
- package/scripts/josh/josh-commands-project.ts +14 -1
- package/scripts/josh/josh-logic.ts +13 -0
- package/scripts/josh/run-github-prerequisite.ts +32 -0
- package/scripts/lane/lane-await.ts +8 -1
- package/scripts/lane/openai-lane-ship.ts +30 -0
- package/scripts/lane/openai-lane-supervisor.ts +35 -8
- package/scripts/lane/openai-review-broker.ts +260 -0
- package/scripts/lint/cspell-cached.ts +21 -0
- package/scripts/lint/lint-parallel.ts +44 -1
- package/scripts/observations/observations-flush.ts +1 -1
- package/scripts/package/production-dependency-graph.ts +68 -0
- package/scripts/package/verify-optional-eslint-install.ts +62 -0
- package/scripts/release/github-release-cli.ts +11 -0
- package/scripts/release/github-release.ts +220 -0
- package/scripts/release/publish-tag-cli.ts +8 -0
- package/scripts/release/publish-tag.ts +11 -0
- package/scripts/release/release-publish.ts +7 -2
- package/scripts/rules/delivered-rules.ts +2 -0
- package/scripts/rules/issue-bug-label.ts +85 -0
- package/scripts/rules/issue-filing-args.ts +50 -0
- package/scripts/run/run-merge-cli.ts +19 -0
- package/scripts/run/run-merge.ts +9 -1
- package/scripts/run/run-ship-cli.ts +20 -0
- package/scripts/run/run-ship-detach.ts +180 -14
- package/scripts/run/run-ship-review-steps.ts +45 -25
- package/scripts/run/run-wake-session.ts +3 -1
- package/scripts/test/pilot-files.ts +0 -2
- package/scripts/test/skip-marker.ts +3 -0
- package/scripts/test/test-declared-changed.ts +2 -1
- package/scripts/test/test-declared-logic.ts +15 -10
- package/scripts/test/test-declared.ts +20 -8
- package/scripts/test/test-red-logic.ts +3 -3
- package/scripts/test/test-unit-guard.ts +24 -12
- package/templates/CLAUDE.static.md +8 -0
- package/templates/cursorrules.static +1 -0
- package/templates/prettierignore.static +12 -0
- package/templates/workflows/ci.yml +5 -0
|
@@ -77,10 +77,12 @@ exports[`COMMAND_MAP — reference metadata > keeps every canonical command and
|
|
|
77
77
|
"obf": "observations:flush",
|
|
78
78
|
"ol": "oracle:list",
|
|
79
79
|
"ov": "overrides",
|
|
80
|
+
"pf": "profile",
|
|
80
81
|
"pg": "propagate",
|
|
81
82
|
"pks": "pkg:scout",
|
|
82
83
|
"pm": "prevent-main-commit",
|
|
83
84
|
"ppu": "pre-push-unit",
|
|
85
|
+
"prc": "pr:classification",
|
|
84
86
|
"pt": "port",
|
|
85
87
|
"ptc": "pre-commit-type-check",
|
|
86
88
|
"ptg": "pretool:guard",
|
|
@@ -131,6 +133,7 @@ exports[`COMMAND_MAP — reference metadata > keeps every canonical command and
|
|
|
131
133
|
"sl": "session:lang",
|
|
132
134
|
"sp": "stash:pop",
|
|
133
135
|
"ss": "secretlint-scan",
|
|
136
|
+
"st": "start",
|
|
134
137
|
"sy": "sync",
|
|
135
138
|
"sys": "sync:scope",
|
|
136
139
|
"t": "test",
|
|
@@ -445,7 +448,7 @@ exports[`COMMAND_MAP — reference metadata > keeps every canonical command and
|
|
|
445
448
|
],
|
|
446
449
|
],
|
|
447
450
|
"init": [
|
|
448
|
-
"",
|
|
451
|
+
"[--profile static|node]",
|
|
449
452
|
"developer",
|
|
450
453
|
[
|
|
451
454
|
"files",
|
|
@@ -717,6 +720,13 @@ exports[`COMMAND_MAP — reference metadata > keeps every canonical command and
|
|
|
717
720
|
"network",
|
|
718
721
|
],
|
|
719
722
|
],
|
|
723
|
+
"pr:classification": [
|
|
724
|
+
"",
|
|
725
|
+
"automation",
|
|
726
|
+
[
|
|
727
|
+
"none",
|
|
728
|
+
],
|
|
729
|
+
],
|
|
720
730
|
"pre-commit-type-check": [
|
|
721
731
|
"",
|
|
722
732
|
"automation",
|
|
@@ -745,6 +755,13 @@ exports[`COMMAND_MAP — reference metadata > keeps every canonical command and
|
|
|
745
755
|
"none",
|
|
746
756
|
],
|
|
747
757
|
],
|
|
758
|
+
"profile": [
|
|
759
|
+
"[--profile static|node]",
|
|
760
|
+
"developer",
|
|
761
|
+
[
|
|
762
|
+
"files",
|
|
763
|
+
],
|
|
764
|
+
],
|
|
748
765
|
"propagate": [
|
|
749
766
|
"[--dry-run] [--skip-publish-wait]",
|
|
750
767
|
"maintainer",
|
|
@@ -1075,6 +1092,15 @@ exports[`COMMAND_MAP — reference metadata > keeps every canonical command and
|
|
|
1075
1092
|
"none",
|
|
1076
1093
|
],
|
|
1077
1094
|
],
|
|
1095
|
+
"start": [
|
|
1096
|
+
"[--profile static|node]",
|
|
1097
|
+
"developer",
|
|
1098
|
+
[
|
|
1099
|
+
"files",
|
|
1100
|
+
"git",
|
|
1101
|
+
"network",
|
|
1102
|
+
],
|
|
1103
|
+
],
|
|
1078
1104
|
"stash:pop": [
|
|
1079
1105
|
"<message>",
|
|
1080
1106
|
"automation",
|
|
@@ -53,11 +53,14 @@ const ALIASES: Record<string, string> = {
|
|
|
53
53
|
c: 'check',
|
|
54
54
|
pt: 'port',
|
|
55
55
|
i: 'init',
|
|
56
|
+
pf: 'profile',
|
|
57
|
+
st: 'start',
|
|
56
58
|
sy: 'sync',
|
|
57
59
|
rmi: 'registry:migrate',
|
|
58
60
|
sys: 'sync:scope',
|
|
59
61
|
g: 'git',
|
|
60
62
|
gp: 'pr',
|
|
63
|
+
prc: 'pr:classification',
|
|
61
64
|
fu: 'followup',
|
|
62
65
|
nf: 'notify',
|
|
63
66
|
obf: 'observations:flush',
|
|
@@ -14,6 +14,12 @@ const PATH_ARGUMENTS = '<path...>'
|
|
|
14
14
|
|
|
15
15
|
/* eslint-disable @typescript-eslint/naming-convention */
|
|
16
16
|
const DEV_COMMANDS: Record<string, CommandEntry> = {
|
|
17
|
+
'pr:classification': {
|
|
18
|
+
script: 'scripts/ci/pr-classification.ts',
|
|
19
|
+
description: 'Require one release classification on a pull request',
|
|
20
|
+
category: 'Development',
|
|
21
|
+
reference: ['', 'automation', ['none']],
|
|
22
|
+
},
|
|
17
23
|
[GATE_COMMAND]: {
|
|
18
24
|
script: 'scripts/gate/verification-gate.ts',
|
|
19
25
|
description: 'Run lint, type check, spell check and unit tests concurrently',
|
|
@@ -4,12 +4,25 @@ import type { CommandEntry } from './josh-command-types'
|
|
|
4
4
|
// no identifier format allows. The same disable sits at the top of `josh-commands-maintenance.ts`
|
|
5
5
|
// for the same reason.
|
|
6
6
|
/* eslint-disable @typescript-eslint/naming-convention */
|
|
7
|
+
const PROFILE_ARGUMENTS = '[--profile static|node]'
|
|
7
8
|
const PROJECT_COMMANDS: Record<string, CommandEntry> = {
|
|
9
|
+
profile: {
|
|
10
|
+
script: 'scripts/init/project-profile-cli.ts',
|
|
11
|
+
description: 'Show the project profile and the reason for it',
|
|
12
|
+
category: 'Project',
|
|
13
|
+
reference: [PROFILE_ARGUMENTS, 'developer', ['files']],
|
|
14
|
+
},
|
|
15
|
+
start: {
|
|
16
|
+
script: 'scripts/init/start.ts',
|
|
17
|
+
description: 'Initialize a project for the GitHub Issue workflow',
|
|
18
|
+
category: 'Project',
|
|
19
|
+
reference: [PROFILE_ARGUMENTS, 'developer', ['files', 'git', 'network']],
|
|
20
|
+
},
|
|
8
21
|
init: {
|
|
9
22
|
script: 'scripts/init/init.ts',
|
|
10
23
|
description: 'Initialize config in a new project',
|
|
11
24
|
category: 'Project',
|
|
12
|
-
reference: [
|
|
25
|
+
reference: [PROFILE_ARGUMENTS, 'developer', ['files', 'processes']],
|
|
13
26
|
},
|
|
14
27
|
sync: {
|
|
15
28
|
script: 'scripts/sync/sync.ts',
|
|
@@ -16,6 +16,7 @@ import {
|
|
|
16
16
|
import { composite_arguments, USAGE_ERROR_EXIT_CODE } from './josh-composite-arguments'
|
|
17
17
|
import { josh_in_process } from './josh-in-process'
|
|
18
18
|
import { kit_only } from './kit-only'
|
|
19
|
+
import { run_github_prerequisite } from './run-github-prerequisite'
|
|
19
20
|
|
|
20
21
|
const COLUMN_WIDTH = 26
|
|
21
22
|
const ALIAS_PAD_WIDTH = 2
|
|
@@ -235,6 +236,15 @@ async function run_script_entry(
|
|
|
235
236
|
return spawn_script_entry(entry, script_path, script_arguments)
|
|
236
237
|
}
|
|
237
238
|
|
|
239
|
+
function github_prerequisite_exit(resolved: string, is_consumer: boolean): number | undefined {
|
|
240
|
+
if (!is_consumer) return undefined
|
|
241
|
+
const prerequisite = run_github_prerequisite.explanation(process.cwd(), resolved)
|
|
242
|
+
if (prerequisite === undefined) return undefined
|
|
243
|
+
console.error(prerequisite)
|
|
244
|
+
|
|
245
|
+
return USAGE_ERROR_EXIT_CODE
|
|
246
|
+
}
|
|
247
|
+
|
|
238
248
|
// The exit a command answers with before it runs, or `undefined` to go ahead: a consumer is refused a
|
|
239
249
|
// kit-only command with guidance, and a composite command rejects extra arguments
|
|
240
250
|
// (joshuafolkken/kit#1988).
|
|
@@ -250,6 +260,9 @@ function pre_dispatch_exit(
|
|
|
250
260
|
return USAGE_ERROR_EXIT_CODE
|
|
251
261
|
}
|
|
252
262
|
|
|
263
|
+
const prerequisite_exit = github_prerequisite_exit(resolved, is_consumer)
|
|
264
|
+
if (prerequisite_exit !== undefined) return prerequisite_exit
|
|
265
|
+
|
|
253
266
|
const rejection = composite_arguments.reject_extra_arguments(
|
|
254
267
|
resolved,
|
|
255
268
|
entry,
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import { existsSync } from 'node:fs'
|
|
2
|
+
import path from 'node:path'
|
|
3
|
+
import { repo_origin } from '#scripts/discovery/repo-origin'
|
|
4
|
+
import { execaSync } from 'execa'
|
|
5
|
+
|
|
6
|
+
const GITHUB_RUN_COMMANDS = new Set([
|
|
7
|
+
'run:entry',
|
|
8
|
+
'run:prep',
|
|
9
|
+
'run:next',
|
|
10
|
+
'run:step',
|
|
11
|
+
'run:merge',
|
|
12
|
+
'run:tail',
|
|
13
|
+
])
|
|
14
|
+
|
|
15
|
+
function explanation(root: string, command: string): string | undefined {
|
|
16
|
+
if (!GITHUB_RUN_COMMANDS.has(command)) return undefined
|
|
17
|
+
|
|
18
|
+
if (!existsSync(path.join(root, '.git'))) {
|
|
19
|
+
return `${command} requires Git and a GitHub origin. Run git init and add a GitHub origin first.`
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
const result = execaSync('git', ['config', '--get', 'remote.origin.url'], {
|
|
23
|
+
cwd: root,
|
|
24
|
+
reject: false,
|
|
25
|
+
})
|
|
26
|
+
if (result.exitCode === 0 && repo_origin.parse_origin_url(result.stdout)) return undefined
|
|
27
|
+
|
|
28
|
+
return `${command} requires a GitHub origin. Add an origin pointing to GitHub first.`
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
const run_github_prerequisite = { explanation }
|
|
32
|
+
export { run_github_prerequisite }
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
import { spawnSync } from 'node:child_process'
|
|
2
2
|
import { setTimeout as sleep } from 'node:timers/promises'
|
|
3
|
+
import { git_common_directory } from '#scripts/git/git-common-directory'
|
|
4
|
+
import { run_ship_detach } from '#scripts/run/run-ship-detach'
|
|
3
5
|
import { lane_child_invocation } from './lane-child-invocation'
|
|
4
6
|
|
|
5
7
|
// Detached lane children are not harness-tracked processes, so their completion fires no
|
|
@@ -49,7 +51,12 @@ function is_process_running_default(issue: string): boolean {
|
|
|
49
51
|
const pattern = lane_child_invocation.process_pattern(issue)
|
|
50
52
|
const result = spawnSync('pgrep', ['-f', pattern], { encoding: 'utf8' })
|
|
51
53
|
|
|
52
|
-
|
|
54
|
+
if (result.status === PROCESS_FOUND) return true
|
|
55
|
+
const repository = git_common_directory.repository(process.cwd())
|
|
56
|
+
|
|
57
|
+
return (
|
|
58
|
+
repository !== undefined && run_ship_detach.read_result(repository, issue)?.result === 'running'
|
|
59
|
+
)
|
|
53
60
|
}
|
|
54
61
|
|
|
55
62
|
function make_initial_state(): AwaitState {
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import { setTimeout as sleep } from 'node:timers/promises'
|
|
2
|
+
import { run_ship_detach } from '#scripts/run/run-ship-detach'
|
|
3
|
+
import { run_ship_probe } from '#scripts/run/run-ship-probe'
|
|
4
|
+
|
|
5
|
+
const POLL_MS = 250
|
|
6
|
+
type ShipEnding = 'none' | 'success' | 'failed' | 'abnormal'
|
|
7
|
+
type ShipReading = NonNullable<ReturnType<typeof run_ship_detach.read_result>>
|
|
8
|
+
|
|
9
|
+
async function current(issue: string): Promise<ShipReading | undefined> {
|
|
10
|
+
const repository = await run_ship_probe.repository_directory()
|
|
11
|
+
|
|
12
|
+
return repository === undefined ? undefined : run_ship_detach.read_result(repository, issue)
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
async function wait_for_ship(issue: string, prior_launch: string | undefined): Promise<ShipEnding> {
|
|
16
|
+
let read = await current(issue)
|
|
17
|
+
if (read === undefined || read.launch_id === prior_launch) return 'none'
|
|
18
|
+
|
|
19
|
+
while (read.result === 'running') {
|
|
20
|
+
await sleep(POLL_MS)
|
|
21
|
+
read = await current(issue)
|
|
22
|
+
if (read === undefined) return 'abnormal'
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
return read.result
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
const openai_lane_ship = { current, wait_for_ship }
|
|
29
|
+
|
|
30
|
+
export { openai_lane_ship }
|
|
@@ -12,7 +12,9 @@ import { lane_child_invocation } from './lane-child-invocation'
|
|
|
12
12
|
import { lane_child_marker } from './lane-child-marker'
|
|
13
13
|
import { lane_dispatch_log } from './lane-dispatch-log'
|
|
14
14
|
import type { LaneInfo } from './lane-registry'
|
|
15
|
+
import { openai_lane_ship } from './openai-lane-ship'
|
|
15
16
|
import { openai_lane_supervisor_decision } from './openai-lane-supervisor-decision'
|
|
17
|
+
import { openai_review_broker } from './openai-review-broker'
|
|
16
18
|
|
|
17
19
|
const OWNER_PREFIX = 'josh-openai-lane-supervisor-'
|
|
18
20
|
const STATE_PREFIX = 'josh-openai-lane-supervisor-state-'
|
|
@@ -187,10 +189,17 @@ async function standing_cut(issue: string): Promise<RunCut | undefined> {
|
|
|
187
189
|
return read_cut.kind === 'carried' && handed_off(read_cut.cut, issue) ? read_cut.cut : undefined
|
|
188
190
|
}
|
|
189
191
|
|
|
190
|
-
function child_argv(
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
192
|
+
function child_argv(
|
|
193
|
+
lane: LaneInfo,
|
|
194
|
+
profile: AgentProfile,
|
|
195
|
+
is_resume: boolean,
|
|
196
|
+
override?: string,
|
|
197
|
+
): LaunchArgv {
|
|
198
|
+
const invocation =
|
|
199
|
+
override ??
|
|
200
|
+
(is_resume
|
|
201
|
+
? lane_child_invocation.resume_invocation(lane.issue)
|
|
202
|
+
: lane_child_invocation.child_invocation(lane.issue))
|
|
194
203
|
const built = agent_argv.with_profile_in(invocation, profile, lane.directory)
|
|
195
204
|
if (built.kind === 'rejected') throw new Error(built.note)
|
|
196
205
|
|
|
@@ -209,6 +218,11 @@ interface Generation {
|
|
|
209
218
|
lane: LaneInfo
|
|
210
219
|
owner: SupervisorOwner
|
|
211
220
|
profile: AgentProfile
|
|
221
|
+
invocation?: string
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
function completion_code(result: ChildResult, ship: string): number {
|
|
225
|
+
return ship === 'abnormal' || result.kind === 'abnormal' ? 1 : 0
|
|
212
226
|
}
|
|
213
227
|
|
|
214
228
|
function record_child(generation: Generation, child_pid: number): void {
|
|
@@ -239,7 +253,7 @@ async function run_child(generation: Generation): Promise<ChildResult> {
|
|
|
239
253
|
const { lane, profile, is_resume } = generation
|
|
240
254
|
const result = await detached_launch.launch_attached(
|
|
241
255
|
{
|
|
242
|
-
argv: child_argv(lane, profile, is_resume),
|
|
256
|
+
argv: child_argv(lane, profile, is_resume, generation.invocation),
|
|
243
257
|
cwd: lane.directory,
|
|
244
258
|
log_path: lane_dispatch_log.default_log_path(lane),
|
|
245
259
|
profile,
|
|
@@ -257,6 +271,7 @@ async function run_child(generation: Generation): Promise<ChildResult> {
|
|
|
257
271
|
}
|
|
258
272
|
|
|
259
273
|
async function run_generations(generation: Generation): Promise<number> {
|
|
274
|
+
const prior = await openai_lane_ship.current(generation.lane.issue)
|
|
260
275
|
const result = await run_child(generation)
|
|
261
276
|
if (result.kind === LAUNCH_FAILED_KIND) return 1
|
|
262
277
|
|
|
@@ -264,9 +279,18 @@ async function run_generations(generation: Generation): Promise<number> {
|
|
|
264
279
|
return await run_generations({ ...generation, epoch: generation.epoch + 1, is_resume: true })
|
|
265
280
|
}
|
|
266
281
|
|
|
267
|
-
|
|
282
|
+
const ship = await openai_lane_ship.wait_for_ship(generation.lane.issue, prior?.launch_id)
|
|
283
|
+
|
|
284
|
+
if (ship === 'failed') {
|
|
285
|
+
return await run_generations({
|
|
286
|
+
...generation,
|
|
287
|
+
epoch: generation.epoch + 1,
|
|
288
|
+
is_resume: false,
|
|
289
|
+
invocation: lane_child_invocation.ship_stop_invocation(generation.lane.issue),
|
|
290
|
+
})
|
|
291
|
+
}
|
|
268
292
|
|
|
269
|
-
return
|
|
293
|
+
return completion_code(result, ship)
|
|
270
294
|
}
|
|
271
295
|
|
|
272
296
|
async function wait_for_inherited_child(
|
|
@@ -308,7 +332,10 @@ async function supervise(lane: LaneInfo, profile: AgentProfile, nonce: string):
|
|
|
308
332
|
try {
|
|
309
333
|
if (!(await openai_lane_supervisor_decision.is_approved(lane.directory, nonce))) return 0
|
|
310
334
|
|
|
311
|
-
return await
|
|
335
|
+
return await openai_review_broker.with_server(
|
|
336
|
+
lane,
|
|
337
|
+
async () => await run_claimed(lane, profile, owner),
|
|
338
|
+
)
|
|
312
339
|
} finally {
|
|
313
340
|
release(lane.directory, nonce)
|
|
314
341
|
}
|
|
@@ -0,0 +1,260 @@
|
|
|
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 { detached_launch } from '#scripts/run/detached-launch'
|
|
10
|
+
import { run_ship_review } from '#scripts/run/run-ship-review'
|
|
11
|
+
import { z } from 'zod'
|
|
12
|
+
import type { LaneInfo } from './lane-registry'
|
|
13
|
+
|
|
14
|
+
const REQUEST_PREFIX = '.josh-review-request-'
|
|
15
|
+
const RESPONSE_PREFIX = '.josh-review-response-'
|
|
16
|
+
const BRIEF_PREFIX = 'josh-ship-review-brief-'
|
|
17
|
+
const FINDINGS_PREFIX = 'josh-ship-review-findings-'
|
|
18
|
+
const LOG_PREFIX = 'josh-ship-review-log-'
|
|
19
|
+
const CACHE_DIRECTORY = ['node_modules', '.cache']
|
|
20
|
+
const POLL_MS = 100
|
|
21
|
+
const HEARTBEAT_MS = 500
|
|
22
|
+
const HEARTBEAT_LIMIT_MS = 3000
|
|
23
|
+
const MINUTE_MS = 60_000
|
|
24
|
+
const REQUEST_LIMIT_MINUTES = 30
|
|
25
|
+
const REQUEST_LIMIT_MS = REQUEST_LIMIT_MINUTES * MINUTE_MS
|
|
26
|
+
const request_schema = z
|
|
27
|
+
.object({ session: z.uuid(), nonce: z.uuid(), round: z.enum(['1', '2']) })
|
|
28
|
+
.strict()
|
|
29
|
+
const response_schema = z
|
|
30
|
+
.object({
|
|
31
|
+
session: z.uuid(),
|
|
32
|
+
nonce: z.string(),
|
|
33
|
+
is_success: z.boolean(),
|
|
34
|
+
heartbeat_at: z.number().int().positive(),
|
|
35
|
+
})
|
|
36
|
+
.strict()
|
|
37
|
+
type ReviewRequest = z.infer<typeof request_schema>
|
|
38
|
+
type ReviewResponse = z.infer<typeof response_schema>
|
|
39
|
+
type ReviewRound = ReviewRequest['round']
|
|
40
|
+
|
|
41
|
+
function review_paths(directory: string): { brief: string; findings: string; log: string } {
|
|
42
|
+
return {
|
|
43
|
+
brief: stamp_file.stamp_path(BRIEF_PREFIX, directory, '.md'),
|
|
44
|
+
findings: stamp_file.stamp_path(FINDINGS_PREFIX, directory, '.txt'),
|
|
45
|
+
log: stamp_file.stamp_path(LOG_PREFIX, directory, '.log'),
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function request_path(directory: string, issue: string): string {
|
|
50
|
+
return path.join(directory, ...CACHE_DIRECTORY, `${REQUEST_PREFIX}${issue}.json`)
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function response_path(directory: string, issue: string): string | undefined {
|
|
54
|
+
const common = git_common_directory.resolve(directory)
|
|
55
|
+
if (common === undefined || path.basename(common) !== '.git') return undefined
|
|
56
|
+
|
|
57
|
+
return path.join(path.dirname(common), ...CACHE_DIRECTORY, `${RESPONSE_PREFIX}${issue}.json`)
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
function read_record<T>(target: string, schema: z.ZodType<T>): T | undefined {
|
|
61
|
+
try {
|
|
62
|
+
const content: unknown = JSON.parse(readFileSync(target, 'utf8'))
|
|
63
|
+
const parsed = schema.safeParse(content)
|
|
64
|
+
|
|
65
|
+
return parsed.success ? parsed.data : undefined
|
|
66
|
+
} catch {
|
|
67
|
+
return undefined
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
async function launch_review(lane: LaneInfo, round: ReviewRound): Promise<boolean> {
|
|
72
|
+
const target = review_paths(lane.directory)
|
|
73
|
+
const prompt_of =
|
|
74
|
+
round === '1' ? run_ship_review.reviewer_prompt : run_ship_review.verification_prompt
|
|
75
|
+
const built = agent_argv.resolve_in(
|
|
76
|
+
prompt_of(target.brief, target.findings),
|
|
77
|
+
agent_role_profile.REVIEWER,
|
|
78
|
+
lane.directory,
|
|
79
|
+
)
|
|
80
|
+
if (built.kind === 'rejected' || built.profile.provider !== 'openai') return false
|
|
81
|
+
|
|
82
|
+
const result = await detached_launch.launch_attached(
|
|
83
|
+
{
|
|
84
|
+
argv: built.argv,
|
|
85
|
+
cwd: lane.directory,
|
|
86
|
+
log_path: target.log,
|
|
87
|
+
profile: built.profile,
|
|
88
|
+
},
|
|
89
|
+
console.error,
|
|
90
|
+
)
|
|
91
|
+
|
|
92
|
+
return result.kind === 'completed' && result.exit_code === 0
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
function write_response(target: string, body: z.infer<typeof response_schema>): void {
|
|
96
|
+
const temporary = `${target}.${String(process.pid)}.tmp`
|
|
97
|
+
|
|
98
|
+
writeFileSync(temporary, JSON.stringify(body))
|
|
99
|
+
renameSync(temporary, target)
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
async function launched(lane: LaneInfo, round: ReviewRound): Promise<boolean> {
|
|
103
|
+
try {
|
|
104
|
+
return await launch_review(lane, round)
|
|
105
|
+
} catch {
|
|
106
|
+
return false
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
async function process_request(
|
|
111
|
+
lane: LaneInfo,
|
|
112
|
+
response: string,
|
|
113
|
+
received: ReviewRequest,
|
|
114
|
+
state: { current: ReviewResponse },
|
|
115
|
+
): Promise<void> {
|
|
116
|
+
const is_success = await launched(lane, received.round)
|
|
117
|
+
|
|
118
|
+
state.current = {
|
|
119
|
+
session: received.session,
|
|
120
|
+
nonce: received.nonce,
|
|
121
|
+
is_success,
|
|
122
|
+
heartbeat_at: Date.now(),
|
|
123
|
+
}
|
|
124
|
+
write_response(response, state.current)
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
async function serve(
|
|
128
|
+
lane: LaneInfo,
|
|
129
|
+
session: string,
|
|
130
|
+
response: string,
|
|
131
|
+
state: { is_running: boolean; current: ReviewResponse },
|
|
132
|
+
): Promise<void> {
|
|
133
|
+
let last_nonce = ''
|
|
134
|
+
const target = request_path(lane.directory, lane.issue)
|
|
135
|
+
|
|
136
|
+
while (state.is_running) {
|
|
137
|
+
const received = read_record(target, request_schema)
|
|
138
|
+
|
|
139
|
+
if (received?.session === session && received.nonce !== last_nonce) {
|
|
140
|
+
last_nonce = received.nonce
|
|
141
|
+
await process_request(lane, response, received, state)
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
await sleep(POLL_MS)
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
function prepare(lane: LaneInfo): { response: string; current: ReviewResponse } {
|
|
149
|
+
const response = response_path(lane.directory, lane.issue)
|
|
150
|
+
if (response === undefined) throw new Error('review broker has no trusted response location')
|
|
151
|
+
|
|
152
|
+
const session = randomUUID()
|
|
153
|
+
|
|
154
|
+
mkdirSync(path.dirname(response), { recursive: true })
|
|
155
|
+
mkdirSync(path.dirname(request_path(lane.directory, lane.issue)), { recursive: true })
|
|
156
|
+
const current: ReviewResponse = {
|
|
157
|
+
session,
|
|
158
|
+
nonce: '',
|
|
159
|
+
is_success: false,
|
|
160
|
+
heartbeat_at: Date.now(),
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
write_response(response, current)
|
|
164
|
+
|
|
165
|
+
return { response, current }
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
function start_heartbeat(state: { current: ReviewResponse }, response: string): NodeJS.Timeout {
|
|
169
|
+
const heartbeat = setInterval(() => {
|
|
170
|
+
state.current = { ...state.current, heartbeat_at: Date.now() }
|
|
171
|
+
write_response(response, state.current)
|
|
172
|
+
}, HEARTBEAT_MS)
|
|
173
|
+
|
|
174
|
+
heartbeat.unref()
|
|
175
|
+
|
|
176
|
+
return heartbeat
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
async function stop_server(
|
|
180
|
+
state: { is_running: boolean },
|
|
181
|
+
heartbeat: NodeJS.Timeout,
|
|
182
|
+
serving: Promise<void>,
|
|
183
|
+
response: string,
|
|
184
|
+
): Promise<void> {
|
|
185
|
+
clearInterval(heartbeat)
|
|
186
|
+
state.is_running = false
|
|
187
|
+
await serving
|
|
188
|
+
unlinkSync(response)
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
async function with_server<T>(lane: LaneInfo, run: () => Promise<T>): Promise<T> {
|
|
192
|
+
const { response, current } = prepare(lane)
|
|
193
|
+
const state = { is_running: true, current }
|
|
194
|
+
const heartbeat = start_heartbeat(state, response)
|
|
195
|
+
const serving = serve(lane, current.session, response, state)
|
|
196
|
+
|
|
197
|
+
try {
|
|
198
|
+
return await run()
|
|
199
|
+
} finally {
|
|
200
|
+
await stop_server(state, heartbeat, serving, response)
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
function is_live_response(
|
|
205
|
+
response: ReviewResponse | undefined,
|
|
206
|
+
session: string,
|
|
207
|
+
): response is ReviewResponse {
|
|
208
|
+
const now = Date.now()
|
|
209
|
+
|
|
210
|
+
return (
|
|
211
|
+
response?.session === session &&
|
|
212
|
+
response.heartbeat_at <= now &&
|
|
213
|
+
response.heartbeat_at >= now - HEARTBEAT_LIMIT_MS
|
|
214
|
+
)
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
function response_verdict(
|
|
218
|
+
response: ReviewResponse | undefined,
|
|
219
|
+
session: string,
|
|
220
|
+
nonce: string,
|
|
221
|
+
): 'wait' | 'success' | 'failure' {
|
|
222
|
+
if (!is_live_response(response, session)) return 'failure'
|
|
223
|
+
if (response.nonce !== nonce) return 'wait'
|
|
224
|
+
|
|
225
|
+
return response.is_success ? 'success' : 'failure'
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
async function wait_response(target: string, session: string, nonce: string): Promise<boolean> {
|
|
229
|
+
const deadline = Date.now() + REQUEST_LIMIT_MS
|
|
230
|
+
|
|
231
|
+
while (Date.now() < deadline) {
|
|
232
|
+
const verdict = response_verdict(read_record(target, response_schema), session, nonce)
|
|
233
|
+
if (verdict !== 'wait') return verdict === 'success'
|
|
234
|
+
|
|
235
|
+
await sleep(POLL_MS)
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
return false
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
async function request(directory: string, issue: string, round: ReviewRound): Promise<boolean> {
|
|
242
|
+
const target = response_path(directory, issue)
|
|
243
|
+
if (target === undefined) return false
|
|
244
|
+
const response = read_record(target, response_schema)
|
|
245
|
+
|
|
246
|
+
if (response === undefined || !is_live_response(response, response.session)) {
|
|
247
|
+
return false
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
const nonce = randomUUID()
|
|
251
|
+
const body: ReviewRequest = { session: response.session, nonce, round }
|
|
252
|
+
|
|
253
|
+
stamp_file.replace_stamp(request_path(directory, issue), body)
|
|
254
|
+
|
|
255
|
+
return await wait_response(target, response.session, nonce)
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
const openai_review_broker = { request, request_path, response_path, review_paths, with_server }
|
|
259
|
+
|
|
260
|
+
export { openai_review_broker }
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
#!/usr/bin/env tsx
|
|
2
2
|
import { fileURLToPath } from 'node:url'
|
|
3
|
+
import { project_checks } from '#scripts/gate/project-checks'
|
|
3
4
|
import { CSPELL_CACHE_FILE, CSPELL_CACHE_FLAGS } from '#scripts/josh/josh-command-types'
|
|
4
5
|
import { lane_cache_run } from '#scripts/lane/lane-cache-run'
|
|
5
6
|
import { buffered_process, FAIL_EXIT_CODE } from '#scripts/lib/buffered-process'
|
|
@@ -22,7 +23,27 @@ const CSPELL_ARGS = [
|
|
|
22
23
|
|
|
23
24
|
// The wrapper puts cache transfer directly around cspell rather than around the whole gate (#2060),
|
|
24
25
|
// so a later failing check cannot prevent a completed cache from reaching the primary checkout.
|
|
26
|
+
function static_skip_reason(directory: string): string | undefined {
|
|
27
|
+
if (!project_checks.is_static(directory)) return undefined
|
|
28
|
+
|
|
29
|
+
if (!project_checks.has_config(directory, project_checks.CSPELL_CONFIGS)) {
|
|
30
|
+
return 'no cspell configuration was found'
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
if (!project_checks.has_bin(directory, 'cspell')) return 'cspell is not installed'
|
|
34
|
+
|
|
35
|
+
return undefined
|
|
36
|
+
}
|
|
37
|
+
|
|
25
38
|
async function run(extra_arguments: ReadonlyArray<string> = []): Promise<number> {
|
|
39
|
+
const reason = static_skip_reason(process.cwd())
|
|
40
|
+
|
|
41
|
+
if (reason !== undefined) {
|
|
42
|
+
console.info(project_checks.skip_notice('cspell', reason))
|
|
43
|
+
|
|
44
|
+
return 0
|
|
45
|
+
}
|
|
46
|
+
|
|
26
47
|
const result = await lane_cache_run.run(
|
|
27
48
|
CSPELL_CACHE_FILE,
|
|
28
49
|
async () => await buffered_process.run_buffered_process([...CSPELL_ARGS, ...extra_arguments]),
|