@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
|
@@ -0,0 +1,206 @@
|
|
|
1
|
+
import { stripVTControlCharacters } from 'node:util'
|
|
2
|
+
import { agent_role_profile } from '#scripts/agent/agent-role-profile'
|
|
3
|
+
import { changed_paths } from '#scripts/git/changed-paths'
|
|
4
|
+
import { stamp_file } from '#scripts/josh/stamp-file'
|
|
5
|
+
import { z } from 'zod'
|
|
6
|
+
import { run_ship_next } from './run-ship-next'
|
|
7
|
+
import { run_ship_probe } from './run-ship-probe'
|
|
8
|
+
|
|
9
|
+
// What a relaunched lane child needs in front of it to fix a stopped ship: the tail of the failed
|
|
10
|
+
// stage's output — a red check, or the High/Medium findings a review stage reports — and the branch's
|
|
11
|
+
// changed files. Without them the resumed session opened `josh ship --log` and `git diff` itself before
|
|
12
|
+
// doing anything (joshuafolkken/kit#3923).
|
|
13
|
+
//
|
|
14
|
+
// **Both are bounded**, so a long gate log never becomes the whole prompt. The tail is kept because a
|
|
15
|
+
// check reports its verdict last; the lines left out go to a file whose path and line range the prompt
|
|
16
|
+
// names, so nothing is lost and the session reads only the range it needs.
|
|
17
|
+
|
|
18
|
+
const EXCERPT_LINE_LIMIT = 40
|
|
19
|
+
const EXCERPT_LINE_WIDTH = 240
|
|
20
|
+
const CHANGED_PATH_LIMIT = 30
|
|
21
|
+
const OUTPUT_PREFIX = 'josh-ship-stop-'
|
|
22
|
+
const OUTPUT_SUFFIX = '.log'
|
|
23
|
+
const KEY_SEPARATOR = '-'
|
|
24
|
+
const LINE_BREAK = '\n'
|
|
25
|
+
const CARRIAGE_RETURN = '\r'
|
|
26
|
+
|
|
27
|
+
const omitted_schema = z
|
|
28
|
+
.object({ path: z.string().optional(), last_line: z.number().int() })
|
|
29
|
+
.strict()
|
|
30
|
+
const stop_context_schema = z
|
|
31
|
+
.object({
|
|
32
|
+
excerpt: z.array(z.string()).readonly(),
|
|
33
|
+
omitted: omitted_schema.optional(),
|
|
34
|
+
changed: z.array(z.string()).readonly().optional(),
|
|
35
|
+
more_changed: z.number().int(),
|
|
36
|
+
})
|
|
37
|
+
.strict()
|
|
38
|
+
|
|
39
|
+
type StopContext = z.infer<typeof stop_context_schema>
|
|
40
|
+
type Omitted = z.infer<typeof omitted_schema>
|
|
41
|
+
|
|
42
|
+
// A spinner redraws its line with a carriage return, so only the frame after the last one is what the
|
|
43
|
+
// terminal showed; a CRLF line ends in one and keeps its whole text.
|
|
44
|
+
function shown_frame(line: string): string {
|
|
45
|
+
return line.trimEnd().split(CARRIAGE_RETURN).at(-1) ?? ''
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function output_lines(output: string): Array<string> {
|
|
49
|
+
return stripVTControlCharacters(output)
|
|
50
|
+
.trimEnd()
|
|
51
|
+
.split(LINE_BREAK)
|
|
52
|
+
.map((line) => agent_role_profile.printable(shown_frame(line)))
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
// The full output is kept beside the ship log, keyed the same way, so the prompt's line range points at
|
|
56
|
+
// text that reads exactly as the excerpt was cut from.
|
|
57
|
+
async function keep_full_output(issue: string, output: string): Promise<string | undefined> {
|
|
58
|
+
const repository = await run_ship_probe.repository_directory()
|
|
59
|
+
|
|
60
|
+
if (repository === undefined) return undefined
|
|
61
|
+
|
|
62
|
+
const target = stamp_file.stamp_path(
|
|
63
|
+
`${OUTPUT_PREFIX}${issue}${KEY_SEPARATOR}`,
|
|
64
|
+
repository,
|
|
65
|
+
OUTPUT_SUFFIX,
|
|
66
|
+
)
|
|
67
|
+
|
|
68
|
+
try {
|
|
69
|
+
return stamp_file.replace_text_stamp(target, output)
|
|
70
|
+
} catch {
|
|
71
|
+
return undefined
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
// A full output that could not be kept still reports the cut, with no path: the prompt then points at
|
|
76
|
+
// the stopped report, which holds the same lines.
|
|
77
|
+
async function omitted_range(issue: string, lines: ReadonlyArray<string>): Promise<Omitted> {
|
|
78
|
+
const path = await keep_full_output(issue, lines.join(LINE_BREAK))
|
|
79
|
+
const last_line = lines.length - EXCERPT_LINE_LIMIT
|
|
80
|
+
|
|
81
|
+
return path === undefined ? { last_line } : { path, last_line }
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
// A line is cut to a width, so one minified or wrapped line cannot spend the prompt's whole budget.
|
|
85
|
+
function clipped(lines: ReadonlyArray<string>): Array<string> {
|
|
86
|
+
return lines.map((line) => line.slice(0, EXCERPT_LINE_WIDTH))
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
async function excerpt_of(
|
|
90
|
+
issue: string,
|
|
91
|
+
output: string,
|
|
92
|
+
): Promise<Pick<StopContext, 'excerpt' | 'omitted'>> {
|
|
93
|
+
const lines = output.trim() === '' ? [] : output_lines(output)
|
|
94
|
+
|
|
95
|
+
if (lines.length <= EXCERPT_LINE_LIMIT) return { excerpt: clipped(lines) }
|
|
96
|
+
|
|
97
|
+
return {
|
|
98
|
+
excerpt: clipped(lines.slice(-EXCERPT_LINE_LIMIT)),
|
|
99
|
+
omitted: await omitted_range(issue, lines),
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
// A git that cannot answer leaves the list out rather than costing the relaunch — never empty, which
|
|
104
|
+
// would read as a branch with no changes.
|
|
105
|
+
async function read_paths(): Promise<Array<string> | undefined> {
|
|
106
|
+
try {
|
|
107
|
+
return await changed_paths.read_changed_paths(false)
|
|
108
|
+
} catch {
|
|
109
|
+
return undefined
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
async function changed_of(): Promise<Pick<StopContext, 'changed' | 'more_changed'>> {
|
|
114
|
+
const paths = await read_paths()
|
|
115
|
+
|
|
116
|
+
if (paths === undefined) return { more_changed: 0 }
|
|
117
|
+
|
|
118
|
+
return {
|
|
119
|
+
changed: paths.slice(0, CHANGED_PATH_LIMIT),
|
|
120
|
+
more_changed: Math.max(0, paths.length - CHANGED_PATH_LIMIT),
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
async function build(issue: string, output: string): Promise<StopContext> {
|
|
125
|
+
return { ...(await excerpt_of(issue, output)), ...(await changed_of()) }
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
function has_excerpt(context: StopContext | undefined): boolean {
|
|
129
|
+
return context !== undefined && context.excerpt.length > 0
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
function excerpt_heading(issue: string, omitted: Omitted | undefined, shown: number): string {
|
|
133
|
+
if (omitted === undefined) return 'The failed stage output:'
|
|
134
|
+
|
|
135
|
+
const source = omitted.path ?? `the stopped report (\`${run_ship_next.COMMAND.LOG} ${issue}\`)`
|
|
136
|
+
const range = `lines 1-${String(omitted.last_line)} are in ${source}`
|
|
137
|
+
|
|
138
|
+
return `The failed stage output (its last ${String(shown)} lines; ${range}):`
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
// The head lines a budget leaves out join the range the heading names; the file, when one was kept,
|
|
142
|
+
// still holds every line.
|
|
143
|
+
function with_dropped(omitted: Omitted | undefined, dropped: number): Omitted | undefined {
|
|
144
|
+
if (dropped === 0) return omitted
|
|
145
|
+
|
|
146
|
+
return { ...omitted, last_line: (omitted?.last_line ?? 0) + dropped }
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
function excerpt_block(
|
|
150
|
+
issue: string,
|
|
151
|
+
context: StopContext,
|
|
152
|
+
changed: string,
|
|
153
|
+
dropped: number,
|
|
154
|
+
): string {
|
|
155
|
+
const shown = context.excerpt.slice(dropped)
|
|
156
|
+
const heading = excerpt_heading(issue, with_dropped(context.omitted, dropped), shown.length)
|
|
157
|
+
|
|
158
|
+
return [heading, ...shown, changed].join(LINE_BREAK)
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
function changed_text(context: StopContext): string {
|
|
162
|
+
if (context.changed === undefined) return 'The changed files could not be read from git.'
|
|
163
|
+
|
|
164
|
+
if (context.changed.length === 0) return 'No files changed against the default branch.'
|
|
165
|
+
|
|
166
|
+
const more = context.more_changed > 0 ? ` (and ${String(context.more_changed)} more)` : ''
|
|
167
|
+
|
|
168
|
+
return `Changed files on the branch: ${context.changed.join(', ')}${more}.`
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
// The block the ship-stop prompt carries between the next command and the closing invocation.
|
|
172
|
+
//
|
|
173
|
+
// **It fits the `budget` the prompt leaves it** — the prompt is one argv element, refused past
|
|
174
|
+
// `MAX_VALUE_LENGTH` (joshuafolkken/kit#3964) — by dropping the excerpt's head first, since a check
|
|
175
|
+
// reports its verdict last; the tail line always stays.
|
|
176
|
+
function describe(
|
|
177
|
+
issue: string,
|
|
178
|
+
context: StopContext,
|
|
179
|
+
budget: number = agent_role_profile.MAX_VALUE_LENGTH,
|
|
180
|
+
): string {
|
|
181
|
+
const changed = changed_text(context)
|
|
182
|
+
|
|
183
|
+
if (!has_excerpt(context)) return changed
|
|
184
|
+
|
|
185
|
+
let dropped = 0
|
|
186
|
+
let block = excerpt_block(issue, context, changed, dropped)
|
|
187
|
+
|
|
188
|
+
while (block.length > budget && dropped < context.excerpt.length - 1) {
|
|
189
|
+
dropped += 1
|
|
190
|
+
block = excerpt_block(issue, context, changed, dropped)
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
return block
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
const run_ship_stop_context = {
|
|
197
|
+
CHANGED_PATH_LIMIT,
|
|
198
|
+
EXCERPT_LINE_LIMIT,
|
|
199
|
+
build,
|
|
200
|
+
describe,
|
|
201
|
+
has_excerpt,
|
|
202
|
+
stop_context_schema,
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
export type { StopContext }
|
|
206
|
+
export { run_ship_stop_context }
|
|
@@ -4,6 +4,7 @@ import { git_command } from '#scripts/git/git-command'
|
|
|
4
4
|
import { main_merge, type MergeOutcome } from '#scripts/git/main-merge'
|
|
5
5
|
import { josh_command, type JoshResult } from '#scripts/josh/josh-run'
|
|
6
6
|
import { error_text } from '#scripts/lib/error-message'
|
|
7
|
+
import { run_ship_overlap } from './run-ship-overlap'
|
|
7
8
|
import { run_ship_scoped } from './run-ship-scoped'
|
|
8
9
|
|
|
9
10
|
// `josh ship` keeps the branch current with the default branch itself: a default branch that moves
|
|
@@ -11,9 +12,9 @@ import { run_ship_scoped } from './run-ship-scoped'
|
|
|
11
12
|
// merging the default branch again usually resolves it.
|
|
12
13
|
//
|
|
13
14
|
// So the merge happens mechanically: once before the commit (`sync_stage`), and once when
|
|
14
|
-
// the followup fails on a pull request GitHub reports `DIRTY` (`followup_stage`).
|
|
15
|
-
//
|
|
16
|
-
// resumed session goes straight to them.
|
|
15
|
+
// the followup fails on a pull request GitHub reports `DIRTY` (`followup_stage`). A conflict only on
|
|
16
|
+
// generated files is regenerated by the merge itself (`main-merge-regenerate.ts`); any other conflict
|
|
17
|
+
// stops the ship as `conflict`, naming the unmerged paths so the resumed session goes straight to them.
|
|
17
18
|
|
|
18
19
|
const should_forward_stderr = true
|
|
19
20
|
const SUCCESS_EXIT_CODE = 0
|
|
@@ -66,12 +67,19 @@ function sync_note(outcome: Exclude<MergeOutcome, { kind: 'conflict' }>): string
|
|
|
66
67
|
// touches, and once that work is committed the followup merges again on a clean tree. A merge that changed
|
|
67
68
|
// the tree is checked by the gate stage that follows, so the commit never carries
|
|
68
69
|
// an unchecked merge and the gate's record names the merge base the push carries.
|
|
69
|
-
|
|
70
|
+
//
|
|
71
|
+
// The merge waits first for any lane whose open pull request changes the same paths
|
|
72
|
+
// (`run-ship-overlap.ts`), so it brings that pull request in before the gate rather than meeting it
|
|
73
|
+
// as `DIRTY` after CI.
|
|
74
|
+
async function sync_stage(issue: string): Promise<StepResult> {
|
|
75
|
+
const waited = run_ship_overlap.note(await run_ship_overlap.await_overlap(issue))
|
|
70
76
|
const outcome = await merge_outcome()
|
|
71
77
|
|
|
72
|
-
if (outcome.kind === 'conflict') return conflict_result(outcome.files,
|
|
78
|
+
if (outcome.kind === 'conflict') return conflict_result(outcome.files, waited)
|
|
79
|
+
|
|
80
|
+
const out = [waited, sync_note(outcome)].filter((line) => line !== '').join('\n')
|
|
73
81
|
|
|
74
|
-
return { code: SUCCESS_EXIT_CODE, out
|
|
82
|
+
return { code: SUCCESS_EXIT_CODE, out }
|
|
75
83
|
}
|
|
76
84
|
|
|
77
85
|
// Any read that fails answers "not conflicting": the followup failure then stands as it was.
|
|
@@ -2,9 +2,13 @@
|
|
|
2
2
|
import { existsSync, globSync } from 'node:fs'
|
|
3
3
|
import path from 'node:path'
|
|
4
4
|
import { fileURLToPath } from 'node:url'
|
|
5
|
+
import { environment_flags } from '@joshuafolkken/kit/env'
|
|
6
|
+
import { core_budget } from '#scripts/gate/core-budget'
|
|
5
7
|
import { execa } from 'execa'
|
|
6
8
|
|
|
7
9
|
const PNPM = 'pnpm'
|
|
10
|
+
// `--workers`, `--workers=<n>` and Playwright's `-j` short form.
|
|
11
|
+
const WORKER_FLAGS = /^(?:--workers(?:=|$)|-j)/u
|
|
8
12
|
const FAIL_EXIT_CODE = 1
|
|
9
13
|
const ARGV_START = 2
|
|
10
14
|
const PLAYWRIGHT_PACKAGE = path.join('node_modules', '@playwright', 'test')
|
|
@@ -45,15 +49,36 @@ function has_e2e_tests(project_directory: string): boolean {
|
|
|
45
49
|
return matches.length > 0
|
|
46
50
|
}
|
|
47
51
|
|
|
52
|
+
// **A local run's worker count is the weight it reserved** (joshuafolkken/kit#3944). Playwright's own
|
|
53
|
+
// default is half the cores, which no ledger entry stood for; CI keeps its config's count, and a
|
|
54
|
+
// count the caller names wins.
|
|
55
|
+
function playwright_arguments(extra_arguments: ReadonlyArray<string>): ReadonlyArray<string> {
|
|
56
|
+
const is_count_named = extra_arguments.some((argument) => WORKER_FLAGS.test(argument))
|
|
57
|
+
|
|
58
|
+
if (is_count_named || environment_flags.is_ci_enabled(process.env['CI'])) return extra_arguments
|
|
59
|
+
|
|
60
|
+
return [`--workers=${String(core_budget.CORE_WEIGHTS.e2e)}`, ...extra_arguments]
|
|
61
|
+
}
|
|
62
|
+
|
|
48
63
|
async function run_playwright(extra_arguments: ReadonlyArray<string>): Promise<number> {
|
|
49
|
-
const result = await execa(
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
64
|
+
const result = await execa(
|
|
65
|
+
PNPM,
|
|
66
|
+
['exec', 'playwright', 'test', ...playwright_arguments(extra_arguments)],
|
|
67
|
+
{ stdio: 'inherit', reject: false },
|
|
68
|
+
)
|
|
53
69
|
|
|
54
70
|
return result.exitCode ?? FAIL_EXIT_CODE
|
|
55
71
|
}
|
|
56
72
|
|
|
73
|
+
// Only a run reserves: a skip starts nothing, and a claim there could wait out a full ledger for it.
|
|
74
|
+
async function run_reserved_playwright(extra_arguments: ReadonlyArray<string>): Promise<number> {
|
|
75
|
+
return await core_budget.with_command_reservation(
|
|
76
|
+
core_budget.CORE_WEIGHTS.e2e,
|
|
77
|
+
async () => await run_playwright(extra_arguments),
|
|
78
|
+
{ command: 'test:e2e' },
|
|
79
|
+
)
|
|
80
|
+
}
|
|
81
|
+
|
|
57
82
|
async function run_guarded_e2e(
|
|
58
83
|
project_directory: string,
|
|
59
84
|
extra_arguments: ReadonlyArray<string>,
|
|
@@ -61,7 +86,7 @@ async function run_guarded_e2e(
|
|
|
61
86
|
const is_installed = is_playwright_installed(project_directory)
|
|
62
87
|
const has_tests = has_e2e_tests(project_directory)
|
|
63
88
|
const action = resolve_guard_action(is_installed, has_tests)
|
|
64
|
-
if (action === 'run') return await
|
|
89
|
+
if (action === 'run') return await run_reserved_playwright(extra_arguments)
|
|
65
90
|
|
|
66
91
|
console.info(`josh test:e2e: ${SKIP_REASONS[action]} — skipping Playwright e2e.`)
|
|
67
92
|
|
|
@@ -1,8 +1,10 @@
|
|
|
1
1
|
#!/usr/bin/env tsx
|
|
2
2
|
import { existsSync, globSync } from 'node:fs'
|
|
3
|
+
import { availableParallelism } from 'node:os'
|
|
3
4
|
import path from 'node:path'
|
|
4
5
|
import { fileURLToPath } from 'node:url'
|
|
5
6
|
import { PROJECT_ENVIRONMENT_KEYS } from '#ports'
|
|
7
|
+
import { free_cores } from '#scripts/gate/free-cores'
|
|
6
8
|
import { project_checks } from '#scripts/gate/project-checks'
|
|
7
9
|
import { execa } from 'execa'
|
|
8
10
|
import { project_test_script, type ProjectScriptState } from './project-test-script'
|
|
@@ -175,11 +177,24 @@ function isolated_unit_environment(
|
|
|
175
177
|
// Reading the share first and claiming afterwards leaves a window in which two hooks firing together
|
|
176
178
|
// both count "alone" and both run uncapped. Inside the marker the number is identical — this run's own
|
|
177
179
|
// record is now on disk and `is_nested_run` therefore contributes nothing — and the window is gone.
|
|
180
|
+
//
|
|
181
|
+
// **The machine is read only when no count was typed.** The reading waits out a sample window, and a
|
|
182
|
+
// caller's `--maxWorkers` — the gate's cap among them, already bounded by the same reading — wins
|
|
183
|
+
// whatever it says.
|
|
184
|
+
async function vitest_share(vitest_arguments: ReadonlyArray<string>): Promise<number | undefined> {
|
|
185
|
+
if (unit_worker_share.has_worker_flag(vitest_arguments)) return undefined
|
|
186
|
+
|
|
187
|
+
const available_cores = availableParallelism()
|
|
188
|
+
const free = await free_cores.read({ available_cores })
|
|
189
|
+
|
|
190
|
+
return unit_worker_share.current_share(available_cores, unit_worker_share.live_run_count(), free)
|
|
191
|
+
}
|
|
192
|
+
|
|
178
193
|
async function run_vitest(vitest_arguments: ReadonlyArray<string>): Promise<number> {
|
|
179
194
|
return await unit_worker_share.with_run_marker(async () => {
|
|
180
195
|
const sized = unit_worker_share.worker_arguments(
|
|
181
196
|
vitest_arguments,
|
|
182
|
-
|
|
197
|
+
await vitest_share(vitest_arguments),
|
|
183
198
|
)
|
|
184
199
|
const result = await execa(PNPM, ['exec', 'vitest', ...vitest_arguments, ...sized], {
|
|
185
200
|
env: isolated_unit_environment(),
|
|
@@ -153,6 +153,22 @@ function resolve_unit_workers(available_cores: number, runs: number): number | u
|
|
|
153
153
|
return Math.max(MIN_WORKERS, Math.floor(available_cores / runs))
|
|
154
154
|
}
|
|
155
155
|
|
|
156
|
+
// **The run count divides the machine; the free cores bound what that division hands out.** The
|
|
157
|
+
// share counts only unit runs, so load from outside the core budget's ledger — Claude sessions, other
|
|
158
|
+
// tools — went unseen: 22 of 38 ledger samples at a load average of 20 or more had vitest as their
|
|
159
|
+
// heaviest process, one at 381% over 30 processes. `free_cores` is `free-cores.ts`'s reading, the
|
|
160
|
+
// cores less that outside load. `undefined` — a CI runner, a CPU quota — and a reading at or above
|
|
161
|
+
// the ceiling both leave `workers` as it was, so an unread or quiet machine changes nothing.
|
|
162
|
+
function cap_to_free(
|
|
163
|
+
workers: number | undefined,
|
|
164
|
+
free_cores: number | undefined,
|
|
165
|
+
available_cores: number,
|
|
166
|
+
): number | undefined {
|
|
167
|
+
if (free_cores === undefined || free_cores >= (workers ?? available_cores)) return workers
|
|
168
|
+
|
|
169
|
+
return Math.max(MIN_WORKERS, Math.floor(free_cores))
|
|
170
|
+
}
|
|
171
|
+
|
|
156
172
|
// The share this run may take, read from the machine. Separate from `resolve_unit_workers` so that
|
|
157
173
|
// one stays a pure function of two numbers and every test of it can pass them.
|
|
158
174
|
//
|
|
@@ -163,10 +179,12 @@ function resolve_unit_workers(available_cores: number, runs: number): number | u
|
|
|
163
179
|
function current_share(
|
|
164
180
|
available_cores: number = availableParallelism(),
|
|
165
181
|
live_runs: number = live_run_count(),
|
|
182
|
+
free_cores?: number,
|
|
166
183
|
): number | undefined {
|
|
167
184
|
const own_run = is_nested_run() ? 0 : SOLO_RUNS
|
|
185
|
+
const share = resolve_unit_workers(available_cores, live_runs + own_run)
|
|
168
186
|
|
|
169
|
-
return
|
|
187
|
+
return cap_to_free(share, free_cores, available_cores)
|
|
170
188
|
}
|
|
171
189
|
|
|
172
190
|
function is_worker_flag(argument: string): boolean {
|
|
@@ -182,13 +200,15 @@ function is_worker_flag(argument: string): boolean {
|
|
|
182
200
|
// would make this function's own answer depend on what happened to be running while it was asked, and
|
|
183
201
|
// `f(args, undefined)` takes the default rather than the value — so the case that matters most, "this
|
|
184
202
|
// run is alone", would be the one case no test could express.
|
|
203
|
+
function has_worker_flag(vitest_arguments: ReadonlyArray<string>): boolean {
|
|
204
|
+
return vitest_arguments.some((argument) => is_worker_flag(argument))
|
|
205
|
+
}
|
|
206
|
+
|
|
185
207
|
function worker_arguments(
|
|
186
208
|
vitest_arguments: ReadonlyArray<string>,
|
|
187
209
|
share: number | undefined,
|
|
188
210
|
): Array<string> {
|
|
189
|
-
if (share === undefined || vitest_arguments
|
|
190
|
-
return []
|
|
191
|
-
}
|
|
211
|
+
if (share === undefined || has_worker_flag(vitest_arguments)) return []
|
|
192
212
|
|
|
193
213
|
return [`${WORKER_FLAG}=${String(share)}`]
|
|
194
214
|
}
|
|
@@ -230,7 +250,9 @@ const unit_worker_share = {
|
|
|
230
250
|
NESTED_VALUE,
|
|
231
251
|
RUN_PREFIX,
|
|
232
252
|
SOLO_RUNS,
|
|
253
|
+
cap_to_free,
|
|
233
254
|
current_share,
|
|
255
|
+
has_worker_flag,
|
|
234
256
|
is_nested_run,
|
|
235
257
|
live_run_count,
|
|
236
258
|
marker_path,
|
|
@@ -32,8 +32,9 @@ const SEGMENT_PATTERN = /&&|\|\||;|\|/u
|
|
|
32
32
|
// last, while `||` keeps the first command's and runs the second only when it failed. `|&` is a pipe
|
|
33
33
|
// and is cut here, its `&` left in the fragment that follows.
|
|
34
34
|
const PIPE_PATTERN = /(?<!\|)\|(?!\|)/u
|
|
35
|
-
// What `SEGMENT_PATTERN` cuts on minus the pipe, for reading *inside* one pipe segment
|
|
36
|
-
|
|
35
|
+
// What `SEGMENT_PATTERN` cuts on minus the pipe, for reading *inside* one pipe segment — plus the
|
|
36
|
+
// newline, which ends a command as `;` does, so a line typed before the piped check is not read as it.
|
|
37
|
+
const CHAIN_PATTERN = /&&|\|\||;|\n/u
|
|
37
38
|
// A quoted span is text, not shell syntax, and it is removed before the pipe reading below. This
|
|
38
39
|
// repository's issue bodies, comment bodies and commit messages quote command chains constantly, so
|
|
39
40
|
// `--body "cd x && pnpm josh gate | tail"` is a `gh` call and nothing else — a reader that split its
|
|
@@ -299,8 +300,8 @@ function drop_redirections(words: ReadonlyArray<string>): Array<string> {
|
|
|
299
300
|
// **The match's own text is what is sliced off, not a fixed word count.** The prefix is two words in
|
|
300
301
|
// `josh lint`, three in `pnpm josh lint` and four in `pnpm exec josh lint`, so counting words would
|
|
301
302
|
// leave `josh` itself in the argument list for one of the three spellings.
|
|
302
|
-
function
|
|
303
|
-
const text = command_words(
|
|
303
|
+
function josh_arguments_of_segment(segment: string): Array<string> {
|
|
304
|
+
const text = command_words(segment).join(' ')
|
|
304
305
|
const match = JOSH_PATTERN.exec(text)
|
|
305
306
|
|
|
306
307
|
if (match === null) return []
|
|
@@ -313,6 +314,29 @@ function josh_arguments(command: string): Array<string> {
|
|
|
313
314
|
return drop_redirections(words)
|
|
314
315
|
}
|
|
315
316
|
|
|
317
|
+
function josh_arguments(command: string): Array<string> {
|
|
318
|
+
return josh_arguments_of_segment(command_segment(command))
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
// The josh subcommand one segment runs plus every argument it passes, redirections dropped, or `''`
|
|
322
|
+
// for a segment that runs none. **Flags are kept**: which of them ask nothing new (`run:board --chat`)
|
|
323
|
+
// and which turn a read into a write (`run:carry --cut`) is the reader's rule, not this one's.
|
|
324
|
+
function josh_signature_of_segment(segment: string): string {
|
|
325
|
+
const name = josh_command_of_segment(segment)
|
|
326
|
+
|
|
327
|
+
if (name === '') return ''
|
|
328
|
+
|
|
329
|
+
return [name, ...josh_arguments_of_segment(segment)].join(' ')
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
// Every segment's signature, read over the same unquoted chain `josh_commands_of` walks.
|
|
333
|
+
function josh_signatures_of(command: string): Array<string> {
|
|
334
|
+
return unquoted(command)
|
|
335
|
+
.split(SEGMENT_PATTERN)
|
|
336
|
+
.map((segment) => josh_signature_of_segment(segment))
|
|
337
|
+
.filter((signature) => signature !== '')
|
|
338
|
+
}
|
|
339
|
+
|
|
316
340
|
const time_shell = {
|
|
317
341
|
JOSH_PREFIX,
|
|
318
342
|
bash_command,
|
|
@@ -323,6 +347,7 @@ const time_shell = {
|
|
|
323
347
|
josh_arguments,
|
|
324
348
|
josh_command_of,
|
|
325
349
|
josh_commands_of,
|
|
350
|
+
josh_signatures_of,
|
|
326
351
|
leading_word,
|
|
327
352
|
unquoted,
|
|
328
353
|
}
|
|
@@ -334,6 +334,7 @@ function to_spans(events: ReadonlyArray<TimelineEvent>, finished: FinishedAt): A
|
|
|
334
334
|
label: event.label,
|
|
335
335
|
josh_command: event.josh_command,
|
|
336
336
|
josh_commands: event.josh_commands,
|
|
337
|
+
josh_signatures: event.josh_signatures,
|
|
337
338
|
check_key: event.check_key,
|
|
338
339
|
marker: event.marker,
|
|
339
340
|
is_bundleable: event.is_bundleable,
|
|
@@ -55,6 +55,9 @@ interface ToolCall {
|
|
|
55
55
|
// A chained `pnpm josh lint:related && pnpm josh test:related` carries both; the count tables expand
|
|
56
56
|
// it so the ones `josh_command` drops are still counted.
|
|
57
57
|
josh_commands: ReadonlyArray<string>
|
|
58
|
+
// Each of those with the operands it named, flags dropped — what tells `lane:output 1` from
|
|
59
|
+
// `lane:output 2` once the span has kept no command string. `run-poll-hook.ts` reads it.
|
|
60
|
+
josh_signatures: ReadonlyArray<string>
|
|
58
61
|
check_key: string
|
|
59
62
|
marker: PhaseMarker
|
|
60
63
|
is_bundleable: boolean
|
|
@@ -83,6 +86,7 @@ const NO_CALL: ToolCall = {
|
|
|
83
86
|
label: '',
|
|
84
87
|
josh_command: '',
|
|
85
88
|
josh_commands: [],
|
|
89
|
+
josh_signatures: [],
|
|
86
90
|
check_key: time_single_check.NO_CHECK,
|
|
87
91
|
marker: time_markers.NO_MARKER,
|
|
88
92
|
message_id: NO_MESSAGE_ID,
|
|
@@ -95,6 +99,7 @@ const UNKNOWN_CALL: ToolCall = {
|
|
|
95
99
|
label: UNKNOWN_TOOL,
|
|
96
100
|
josh_command: '',
|
|
97
101
|
josh_commands: [],
|
|
102
|
+
josh_signatures: [],
|
|
98
103
|
check_key: time_single_check.NO_CHECK,
|
|
99
104
|
marker: time_markers.NO_MARKER,
|
|
100
105
|
message_id: NO_MESSAGE_ID,
|
|
@@ -111,6 +116,7 @@ function non_bash_call(name: string, input: unknown, message_id: string): ToolCa
|
|
|
111
116
|
label: name,
|
|
112
117
|
josh_command: '',
|
|
113
118
|
josh_commands: [],
|
|
119
|
+
josh_signatures: [],
|
|
114
120
|
check_key: time_single_check.NO_CHECK,
|
|
115
121
|
marker: time_markers.tool_marker(name, input),
|
|
116
122
|
message_id,
|
|
@@ -132,6 +138,7 @@ function bash_call(input: unknown, message_id: string): ToolCall {
|
|
|
132
138
|
label: time_shell.bash_label(command),
|
|
133
139
|
josh_command,
|
|
134
140
|
josh_commands: time_shell.josh_commands_of(command),
|
|
141
|
+
josh_signatures: time_shell.josh_signatures_of(command),
|
|
135
142
|
check_key: time_single_check.check_key(josh_command, command),
|
|
136
143
|
marker: time_markers.bash_marker(command),
|
|
137
144
|
message_id,
|
package/templates/gitignore
CHANGED
|
@@ -43,13 +43,17 @@ project.inlang/cache/
|
|
|
43
43
|
*storybook.log
|
|
44
44
|
storybook-static
|
|
45
45
|
|
|
46
|
-
# Verification gate caches (josh lint / josh check / josh cspell:dot), plus the edit hook's
|
|
47
|
-
#
|
|
46
|
+
# Verification gate caches (josh lint / josh check / josh cspell:dot), plus the edit hook's, the
|
|
47
|
+
# scoped lint's and the pre-commit hook's own, which are kept apart from the gate's on purpose
|
|
48
48
|
.eslintcache
|
|
49
49
|
.eslintcache.edit
|
|
50
50
|
.eslintcache.related
|
|
51
|
+
.eslintcache.commit
|
|
51
52
|
.tsbuildinfo
|
|
52
53
|
.cspellcache
|
|
54
|
+
.cspellcache.commit
|
|
55
|
+
.prettiercache
|
|
56
|
+
.prettiercache.related
|
|
53
57
|
|
|
54
58
|
# Bundle analysis
|
|
55
59
|
stats-client.html
|
|
@@ -1,68 +0,0 @@
|
|
|
1
|
-
import {
|
|
2
|
-
investigation_reads
|
|
3
|
-
} from "./chunk-H5HVWYXK.js";
|
|
4
|
-
import {
|
|
5
|
-
lane_guard_policy
|
|
6
|
-
} from "./chunk-SHGPANFB.js";
|
|
7
|
-
import {
|
|
8
|
-
hook_decision
|
|
9
|
-
} from "./chunk-J47HU5H5.js";
|
|
10
|
-
|
|
11
|
-
// scripts/delegation/investigation-guard.ts
|
|
12
|
-
import { text } from "node:stream/consumers";
|
|
13
|
-
import { fileURLToPath } from "node:url";
|
|
14
|
-
var SWITCH_ENV_KEY = "JOSH_INVESTIGATION_GUARD";
|
|
15
|
-
var STAMP_PREFIX = "josh-investigation-guard-";
|
|
16
|
-
var NOTICE_STAMP_PREFIX = "josh-investigation-guard-notice-";
|
|
17
|
-
function investigation_mode() {
|
|
18
|
-
return lane_guard_policy.mode_here("investigation");
|
|
19
|
-
}
|
|
20
|
-
function should_block(tail, call, refused_at_ms, run) {
|
|
21
|
-
if (investigation_mode() !== "refuse") return false;
|
|
22
|
-
return investigation_reads.should_block(tail, call, refused_at_ms, run);
|
|
23
|
-
}
|
|
24
|
-
function should_notify(tail, call, notified_at_ms, run) {
|
|
25
|
-
if (investigation_mode() !== "notice") return false;
|
|
26
|
-
return investigation_reads.should_block(tail, call, notified_at_ms, run);
|
|
27
|
-
}
|
|
28
|
-
var NOTICE = `\u{1F4A1} investigation: files read and not edited, or search turns, since the last delegated unit reached the threshold; the read proceeds. Run \`pnpm josh delegate investigation\`, then dispatch the \`${investigation_reads.INVESTIGATOR_AGENT}\` agent (\`kit:${investigation_reads.INVESTIGATOR_AGENT}\` in a consumer); it returns \`file:line\` citations, never the file text. Keep main-line reads to files you will edit.`;
|
|
29
|
-
function notice_text(_call, tail) {
|
|
30
|
-
const pending = investigation_reads.tally_of(tail).pending.map((absolute) => investigation_reads.repository_relative(absolute));
|
|
31
|
-
return pending.length === 0 ? NOTICE : `${NOTICE} Read and not edited so far: ${pending.join(" ")}.`;
|
|
32
|
-
}
|
|
33
|
-
var GUARD = hook_decision.create_transcript_guard({
|
|
34
|
-
prefix: STAMP_PREFIX,
|
|
35
|
-
switch_key: SWITCH_ENV_KEY,
|
|
36
|
-
is_candidate: investigation_reads.is_refusable_call,
|
|
37
|
-
should_block,
|
|
38
|
-
reason: investigation_reads.REASON,
|
|
39
|
-
// The lane-child notice fires where the refusal is withheld, on a record of its own so it
|
|
40
|
-
// never spends the refusal's stamp.
|
|
41
|
-
notify: {
|
|
42
|
-
prefix: NOTICE_STAMP_PREFIX,
|
|
43
|
-
should_notify,
|
|
44
|
-
text: notice_text
|
|
45
|
-
}
|
|
46
|
-
});
|
|
47
|
-
var investigation_refusal = GUARD.refusal;
|
|
48
|
-
var investigation_outcome = GUARD.outcome;
|
|
49
|
-
var { is_enabled, refusal_path } = GUARD;
|
|
50
|
-
var investigation_notice_path = hook_decision.create_refusal_stamp(NOTICE_STAMP_PREFIX).path;
|
|
51
|
-
var { deny_envelope, load_environment_file, DISABLED_VALUES } = hook_decision;
|
|
52
|
-
if (process.argv[1] === fileURLToPath(import.meta.url)) {
|
|
53
|
-
if (process.stdin.isTTY) hook_decision.report_no_payload("investigation:guard");
|
|
54
|
-
else hook_decision.write_outcome(await text(process.stdin), investigation_outcome);
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
export {
|
|
58
|
-
SWITCH_ENV_KEY,
|
|
59
|
-
NOTICE,
|
|
60
|
-
investigation_refusal,
|
|
61
|
-
investigation_outcome,
|
|
62
|
-
is_enabled,
|
|
63
|
-
refusal_path,
|
|
64
|
-
investigation_notice_path,
|
|
65
|
-
deny_envelope,
|
|
66
|
-
load_environment_file,
|
|
67
|
-
DISABLED_VALUES
|
|
68
|
-
};
|