@joshuafolkken/kit 1.1453.0 → 1.1471.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/dependency-update/SKILL.md +26 -55
- package/.claude/skills/epic-commands/SKILL.md +87 -148
- package/.claude/skills/epic-commands/epic-bundle.md +45 -77
- package/.claude/skills/epic-commands/execution-waves.md +44 -81
- package/.claude/skills/verify-ui/SKILL.md +25 -50
- package/.claude/skills/workflow-commands/background-commands.md +36 -47
- package/.claude/skills/workflow-commands/backlogrun-child.md +72 -85
- package/.claude/skills/workflow-commands/backlogrun-handoff-report.md +1 -1
- package/.claude/skills/workflow-commands/backlogrun-lanes.md +91 -121
- package/.claude/skills/workflow-commands/backlogrun-park.md +73 -117
- package/.claude/skills/workflow-commands/backlogrun-progress.md +7 -9
- package/.claude/skills/workflow-commands/backlogrun-recovery.md +11 -20
- package/.claude/skills/workflow-commands/backlogrun-steps.md +43 -52
- package/.claude/skills/workflow-commands/chain-rule.md +24 -34
- package/.claude/skills/workflow-commands/followup.md +57 -76
- package/.claude/skills/workflow-commands/halfrun.md +2 -1
- package/.claude/skills/workflow-commands/issue-comments.md +43 -60
- package/.claude/skills/workflow-commands/kickoff.md +65 -74
- package/.claude/skills/workflow-commands/observation-filing.md +62 -107
- package/.claude/skills/workflow-commands/observation-ledger.md +56 -111
- package/.claude/skills/workflow-commands/pre-gate-cut.md +89 -138
- package/.claude/skills/workflow-commands/prerequisite.md +10 -15
- package/.claude/skills/workflow-commands/progress-watcher.md +21 -26
- package/.claude/skills/workflow-commands/prrun.md +2 -1
- package/.claude/skills/workflow-commands/retrospective.md +31 -50
- package/.claude/skills/workflow-commands/split-assessment.md +34 -58
- package/.claude/skills/workflow-commands/working-tree-hold.md +54 -71
- package/CLAUDE.md +7 -6
- package/README.md +2 -9
- package/dist/CLAUDE.md +7 -6
- package/dist/hooks/batch-guard.js +6 -6
- package/dist/hooks/{chunk-Y3GKIKGW.js → chunk-43ZHT22P.js} +31 -15
- package/dist/hooks/{chunk-MBJYQPEK.js → chunk-AS74OU3D.js} +24 -8
- package/dist/hooks/{chunk-3C4XRYG6.js → chunk-E7OIKX5Z.js} +17 -12
- package/dist/hooks/{chunk-Z4XI7CZS.js → chunk-EUMACUK2.js} +18 -12
- package/dist/hooks/{chunk-NO4AALP5.js → chunk-H5HVWYXK.js} +4 -4
- package/dist/hooks/{chunk-5C7L6FRC.js → chunk-IIGFYXKC.js} +346 -241
- package/dist/hooks/{chunk-W5ZKGTSY.js → chunk-J47HU5H5.js} +14 -3
- package/dist/hooks/{chunk-OLMO72ON.js → chunk-K4EOONYM.js} +1 -1
- package/dist/hooks/{chunk-LBFYEOXJ.js → chunk-K4LMBQC2.js} +4 -4
- package/dist/hooks/{chunk-D3NJXN5M.js → chunk-KC5AK2GF.js} +8 -6
- package/dist/hooks/chunk-SHGPANFB.js +56 -0
- package/dist/hooks/{chunk-EF5GQ6WS.js → chunk-YHBZAJSV.js} +3 -3
- package/dist/hooks/{chunk-SHYUQOIS.js → chunk-YYPI7SJZ.js} +5 -5
- package/dist/hooks/codex-hook-adapter.js +13 -13
- package/dist/hooks/duplicate-read-guard.js +7 -7
- package/dist/hooks/format-edited.js +3 -3
- package/dist/hooks/inputs.json +251 -247
- package/dist/hooks/investigation-guard.js +7 -7
- package/dist/hooks/pretool-guard.js +12 -12
- package/dist/hooks/session-lang.js +6 -5
- package/dist/hooks/stop-guard.js +75 -18
- package/dist/josh.js +6 -7
- package/dist/managed-marker/index.js +1 -2
- package/dist/version/index.js +15 -12
- package/eslint/base.js +2 -1
- package/eslint/rules/unicorn.js +16 -0
- package/package.json +3 -3
- package/prompts/coding-standards.md +12 -22
- package/prompts/collaboration-workflow/file-edits.md +26 -44
- package/prompts/collaboration-workflow/gh-rest.md +20 -36
- package/prompts/collaboration-workflow/glossary.md +5 -7
- package/prompts/collaboration-workflow/issue-citation.md +7 -13
- package/prompts/collaboration-workflow/issue-template.md +29 -29
- package/prompts/collaboration-workflow/operating-rules.md +35 -44
- package/prompts/collaboration-workflow/output-bounds.md +15 -15
- package/prompts/collaboration-workflow/overview.md +14 -19
- package/prompts/collaboration-workflow/plan-comment.md +13 -13
- package/prompts/collaboration-workflow/principles.md +50 -53
- package/prompts/collaboration-workflow/proposal-request.md +1 -9
- package/prompts/collaboration-workflow/report-format.md +32 -49
- package/prompts/collaboration-workflow/residency.md +10 -10
- package/prompts/collaboration-workflow/rule-delivery.md +9 -9
- package/prompts/collaboration-workflow/shell-body.md +11 -21
- package/prompts/collaboration-workflow/turn-batching.md +8 -14
- package/prompts/collaboration-workflow/upstream-interrupt.md +26 -34
- package/prompts/collaboration-workflow/wip-cap.md +24 -30
- package/prompts/collaboration-workflow.md +24 -25
- package/prompts/refactoring.md +33 -43
- package/prompts/review-rubric.md +84 -129
- package/prompts/review.md +111 -141
- package/prompts/sonar-hotspot-handling.md +22 -50
- package/prompts/testing-guide.md +48 -99
- package/scripts/adopt/adopt-toolkits.ts +2 -4
- package/scripts/agent/agent-exit-record.ts +23 -1
- package/scripts/agent/codex-agent-argv.ts +7 -2
- package/scripts/backlog/backlog-budget-cli.ts +1 -2
- package/scripts/backlog/backlog-drive-carried.ts +26 -0
- package/scripts/backlog/backlog-drive-cli.ts +10 -5
- package/scripts/backlog/backlog-drive-named.ts +2 -4
- package/scripts/backlog/backlog-drive-restore.ts +19 -1
- package/scripts/backlog/backlog-drive-seats.ts +27 -0
- package/scripts/backlog/backlog-drive.ts +11 -6
- package/scripts/backlog/backlog-ready.ts +13 -6
- package/scripts/behavior/behavior-rules.ts +3 -2
- package/scripts/cost-runtime/codex-usage.ts +4 -2
- package/scripts/cost-runtime/cost-cli.ts +1 -2
- package/scripts/delegation/investigation-guard.ts +1 -1
- package/scripts/delegation/investigation-reads.ts +1 -1
- package/scripts/discovery/repo-discovery.ts +4 -2
- package/scripts/discovery/repo-origin.ts +2 -4
- package/scripts/doctor/doctor-logic.ts +6 -3
- package/scripts/document/document-byte-budget.ts +27 -27
- package/scripts/document/document-byte-check.ts +1 -2
- package/scripts/document/entry-read-budget.ts +2 -2
- package/scripts/epic/epic-audit-rationale.ts +8 -2
- package/scripts/epic/epic-bundle-cli.ts +1 -2
- package/scripts/epic/epic-chains.ts +6 -2
- package/scripts/epic/epic-close-comment.ts +1 -2
- package/scripts/gate/verification-gate.ts +7 -2
- package/scripts/gh/git-pr-ai-review.ts +12 -1
- package/scripts/gh/git-pr-confirmation.ts +11 -7
- package/scripts/gh/git-pr.ts +6 -2
- package/scripts/git/git-common-directory.ts +3 -3
- package/scripts/git/git-diff-reads.ts +16 -9
- package/scripts/git/git-execa-error.ts +1 -2
- package/scripts/hooks/format-edited-file.ts +10 -4
- package/scripts/hooks/pre-push-unit.ts +15 -4
- package/scripts/hooks/pretool-guard.ts +23 -29
- package/scripts/hooks/step-zero-notice.ts +3 -2
- package/scripts/init/hook-command-rewrite.ts +6 -2
- package/scripts/init/init-logic-deploy-vps.ts +4 -2
- package/scripts/issue/behavior-change-lint.ts +12 -6
- package/scripts/issue/issue-scout-cli.ts +3 -2
- package/scripts/josh/file-map-stamp.ts +1 -3
- package/scripts/josh/hook-decision.ts +17 -2
- package/scripts/josh/josh-commands-workflow.ts +1 -1
- package/scripts/lane/lane-await.ts +3 -2
- package/scripts/lane/lane-child-invocation.ts +14 -1
- package/scripts/lane/lane-guard-policy.ts +4 -4
- package/scripts/lane/lane-ledger.ts +9 -0
- package/scripts/lane/lane-load.ts +9 -2
- package/scripts/lane/lane-measure-cli.ts +2 -0
- package/scripts/lane/lane-parked.ts +18 -0
- package/scripts/lane/lane-process-load.ts +125 -0
- package/scripts/lane/lane-process-stats.ts +93 -0
- package/scripts/lane/lane-registry.ts +8 -3
- package/scripts/lane/lane-session-broker.ts +29 -5
- package/scripts/lane/lane-spent.ts +14 -3
- package/scripts/lane/lane-stats.ts +1 -0
- package/scripts/managed-marker/managed-marker-logic.ts +1 -2
- package/scripts/notify/confirmation-options.ts +59 -0
- package/scripts/notify/notify-opt-in.ts +33 -0
- package/scripts/notify/telegram-notify.ts +25 -8
- package/scripts/notify/telegram-test-logic.ts +33 -1
- package/scripts/notify/telegram-test.ts +4 -2
- package/scripts/overrides/lockfile-overrides.ts +1 -3
- package/scripts/propagate/propagate.ts +1 -2
- package/scripts/release/release-progress.ts +3 -2
- package/scripts/report/report-lint.ts +7 -4
- package/scripts/review/review-record-cli.ts +6 -2
- package/scripts/rules/destructive-command.ts +6 -9
- package/scripts/rules/direct-filing.ts +3 -7
- package/scripts/rules/early-heartbeat.ts +7 -18
- package/scripts/rules/file-body.ts +6 -13
- package/scripts/rules/filing-cap.ts +3 -6
- package/scripts/rules/git-force.ts +5 -9
- package/scripts/rules/implementation-cut.ts +37 -35
- package/scripts/rules/index-guard.ts +4 -7
- package/scripts/rules/issue-comments.ts +5 -15
- package/scripts/rules/josh-git-bare.ts +3 -6
- package/scripts/rules/lane-background.ts +10 -21
- package/scripts/rules/lane-carry-conflict.ts +7 -17
- package/scripts/rules/lane-interactive-ask.ts +6 -11
- package/scripts/rules/lane-park.ts +5 -13
- package/scripts/rules/lane-split-park.ts +2 -7
- package/scripts/rules/lane-switch-main.ts +9 -12
- package/scripts/rules/oracle-consulted.ts +4 -5
- package/scripts/rules/piped-verification.ts +81 -31
- package/scripts/rules/poll-loop.ts +7 -17
- package/scripts/rules/pre-gate-cut.ts +7 -21
- package/scripts/rules/prior-comment-read.ts +7 -5
- package/scripts/rules/protected-files.ts +5 -9
- package/scripts/rules/raw-field-body.ts +3 -7
- package/scripts/rules/rule-body-guard.ts +4 -11
- package/scripts/rules/run-tail-rule.ts +6 -22
- package/scripts/rules/shell-body-trigger.ts +7 -8
- package/scripts/rules/shell-segments.ts +7 -0
- package/scripts/rules/stop-rules.ts +17 -29
- package/scripts/rules/test-declared-commit.ts +4 -10
- package/scripts/rules/third-party-write.ts +5 -10
- package/scripts/rules/worktree-guard.ts +5 -12
- package/scripts/run/board/run-board-github.ts +11 -3
- package/scripts/run/board/run-board-idle.ts +31 -0
- package/scripts/run/board/run-board-read.ts +9 -12
- package/scripts/run/board/run-board-reason.ts +7 -3
- package/scripts/run/board/run-board-state.ts +4 -0
- package/scripts/run/board/run-board-status.ts +18 -3
- package/scripts/run/board/run-board-thanks.ts +7 -2
- package/scripts/run/board/run-board-tick.ts +32 -7
- package/scripts/run/carry/run-carry-args.ts +1 -3
- package/scripts/run/carry/run-carry-conversation.ts +7 -2
- package/scripts/run/cut/run-cut-cli.ts +15 -20
- package/scripts/run/cut/run-cut-decline.ts +72 -0
- package/scripts/run/cut/run-cut-ready.ts +74 -0
- package/scripts/run/cut/run-cut-report.ts +13 -0
- package/scripts/run/cut/run-cut.ts +10 -1
- package/scripts/run/entry/run-entry-stop.ts +6 -3
- package/scripts/run/hold/run-hold-cli.ts +14 -3
- package/scripts/run/hold/run-hold.ts +6 -3
- package/scripts/run/merge/run-merge-cli.ts +30 -7
- package/scripts/run/merge/run-merge-ship.ts +59 -1
- package/scripts/run/merge/run-merge.ts +3 -2
- package/scripts/run/run-ending.ts +31 -3
- package/scripts/run/run-parent-cut-hook.ts +5 -10
- package/scripts/run/run-preflight.ts +1 -2
- package/scripts/run/run-stop-notify.ts +3 -2
- package/scripts/run/run-watcher-guard.ts +6 -3
- package/scripts/run/ship/run-ship-review-steps.ts +13 -4
- package/scripts/run/ship/run-ship-review.ts +15 -2
- package/scripts/run/tidy/run-tidy-cli.ts +3 -3
- package/scripts/run/tidy/run-tidy-lanes.ts +3 -2
- package/scripts/safe-chain/project-config.ts +7 -2
- package/scripts/sync/sync-hook-safety.ts +8 -2
- package/scripts/test/unit-worker-share.ts +3 -2
- package/scripts/time-runtime/time-batch-guard.ts +3 -3
- package/scripts/time-runtime/time-hook-transcript.ts +3 -2
- package/scripts/time-runtime/time-shell.ts +10 -0
- package/scripts/version/effective-upstream.ts +3 -4
- package/scripts/version/kit-version-config.ts +8 -2
- package/scripts/version/latest-corepack.ts +1 -2
- package/scripts/version/release-hold.ts +11 -4
- package/scripts/version/version-commands.ts +12 -4
- package/dist/hooks/chunk-UT7YC3N2.js +0 -56
|
@@ -65,8 +65,9 @@ function notice(raw_payload: string, root: string = process.cwd()): string | und
|
|
|
65
65
|
|
|
66
66
|
const target = STAMP.path(transcript)
|
|
67
67
|
|
|
68
|
-
if (STAMP.last_ms(target) !== hook_decision.NEVER_MS)
|
|
69
|
-
|
|
68
|
+
if (STAMP.last_ms(target) !== hook_decision.NEVER_MS || !STAMP.record(target, Date.now())) {
|
|
69
|
+
return undefined
|
|
70
|
+
}
|
|
70
71
|
|
|
71
72
|
return NOTICE
|
|
72
73
|
}
|
|
@@ -65,8 +65,12 @@ function rewrite_command_value(value: string): string {
|
|
|
65
65
|
.split(PNPM_JOSH_PREFIX)
|
|
66
66
|
.join(BUNDLE_INVOCATION)
|
|
67
67
|
|
|
68
|
-
if (
|
|
69
|
-
|
|
68
|
+
if (
|
|
69
|
+
!rewritten.includes(claude_plugin_config.MARKETPLACE_PATH) ||
|
|
70
|
+
rewritten.includes(MISSING_INSTALL_GUARD)
|
|
71
|
+
) {
|
|
72
|
+
return rewritten
|
|
73
|
+
}
|
|
70
74
|
|
|
71
75
|
return `${MISSING_INSTALL_GUARD}${rewritten}`
|
|
72
76
|
}
|
|
@@ -30,8 +30,10 @@ function patch_deploy_vps_pnpm(content: string): string {
|
|
|
30
30
|
const with_pnpm12 = content.replaceAll(PNPM_INSTALL_PATTERN, (command) =>
|
|
31
31
|
replace_outdated_install(command),
|
|
32
32
|
)
|
|
33
|
-
|
|
34
|
-
if (!OLD_PNPM_CHECK_PATTERN.test(with_pnpm12))
|
|
33
|
+
|
|
34
|
+
if (with_pnpm12.includes(VERSION_CHECK_MARKER) || !OLD_PNPM_CHECK_PATTERN.test(with_pnpm12)) {
|
|
35
|
+
return with_pnpm12
|
|
36
|
+
}
|
|
35
37
|
|
|
36
38
|
return with_pnpm12
|
|
37
39
|
.replace(OLD_MAJOR_LINE_PATTERN, '')
|
|
@@ -76,9 +76,12 @@ function firing_point_problem(body: string): string | undefined {
|
|
|
76
76
|
}
|
|
77
77
|
|
|
78
78
|
function baseline_problem(body: string): string | undefined {
|
|
79
|
-
if (
|
|
80
|
-
|
|
81
|
-
|
|
79
|
+
if (
|
|
80
|
+
!markdown_section.has_line(body, BASELINE_HEADING) ||
|
|
81
|
+
baseline_measure.has_command_value(body)
|
|
82
|
+
) {
|
|
83
|
+
return undefined
|
|
84
|
+
}
|
|
82
85
|
|
|
83
86
|
return 'baseline must be written as `command → value`, not prose (it has to be re-runnable)'
|
|
84
87
|
}
|
|
@@ -86,9 +89,12 @@ function baseline_problem(body: string): string | undefined {
|
|
|
86
89
|
// The reproduction section is checked for format only where the heading is present — a missing heading
|
|
87
90
|
// is already reported by `heading_problems`, so reporting it here too would double the message.
|
|
88
91
|
function reproduction_problem(body: string): string | undefined {
|
|
89
|
-
if (
|
|
90
|
-
|
|
91
|
-
|
|
92
|
+
if (
|
|
93
|
+
!markdown_section.has_line(body, REPRODUCTION_HEADING) ||
|
|
94
|
+
reproduction_measure.has_command_output(body)
|
|
95
|
+
) {
|
|
96
|
+
return undefined
|
|
97
|
+
}
|
|
92
98
|
|
|
93
99
|
return 'reproduction must be a command and its actual output (a fenced block), not prose ("確認した" is not re-runnable)'
|
|
94
100
|
}
|
|
@@ -73,8 +73,9 @@ function read_arguments(argv: ReadonlyArray<string>): ScoutArguments | undefined
|
|
|
73
73
|
const { values, positionals } = parsed
|
|
74
74
|
const [title] = positionals
|
|
75
75
|
|
|
76
|
-
if (has_invalid_arguments(title, values.body, values['body-file']))
|
|
77
|
-
|
|
76
|
+
if (title === undefined || has_invalid_arguments(title, values.body, values['body-file'])) {
|
|
77
|
+
return undefined
|
|
78
|
+
}
|
|
78
79
|
|
|
79
80
|
return { title, body: values.body ?? '', body_file: values['body-file'] }
|
|
80
81
|
}
|
|
@@ -211,9 +211,7 @@ function is_own_record(stamp: FileMapStamp): boolean {
|
|
|
211
211
|
function complete_at(target: string): string | undefined {
|
|
212
212
|
const stamp = read_at(target)
|
|
213
213
|
|
|
214
|
-
if (stamp === undefined) return undefined
|
|
215
|
-
|
|
216
|
-
if (!is_own_record(stamp)) return undefined
|
|
214
|
+
if (stamp === undefined || !is_own_record(stamp)) return undefined
|
|
217
215
|
|
|
218
216
|
return stamp_file.write_stamp(target, { ...stamp, completed_at: new Date().toISOString() })
|
|
219
217
|
}
|
|
@@ -87,8 +87,8 @@ function deny_envelope(reason: string): string {
|
|
|
87
87
|
}
|
|
88
88
|
|
|
89
89
|
// **A rewrite, not a refusal**: the call proceeds with `updated_input` merged
|
|
90
|
-
// over its own
|
|
91
|
-
//
|
|
90
|
+
// over its own and is allowed past the permission prompt; a settings `deny` rule still wins over it.
|
|
91
|
+
// Where the line may carry commands the hook did not write, `flow_rewrite_envelope` is the one to use.
|
|
92
92
|
function rewrite_envelope(updated_input: Record<string, unknown>, context: string): string {
|
|
93
93
|
return JSON.stringify({
|
|
94
94
|
systemMessage: context,
|
|
@@ -101,6 +101,20 @@ function rewrite_envelope(updated_input: Record<string, unknown>, context: strin
|
|
|
101
101
|
})
|
|
102
102
|
}
|
|
103
103
|
|
|
104
|
+
// **A rewrite that leaves the permission to the harness.** No `permissionDecision`, so the rewritten
|
|
105
|
+
// call meets the same permission flow the typed one would have — whatever else rides the line beside
|
|
106
|
+
// the rewritten part is prompted for, or not, exactly as before (joshuafolkken/kit#3875).
|
|
107
|
+
function flow_rewrite_envelope(updated_input: Record<string, unknown>, context: string): string {
|
|
108
|
+
return JSON.stringify({
|
|
109
|
+
systemMessage: context,
|
|
110
|
+
hookSpecificOutput: {
|
|
111
|
+
hookEventName: HOOK_EVENT_NAME,
|
|
112
|
+
updatedInput: updated_input,
|
|
113
|
+
additionalContext: context,
|
|
114
|
+
},
|
|
115
|
+
})
|
|
116
|
+
}
|
|
117
|
+
|
|
104
118
|
// **A notice, not a decision.** It carries no `permissionDecision`, so the call proceeds exactly as it
|
|
105
119
|
// would have; `additionalContext` is what reaches the model, and `systemMessage` is what the person
|
|
106
120
|
// watching sees. Both are filled because either channel alone leaves one of the two blind — and the
|
|
@@ -476,6 +490,7 @@ const hook_decision = {
|
|
|
476
490
|
deny_envelope,
|
|
477
491
|
emit_outcome,
|
|
478
492
|
fault_notice,
|
|
493
|
+
flow_rewrite_envelope,
|
|
479
494
|
is_switch_enabled,
|
|
480
495
|
is_switch_opt_in,
|
|
481
496
|
load_environment_file,
|
|
@@ -47,7 +47,7 @@ const WORKFLOW_COMMANDS: Record<string, CommandEntry> = {
|
|
|
47
47
|
description: 'Send Telegram notification',
|
|
48
48
|
category: 'Workflow',
|
|
49
49
|
reference: [
|
|
50
|
-
'[--task-type <type>] [--body <text> | --body-file <path>] [--issue-url <url>] [--pr-url <url>] [--issue-title <text>] [--repo-name <name>]',
|
|
50
|
+
'[--task-type <type>] [--body <text> | --body-file <path>] [--issue-url <url>] [--pr-url <url>] [--issue-title <text>] [--repo-name <name>] [--recommended <text>] [--alternative <text>]...',
|
|
51
51
|
'automation',
|
|
52
52
|
['notifications'],
|
|
53
53
|
],
|
|
@@ -89,8 +89,9 @@ function process_trace_default(issue: string): ProcessTrace {
|
|
|
89
89
|
|
|
90
90
|
process_tools.throw_if_missing(PGREP_COMMAND, result.error)
|
|
91
91
|
|
|
92
|
-
if (result.status === PROCESS_FOUND)
|
|
93
|
-
|
|
92
|
+
if (result.status === PROCESS_FOUND || lane_handoff.is_ship_running(issue, process.cwd())) {
|
|
93
|
+
return PROCESS_ALIVE
|
|
94
|
+
}
|
|
94
95
|
|
|
95
96
|
return result.status === PROCESS_NOT_FOUND ? PROCESS_NONE : PROCESS_UNKNOWN
|
|
96
97
|
}
|
|
@@ -8,6 +8,8 @@ interface ShipStop extends ShipResume {
|
|
|
8
8
|
}
|
|
9
9
|
|
|
10
10
|
const CHILD_INVOCATION = 'fullrun'
|
|
11
|
+
// What a brokered reviewer's prompt ends with, so the one pattern below finds it too.
|
|
12
|
+
const REVIEW_INVOCATION = 'the reviewer of lane'
|
|
11
13
|
const RESUME_GUIDE = '.claude/skills/workflow-commands/pre-gate-cut.md'
|
|
12
14
|
// What a detached `josh ship --detach` supervisor's command line carries. It ends
|
|
13
15
|
// with the `"<title> #<N>"` title on purpose, so the liveness pattern below finds it by the same trailing
|
|
@@ -31,10 +33,20 @@ function child_invocation(issue: string): string {
|
|
|
31
33
|
// pattern stopped matching as finished — parking an issue that was still shipping. The supervisor's
|
|
32
34
|
// command line ends with its `#<N>` title, so the one extended pattern covers both, and the wait, the
|
|
33
35
|
// liveness read and the reaper keep searching with a single pattern.
|
|
36
|
+
//
|
|
37
|
+
// **A brokered reviewer counts as the child too** (joshuafolkken/kit#3878). The lane's broker starts it
|
|
38
|
+
// for a ship and it outlives a ship that stopped waiting: unmatched, the lane read as finished, the
|
|
39
|
+
// run parked the issue under it, and the reap left it running checks in the parked lane.
|
|
34
40
|
function process_pattern(issue: string): string {
|
|
35
41
|
issue_number_shape.require_issue_number(issue)
|
|
36
42
|
|
|
37
|
-
return `(${CHILD_INVOCATION}|${SHIP_PROCESS} .*) ${issue_cite.plain(issue)}$`
|
|
43
|
+
return `(${CHILD_INVOCATION}|${REVIEW_INVOCATION}|${SHIP_PROCESS} .*) ${issue_cite.plain(issue)}$`
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function review_invocation(issue: string): string {
|
|
47
|
+
issue_number_shape.require_issue_number(issue)
|
|
48
|
+
|
|
49
|
+
return `${REVIEW_INVOCATION} ${issue_cite.plain(issue)}`
|
|
38
50
|
}
|
|
39
51
|
|
|
40
52
|
function resume_invocation(issue: string): string {
|
|
@@ -82,6 +94,7 @@ const lane_child_invocation = {
|
|
|
82
94
|
outage_resume_invocation,
|
|
83
95
|
process_pattern,
|
|
84
96
|
resume_invocation,
|
|
97
|
+
review_invocation,
|
|
85
98
|
ship_stop_invocation,
|
|
86
99
|
}
|
|
87
100
|
|
|
@@ -47,25 +47,25 @@ const LANE_GUARD_POLICY: ReadonlyArray<LaneGuardEntry> = [
|
|
|
47
47
|
id: 'investigation',
|
|
48
48
|
mode_in_lane_child: 'notice',
|
|
49
49
|
because:
|
|
50
|
-
'a refusal ends a child turn
|
|
50
|
+
'a refusal ends a child turn, yet lane children were measured reading files they never edited, so the child is nudged to delegate by a notice rather than killed',
|
|
51
51
|
},
|
|
52
52
|
{
|
|
53
53
|
id: 'batching',
|
|
54
54
|
mode_in_lane_child: 'off',
|
|
55
55
|
because:
|
|
56
|
-
'a refusal ends a child turn
|
|
56
|
+
'a refusal ends a child turn and every advisory notice was measured not to move the call density, so the guard is off and the density stays readable through `josh time:density`',
|
|
57
57
|
},
|
|
58
58
|
{
|
|
59
59
|
id: 'duplicate-read',
|
|
60
60
|
mode_in_lane_child: 'notice',
|
|
61
61
|
because:
|
|
62
|
-
'a refusal ends a child turn
|
|
62
|
+
'a refusal ends a child turn, yet unchanged re-reads are measured in lane children above all, so the child is nudged to scroll up to the earlier read by a notice rather than killed',
|
|
63
63
|
},
|
|
64
64
|
{
|
|
65
65
|
id: 'rule',
|
|
66
66
|
mode_in_lane_child: 'refuse',
|
|
67
67
|
because:
|
|
68
|
-
|
|
68
|
+
'it carries the lane-only rules a child depends on (`pre-gate-cut`, `implementation-cut`, `lane-park`) and the safety rules it must still obey, and a notice was rejected despite the collateral a refusal costs a batched sibling, because a cut carried as prose was measured never to fire',
|
|
69
69
|
},
|
|
70
70
|
]
|
|
71
71
|
|
|
@@ -56,6 +56,12 @@ const gate_schema = z.object({
|
|
|
56
56
|
// worth its load. `swapped_mb` is `machine_capacity`'s counter, every page swapped since boot.
|
|
57
57
|
// Both memory fields were renamed with their meaning (joshuafolkken/kit#3593): an older row's
|
|
58
58
|
// `free_mb` / `swap_mb` are stripped on read, so the two measurements never share a column.
|
|
59
|
+
const process_kind_schema = z.object({
|
|
60
|
+
kind: z.string(),
|
|
61
|
+
cpu_percent: z.number(),
|
|
62
|
+
rss_mb: z.number(),
|
|
63
|
+
count: z.number(),
|
|
64
|
+
})
|
|
59
65
|
const load_schema = z.object({
|
|
60
66
|
kind: z.literal(KIND.LOAD),
|
|
61
67
|
at: z.string(),
|
|
@@ -63,6 +69,9 @@ const load_schema = z.object({
|
|
|
63
69
|
available_mb: z.number().optional(),
|
|
64
70
|
swapped_mb: z.number().optional(),
|
|
65
71
|
lanes: z.number().optional(),
|
|
72
|
+
// The heaviest process kinds at the instant of the sample (`lane-process-load.ts`), so the load the
|
|
73
|
+
// core budget's ledger does not explain can be read back by what made it (joshuafolkken/kit#3874).
|
|
74
|
+
processes: z.array(process_kind_schema).optional(),
|
|
66
75
|
})
|
|
67
76
|
// A lane child started on `issue` — the instant its implementation is timed from.
|
|
68
77
|
const dispatch_schema = z.object({
|
|
@@ -2,10 +2,12 @@ import { loadavg } from 'node:os'
|
|
|
2
2
|
import { machine_capacity } from '#scripts/gate/machine-capacity'
|
|
3
3
|
import { lane_await } from './lane-await'
|
|
4
4
|
import type { LoadEntry } from './lane-ledger'
|
|
5
|
+
import { lane_process_load } from './lane-process-load'
|
|
5
6
|
import { lane_registry, type LaneInfo } from './lane-registry'
|
|
6
7
|
|
|
7
8
|
// One machine-load sample for the lane-limit measurement: the one-minute load
|
|
8
|
-
// average, available memory, the swap counter
|
|
9
|
+
// average, available memory, the swap counter, how many lanes have a child working and the heaviest
|
|
10
|
+
// process kinds (`lane-process-load.ts`), read at one
|
|
9
11
|
// instant so `josh lane:stats` can set the load beside the lane count that produced it.
|
|
10
12
|
//
|
|
11
13
|
// **The memory is `machine_capacity`'s reading, the one the gate admits against.** `os.freemem()` reads
|
|
@@ -38,7 +40,11 @@ function rounded(megabytes: number | undefined): number | undefined {
|
|
|
38
40
|
}
|
|
39
41
|
|
|
40
42
|
async function sample(at: string): Promise<LoadEntry> {
|
|
41
|
-
const [memory, lanes] = await Promise.all([
|
|
43
|
+
const [memory, lanes, processes] = await Promise.all([
|
|
44
|
+
machine_capacity.read_memory(),
|
|
45
|
+
read_lanes(),
|
|
46
|
+
lane_process_load.read(),
|
|
47
|
+
])
|
|
42
48
|
const [load = 0] = loadavg()
|
|
43
49
|
|
|
44
50
|
return {
|
|
@@ -48,6 +54,7 @@ async function sample(at: string): Promise<LoadEntry> {
|
|
|
48
54
|
available_mb: rounded(memory.available_mb),
|
|
49
55
|
swapped_mb: rounded(memory.swapped_mb),
|
|
50
56
|
lanes,
|
|
57
|
+
processes,
|
|
51
58
|
}
|
|
52
59
|
}
|
|
53
60
|
|
|
@@ -4,6 +4,7 @@ import { fileURLToPath } from 'node:url'
|
|
|
4
4
|
import { cli_flags } from '#scripts/lib/cli-flags'
|
|
5
5
|
import { lane_capacity } from './lane-capacity'
|
|
6
6
|
import { lane_ledger } from './lane-ledger'
|
|
7
|
+
import { lane_process_stats } from './lane-process-stats'
|
|
7
8
|
import { lane_sampler, type MeasureContext } from './lane-sampler'
|
|
8
9
|
import { lane_stage_stats } from './lane-stage-stats'
|
|
9
10
|
import { lane_stats } from './lane-stats'
|
|
@@ -119,6 +120,7 @@ async function stats_command(
|
|
|
119
120
|
`${lane_stats.header()}\n${row}`,
|
|
120
121
|
lane_stage_stats.table(entries, window),
|
|
121
122
|
lane_stop_stats.table(entries, window),
|
|
123
|
+
lane_process_stats.table(entries, window),
|
|
122
124
|
].join('\n\n'),
|
|
123
125
|
)
|
|
124
126
|
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { has_label_name, NEEDS_DECISION_LABEL } from '#scripts/issue/issue-labels'
|
|
2
|
+
import { issue_state_cli } from '#scripts/issue/issue-state-cli'
|
|
3
|
+
|
|
4
|
+
// Whether an issue is parked for a person — the `needs-decision` label — read live, so a session a
|
|
5
|
+
// lane's broker would start for it is refused once the run has parked it (joshuafolkken/kit#3878).
|
|
6
|
+
//
|
|
7
|
+
// **An issue whose state cannot be read is not parked**: the read gates every brokered review, so
|
|
8
|
+
// reading an outage as a park would fail every review in flight for as long as GitHub is away.
|
|
9
|
+
|
|
10
|
+
async function is_parked(issue: string): Promise<boolean> {
|
|
11
|
+
const read = await issue_state_cli.read_issue(issue)
|
|
12
|
+
|
|
13
|
+
return read.kind === 'state' && has_label_name(read.state.labels, NEEDS_DECISION_LABEL)
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
const lane_parked = { is_parked }
|
|
17
|
+
|
|
18
|
+
export { lane_parked }
|
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
import { PROBE_TIMEOUT_MS } from '#scripts/lib/timeouts'
|
|
2
|
+
import { execa } from 'execa'
|
|
3
|
+
|
|
4
|
+
// What the machine's load is made of at one instant: every process grouped by the kind of tool it
|
|
5
|
+
// runs, the heaviest kinds kept with their CPU and resident size, for a `load` sample to carry.
|
|
6
|
+
//
|
|
7
|
+
// **Why a load sample needs it.** The gate's static checks and unit suite are on the core budget's
|
|
8
|
+
// ledger, so the load the ledger does not explain comes from somewhere else — review subagents, the
|
|
9
|
+
// sessions themselves, a hook's unit run, an IDE — and a load average alone cannot say which.
|
|
10
|
+
// joshuafolkken/kit#3874 measured load 93 with 1.6 lanes working and nothing on record to explain it.
|
|
11
|
+
//
|
|
12
|
+
// **The kind is read from the command line, not the executable name.** Every tool here is a `node`
|
|
13
|
+
// process, so the name says nothing; the first pattern a command line matches names its kind, in the
|
|
14
|
+
// order below, so `pnpm exec vitest` is `vitest` rather than `josh`. The IDE comes first: its language
|
|
15
|
+
// servers run out of a `typescript` or `eslint` directory, and read as `tsc` or `eslint` they would
|
|
16
|
+
// pass the IDE's load off as the gate's own checks — and a server the IDE starts on a plain `node`
|
|
17
|
+
// runtime is known only by its `.vscode/extensions/` path. `claude` is the executable or its package,
|
|
18
|
+
// never a `.claude/` path a session's Bash-tool shell or hook script runs through. `pnpm` is the
|
|
19
|
+
// command, never the `.pnpm/` store a resolved binary's path runs through.
|
|
20
|
+
//
|
|
21
|
+
// **`%cpu` is the share of one core**, so 300 is three cores busy; it is `ps`'s decaying average,
|
|
22
|
+
// which is what a one-minute sample beside the one-minute load average wants.
|
|
23
|
+
|
|
24
|
+
const PS_ARGUMENTS = ['-A', '-o', 'rss=,%cpu=,args=']
|
|
25
|
+
// `ps` prints `%cpu` with the locale's decimal separator, and a `0,7` would parse as NaN.
|
|
26
|
+
const PS_ENVIRONMENT = { LC_ALL: 'C' }
|
|
27
|
+
const FIELD_SEPARATOR = /\s+/u
|
|
28
|
+
const KB_PER_MB = 1024
|
|
29
|
+
// Enough to name what dominates and the runner-up behind it, short enough for one ledger line a minute.
|
|
30
|
+
const TOP_KINDS = 5
|
|
31
|
+
const OTHER_KIND = 'other'
|
|
32
|
+
|
|
33
|
+
const KIND_PATTERNS: ReadonlyArray<readonly [string, RegExp]> = [
|
|
34
|
+
['vscode', /Visual Studio Code|Code Helper|\.vscode\/extensions\//u],
|
|
35
|
+
['vitest', /vitest/u],
|
|
36
|
+
['eslint', /eslint/u],
|
|
37
|
+
['tsc', /\btsc\b|svelte-check|typescript/u],
|
|
38
|
+
['cspell', /cspell/u],
|
|
39
|
+
['prettier', /prettier/u],
|
|
40
|
+
['playwright', /playwright/u],
|
|
41
|
+
['chrome', /Google Chrome/u],
|
|
42
|
+
['chromium', /chromium|headless_shell/iu],
|
|
43
|
+
['claude', /(?:^|\/)claude(?:\s|$)|@anthropic-ai\/claude-code\//u],
|
|
44
|
+
['josh', /josh\.ts|\btsx\b|(?<!\.)\bpnpm\b/u],
|
|
45
|
+
['node', /\bnode\b/u],
|
|
46
|
+
]
|
|
47
|
+
|
|
48
|
+
interface ProcessKind {
|
|
49
|
+
kind: string
|
|
50
|
+
cpu_percent: number
|
|
51
|
+
rss_mb: number
|
|
52
|
+
count: number
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function kind_of(command_line: string): string {
|
|
56
|
+
return KIND_PATTERNS.find(([, pattern]) => pattern.test(command_line))?.[0] ?? OTHER_KIND
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
// One line of `ps`'s listing as its kind and readings; a header or a cut-short line reads as nothing.
|
|
60
|
+
function process_of(line: string): Omit<ProcessKind, 'count'> | undefined {
|
|
61
|
+
const [rss, cpu, ...command] = line.trim().split(FIELD_SEPARATOR)
|
|
62
|
+
const reading = { cpu_percent: Number(cpu), rss_mb: Number(rss) / KB_PER_MB }
|
|
63
|
+
|
|
64
|
+
if (command.length === 0 || Number.isNaN(reading.cpu_percent + reading.rss_mb)) return undefined
|
|
65
|
+
|
|
66
|
+
return { kind: kind_of(command.join(' ')), ...reading }
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
function add_line(totals: Map<string, ProcessKind>, line: string): void {
|
|
70
|
+
const reading = process_of(line)
|
|
71
|
+
|
|
72
|
+
if (reading === undefined) return
|
|
73
|
+
|
|
74
|
+
const { kind } = reading
|
|
75
|
+
const total = totals.get(kind) ?? { kind, cpu_percent: 0, rss_mb: 0, count: 0 }
|
|
76
|
+
|
|
77
|
+
total.cpu_percent += reading.cpu_percent
|
|
78
|
+
total.rss_mb += reading.rss_mb
|
|
79
|
+
total.count += 1
|
|
80
|
+
totals.set(kind, total)
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
function rounded(total: ProcessKind): ProcessKind {
|
|
84
|
+
return { ...total, cpu_percent: Math.round(total.cpu_percent), rss_mb: Math.round(total.rss_mb) }
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
// The heaviest kinds first: by CPU, the load average's own measure, then by memory for idle kinds.
|
|
88
|
+
function by_weight(left: ProcessKind, right: ProcessKind): number {
|
|
89
|
+
return right.cpu_percent - left.cpu_percent || right.rss_mb - left.rss_mb
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
function summarize(listing: string): Array<ProcessKind> {
|
|
93
|
+
const totals = new Map<string, ProcessKind>()
|
|
94
|
+
|
|
95
|
+
for (const line of listing.split('\n')) add_line(totals, line)
|
|
96
|
+
|
|
97
|
+
return [...totals.values()]
|
|
98
|
+
.map((total) => rounded(total))
|
|
99
|
+
.toSorted(by_weight)
|
|
100
|
+
.slice(0, TOP_KINDS)
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
// `undefined` where `ps` did not answer, so the sample records no breakdown rather than an empty one.
|
|
104
|
+
async function read(): Promise<Array<ProcessKind> | undefined> {
|
|
105
|
+
try {
|
|
106
|
+
const { stdout } = await execa('ps', PS_ARGUMENTS, {
|
|
107
|
+
env: PS_ENVIRONMENT,
|
|
108
|
+
timeout: PROBE_TIMEOUT_MS,
|
|
109
|
+
})
|
|
110
|
+
|
|
111
|
+
return summarize(stdout)
|
|
112
|
+
} catch {
|
|
113
|
+
return undefined
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
const lane_process_load = {
|
|
118
|
+
TOP_KINDS,
|
|
119
|
+
kind_of,
|
|
120
|
+
read,
|
|
121
|
+
summarize,
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
export type { ProcessKind }
|
|
125
|
+
export { lane_process_load }
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
import type { LedgerEntry, LoadEntry } from './lane-ledger'
|
|
2
|
+
import type { ProcessKind } from './lane-process-load'
|
|
3
|
+
import { lane_stats, type StatsWindow } from './lane-stats'
|
|
4
|
+
|
|
5
|
+
// What the period's machine load was made of: the process kinds the load samples carried
|
|
6
|
+
// (`lane-process-load.ts`), one row per kind, printed by `josh lane:stats` under the stop table so the
|
|
7
|
+
// load the core budget's ledger does not explain is read back by what made it (joshuafolkken/kit#3874).
|
|
8
|
+
//
|
|
9
|
+
// **A kind is averaged over the samples it appears in**, never over every sample. A sample keeps only
|
|
10
|
+
// its heaviest kinds, so a kind absent from one was below them rather than idle, and a zero there would
|
|
11
|
+
// understate it; `samples` says how often it was among the heaviest, which is the weight to read it by.
|
|
12
|
+
//
|
|
13
|
+
// **CPU is in cores** — `ps`'s share of one core over a hundred — so the column reads against the load
|
|
14
|
+
// average and the machine's core count without a conversion.
|
|
15
|
+
|
|
16
|
+
const COLUMNS = [
|
|
17
|
+
'process',
|
|
18
|
+
'cpu mean (cores)',
|
|
19
|
+
'cpu peak (cores)',
|
|
20
|
+
'rss peak (GB)',
|
|
21
|
+
'samples',
|
|
22
|
+
] as const
|
|
23
|
+
const PERCENT_PER_CORE = 100
|
|
24
|
+
const MB_PER_GB = 1024
|
|
25
|
+
const DECIMALS = 1
|
|
26
|
+
|
|
27
|
+
interface KindRow {
|
|
28
|
+
kind: string
|
|
29
|
+
readings: Array<ProcessKind>
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function is_load(entry: LedgerEntry): entry is LoadEntry {
|
|
33
|
+
return entry.kind === 'load'
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function group(samples: ReadonlyArray<LoadEntry>): Array<KindRow> {
|
|
37
|
+
const rows = new Map<string, KindRow>()
|
|
38
|
+
const readings = samples.flatMap((sample) => sample.processes ?? [])
|
|
39
|
+
|
|
40
|
+
for (const reading of readings) {
|
|
41
|
+
const row = rows.get(reading.kind) ?? { kind: reading.kind, readings: [] }
|
|
42
|
+
|
|
43
|
+
row.readings.push(reading)
|
|
44
|
+
rows.set(reading.kind, row)
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
return [...rows.values()]
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function cores(readings: ReadonlyArray<ProcessKind>): Array<number> {
|
|
51
|
+
return readings.map((reading) => reading.cpu_percent / PERCENT_PER_CORE)
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
// A row has one reading at least, so its mean always exists.
|
|
55
|
+
function mean_cores(row: KindRow): number {
|
|
56
|
+
return lane_stats.mean(cores(row.readings)) ?? 0
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function cell(value: number | undefined): string {
|
|
60
|
+
return value === undefined ? lane_stats.MISSING : value.toFixed(DECIMALS)
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
function kind_cells(row: KindRow): Array<string> {
|
|
64
|
+
const rss_peak = lane_stats.peak(row.readings.map((reading) => reading.rss_mb))
|
|
65
|
+
|
|
66
|
+
return [
|
|
67
|
+
row.kind,
|
|
68
|
+
cell(mean_cores(row)),
|
|
69
|
+
cell(lane_stats.peak(cores(row.readings))),
|
|
70
|
+
cell(rss_peak === undefined ? undefined : rss_peak / MB_PER_GB),
|
|
71
|
+
String(row.readings.length),
|
|
72
|
+
]
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
function by_mean_cpu(left: KindRow, right: KindRow): number {
|
|
76
|
+
return mean_cores(right) - mean_cores(left)
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
// The period's table: `entries` is the whole ledger, filtered here to `window`. A period whose samples
|
|
80
|
+
// carried no breakdown prints the header alone.
|
|
81
|
+
function table(entries: ReadonlyArray<LedgerEntry>, window: StatsWindow): string {
|
|
82
|
+
const rows = group(lane_stats.in_window(entries, window).filter(is_load)).toSorted(by_mean_cpu)
|
|
83
|
+
|
|
84
|
+
return [
|
|
85
|
+
lane_stats.to_row(COLUMNS),
|
|
86
|
+
lane_stats.to_row(COLUMNS.map(() => '---')),
|
|
87
|
+
...rows.map((row) => lane_stats.to_row(kind_cells(row))),
|
|
88
|
+
].join('\n')
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
const lane_process_stats = { table }
|
|
92
|
+
|
|
93
|
+
export { lane_process_stats }
|
|
@@ -175,9 +175,14 @@ function parse_block(block: string, root: string): LaneInfo | undefined {
|
|
|
175
175
|
const branch = line_value(lines, BRANCH_PREFIX)
|
|
176
176
|
const issue = branch_issue(branch)
|
|
177
177
|
|
|
178
|
-
if (
|
|
179
|
-
|
|
180
|
-
|
|
178
|
+
if (
|
|
179
|
+
directory === undefined ||
|
|
180
|
+
branch === undefined ||
|
|
181
|
+
issue === undefined ||
|
|
182
|
+
!is_lane_directory(root, issue, directory)
|
|
183
|
+
) {
|
|
184
|
+
return undefined
|
|
185
|
+
}
|
|
181
186
|
|
|
182
187
|
return build_lane(issue, branch, directory, is_gone(directory))
|
|
183
188
|
}
|
|
@@ -15,6 +15,7 @@ import { z } from 'zod'
|
|
|
15
15
|
import { lane_broker_server, type BrokerServer } from './lane-broker-server'
|
|
16
16
|
import { lane_child_invocation, type ShipStop } from './lane-child-invocation'
|
|
17
17
|
import { lane_child_marker } from './lane-child-marker'
|
|
18
|
+
import { lane_parked } from './lane-parked'
|
|
18
19
|
import type { LaneInfo } from './lane-registry'
|
|
19
20
|
import { lane_relaunch } from './lane-relaunch'
|
|
20
21
|
|
|
@@ -105,14 +106,22 @@ function provider_of(lane: LaneInfo): AgentProvider {
|
|
|
105
106
|
|
|
106
107
|
// **The reviewer carries the lane's mark explicitly**: the supervisor serving a Claude lane is no lane child,
|
|
107
108
|
// so a reviewer inheriting its environment would descend from the carry record's owner unmarked and be
|
|
108
|
-
// read as the `backlogrun` parent by every hook.
|
|
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).
|
|
109
111
|
|
|
110
|
-
|
|
112
|
+
function review_prompt(lane: LaneInfo, round: ReviewRound): string {
|
|
111
113
|
const target = review_paths(lane.directory)
|
|
112
114
|
const prompt_of =
|
|
113
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)
|
|
114
123
|
const built = agent_argv.resolve_in(
|
|
115
|
-
|
|
124
|
+
review_prompt(lane, round),
|
|
116
125
|
agent_role_profile.REVIEWER,
|
|
117
126
|
lane.directory,
|
|
118
127
|
)
|
|
@@ -143,7 +152,11 @@ function relaunch(lane: LaneInfo, stop: ShipStop): boolean {
|
|
|
143
152
|
return result.kind === 'launched'
|
|
144
153
|
}
|
|
145
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).
|
|
146
157
|
async function perform(lane: LaneInfo, action: BrokerAction): Promise<boolean> {
|
|
158
|
+
if (await lane_parked.is_parked(lane.issue)) return false
|
|
159
|
+
|
|
147
160
|
if (action.kind === 'relaunch') return relaunch(lane, action.stop)
|
|
148
161
|
|
|
149
162
|
return await launch_review(lane, action.round)
|
|
@@ -336,6 +349,13 @@ function is_serving(directory: string, issue: string): boolean {
|
|
|
336
349
|
return target !== undefined && live_response(target) !== undefined
|
|
337
350
|
}
|
|
338
351
|
|
|
352
|
+
// **A request nobody waits for any more is taken back**: a server busy with an earlier request reads the
|
|
353
|
+
// file only once it is free, and a relaunch the ship had already given up on then started a session in a
|
|
354
|
+
// lane the run had parked in the meantime (joshuafolkken/kit#3878). A newer request is left alone.
|
|
355
|
+
function withdraw(pending: string, nonce: string): void {
|
|
356
|
+
if (read_record(pending, request_schema)?.nonce === nonce) stamp_file.remove_stamp(pending)
|
|
357
|
+
}
|
|
358
|
+
|
|
339
359
|
async function request(directory: string, issue: string, action: BrokerAction): Promise<boolean> {
|
|
340
360
|
const target = response_path(directory, issue)
|
|
341
361
|
const response = target === undefined ? undefined : live_response(target)
|
|
@@ -344,10 +364,14 @@ async function request(directory: string, issue: string, action: BrokerAction):
|
|
|
344
364
|
|
|
345
365
|
const nonce = randomUUID()
|
|
346
366
|
const body = { session: response.session, nonce, action }
|
|
367
|
+
const pending = request_path(directory, issue)
|
|
368
|
+
|
|
369
|
+
stamp_file.replace_stamp(pending, body)
|
|
370
|
+
const is_success = await wait_response(target, response.session, nonce)
|
|
347
371
|
|
|
348
|
-
|
|
372
|
+
withdraw(pending, nonce)
|
|
349
373
|
|
|
350
|
-
return
|
|
374
|
+
return is_success
|
|
351
375
|
}
|
|
352
376
|
|
|
353
377
|
const lane_session_broker = {
|