@joshuafolkken/kit 1.1471.0 → 1.1514.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/settings.json +6 -6
- package/.claude/skills/epic-commands/SKILL.md +2 -14
- package/.claude/skills/epic-commands/epic-bundle.md +9 -5
- package/.claude/skills/epic-commands/execution-waves.md +1 -2
- package/.claude/skills/verify-ui/SKILL.md +1 -2
- package/.claude/skills/workflow-commands/SKILL.md +7 -9
- package/.claude/skills/workflow-commands/background-commands.md +3 -7
- package/.claude/skills/workflow-commands/backlogrun-child.md +3 -10
- package/.claude/skills/workflow-commands/backlogrun-lanes.md +6 -15
- package/.claude/skills/workflow-commands/backlogrun-park.md +2 -8
- package/.claude/skills/workflow-commands/backlogrun-progress.md +10 -19
- package/.claude/skills/workflow-commands/backlogrun-recovery.md +8 -8
- package/.claude/skills/workflow-commands/backlogrun-steps.md +2 -10
- package/.claude/skills/workflow-commands/backlogrun.md +31 -50
- package/.claude/skills/workflow-commands/chain-rule.md +7 -6
- package/.claude/skills/workflow-commands/delegation.md +12 -8
- package/.claude/skills/workflow-commands/entry-sequence.md +1 -3
- package/.claude/skills/workflow-commands/followup.md +3 -6
- package/.claude/skills/workflow-commands/fullrun-steps.md +3 -5
- package/.claude/skills/workflow-commands/fullrun.md +4 -6
- package/.claude/skills/workflow-commands/halfrun.md +3 -7
- package/.claude/skills/workflow-commands/into-target.md +1 -2
- package/.claude/skills/workflow-commands/issue-comments.md +3 -3
- package/.claude/skills/workflow-commands/issue-scout.md +3 -2
- package/.claude/skills/workflow-commands/latest-gate.md +2 -6
- package/.claude/skills/workflow-commands/needs-human-review.md +2 -5
- package/.claude/skills/workflow-commands/observation-filing.md +0 -14
- package/.claude/skills/workflow-commands/observation-ledger.md +4 -9
- package/.claude/skills/workflow-commands/pre-gate-cut.md +11 -16
- package/.claude/skills/workflow-commands/prerequisite.md +1 -2
- package/.claude/skills/workflow-commands/progress-watcher.md +2 -7
- package/.claude/skills/workflow-commands/prrun.md +3 -5
- package/.claude/skills/workflow-commands/retrospective.md +4 -4
- package/.claude/skills/workflow-commands/split-assessment.md +1 -3
- package/.claude/skills/workflow-commands/target-repository.md +1 -2
- package/.claude/skills/workflow-commands/working-tree-hold.md +2 -4
- package/.codex/hooks.json +6 -6
- package/.github/actions/setup-node/action.yml +1 -1
- package/.github/workflows/ci.yml +1 -1
- package/.github/workflows/publish.yml +2 -2
- package/CLAUDE.md +1 -1
- package/cspell/index.yaml +7 -0
- package/dist/CLAUDE.md +1 -1
- package/dist/commands/document-section-cli.js +67 -34
- package/dist/hooks/batch-guard.js +6 -6
- package/dist/hooks/chunk-4KKD2UTK.js +66 -0
- package/dist/hooks/{chunk-J47HU5H5.js → chunk-6TS2JD6D.js} +3 -3
- package/dist/hooks/chunk-7DSBJVFH.js +50 -0
- package/dist/hooks/{chunk-YHBZAJSV.js → chunk-A65Y4YOR.js} +3 -3
- package/dist/hooks/{chunk-E7OIKX5Z.js → chunk-APUO6U4J.js} +85 -7
- package/dist/hooks/{chunk-IX6TTTI6.js → chunk-EFXE6C37.js} +8 -22
- package/dist/hooks/chunk-GP4IEC4O.js +20 -0
- package/dist/hooks/{chunk-SHGPANFB.js → chunk-JP45SOCJ.js} +3 -3
- package/dist/hooks/{chunk-K4EOONYM.js → chunk-LAOV7NLM.js} +1 -1
- package/dist/hooks/chunk-M4HGZV7U.js +83 -0
- package/dist/hooks/{chunk-KC5AK2GF.js → chunk-O6GDRNJB.js} +3 -3
- package/dist/hooks/{chunk-YYPI7SJZ.js → chunk-PF2NZL2A.js} +5 -5
- package/dist/hooks/{chunk-H5HVWYXK.js → chunk-PTICX4OL.js} +3 -4
- package/dist/hooks/{chunk-IIGFYXKC.js → chunk-RT27QRJW.js} +581 -1438
- package/dist/hooks/{chunk-43ZHT22P.js → chunk-SYOTDYCI.js} +1127 -1089
- package/dist/hooks/{chunk-AS74OU3D.js → chunk-TNUMY6NL.js} +1507 -1336
- package/dist/hooks/chunk-UPUZF3AN.js +279 -0
- package/dist/hooks/{chunk-EUMACUK2.js → chunk-WK6SMBIC.js} +1948 -806
- package/dist/hooks/codex-hook-adapter.js +17 -14
- package/dist/hooks/duplicate-read-guard.js +7 -7
- package/dist/hooks/format-edited.js +5 -4
- package/dist/hooks/inputs.json +317 -264
- package/dist/hooks/investigation-guard.js +8 -11
- package/dist/hooks/pretool-guard.js +16 -13
- package/dist/hooks/session-lang.js +370 -25
- package/dist/hooks/stop-guard.js +2576 -176
- package/dist/josh.js +2124 -277
- package/lefthook/base.yml +5 -3
- package/package.json +12 -10
- package/prompts/collaboration-workflow/file-edits.md +1 -3
- package/prompts/collaboration-workflow/gh-rest.md +1 -2
- package/prompts/collaboration-workflow/issue-citation.md +2 -4
- package/prompts/collaboration-workflow/issue-template.md +10 -2
- package/prompts/collaboration-workflow/lane-instructions.md +35 -0
- package/prompts/collaboration-workflow/operating-rules.md +2 -6
- package/prompts/collaboration-workflow/output-bounds.md +3 -3
- package/prompts/collaboration-workflow/overview.md +1 -1
- package/prompts/collaboration-workflow/principles.md +14 -8
- package/prompts/collaboration-workflow/report-format.md +9 -0
- package/prompts/collaboration-workflow/residency.md +12 -8
- package/prompts/collaboration-workflow/rule-delivery.md +3 -11
- package/prompts/collaboration-workflow/shell-body.md +6 -6
- package/prompts/collaboration-workflow/turn-batching.md +5 -5
- package/prompts/collaboration-workflow/upstream-interrupt.md +2 -2
- package/prompts/collaboration-workflow/wip-cap.md +6 -6
- package/prompts/collaboration-workflow.md +1 -0
- package/prompts/review.md +4 -5
- package/prompts/testing-guide.md +2 -2
- package/scripts/agent/agent-argv.ts +20 -4
- package/scripts/agent/agent-role-profile.ts +101 -45
- package/scripts/agent/agent-safe-text.ts +60 -0
- package/scripts/agent/claude-agent-argv.ts +14 -0
- package/scripts/agent/codex-agent-argv.ts +2 -0
- package/scripts/agent/lane-instructions.ts +144 -0
- package/scripts/backlog/backlog-budget.ts +4 -4
- package/scripts/backlog/backlog-drive-named.ts +1 -1
- package/scripts/backlog/backlog-drive-restore.ts +9 -1
- package/scripts/backlog/backlog-occupied.ts +56 -0
- package/scripts/backlog/backlog-ready.ts +47 -10
- package/scripts/delegation/duplicate-read-guard.ts +2 -2
- package/scripts/delegation/investigation-guard.ts +15 -65
- package/scripts/delegation/investigation-reads.ts +0 -1
- package/scripts/document/document-byte-budget.ts +6 -5
- package/scripts/document/document-section-cli.ts +17 -5
- package/scripts/document/entry-read-budget.ts +2 -2
- package/scripts/document/read-set-cli.ts +4 -1
- package/scripts/epic/epic-audit-orphans.ts +5 -8
- package/scripts/epic/epic-audit.ts +52 -1
- package/scripts/epic/epic-bundle-evidence.ts +49 -26
- package/scripts/epic/epic-bundle-referenced.ts +14 -3
- package/scripts/epic/epic-bundle.ts +17 -9
- package/scripts/followup/git-followup-workflow.ts +19 -1
- package/scripts/gate/check-inputs.ts +129 -0
- package/scripts/gate/check-reuse.ts +162 -0
- package/scripts/gate/core-budget.ts +93 -44
- package/scripts/gate/core-wait.ts +130 -0
- package/scripts/gate/free-cores.ts +49 -0
- package/scripts/gate/gate-ledger.ts +17 -3
- package/scripts/gate/gate-plan.ts +23 -1
- package/scripts/gate/gate-report.ts +15 -2
- package/scripts/gate/gate-run-steps.ts +150 -0
- package/scripts/gate/held-mark.ts +55 -0
- package/scripts/gate/machine-capacity.ts +70 -11
- package/scripts/gate/pre-commit-type-check.ts +14 -1
- package/scripts/gate/verification-gate.ts +58 -125
- package/scripts/gh/git-gh-issue-rest.ts +1 -0
- package/scripts/gh/git-gh-issue-write.ts +24 -2
- package/scripts/gh/git-pr.ts +13 -1
- package/scripts/git/main-merge-regenerate.ts +78 -0
- package/scripts/git/main-merge.ts +14 -5
- package/scripts/hooks/batch-guard.ts +5 -8
- package/scripts/hooks/pre-commit-lint.ts +91 -0
- package/scripts/hooks/pre-push-unit.ts +1 -1
- package/scripts/hooks/pretool-guard.ts +27 -52
- package/scripts/hooks/reserved-run.ts +1 -0
- package/scripts/hooks/run-hook.sh +10 -12
- package/scripts/hooks/stop-actions.ts +204 -0
- package/scripts/hooks/stop-guard.ts +62 -21
- package/scripts/init/hook-launch.ts +12 -7
- package/scripts/issue/expected-effect-lint.ts +59 -0
- package/scripts/issue/issue-cite-cli.ts +35 -15
- package/scripts/issue/issue-classification.ts +1 -1
- package/scripts/issue/issue-lint-cli.ts +5 -3
- package/scripts/issue/issue-scout-cli.ts +19 -14
- package/scripts/issue/markdown-section.ts +19 -5
- package/scripts/josh/command-usage.ts +110 -0
- package/scripts/josh/josh-command-map.ts +2 -0
- package/scripts/josh/josh-command-types.ts +55 -9
- package/scripts/josh/josh-commands-ai.ts +13 -0
- package/scripts/josh/josh-commands-development.ts +1 -1
- package/scripts/josh/josh-commands-hooks.ts +6 -0
- package/scripts/josh/josh-commands-lane.ts +10 -1
- package/scripts/josh/josh-commands-workflow.ts +5 -0
- package/scripts/josh/josh-logic.ts +6 -15
- package/scripts/josh/josh-usage-automation.ts +121 -0
- package/scripts/josh/josh-usage-backlog.ts +180 -0
- package/scripts/josh/josh-usage-developer.ts +230 -0
- package/scripts/josh/josh-usage-hooks.ts +38 -0
- package/scripts/josh/josh-usage-issue.ts +205 -0
- package/scripts/josh/josh-usage-review.ts +101 -0
- package/scripts/josh/josh-usage-run.ts +252 -0
- package/scripts/josh/josh-usage-session.ts +238 -0
- package/scripts/josh/josh-usage-workflow.ts +212 -0
- package/scripts/josh/josh.ts +14 -5
- package/scripts/josh/package-directory.ts +20 -0
- package/scripts/lane/lane-capacity.ts +8 -2
- package/scripts/lane/lane-check-stats.ts +56 -0
- package/scripts/lane/lane-child-invocation.ts +35 -4
- package/scripts/lane/lane-dispatch.ts +3 -3
- package/scripts/lane/lane-guard-policy.ts +2 -2
- package/scripts/lane/lane-handoff.ts +8 -1
- package/scripts/lane/lane-install.ts +12 -4
- package/scripts/lane/lane-ledger.ts +43 -1
- package/scripts/lane/lane-limit-cli.ts +8 -9
- package/scripts/lane/lane-measure-cli.ts +2 -0
- package/scripts/lane/lane-preserve.ts +29 -1
- package/scripts/lane/lane-registry.ts +9 -7
- package/scripts/lane/lane-restart.ts +5 -26
- package/scripts/lane/lane-session-action.ts +146 -0
- package/scripts/lane/lane-session-broker.ts +57 -138
- package/scripts/lane/lane-withdraw-cli.ts +71 -0
- package/scripts/lane/lane-withdraw.ts +182 -0
- package/scripts/lint/format.ts +4 -4
- package/scripts/lint/lint-parallel.ts +10 -3
- package/scripts/lint/lint-related-scope.ts +4 -1
- package/scripts/notify/telegram-test.ts +10 -5
- package/scripts/report/report-format-reference.ts +2 -0
- package/scripts/review/acceptance-map-check.ts +61 -0
- package/scripts/review/acceptance-map.ts +205 -0
- package/scripts/review/review-brief-cli.ts +1 -1
- package/scripts/rules/agent-model.ts +59 -0
- package/scripts/rules/delivered-rules.ts +11 -2
- package/scripts/rules/filing-cap.ts +1 -0
- package/scripts/rules/firing-point.ts +6 -5
- package/scripts/rules/guard-remedy.ts +117 -0
- package/scripts/rules/issue-comments.ts +67 -6
- package/scripts/rules/last-prompt.ts +50 -5
- package/scripts/rules/oracle-list-cli.ts +7 -0
- package/scripts/rules/piped-verification.ts +3 -1
- package/scripts/rules/rule-list.ts +4 -1
- package/scripts/rules/rule-registry-rows.ts +10 -0
- package/scripts/rules/rule-registry.ts +7 -4
- package/scripts/rules/rule-rewrite.ts +114 -0
- package/scripts/rules/rule-value.ts +7 -1
- package/scripts/rules/shell-segments.ts +15 -0
- package/scripts/rules/stop-decision.ts +77 -0
- package/scripts/rules/stop-rules.ts +147 -47
- package/scripts/rules/tail-commands.ts +15 -1
- package/scripts/run/board/run-board-cli.ts +2 -0
- package/scripts/run/board/run-board-closed.ts +8 -2
- package/scripts/run/board/run-board-github.ts +9 -1
- package/scripts/run/board/run-board-header.ts +4 -1
- package/scripts/run/board/run-board-labels.ts +2 -0
- package/scripts/run/board/run-board-layout.ts +1 -1
- package/scripts/run/board/run-board-read.ts +16 -0
- package/scripts/run/board/run-board-reason.ts +10 -1
- package/scripts/run/board/run-board-render-legend.ts +1 -0
- package/scripts/run/board/run-board-render-slot.ts +1 -1
- package/scripts/run/board/run-board-render-waits.ts +41 -0
- package/scripts/run/board/run-board-render.ts +6 -1
- package/scripts/run/board/run-board-row-yield.ts +1 -0
- package/scripts/run/board/run-board-state.ts +5 -0
- package/scripts/run/board/run-board-status.ts +72 -21
- package/scripts/run/board/run-board-tick.ts +5 -1
- package/scripts/run/cut/run-cut-cli.ts +3 -2
- package/scripts/run/cut/run-cut-report.ts +16 -0
- package/scripts/run/detached-launch.ts +23 -4
- package/scripts/run/entry/run-entry-cli.ts +8 -1
- package/scripts/run/event/run-event-render.ts +1 -0
- package/scripts/run/event/run-event-scope.ts +19 -1
- package/scripts/run/event/run-event-stream-emit.ts +13 -6
- package/scripts/run/event/run-event-stream.ts +4 -0
- package/scripts/run/hold/run-hold-session.ts +46 -0
- package/scripts/run/hold/run-hold.ts +7 -1
- package/scripts/run/launch-priority.ts +32 -0
- package/scripts/run/merge/run-merge-cli.ts +16 -65
- package/scripts/run/merge/run-merge-signals.ts +99 -0
- package/scripts/run/merge/run-merge-steps.ts +11 -1
- package/scripts/run/merge/run-merge.ts +21 -9
- package/scripts/run/run-liveness.ts +1 -1
- package/scripts/run/run-poll-hook.ts +157 -0
- package/scripts/run/run-step-cli.ts +22 -15
- package/scripts/run/run-step-section.ts +85 -0
- package/scripts/run/ship/run-ship-cli.ts +5 -12
- package/scripts/run/ship/run-ship-detach.ts +9 -0
- package/scripts/run/ship/run-ship-next.ts +10 -2
- package/scripts/run/ship/run-ship-overlap.ts +195 -0
- package/scripts/run/ship/run-ship-pre-detach.ts +33 -15
- package/scripts/run/ship/run-ship-preflight.ts +28 -2
- package/scripts/run/ship/run-ship-return.ts +31 -7
- package/scripts/run/ship/run-ship-review-steps.ts +12 -10
- package/scripts/run/ship/run-ship-steps.ts +1 -1
- package/scripts/run/ship/run-ship-stop-context.ts +206 -0
- package/scripts/run/ship/run-ship-sync.ts +14 -6
- package/scripts/test/test-e2e-guard.ts +30 -5
- package/scripts/test/test-unit-guard.ts +16 -1
- package/scripts/test/unit-worker-share.ts +26 -4
- package/scripts/time-runtime/time-shell.ts +29 -4
- package/scripts/time-runtime/time-spans.ts +1 -0
- package/scripts/time-runtime/time-tool-call.ts +7 -0
- package/templates/gitignore +6 -2
- package/dist/hooks/chunk-K4LMBQC2.js +0 -68
|
@@ -1,23 +1,14 @@
|
|
|
1
1
|
import { randomUUID } from 'node:crypto'
|
|
2
|
-
import { mkdirSync, readFileSync, renameSync, unlinkSync, writeFileSync } from 'node:fs'
|
|
2
|
+
import { mkdirSync, readFileSync, renameSync, rmSync, unlinkSync, writeFileSync } from 'node:fs'
|
|
3
3
|
import path from 'node:path'
|
|
4
4
|
import { setTimeout as sleep } from 'node:timers/promises'
|
|
5
|
-
import { agent_argv } from '#scripts/agent/agent-argv'
|
|
6
|
-
import { agent_role_profile, type AgentProvider } from '#scripts/agent/agent-role-profile'
|
|
7
5
|
import { git_common_directory } from '#scripts/git/git-common-directory'
|
|
8
6
|
import { stamp_file } from '#scripts/josh/stamp-file'
|
|
9
|
-
import { error_text } from '#scripts/lib/error-message'
|
|
10
7
|
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 { run_ship_stage } from '#scripts/run/ship/run-ship-stage'
|
|
14
8
|
import { z } from 'zod'
|
|
15
9
|
import { lane_broker_server, type BrokerServer } from './lane-broker-server'
|
|
16
|
-
import { lane_child_invocation, type ShipStop } from './lane-child-invocation'
|
|
17
|
-
import { lane_child_marker } from './lane-child-marker'
|
|
18
|
-
import { lane_parked } from './lane-parked'
|
|
19
10
|
import type { LaneInfo } from './lane-registry'
|
|
20
|
-
import {
|
|
11
|
+
import { lane_session_action, type BrokerAction, type BrokerAnswer } from './lane-session-action'
|
|
21
12
|
|
|
22
13
|
// The sessions a lane child's detached `josh ship` cannot start itself, started for it by a process
|
|
23
14
|
// that can: the round-1 and round-2 reviewer, and the fresh child a stopped ship hands back to. An
|
|
@@ -25,13 +16,11 @@ import { lane_relaunch } from './lane-relaunch'
|
|
|
25
16
|
// Claude lane is served by the `run:wake` supervisor (`claude-lane-broker.ts`), because Claude Code removes its
|
|
26
17
|
// token from what its Bash tool starts, so a token-only credential never reaches the ship the child
|
|
27
18
|
// launched (joshuafolkken/kit#3817). **The request names an action, never a command**: the server
|
|
28
|
-
// rebuilds the argv from the registered lane, so a child can ask for nothing but these two sessions
|
|
19
|
+
// rebuilds the argv from the registered lane, so a child can ask for nothing but these two sessions
|
|
20
|
+
// (`lane-session-action.ts`).
|
|
29
21
|
|
|
30
22
|
const REQUEST_PREFIX = '.josh-review-request-'
|
|
31
23
|
const RESPONSE_PREFIX = '.josh-review-response-'
|
|
32
|
-
const BRIEF_PREFIX = 'josh-ship-review-brief-'
|
|
33
|
-
const FINDINGS_PREFIX = 'josh-ship-review-findings-'
|
|
34
|
-
const LOG_PREFIX = 'josh-ship-review-log-'
|
|
35
24
|
const CACHE_DIRECTORY = ['node_modules', '.cache']
|
|
36
25
|
const POLL_MS = 100
|
|
37
26
|
// The heartbeat is kept for a ship branched before the server record, and read only where a
|
|
@@ -41,22 +30,8 @@ const LEGACY_HEARTBEAT_LIMIT_MS = 3000
|
|
|
41
30
|
const MINUTE_MS = 60_000
|
|
42
31
|
const REQUEST_LIMIT_MINUTES = 30
|
|
43
32
|
const REQUEST_LIMIT_MS = REQUEST_LIMIT_MINUTES * MINUTE_MS
|
|
44
|
-
const ANTHROPIC_PROVIDER: AgentProvider = 'anthropic'
|
|
45
|
-
const stop_schema = z
|
|
46
|
-
.object({
|
|
47
|
-
stage: z.enum(run_ship_stage.STAGE),
|
|
48
|
-
title: z.string(),
|
|
49
|
-
flags: z.array(z.string()).readonly(),
|
|
50
|
-
is_review: z.boolean(),
|
|
51
|
-
conflicts: z.array(z.string()).readonly().default([]),
|
|
52
|
-
})
|
|
53
|
-
.strict()
|
|
54
|
-
const action_schema = z.discriminatedUnion('kind', [
|
|
55
|
-
z.object({ kind: z.literal('review'), round: z.enum(['1', '2']) }).strict(),
|
|
56
|
-
z.object({ kind: z.literal('relaunch'), stop: stop_schema }).strict(),
|
|
57
|
-
])
|
|
58
33
|
const request_schema = z
|
|
59
|
-
.object({ session: z.uuid(), nonce: z.uuid(), action: action_schema })
|
|
34
|
+
.object({ session: z.uuid(), nonce: z.uuid(), action: lane_session_action.action_schema })
|
|
60
35
|
.strict()
|
|
61
36
|
const response_schema = z
|
|
62
37
|
.object({
|
|
@@ -66,18 +41,13 @@ const response_schema = z
|
|
|
66
41
|
heartbeat_at: z.number().int().positive(),
|
|
67
42
|
})
|
|
68
43
|
.strict()
|
|
44
|
+
// **A failure's reason sits beside the response, never in it**: a ship branched before the reason parses
|
|
45
|
+
// the response strictly, so a new field would read as no server (joshuafolkken/kit#3974).
|
|
46
|
+
const reason_schema = z.object({ nonce: z.string(), reason: z.string() }).strict()
|
|
69
47
|
type BrokerRequest = z.infer<typeof request_schema>
|
|
70
48
|
type BrokerResponse = z.infer<typeof response_schema>
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
function review_paths(directory: string): { brief: string; findings: string; log: string } {
|
|
75
|
-
return {
|
|
76
|
-
brief: stamp_file.stamp_path(BRIEF_PREFIX, directory, '.md'),
|
|
77
|
-
findings: stamp_file.stamp_path(FINDINGS_PREFIX, directory, '.txt'),
|
|
78
|
-
log: stamp_file.stamp_path(LOG_PREFIX, directory, '.log'),
|
|
79
|
-
}
|
|
80
|
-
}
|
|
49
|
+
|
|
50
|
+
const { failure } = lane_session_action
|
|
81
51
|
|
|
82
52
|
function request_path(directory: string, issue: string): string {
|
|
83
53
|
return path.join(directory, ...CACHE_DIRECTORY, `${REQUEST_PREFIX}${issue}.json`)
|
|
@@ -90,6 +60,10 @@ function response_path(directory: string, issue: string): string | undefined {
|
|
|
90
60
|
return path.join(path.dirname(common), ...CACHE_DIRECTORY, `${RESPONSE_PREFIX}${issue}.json`)
|
|
91
61
|
}
|
|
92
62
|
|
|
63
|
+
function reason_path(response: string): string {
|
|
64
|
+
return `${response}.reason`
|
|
65
|
+
}
|
|
66
|
+
|
|
93
67
|
function read_record<T>(target: string, schema: z.ZodType<T>): T | undefined {
|
|
94
68
|
try {
|
|
95
69
|
return json_value.parse_with(readFileSync(target, 'utf8'), schema)
|
|
@@ -98,102 +72,25 @@ function read_record<T>(target: string, schema: z.ZodType<T>): T | undefined {
|
|
|
98
72
|
}
|
|
99
73
|
}
|
|
100
74
|
|
|
101
|
-
|
|
102
|
-
// and an OpenAI lane's never starts a Claude one.
|
|
103
|
-
function provider_of(lane: LaneInfo): AgentProvider {
|
|
104
|
-
return lane.profile?.provider ?? ANTHROPIC_PROVIDER
|
|
105
|
-
}
|
|
106
|
-
|
|
107
|
-
// **The reviewer carries the lane's mark explicitly**: the supervisor serving a Claude lane is no lane child,
|
|
108
|
-
// so a reviewer inheriting its environment would descend from the carry record's owner unmarked and be
|
|
109
|
-
// read as the `backlogrun` parent by every hook. **Its prompt ends with the review invocation**, so the
|
|
110
|
-
// lane's liveness read and the park's reap find it as they find the child (joshuafolkken/kit#3878).
|
|
111
|
-
|
|
112
|
-
function review_prompt(lane: LaneInfo, round: ReviewRound): string {
|
|
113
|
-
const target = review_paths(lane.directory)
|
|
114
|
-
const prompt_of =
|
|
115
|
-
round === '1' ? run_ship_review.reviewer_prompt : run_ship_review.verification_prompt
|
|
116
|
-
const tail = lane_child_invocation.review_invocation(lane.issue)
|
|
117
|
-
|
|
118
|
-
return `${prompt_of(target.brief, target.findings, lane.issue)} This is ${tail}`
|
|
119
|
-
}
|
|
120
|
-
|
|
121
|
-
async function launch_review(lane: LaneInfo, round: ReviewRound): Promise<boolean> {
|
|
122
|
-
const target = review_paths(lane.directory)
|
|
123
|
-
const built = agent_argv.resolve_in(
|
|
124
|
-
review_prompt(lane, round),
|
|
125
|
-
agent_role_profile.REVIEWER,
|
|
126
|
-
lane.directory,
|
|
127
|
-
)
|
|
128
|
-
if (built.kind === 'rejected' || built.profile.provider !== provider_of(lane)) return false
|
|
129
|
-
|
|
130
|
-
const result = await detached_launch.launch_attached(
|
|
131
|
-
{
|
|
132
|
-
argv: built.argv,
|
|
133
|
-
cwd: lane.directory,
|
|
134
|
-
log_path: target.log,
|
|
135
|
-
profile: built.profile,
|
|
136
|
-
env: lane_child_marker.env_for(lane.issue),
|
|
137
|
-
},
|
|
138
|
-
console.error,
|
|
139
|
-
)
|
|
140
|
-
|
|
141
|
-
return result.kind === 'completed' && result.exit_code === 0
|
|
142
|
-
}
|
|
143
|
-
|
|
144
|
-
// An OpenAI lane's successor is its supervisor's to start, never a relaunch's.
|
|
145
|
-
function relaunch(lane: LaneInfo, stop: ShipStop): boolean {
|
|
146
|
-
if (lane_relaunch.is_openai_lane(lane)) return false
|
|
147
|
-
|
|
148
|
-
const invocation = lane_child_invocation.ship_stop_invocation(lane.issue, stop)
|
|
149
|
-
const phase = agent_role_profile.IMPLEMENTATION_PHASE
|
|
150
|
-
const result = lane_relaunch.relaunch(lane, invocation, phase, console.error)
|
|
151
|
-
|
|
152
|
-
return result.kind === 'launched'
|
|
153
|
-
}
|
|
154
|
-
|
|
155
|
-
// **A parked issue gets no session**: a request can be read long after it was written, so the park is
|
|
156
|
-
// read when the action runs, never when it was asked for (joshuafolkken/kit#3878).
|
|
157
|
-
async function perform(lane: LaneInfo, action: BrokerAction): Promise<boolean> {
|
|
158
|
-
if (await lane_parked.is_parked(lane.issue)) return false
|
|
159
|
-
|
|
160
|
-
if (action.kind === 'relaunch') return relaunch(lane, action.stop)
|
|
161
|
-
|
|
162
|
-
return await launch_review(lane, action.round)
|
|
163
|
-
}
|
|
164
|
-
|
|
165
|
-
function write_response(target: string, body: BrokerResponse): void {
|
|
75
|
+
function write_record(target: string, body: BrokerResponse | z.infer<typeof reason_schema>): void {
|
|
166
76
|
const temporary = `${target}.${String(process.pid)}.tmp`
|
|
167
77
|
|
|
168
78
|
writeFileSync(temporary, JSON.stringify(body))
|
|
169
79
|
renameSync(temporary, target)
|
|
170
80
|
}
|
|
171
81
|
|
|
172
|
-
async function launched(lane: LaneInfo, action: BrokerAction): Promise<boolean> {
|
|
173
|
-
try {
|
|
174
|
-
return await perform(lane, action)
|
|
175
|
-
} catch (error) {
|
|
176
|
-
error_text.trace_swallowed('lane_session_broker.launched', error)
|
|
177
|
-
|
|
178
|
-
return false
|
|
179
|
-
}
|
|
180
|
-
}
|
|
181
|
-
|
|
182
82
|
async function process_request(
|
|
183
83
|
lane: LaneInfo,
|
|
184
84
|
response: string,
|
|
185
85
|
received: BrokerRequest,
|
|
186
86
|
state: { current: BrokerResponse },
|
|
187
87
|
): Promise<void> {
|
|
188
|
-
const
|
|
88
|
+
const answer = await lane_session_action.answer(lane, received.action)
|
|
89
|
+
const { session, nonce } = received
|
|
189
90
|
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
is_success,
|
|
194
|
-
heartbeat_at: Date.now(),
|
|
195
|
-
}
|
|
196
|
-
write_response(response, state.current)
|
|
91
|
+
if (!answer.is_success) write_record(reason_path(response), { nonce, reason: answer.reason })
|
|
92
|
+
state.current = { session, nonce, is_success: answer.is_success, heartbeat_at: Date.now() }
|
|
93
|
+
write_record(response, state.current)
|
|
197
94
|
}
|
|
198
95
|
|
|
199
96
|
async function serve(
|
|
@@ -236,7 +133,7 @@ function prepare(lane: LaneInfo): { response: string; current: BrokerResponse }
|
|
|
236
133
|
heartbeat_at: Date.now(),
|
|
237
134
|
}
|
|
238
135
|
|
|
239
|
-
|
|
136
|
+
write_record(response, current)
|
|
240
137
|
|
|
241
138
|
return { response, current }
|
|
242
139
|
}
|
|
@@ -244,7 +141,7 @@ function prepare(lane: LaneInfo): { response: string; current: BrokerResponse }
|
|
|
244
141
|
function start_heartbeat(state: { current: BrokerResponse }, response: string): NodeJS.Timeout {
|
|
245
142
|
const heartbeat = setInterval(() => {
|
|
246
143
|
state.current = { ...state.current, heartbeat_at: Date.now() }
|
|
247
|
-
|
|
144
|
+
write_record(response, state.current)
|
|
248
145
|
}, LEGACY_HEARTBEAT_MS)
|
|
249
146
|
|
|
250
147
|
heartbeat.unref()
|
|
@@ -264,6 +161,7 @@ async function stop_server(
|
|
|
264
161
|
await serving
|
|
265
162
|
clearInterval(heartbeat)
|
|
266
163
|
unlinkSync(response)
|
|
164
|
+
rmSync(reason_path(response), { force: true })
|
|
267
165
|
lane_broker_server.remove(response)
|
|
268
166
|
}
|
|
269
167
|
|
|
@@ -306,31 +204,47 @@ function session_response(target: string, session: string): BrokerResponse | und
|
|
|
306
204
|
return response?.session === session ? response : undefined
|
|
307
205
|
}
|
|
308
206
|
|
|
207
|
+
// A failure from a server that predates the reason leaves none.
|
|
208
|
+
function answer_of(target: string, response: BrokerResponse): BrokerAnswer {
|
|
209
|
+
if (response.is_success) return lane_session_action.SUCCESS
|
|
210
|
+
|
|
211
|
+
const record = read_record(reason_path(target), reason_schema)
|
|
212
|
+
const reason = record?.nonce === response.nonce ? record.reason : 'it gave no reason'
|
|
213
|
+
|
|
214
|
+
return failure(`the broker answered a failure: ${reason}`)
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
// The server's answer to this request, or `undefined` while it is still being served.
|
|
309
218
|
function response_verdict(
|
|
310
219
|
target: string,
|
|
311
220
|
session: string,
|
|
312
221
|
nonce: string,
|
|
313
|
-
):
|
|
222
|
+
): BrokerAnswer | undefined {
|
|
314
223
|
const response = session_response(target, session)
|
|
315
224
|
|
|
316
|
-
if (!is_live_response(target, response, lane_broker_server.is_running))
|
|
317
|
-
|
|
225
|
+
if (!is_live_response(target, response, lane_broker_server.is_running)) {
|
|
226
|
+
return failure('the broker stopped serving before it answered')
|
|
227
|
+
}
|
|
318
228
|
|
|
319
|
-
return response.
|
|
229
|
+
return response.nonce === nonce ? answer_of(target, response) : undefined
|
|
320
230
|
}
|
|
321
231
|
|
|
322
|
-
async function wait_response(
|
|
232
|
+
async function wait_response(
|
|
233
|
+
target: string,
|
|
234
|
+
session: string,
|
|
235
|
+
nonce: string,
|
|
236
|
+
): Promise<BrokerAnswer> {
|
|
323
237
|
const deadline = Date.now() + REQUEST_LIMIT_MS
|
|
324
238
|
|
|
325
239
|
while (Date.now() < deadline) {
|
|
326
240
|
const verdict = response_verdict(target, session, nonce)
|
|
327
|
-
if (verdict !==
|
|
241
|
+
if (verdict !== undefined) return verdict
|
|
328
242
|
|
|
329
243
|
// eslint-disable-next-line no-await-in-loop -- polling: each read waits on the state the previous one saw
|
|
330
244
|
await sleep(POLL_MS)
|
|
331
245
|
}
|
|
332
246
|
|
|
333
|
-
return
|
|
247
|
+
return failure(`the broker gave no answer within ${String(REQUEST_LIMIT_MINUTES)} minutes`)
|
|
334
248
|
}
|
|
335
249
|
|
|
336
250
|
function live_response(target: string): BrokerResponse | undefined {
|
|
@@ -356,22 +270,26 @@ function withdraw(pending: string, nonce: string): void {
|
|
|
356
270
|
if (read_record(pending, request_schema)?.nonce === nonce) stamp_file.remove_stamp(pending)
|
|
357
271
|
}
|
|
358
272
|
|
|
359
|
-
async function request(
|
|
273
|
+
async function request(
|
|
274
|
+
directory: string,
|
|
275
|
+
issue: string,
|
|
276
|
+
action: BrokerAction,
|
|
277
|
+
): Promise<BrokerAnswer> {
|
|
360
278
|
const target = response_path(directory, issue)
|
|
361
279
|
const response = target === undefined ? undefined : live_response(target)
|
|
362
280
|
|
|
363
|
-
if (target === undefined || response === undefined) return
|
|
281
|
+
if (target === undefined || response === undefined) return failure('no broker serves this lane')
|
|
364
282
|
|
|
365
283
|
const nonce = randomUUID()
|
|
366
284
|
const body = { session: response.session, nonce, action }
|
|
367
285
|
const pending = request_path(directory, issue)
|
|
368
286
|
|
|
369
287
|
stamp_file.replace_stamp(pending, body)
|
|
370
|
-
const
|
|
288
|
+
const answer = await wait_response(target, response.session, nonce)
|
|
371
289
|
|
|
372
290
|
withdraw(pending, nonce)
|
|
373
291
|
|
|
374
|
-
return
|
|
292
|
+
return answer
|
|
375
293
|
}
|
|
376
294
|
|
|
377
295
|
const lane_session_broker = {
|
|
@@ -379,9 +297,10 @@ const lane_session_broker = {
|
|
|
379
297
|
request,
|
|
380
298
|
request_path,
|
|
381
299
|
response_path,
|
|
382
|
-
review_paths,
|
|
300
|
+
review_paths: lane_session_action.review_paths,
|
|
383
301
|
with_server,
|
|
384
302
|
}
|
|
385
303
|
|
|
386
|
-
export type { BrokerAction, ReviewRound }
|
|
387
304
|
export { lane_session_broker }
|
|
305
|
+
|
|
306
|
+
export { type ReviewRound, type BrokerAction, type BrokerAnswer } from './lane-session-action'
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
#!/usr/bin/env tsx
|
|
2
|
+
import { fileURLToPath } from 'node:url'
|
|
3
|
+
import { issue_number_shape } from '#scripts/issue/issue-number-shape'
|
|
4
|
+
import { cli_flags } from '#scripts/lib/cli-flags'
|
|
5
|
+
import { lane_withdraw, type WithdrawOutcome } from './lane-withdraw'
|
|
6
|
+
|
|
7
|
+
// `josh lane:withdraw <issue> --reason <text>` — every explanation on standard error, and a withdrawal
|
|
8
|
+
// that did not end with the Issue closed exits non-zero, so a caller never takes a half-done one as done.
|
|
9
|
+
|
|
10
|
+
const ARGV_OFFSET = 2
|
|
11
|
+
const SUCCESS_EXIT_CODE = 0
|
|
12
|
+
const FAILURE_EXIT_CODE = 1
|
|
13
|
+
const USAGE = 'Usage: josh lane:withdraw <issue-number> --reason <text>'
|
|
14
|
+
const OPTIONS = { reason: { type: 'string' } } as const
|
|
15
|
+
|
|
16
|
+
interface Request {
|
|
17
|
+
issue: string
|
|
18
|
+
reason: string
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
function issue_of(positionals: ReadonlyArray<string>): string | undefined {
|
|
22
|
+
const [issue, ...rest] = positionals
|
|
23
|
+
|
|
24
|
+
return rest.length === 0 && issue_number_shape.is_issue_number(issue ?? '') ? issue : undefined
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
// A reason of only whitespace is no reason: the Issue would close with an empty `Why:` line.
|
|
28
|
+
function reason_of(value: unknown): string | undefined {
|
|
29
|
+
const reason = cli_flags.string_of(value)?.trim()
|
|
30
|
+
|
|
31
|
+
return reason === '' ? undefined : reason
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function parse_request(argv: ReadonlyArray<string>): Request | undefined {
|
|
35
|
+
const parsed = cli_flags.arguments_of(argv, OPTIONS)
|
|
36
|
+
if (parsed === undefined) return undefined
|
|
37
|
+
const issue = issue_of(parsed.positionals)
|
|
38
|
+
const reason = reason_of(parsed.values.reason)
|
|
39
|
+
|
|
40
|
+
return issue === undefined || reason === undefined ? undefined : { issue, reason }
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function exit_code_of(outcome: WithdrawOutcome): number {
|
|
44
|
+
return outcome.kind === 'withdrawn' && outcome.is_closed ? SUCCESS_EXIT_CODE : FAILURE_EXIT_CODE
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
async function run(argv: ReadonlyArray<string>): Promise<number> {
|
|
48
|
+
const request = parse_request(argv)
|
|
49
|
+
|
|
50
|
+
if (request === undefined) {
|
|
51
|
+
console.error(USAGE)
|
|
52
|
+
|
|
53
|
+
return FAILURE_EXIT_CODE
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
const outcome = await lane_withdraw.withdraw(request.issue, request.reason)
|
|
57
|
+
|
|
58
|
+
console.error(lane_withdraw.describe(outcome, request.issue))
|
|
59
|
+
|
|
60
|
+
return exit_code_of(outcome)
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
async function main(argv: ReadonlyArray<string>): Promise<void> {
|
|
64
|
+
process.exitCode = await run(argv)
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
const lane_withdraw_cli = { run }
|
|
68
|
+
|
|
69
|
+
if (process.argv[1] === fileURLToPath(import.meta.url)) await main(process.argv.slice(ARGV_OFFSET))
|
|
70
|
+
|
|
71
|
+
export { lane_withdraw_cli }
|
|
@@ -0,0 +1,182 @@
|
|
|
1
|
+
import { git_gh_issue_write } from '#scripts/gh/git-gh-issue-write'
|
|
2
|
+
import { issue_cite } from '#scripts/issue/issue-cite'
|
|
3
|
+
import { issue_number_shape } from '#scripts/issue/issue-number-shape'
|
|
4
|
+
import { issue_state_cli } from '#scripts/issue/issue-state-cli'
|
|
5
|
+
import { session_cite } from '#scripts/issue/session-cite'
|
|
6
|
+
import { run_event_stream } from '#scripts/run/event/run-event-stream'
|
|
7
|
+
import { run_event_stream_emit } from '#scripts/run/event/run-event-stream-emit'
|
|
8
|
+
import { run_label } from '#scripts/run/run-label'
|
|
9
|
+
import { lane_close, type CloseKind } from './lane-close'
|
|
10
|
+
import { lane_preserve, type KeptWork } from './lane-preserve'
|
|
11
|
+
import { lane_reap } from './lane-reap'
|
|
12
|
+
import { lane_registry, type LaneInfo } from './lane-registry'
|
|
13
|
+
|
|
14
|
+
// `josh lane:withdraw <N> --reason <text>` — a person takes a running lane child's issue out of the run
|
|
15
|
+
// for good: the child is stopped, its work stashed, the Issue closed as not planned and the lane closed.
|
|
16
|
+
//
|
|
17
|
+
// **The `withdraw` event is written first, before anything is stopped.** Killing the child alone would
|
|
18
|
+
// let `lane:await` book it stopped and `run:merge` classify it from GitHub — an open issue resumed as an
|
|
19
|
+
// `outage` or parked `needs-decision`. `run:merge` reads the event stream before GitHub, so a child whose
|
|
20
|
+
// newest launch-or-withdraw event is a withdraw is booked `withdrawn` whatever the Issue's state reads
|
|
21
|
+
// at that moment, and the race between the kill and the close cannot book it as anything else.
|
|
22
|
+
//
|
|
23
|
+
// **The issue is read again once the child is stopped.** It may merge between the first read and the
|
|
24
|
+
// reap; the reap stopped it, so the second read is final, and an issue it already closed keeps
|
|
25
|
+
// GitHub's close — `run:merge` books a merged close as merged whatever the event stream says.
|
|
26
|
+
//
|
|
27
|
+
// A tree that could not be stashed is left in place with the Issue open: closing the lane would delete
|
|
28
|
+
// the work, and the person decides what to keep.
|
|
29
|
+
|
|
30
|
+
const WITHDRAW_OCCASION = 'lane withdraw'
|
|
31
|
+
const WITHDRAW_CAUSE = 'a person withdrew it with `lane:withdraw`'
|
|
32
|
+
const CLOSED_STATE = 'CLOSED'
|
|
33
|
+
const CLOSED_KIND = 'closed'
|
|
34
|
+
const NO_LANE_KIND = 'no-lane'
|
|
35
|
+
const UNREADABLE_KIND = 'unreadable'
|
|
36
|
+
|
|
37
|
+
type Unsaved = Extract<KeptWork, { kind: 'unsaved' }>
|
|
38
|
+
|
|
39
|
+
interface Withdrawn {
|
|
40
|
+
kind: 'withdrawn'
|
|
41
|
+
// How many of the child's processes the withdrawal stopped — 0 for a child that had already ended.
|
|
42
|
+
reaped: number
|
|
43
|
+
stash: string | undefined
|
|
44
|
+
is_closed: boolean
|
|
45
|
+
// The issue had closed on its own — merged — before the withdrawal reached it, so it was left as is.
|
|
46
|
+
had_closed: boolean
|
|
47
|
+
lane: CloseKind
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
type IssueSettled = Pick<Withdrawn, 'had_closed' | 'is_closed'>
|
|
51
|
+
|
|
52
|
+
type Refused =
|
|
53
|
+
{ kind: typeof CLOSED_KIND } | { kind: typeof NO_LANE_KIND } | { kind: typeof UNREADABLE_KIND }
|
|
54
|
+
type WithdrawOutcome = Withdrawn | Unsaved | Refused
|
|
55
|
+
|
|
56
|
+
function close_comment(reason: string): string {
|
|
57
|
+
return [
|
|
58
|
+
'Withdrawn by a person with `pnpm josh lane:withdraw`.',
|
|
59
|
+
'Its lane child was stopped and the lane closed.',
|
|
60
|
+
`- Why: ${reason}`,
|
|
61
|
+
].join('\n')
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
async function record_withdraw(issue: string, reason: string): Promise<void> {
|
|
65
|
+
await run_event_stream_emit.emit(
|
|
66
|
+
run_event_stream.EVENT_KIND.WITHDRAW,
|
|
67
|
+
`${issue_cite.plain(issue)} withdrawn by a person: ${reason}`,
|
|
68
|
+
)
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
// A closed issue has nothing to withdraw, and an unread one might be: neither writes anything.
|
|
72
|
+
async function refusal_of(issue: string): Promise<Refused | undefined> {
|
|
73
|
+
const read = await issue_state_cli.read_issue(issue)
|
|
74
|
+
|
|
75
|
+
if (read.kind !== 'state') return { kind: UNREADABLE_KIND }
|
|
76
|
+
if (read.state.state.toUpperCase() === CLOSED_STATE) return { kind: CLOSED_KIND }
|
|
77
|
+
|
|
78
|
+
return undefined
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
async function close_open_issue(issue: string, reason: string): Promise<IssueSettled> {
|
|
82
|
+
const refusal = await refusal_of(issue)
|
|
83
|
+
|
|
84
|
+
// An unread issue may have merged during the reap, so it is left open for a person to close.
|
|
85
|
+
if (refusal !== undefined) {
|
|
86
|
+
const is_closed = refusal.kind === CLOSED_KIND
|
|
87
|
+
|
|
88
|
+
return { is_closed, had_closed: is_closed }
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
const is_closed = await git_gh_issue_write.issue_close_not_planned(issue, close_comment(reason))
|
|
92
|
+
|
|
93
|
+
return { is_closed, had_closed: false }
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
async function close_issue(
|
|
97
|
+
issue: string,
|
|
98
|
+
reason: string,
|
|
99
|
+
stash: string | undefined,
|
|
100
|
+
): Promise<IssueSettled> {
|
|
101
|
+
const settled = await close_open_issue(issue, reason)
|
|
102
|
+
|
|
103
|
+
if (stash !== undefined) await lane_preserve.announce(issue, stash, WITHDRAW_CAUSE)
|
|
104
|
+
await run_label.unmark(issue)
|
|
105
|
+
|
|
106
|
+
return settled
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
async function stop_and_close(lane: LaneInfo, reason: string): Promise<WithdrawOutcome> {
|
|
110
|
+
await record_withdraw(lane.issue, reason)
|
|
111
|
+
const reaped = lane_reap.reap_child(lane.issue).length
|
|
112
|
+
const saved = await lane_preserve.keep_work(lane.issue, lane.directory, WITHDRAW_OCCASION)
|
|
113
|
+
|
|
114
|
+
if (saved.kind === 'unsaved') return saved
|
|
115
|
+
const settled = await close_issue(lane.issue, reason, saved.stash)
|
|
116
|
+
const closed = await lane_close.close_lane(lane.issue)
|
|
117
|
+
|
|
118
|
+
return { kind: 'withdrawn', reaped, stash: saved.stash, ...settled, lane: closed.kind }
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
/** Stop `#<N>`'s lane child, stash its work, close the Issue as not planned and close the lane. */
|
|
122
|
+
async function withdraw(issue: string, reason: string): Promise<WithdrawOutcome> {
|
|
123
|
+
issue_number_shape.require_issue_number(issue)
|
|
124
|
+
const refusal = await refusal_of(issue)
|
|
125
|
+
|
|
126
|
+
if (refusal !== undefined) return refusal
|
|
127
|
+
const lane = await lane_registry.find_open_lane(issue)
|
|
128
|
+
|
|
129
|
+
if (lane === undefined) return { kind: NO_LANE_KIND }
|
|
130
|
+
|
|
131
|
+
return await stop_and_close(lane, reason)
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
function stash_sentence(stash: string | undefined): string {
|
|
135
|
+
if (stash === undefined) return 'The lane held no uncommitted work.'
|
|
136
|
+
|
|
137
|
+
return `Its uncommitted work was stashed — recover with \`pnpm josh stash:pop "${stash}"\`.`
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
function withdrawn_text(outcome: Withdrawn, issue: string): string {
|
|
141
|
+
const cited = session_cite.issue(issue)
|
|
142
|
+
const stopped = outcome.reaped === 0 ? 'Its child had already ended.' : 'Stopped its child.'
|
|
143
|
+
const head = `Withdrew ${cited}. ${stopped} ${stash_sentence(outcome.stash)}`
|
|
144
|
+
const lane = outcome.lane === 'incomplete' ? ' The lane could not be fully removed.' : ''
|
|
145
|
+
|
|
146
|
+
if (outcome.had_closed) return `${head} The Issue had already closed and was left as is.${lane}`
|
|
147
|
+
|
|
148
|
+
if (outcome.is_closed) return `${head} The Issue is closed as not planned.${lane}`
|
|
149
|
+
|
|
150
|
+
return `${head} The Issue could not be closed — close ${cited} by hand; \`run:merge\` books it withdrawn either way.${lane}`
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
function unsaved_text(outcome: Unsaved, issue: string): string {
|
|
154
|
+
return `The child for ${session_cite.issue(issue)} was stopped, but its uncommitted work could not be stashed (${outcome.reason}), so the Issue is left open and the lane at ${outcome.directory}; \`run:merge\` books it withdrawn.`
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
function refused_text(outcome: Refused, issue: string): string {
|
|
158
|
+
const cited = session_cite.issue(issue)
|
|
159
|
+
|
|
160
|
+
if (outcome.kind === CLOSED_KIND) {
|
|
161
|
+
return `${cited} is already closed, so there is nothing to withdraw.`
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
if (outcome.kind === NO_LANE_KIND) {
|
|
165
|
+
return `No lane is open for ${cited}, so there is no child to withdraw.`
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
return `The state of ${cited} could not be read, so nothing was stopped or closed — try again.`
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
function describe(outcome: WithdrawOutcome, issue: string): string {
|
|
172
|
+
if (outcome.kind === 'unsaved') return unsaved_text(outcome, issue)
|
|
173
|
+
|
|
174
|
+
return outcome.kind === 'withdrawn'
|
|
175
|
+
? withdrawn_text(outcome, issue)
|
|
176
|
+
: refused_text(outcome, issue)
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
const lane_withdraw = { describe, withdraw }
|
|
180
|
+
|
|
181
|
+
export type { WithdrawOutcome }
|
|
182
|
+
export { lane_withdraw }
|
package/scripts/lint/format.ts
CHANGED
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
#!/usr/bin/env tsx
|
|
2
2
|
import { fileURLToPath } from 'node:url'
|
|
3
3
|
import { project_checks } from '#scripts/gate/project-checks'
|
|
4
|
-
import { ESLINT_CACHE_FLAGS } from '#scripts/josh/josh-command-types'
|
|
4
|
+
import { ESLINT_CACHE_FLAGS, PRETTIER_CACHE_FLAGS } from '#scripts/josh/josh-command-types'
|
|
5
5
|
import { composite_arguments, USAGE_ERROR_EXIT_CODE } from '#scripts/josh/josh-composite-arguments'
|
|
6
6
|
import { execa } from 'execa'
|
|
7
7
|
|
|
8
8
|
const COMMAND_NAME = 'format'
|
|
9
9
|
const ARGV_OFFSET = 2
|
|
10
10
|
const FAIL_EXIT_CODE = 1
|
|
11
|
-
const
|
|
11
|
+
const PRETTIER_WRITE_ARGS = ['exec', 'prettier', '--write', '.', ...PRETTIER_CACHE_FLAGS] as const
|
|
12
12
|
const ESLINT_FIX_ARGS = ['exec', 'eslint', '.', '--fix', ...ESLINT_CACHE_FLAGS] as const
|
|
13
13
|
|
|
14
14
|
async function run_step(args: ReadonlyArray<string>, directory: string): Promise<number> {
|
|
@@ -37,7 +37,7 @@ async function run_format(directory: string): Promise<number> {
|
|
|
37
37
|
const prettier_exit_code = await run_unless_skipped(
|
|
38
38
|
'prettier',
|
|
39
39
|
prettier_reason,
|
|
40
|
-
|
|
40
|
+
PRETTIER_WRITE_ARGS,
|
|
41
41
|
directory,
|
|
42
42
|
)
|
|
43
43
|
if (prettier_exit_code !== 0) return prettier_exit_code
|
|
@@ -64,4 +64,4 @@ if (process.argv[1] === fileURLToPath(import.meta.url)) {
|
|
|
64
64
|
|
|
65
65
|
const format = { run, run_format }
|
|
66
66
|
|
|
67
|
-
export { ESLINT_FIX_ARGS, format }
|
|
67
|
+
export { ESLINT_FIX_ARGS, format, PRETTIER_WRITE_ARGS }
|
|
@@ -1,7 +1,11 @@
|
|
|
1
1
|
#!/usr/bin/env tsx
|
|
2
2
|
import { fileURLToPath } from 'node:url'
|
|
3
3
|
import { project_checks } from '#scripts/gate/project-checks'
|
|
4
|
-
import {
|
|
4
|
+
import {
|
|
5
|
+
ESLINT_CACHE_FILE,
|
|
6
|
+
ESLINT_CACHE_FLAGS,
|
|
7
|
+
PRETTIER_CACHE_FLAGS,
|
|
8
|
+
} from '#scripts/josh/josh-command-types'
|
|
5
9
|
import { lane_cache_run } from '#scripts/lane/lane-cache-run'
|
|
6
10
|
import {
|
|
7
11
|
buffered_process,
|
|
@@ -11,7 +15,6 @@ import {
|
|
|
11
15
|
import { SKIP_MARKER } from '#scripts/test/skip-marker'
|
|
12
16
|
import { cross_file_recheck, type CrossFileRecheckOptions } from './cross-file-recheck'
|
|
13
17
|
|
|
14
|
-
const PRETTIER_ARGS = ['exec', 'prettier', '--check', '.'] as const
|
|
15
18
|
// This — not the `lint:eslint` map entry — is the eslint invocation `josh gate` reaches, so the
|
|
16
19
|
// cache flags come from the same constant the ignore rules are asserted against.
|
|
17
20
|
// A copy here would keep writing the old location the day that constant
|
|
@@ -23,6 +26,10 @@ const WHOLE_TREE_RECHECK: CrossFileRecheckOptions = {
|
|
|
23
26
|
patterns: [WHOLE_TREE],
|
|
24
27
|
}
|
|
25
28
|
|
|
29
|
+
// Prettier's cache stays this checkout's own, never shared through `lane_cache_run` as eslint's is:
|
|
30
|
+
// its entries do not survive a move (`josh-command-types.ts` → `GATE_CACHE_SPECS`).
|
|
31
|
+
const PRETTIER_ARGS = ['exec', 'prettier', '--check', WHOLE_TREE, ...PRETTIER_CACHE_FLAGS] as const
|
|
32
|
+
|
|
26
33
|
// What one eslint run is made of: the child's arguments, and what re-reading a cached cross-file report
|
|
27
34
|
// (`cross-file-recheck.ts`) needs to ask the same question again — the cache file and the targets.
|
|
28
35
|
interface EslintRun {
|
|
@@ -132,4 +139,4 @@ if (process.argv[1] === fileURLToPath(import.meta.url)) {
|
|
|
132
139
|
|
|
133
140
|
const lint_parallel = { run_lint_checks, run_lint_parallel_checks }
|
|
134
141
|
|
|
135
|
-
export { ESLINT_ARGS, lint_parallel }
|
|
142
|
+
export { ESLINT_ARGS, lint_parallel, PRETTIER_ARGS }
|
|
@@ -8,6 +8,7 @@ import {
|
|
|
8
8
|
import {
|
|
9
9
|
ESLINT_RELATED_CACHE_FILE,
|
|
10
10
|
ESLINT_RELATED_CACHE_FLAGS,
|
|
11
|
+
PRETTIER_RELATED_CACHE_FLAGS,
|
|
11
12
|
} from '#scripts/josh/josh-command-types'
|
|
12
13
|
import type { CrossFileRecheckOptions } from './cross-file-recheck'
|
|
13
14
|
|
|
@@ -94,8 +95,10 @@ function describe_scope(scope: ChangedFileScope, root: string): string {
|
|
|
94
95
|
return changed_file_scope.describe_scope(scope, root, VOCABULARY)
|
|
95
96
|
}
|
|
96
97
|
|
|
98
|
+
// The scoped run's own cache file, for the reason eslint's below gives: prettier too rewrites its
|
|
99
|
+
// cache file whole, so a narrowed run sharing the gate's would discard entries a concurrent gate wrote.
|
|
97
100
|
function prettier_arguments(files: ReadonlyArray<string>): ReadonlyArray<string> {
|
|
98
|
-
return [...PRETTIER_ARGS, ...files]
|
|
101
|
+
return [...PRETTIER_ARGS, ...files, ...PRETTIER_RELATED_CACHE_FLAGS]
|
|
99
102
|
}
|
|
100
103
|
|
|
101
104
|
// The cache file is this command's own rather than the gate's, because the two run at the same time.
|