@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
package/prompts/review.md
CHANGED
|
@@ -48,8 +48,7 @@ review, its severity tests and the round cap are unchanged.
|
|
|
48
48
|
Run `pnpm josh review:brief --level-only` and use what it prints — `low` or `medium`, from the changed
|
|
49
49
|
paths only, never "this one is small". `--staged` classifies the staged diff, `--json` adds the reason;
|
|
50
50
|
inside a workflow `pnpm josh review:brief` prints the level on its first line. `low` gets 1 round,
|
|
51
|
-
`medium` up to 2.
|
|
52
|
-
`docs/maintainers/review-history.md` → "The level is decided from the changed paths".
|
|
51
|
+
`medium` up to 2. A diff that touches documentation is not inert: it stays at `medium`.
|
|
53
52
|
|
|
54
53
|
```bash
|
|
55
54
|
pnpm josh review:brief --level-only # the branch diff
|
|
@@ -94,8 +93,7 @@ deferred. Without the flag, and on any uncertainty (a missing round-1 snapshot i
|
|
|
94
93
|
as code).
|
|
95
94
|
|
|
96
95
|
One path outside that set makes it `required` — `.claude/**`, `CLAUDE.md`, a test, configuration, a
|
|
97
|
-
code line, or a source file whose comment-free text cannot be read.
|
|
98
|
-
→ "The two skip arms".
|
|
96
|
+
code line, or a source file whose comment-free text cannot be read.
|
|
99
97
|
|
|
100
98
|
Record a skipped round on the Issue:
|
|
101
99
|
|
|
@@ -129,7 +127,8 @@ Findings that reduce to one root judgement are filed as one Issue with a section
|
|
|
129
127
|
|
|
130
128
|
Branch 2 only:
|
|
131
129
|
|
|
132
|
-
- File a follow-up Issue
|
|
130
|
+
- File a follow-up Issue whose `parent: #N` line names the current one — a mention in its background
|
|
131
|
+
is not read by `epic:bundle`; the current Issue completes.
|
|
133
132
|
- An Issue in no epic is never offered by `epic:next`, so filing continues to the epic. Run these steps
|
|
134
133
|
inside the CI wait: in `fullrun` / `backlogrun` after `pnpm josh git -y` and before
|
|
135
134
|
`pnpm josh followup`; in `halfrun` or a standalone pre-commit self-review as soon as the disposition
|
package/prompts/testing-guide.md
CHANGED
|
@@ -50,7 +50,7 @@ Req N: ...
|
|
|
50
50
|
| Component / browser | `*.svelte.test.ts` | browser/component vitest project |
|
|
51
51
|
| E2E | `src/routes/**/*.e2e.ts` | Playwright |
|
|
52
52
|
|
|
53
|
-
- **Never `*.spec.ts`** — lint-enforced by `eslint/rules/test-filename.js` (wired into `eslint/base.js`) for every JS/TS extension
|
|
53
|
+
- **Never `*.spec.ts`** — lint-enforced by `eslint/rules/test-filename.js` (wired into `eslint/base.js`) for every JS/TS extension.
|
|
54
54
|
- **Keep the `.svelte.` infix** on component/browser tests: `*.svelte.test.ts` matches the browser project (`include: src/**/*.svelte.{test,spec}.{js,ts}`), plain `*.test.ts` the node project (`include: src/**/*.{test,spec}.{js,ts}`); dropping it silently moves the file to the wrong project.
|
|
55
55
|
- **Colocate** each test beside its code (`foo.test.ts` next to `foo.ts`); no top-level `tests/` (lint-enforced by the same rule). E2E specs go under `src/routes/**` — enforced by review only, since `playwright.config.ts` `testMatch: '**/*.e2e.{ts,js}'` runs them anywhere.
|
|
56
56
|
|
|
@@ -148,7 +148,7 @@ Reset module state at the start of each test (`reset_praise_audio_index()` in `s
|
|
|
148
148
|
|
|
149
149
|
## 6. Closing the E2E gate without a human run
|
|
150
150
|
|
|
151
|
-
No human E2E run closes the gate, and nothing a red local run stopped is let through
|
|
151
|
+
No human E2E run closes the gate, and nothing a red local run stopped is let through.
|
|
152
152
|
|
|
153
153
|
### Which result closes the gate
|
|
154
154
|
|
|
@@ -4,6 +4,7 @@ import {
|
|
|
4
4
|
type AgentEnvironment,
|
|
5
5
|
type AgentProfile,
|
|
6
6
|
type AgentRole,
|
|
7
|
+
type ProfileResult,
|
|
7
8
|
} from './agent-role-profile'
|
|
8
9
|
import { claude_agent_argv } from './claude-agent-argv'
|
|
9
10
|
import { codex_agent_argv } from './codex-agent-argv'
|
|
@@ -11,6 +12,8 @@ import { codex_agent_argv } from './codex-agent-argv'
|
|
|
11
12
|
interface AgentArgv {
|
|
12
13
|
command: string
|
|
13
14
|
args: ReadonlyArray<string>
|
|
15
|
+
// The invocation is the last element, so `detached-launch.ts` lets that one break lines.
|
|
16
|
+
ends_with_prompt: true
|
|
14
17
|
}
|
|
15
18
|
|
|
16
19
|
type AgentArgvResult =
|
|
@@ -112,22 +115,35 @@ function resume_argv(
|
|
|
112
115
|
: with_profile_in(invocation, resolved.profile, cwd)
|
|
113
116
|
}
|
|
114
117
|
|
|
118
|
+
function profiled_in(invocation: string, resolved: ProfileResult, cwd: string): AgentArgvResult {
|
|
119
|
+
return resolved.kind === 'rejected'
|
|
120
|
+
? resolved
|
|
121
|
+
: with_profile_in(invocation, resolved.profile, cwd)
|
|
122
|
+
}
|
|
123
|
+
|
|
115
124
|
function resolve_in(
|
|
116
125
|
invocation: string,
|
|
117
126
|
role: AgentRole,
|
|
118
127
|
cwd: string,
|
|
119
128
|
environment: AgentEnvironment = process.env,
|
|
120
129
|
): AgentArgvResult {
|
|
121
|
-
|
|
130
|
+
return profiled_in(invocation, agent_role_profile.resolve(role, environment), cwd)
|
|
131
|
+
}
|
|
122
132
|
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
133
|
+
// The reviewer for one review round, so a verification pass runs on its own profile.
|
|
134
|
+
function review_in(
|
|
135
|
+
invocation: string,
|
|
136
|
+
round: number,
|
|
137
|
+
cwd: string,
|
|
138
|
+
environment: AgentEnvironment = process.env,
|
|
139
|
+
): AgentArgvResult {
|
|
140
|
+
return profiled_in(invocation, agent_role_profile.resolve_review(round, environment), cwd)
|
|
126
141
|
}
|
|
127
142
|
|
|
128
143
|
const agent_argv = {
|
|
129
144
|
resolve,
|
|
130
145
|
resolve_in,
|
|
146
|
+
review_in,
|
|
131
147
|
resume_argv,
|
|
132
148
|
with_profile,
|
|
133
149
|
with_profile_in,
|
|
@@ -1,11 +1,8 @@
|
|
|
1
1
|
import { agent_session_environment } from '#scripts/josh/agent-session-environment'
|
|
2
2
|
import { z } from 'zod'
|
|
3
|
+
import { agent_safe_text } from './agent-safe-text'
|
|
3
4
|
|
|
4
|
-
const FIRST_PRINTABLE_CODE = 0x20
|
|
5
|
-
const DELETE_CODE = 0x7f
|
|
6
|
-
const FIRST_CODE_POINT = 0
|
|
7
5
|
const EMPTY_LENGTH = 0
|
|
8
|
-
const MAX_VALUE_LENGTH = 4096
|
|
9
6
|
|
|
10
7
|
const ROLE_SCHEMA = z.enum(['scheduler', 'worker', 'reviewer'])
|
|
11
8
|
const PROVIDER_SCHEMA = z.enum(['anthropic', 'openai'])
|
|
@@ -32,6 +29,18 @@ type ProfileResult = { kind: 'profile'; profile: AgentProfile } | { kind: 'rejec
|
|
|
32
29
|
type Rejected = Extract<ProfileResult, { kind: 'rejected' }>
|
|
33
30
|
type EffortResult = Rejected | { kind: 'effort'; effort: AgentEffort }
|
|
34
31
|
type ProviderResult = Rejected | { kind: 'provider'; provider: AgentProvider; is_default?: true }
|
|
32
|
+
type OverrideField = 'model' | 'effort'
|
|
33
|
+
|
|
34
|
+
interface Override {
|
|
35
|
+
key: string
|
|
36
|
+
value: string | undefined
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
// What a resolution is keyed on beyond the role: the run phase (effort) and the review round (profile).
|
|
40
|
+
interface AgentStage {
|
|
41
|
+
phase?: string | undefined
|
|
42
|
+
round?: number
|
|
43
|
+
}
|
|
35
44
|
|
|
36
45
|
const SCHEDULER: AgentRole = 'scheduler'
|
|
37
46
|
const WORKER: AgentRole = 'worker'
|
|
@@ -72,6 +81,22 @@ const OPENAI_PROFILES: Readonly<Record<AgentRole, AgentProfile>> = {
|
|
|
72
81
|
|
|
73
82
|
const PROVIDER_PROFILES = { anthropic: DEFAULT_PROFILES, openai: OPENAI_PROFILES }
|
|
74
83
|
|
|
84
|
+
// **The verification pass resolves a profile of its own** (joshuafolkken/kit#3926). A round after the
|
|
85
|
+
// first only checks that the first round's findings were fixed, so its model and effort are set apart:
|
|
86
|
+
// its own keys win, then the reviewer's, then its own default.
|
|
87
|
+
const FIRST_REVIEW_ROUND = 1
|
|
88
|
+
// Sonnet holds the verification pass by the eval criterion: on every `verification-fix-*` scenario its
|
|
89
|
+
// pass/fail equals Opus's (100% agreement), and Opus passes all 4.
|
|
90
|
+
const VERIFICATION_MODEL = 'claude-sonnet-5-5'
|
|
91
|
+
const VERIFICATION_PROFILES: Readonly<Record<AgentProvider, AgentProfile>> = {
|
|
92
|
+
anthropic: { ...DEFAULT_PROFILES.reviewer, model: VERIFICATION_MODEL },
|
|
93
|
+
openai: OPENAI_PROFILES.reviewer,
|
|
94
|
+
}
|
|
95
|
+
const VERIFICATION_KEYS = {
|
|
96
|
+
model: 'JOSH_REVIEWER_VERIFICATION_MODEL',
|
|
97
|
+
effort: 'JOSH_REVIEWER_VERIFICATION_EFFORT',
|
|
98
|
+
}
|
|
99
|
+
|
|
75
100
|
// **Effort as a function of the run phase, not the role alone**. For the first
|
|
76
101
|
// merge only the mechanical ship/bookkeeping region is lowered: the pre-gate resume drives the gate,
|
|
77
102
|
// commit, PR and merge, applying fixes the gate has already named — work delegation.md calls the opposite
|
|
@@ -93,19 +118,7 @@ const ENV_KEYS: Readonly<Record<AgentRole, { model: string; effort: string }>> =
|
|
|
93
118
|
|
|
94
119
|
const LEGACY_WORKER_KEYS = { model: 'JOSH_LANE_MODEL', effort: 'JOSH_LANE_EFFORT' }
|
|
95
120
|
|
|
96
|
-
|
|
97
|
-
const code = character.codePointAt(FIRST_CODE_POINT) ?? FIRST_PRINTABLE_CODE
|
|
98
|
-
|
|
99
|
-
return code < FIRST_PRINTABLE_CODE || code === DELETE_CODE
|
|
100
|
-
}
|
|
101
|
-
|
|
102
|
-
function is_safe_value(value: string): boolean {
|
|
103
|
-
if (value.length === EMPTY_LENGTH || value.length > MAX_VALUE_LENGTH) return false
|
|
104
|
-
|
|
105
|
-
for (const character of value) if (is_control_character(character)) return false
|
|
106
|
-
|
|
107
|
-
return true
|
|
108
|
-
}
|
|
121
|
+
const { MAX_VALUE_LENGTH, is_safe_prompt, is_safe_value, printable } = agent_safe_text
|
|
109
122
|
|
|
110
123
|
function trimmed(value: string | undefined): string | undefined {
|
|
111
124
|
const result = value?.trim()
|
|
@@ -113,44 +126,53 @@ function trimmed(value: string | undefined): string | undefined {
|
|
|
113
126
|
return result === undefined || result.length === EMPTY_LENGTH ? undefined : result
|
|
114
127
|
}
|
|
115
128
|
|
|
116
|
-
function
|
|
129
|
+
function is_verification(role: AgentRole, round: number | undefined): boolean {
|
|
130
|
+
return role === REVIEWER && round !== undefined && round > FIRST_REVIEW_ROUND
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
// The keys an override is read from, first match wins: a worker falls back to its legacy lane keys, and
|
|
134
|
+
// a verification round reads its own keys ahead of the reviewer's.
|
|
135
|
+
function override_keys(
|
|
117
136
|
role: AgentRole,
|
|
118
|
-
field:
|
|
119
|
-
|
|
120
|
-
):
|
|
137
|
+
field: OverrideField,
|
|
138
|
+
round: number | undefined,
|
|
139
|
+
): readonly [string, ...Array<string>] {
|
|
121
140
|
const key = ENV_KEYS[role][field]
|
|
122
|
-
|
|
123
|
-
if (value !== undefined) return { key, value }
|
|
124
|
-
if (role !== WORKER) return { key, value: undefined }
|
|
141
|
+
if (role === WORKER) return [key, LEGACY_WORKER_KEYS[field]]
|
|
125
142
|
|
|
126
|
-
|
|
127
|
-
|
|
143
|
+
return is_verification(role, round) ? [VERIFICATION_KEYS[field], key] : [key]
|
|
144
|
+
}
|
|
128
145
|
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
146
|
+
function override_value(
|
|
147
|
+
role: AgentRole,
|
|
148
|
+
field: OverrideField,
|
|
149
|
+
environment: AgentEnvironment,
|
|
150
|
+
round?: number,
|
|
151
|
+
): Override {
|
|
152
|
+
const keys = override_keys(role, field, round)
|
|
153
|
+
const key = keys.find((candidate) => trimmed(environment[candidate]) !== undefined)
|
|
154
|
+
|
|
155
|
+
return key === undefined
|
|
156
|
+
? { key: keys[0], value: undefined }
|
|
157
|
+
: { key, value: trimmed(environment[key]) }
|
|
132
158
|
}
|
|
133
159
|
|
|
134
160
|
function model_override(
|
|
135
161
|
role: AgentRole,
|
|
136
162
|
environment: AgentEnvironment,
|
|
137
163
|
provider: AgentProvider,
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
164
|
+
round: number | undefined,
|
|
165
|
+
): Override {
|
|
141
166
|
return provider === OPENAI_PROVIDER
|
|
142
|
-
? { key, value: undefined }
|
|
143
|
-
: override_value(role, 'model', environment)
|
|
167
|
+
? { key: ENV_KEYS[role].model, value: undefined }
|
|
168
|
+
: override_value(role, 'model', environment, round)
|
|
144
169
|
}
|
|
145
170
|
|
|
146
171
|
function rejection(key: string, value: string, expected: string): Rejected {
|
|
147
172
|
return { kind: 'rejected', note: `${key}=${value} ${expected}, so the agent was not started` }
|
|
148
173
|
}
|
|
149
174
|
|
|
150
|
-
function resolved_effort(
|
|
151
|
-
override: { key: string; value: string | undefined },
|
|
152
|
-
fallback: AgentEffort,
|
|
153
|
-
): EffortResult {
|
|
175
|
+
function resolved_effort(override: Override, fallback: AgentEffort): EffortResult {
|
|
154
176
|
const parsed = EFFORT_SCHEMA.safeParse(override.value ?? fallback)
|
|
155
177
|
|
|
156
178
|
return parsed.success
|
|
@@ -290,17 +312,27 @@ function with_phase_effort(
|
|
|
290
312
|
return { ...profile, effort: overridden_effort(profile, phase, environment) }
|
|
291
313
|
}
|
|
292
314
|
|
|
293
|
-
function
|
|
315
|
+
function stage_defaults(
|
|
316
|
+
provider: AgentProvider,
|
|
294
317
|
role: AgentRole,
|
|
295
|
-
|
|
296
|
-
|
|
318
|
+
round: number | undefined,
|
|
319
|
+
): AgentProfile {
|
|
320
|
+
return is_verification(role, round)
|
|
321
|
+
? VERIFICATION_PROFILES[provider]
|
|
322
|
+
: PROVIDER_PROFILES[provider][role]
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
function resolve_stage(
|
|
326
|
+
role: AgentRole,
|
|
327
|
+
environment: AgentEnvironment,
|
|
328
|
+
stage: AgentStage,
|
|
297
329
|
): ProfileResult {
|
|
298
330
|
const selected = resolve_provider(environment)
|
|
299
331
|
if (selected.kind === 'rejected') return selected
|
|
300
|
-
const defaults =
|
|
301
|
-
const model = model_override(role, environment, selected.provider)
|
|
302
|
-
const effort = override_value(role, 'effort', environment)
|
|
303
|
-
const resolved = resolved_effort(effort, phase_effort(role, phase, defaults.effort))
|
|
332
|
+
const defaults = stage_defaults(selected.provider, role, stage.round)
|
|
333
|
+
const model = model_override(role, environment, selected.provider, stage.round)
|
|
334
|
+
const effort = override_value(role, 'effort', environment, stage.round)
|
|
335
|
+
const resolved = resolved_effort(effort, phase_effort(role, stage.phase, defaults.effort))
|
|
304
336
|
|
|
305
337
|
if (resolved.kind === 'rejected') return resolved
|
|
306
338
|
|
|
@@ -314,10 +346,29 @@ function resolve(
|
|
|
314
346
|
return validate(profile, model.key)
|
|
315
347
|
}
|
|
316
348
|
|
|
349
|
+
function resolve(
|
|
350
|
+
role: AgentRole,
|
|
351
|
+
environment: AgentEnvironment = process.env,
|
|
352
|
+
phase?: string,
|
|
353
|
+
): ProfileResult {
|
|
354
|
+
return resolve_stage(role, environment, { phase })
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
// The reviewer for one review round: round 1 as `resolve(REVIEWER)` does, any later round the
|
|
358
|
+
// verification pass's own profile.
|
|
359
|
+
function resolve_review(round: number, environment: AgentEnvironment = process.env): ProfileResult {
|
|
360
|
+
return resolve_stage(REVIEWER, environment, { round })
|
|
361
|
+
}
|
|
362
|
+
|
|
317
363
|
function describe(profile: AgentProfile): string {
|
|
318
364
|
return `provider=${profile.provider} role=${profile.role} model=${profile.model} effort=${profile.effort}`
|
|
319
365
|
}
|
|
320
366
|
|
|
367
|
+
// The run-log line a review launch writes, so each round's model is on record beside its round.
|
|
368
|
+
function describe_review(round: number, profile: AgentProfile): string {
|
|
369
|
+
return `review round ${String(round)} · ${describe(profile)}`
|
|
370
|
+
}
|
|
371
|
+
|
|
321
372
|
function parse(value: unknown): AgentProfile | undefined {
|
|
322
373
|
const parsed = PROFILE_SCHEMA.safeParse(value)
|
|
323
374
|
|
|
@@ -335,15 +386,20 @@ const agent_role_profile = {
|
|
|
335
386
|
PROFILE_SCHEMA,
|
|
336
387
|
REVIEWER,
|
|
337
388
|
SCHEDULER,
|
|
389
|
+
VERIFICATION_PROFILES,
|
|
338
390
|
WORKER,
|
|
339
391
|
default_notice,
|
|
340
392
|
describe,
|
|
393
|
+
describe_review,
|
|
341
394
|
handoff_environment,
|
|
342
395
|
has_completion_callback,
|
|
396
|
+
is_safe_prompt,
|
|
343
397
|
is_safe_value,
|
|
344
398
|
parse,
|
|
399
|
+
printable,
|
|
345
400
|
resolve,
|
|
346
401
|
resolve_provider,
|
|
402
|
+
resolve_review,
|
|
347
403
|
warn_of_default,
|
|
348
404
|
with_phase_effort,
|
|
349
405
|
}
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
// The text an agent launch may carry: a model id, an effort and a prompt, refused when empty, over
|
|
2
|
+
// length or carrying a control character, so nothing a value holds can inject a further argument.
|
|
3
|
+
|
|
4
|
+
const FIRST_PRINTABLE_CODE = 0x20
|
|
5
|
+
const DELETE_CODE = 0x7f
|
|
6
|
+
const FIRST_CODE_POINT = 0
|
|
7
|
+
const EMPTY_LENGTH = 0
|
|
8
|
+
const MAX_VALUE_LENGTH = 4096
|
|
9
|
+
const LINE_FEED = '\n'
|
|
10
|
+
const SPACE = ' '
|
|
11
|
+
|
|
12
|
+
function is_control_character(character: string): boolean {
|
|
13
|
+
const code = character.codePointAt(FIRST_CODE_POINT) ?? FIRST_PRINTABLE_CODE
|
|
14
|
+
|
|
15
|
+
return code < FIRST_PRINTABLE_CODE || code === DELETE_CODE
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
function is_line_feed(character: string): boolean {
|
|
19
|
+
return character === LINE_FEED
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function is_never_allowed(): boolean {
|
|
23
|
+
return false
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function is_refused(character: string, is_allowed: (character: string) => boolean): boolean {
|
|
27
|
+
return is_control_character(character) && !is_allowed(character)
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function is_safe_text(value: string, is_allowed: (character: string) => boolean): boolean {
|
|
31
|
+
if (value.length === EMPTY_LENGTH || value.length > MAX_VALUE_LENGTH) return false
|
|
32
|
+
|
|
33
|
+
for (const character of value) if (is_refused(character, is_allowed)) return false
|
|
34
|
+
|
|
35
|
+
return true
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function is_safe_value(value: string): boolean {
|
|
39
|
+
return is_safe_text(value, is_never_allowed)
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
// **An agent's prompt is the one value a line break belongs in.** A ship-stop prompt carries the failed
|
|
43
|
+
// output's lines (joshuafolkken/kit#3964); every other control character is refused as in any value.
|
|
44
|
+
function is_safe_prompt(value: string): boolean {
|
|
45
|
+
return is_safe_text(value, is_line_feed)
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function printable_character(character: string): string {
|
|
49
|
+
return is_refused(character, is_line_feed) ? SPACE : character
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
// Command output made fit for a prompt: every control character but a line break becomes a space, so
|
|
53
|
+
// the text passes `is_safe_prompt` whatever the command printed.
|
|
54
|
+
function printable(text: string): string {
|
|
55
|
+
return Array.from(text, printable_character).join('')
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
const agent_safe_text = { MAX_VALUE_LENGTH, is_safe_prompt, is_safe_value, printable }
|
|
59
|
+
|
|
60
|
+
export { agent_safe_text }
|
|
@@ -6,6 +6,7 @@ import {
|
|
|
6
6
|
type AgentProfile,
|
|
7
7
|
type AgentRole,
|
|
8
8
|
} from './agent-role-profile'
|
|
9
|
+
import { lane_instructions } from './lane-instructions'
|
|
9
10
|
|
|
10
11
|
const AGENT_COMMAND = 'claude'
|
|
11
12
|
// **The permission mode is passed, not left to the checkout's `.claude/settings.json`.** Claude Code
|
|
@@ -63,6 +64,7 @@ const PROJECT_MCP_FILE = '.mcp.json'
|
|
|
63
64
|
interface ClaudeArgv {
|
|
64
65
|
command: string
|
|
65
66
|
args: ReadonlyArray<string>
|
|
67
|
+
ends_with_prompt: true
|
|
66
68
|
}
|
|
67
69
|
|
|
68
70
|
type ClaudeArgvResult =
|
|
@@ -87,6 +89,14 @@ function tool_flags(cwd: string = process.cwd()): ReadonlyArray<string> {
|
|
|
87
89
|
return [TOOLS_FLAG, LAUNCHED_TOOLS.join(','), ...mcp_flags]
|
|
88
90
|
}
|
|
89
91
|
|
|
92
|
+
// **A lane child and a review session start from the lane instruction set** (`lane-instructions.ts`);
|
|
93
|
+
// the scheduler keeps `CLAUDE.md`, because it is the one that stops and asks a person.
|
|
94
|
+
function instruction_flags(profile: AgentProfile, cwd?: string): ReadonlyArray<string> {
|
|
95
|
+
if (profile.role === agent_role_profile.SCHEDULER) return []
|
|
96
|
+
|
|
97
|
+
return lane_instructions.flags(cwd)
|
|
98
|
+
}
|
|
99
|
+
|
|
90
100
|
function build(
|
|
91
101
|
invocation: string,
|
|
92
102
|
profile: AgentProfile,
|
|
@@ -98,6 +108,7 @@ function build(
|
|
|
98
108
|
args: [
|
|
99
109
|
...AGENT_FLAGS,
|
|
100
110
|
...tool_flags(cwd),
|
|
111
|
+
...instruction_flags(profile, cwd),
|
|
101
112
|
...session_id_flags(session_id),
|
|
102
113
|
MODEL_FLAG,
|
|
103
114
|
profile.model,
|
|
@@ -105,6 +116,7 @@ function build(
|
|
|
105
116
|
profile.effort,
|
|
106
117
|
invocation,
|
|
107
118
|
],
|
|
119
|
+
ends_with_prompt: true,
|
|
108
120
|
}
|
|
109
121
|
}
|
|
110
122
|
|
|
@@ -121,6 +133,7 @@ function build_resume(
|
|
|
121
133
|
args: [
|
|
122
134
|
...AGENT_FLAGS,
|
|
123
135
|
...tool_flags(cwd),
|
|
136
|
+
...instruction_flags(profile, cwd),
|
|
124
137
|
RESUME_FLAG,
|
|
125
138
|
session_id,
|
|
126
139
|
MODEL_FLAG,
|
|
@@ -129,6 +142,7 @@ function build_resume(
|
|
|
129
142
|
profile.effort,
|
|
130
143
|
invocation,
|
|
131
144
|
],
|
|
145
|
+
ends_with_prompt: true,
|
|
132
146
|
}
|
|
133
147
|
}
|
|
134
148
|
|
|
@@ -19,6 +19,7 @@ const LEDGER_DIRECTORY = path.dirname(OBSERVATION_LEDGER_DIRECTORY)
|
|
|
19
19
|
interface CodexArgv {
|
|
20
20
|
command: string
|
|
21
21
|
args: ReadonlyArray<string>
|
|
22
|
+
ends_with_prompt: true
|
|
22
23
|
}
|
|
23
24
|
|
|
24
25
|
function effort_config(profile: AgentProfile): string {
|
|
@@ -91,6 +92,7 @@ function build(invocation: string, profile: AgentProfile, cwd?: string): CodexAr
|
|
|
91
92
|
JSON_FLAG,
|
|
92
93
|
invocation,
|
|
93
94
|
],
|
|
95
|
+
ends_with_prompt: true,
|
|
94
96
|
}
|
|
95
97
|
}
|
|
96
98
|
|
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
import path from 'node:path'
|
|
2
|
+
import { fileURLToPath } from 'node:url'
|
|
3
|
+
import { cost_tokens } from '#scripts/cost-runtime/cost-tokens'
|
|
4
|
+
import { document_section } from '#scripts/document/document-section'
|
|
5
|
+
import { distributed_paths } from '#scripts/init/distributed-paths'
|
|
6
|
+
import { find_package_directory } from '#scripts/josh/package-directory'
|
|
7
|
+
import { stamp_file } from '#scripts/josh/stamp-file'
|
|
8
|
+
|
|
9
|
+
interface LaneSet {
|
|
10
|
+
text: string
|
|
11
|
+
rules_path: string
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
// **A headless lane child or review session starts from the lane instruction set, not `CLAUDE.md`.**
|
|
15
|
+
// `CLAUDE.md` carries rules only an interactive session can act on — consultation versus execution,
|
|
16
|
+
// explicit workflow invocation, stop notifications — and every request re-reads it. The lane document
|
|
17
|
+
// names the rules a lane does need by `CLAUDE.md` heading or bold label, and each `include` line is
|
|
18
|
+
// expanded from the launch checkout's own `CLAUDE.md` here, so the rule text keeps one source (#3929).
|
|
19
|
+
const LANE_DOCUMENT = 'prompts/collaboration-workflow/lane-instructions.md'
|
|
20
|
+
const RULES_FILE = 'CLAUDE.md'
|
|
21
|
+
// `AGENTS.md` only points at `CLAUDE.md`, and Claude Code loads it once `CLAUDE.md` is excluded.
|
|
22
|
+
const POINTER_FILE = 'AGENTS.md'
|
|
23
|
+
const INCLUDE_PATTERN = /^<!-- include: CLAUDE\.md → "(?<wanted>.+)" -->$/u
|
|
24
|
+
const PROMPT_FILE_FLAG = '--append-system-prompt-file'
|
|
25
|
+
const SETTINGS_FLAG = '--settings'
|
|
26
|
+
const PROMPT_FILE_PREFIX = 'josh-lane-instructions-'
|
|
27
|
+
const PROMPT_FILE_SUFFIX = '.md'
|
|
28
|
+
const PACKAGE_DIRECTORY = find_package_directory(path.dirname(fileURLToPath(import.meta.url)))
|
|
29
|
+
|
|
30
|
+
// An include that resolves to nothing is `undefined`, never an empty string, so a renamed heading
|
|
31
|
+
// fails the whole composition rather than silently dropping a rule.
|
|
32
|
+
function expand_line(line: string, rules: string): string | undefined {
|
|
33
|
+
const wanted = INCLUDE_PATTERN.exec(line)?.groups?.['wanted']
|
|
34
|
+
|
|
35
|
+
if (wanted === undefined) return line
|
|
36
|
+
|
|
37
|
+
return document_section.section(rules, wanted)?.text
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function compose(lane_markdown: string, rules: string): string | undefined {
|
|
41
|
+
const expanded = lane_markdown.split('\n').map((line) => expand_line(line, rules))
|
|
42
|
+
|
|
43
|
+
if (expanded.includes(undefined)) return undefined
|
|
44
|
+
|
|
45
|
+
return expanded.join('\n')
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
// **Where the rules are: the checkout's `CLAUDE.md`, else the package copy it imports.** kit and a
|
|
49
|
+
// consumer holding a full copy keep them in `CLAUDE.md`; a consumer written by `josh init` imports
|
|
50
|
+
// them from the package, and its own `CLAUDE.md` — the bootstrap note and the project's additions —
|
|
51
|
+
// stays loaded, because only the file the set was composed from is excluded.
|
|
52
|
+
function rules_sources(cwd: string): ReadonlyArray<string> {
|
|
53
|
+
const imported = distributed_paths.CLAUDE_MD_IMPORT_LINE.replace(/^@/u, '')
|
|
54
|
+
|
|
55
|
+
return [path.join(cwd, RULES_FILE), path.join(cwd, imported)]
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function composed_from(lane_markdown: string, rules_path: string): string | undefined {
|
|
59
|
+
const rules = document_section.read_optional(rules_path)
|
|
60
|
+
|
|
61
|
+
return rules === undefined ? undefined : compose(lane_markdown, rules)
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
function composed_set(cwd: string, package_directory: string): LaneSet | undefined {
|
|
65
|
+
const lane_markdown = document_section.read_optional(path.join(package_directory, LANE_DOCUMENT))
|
|
66
|
+
|
|
67
|
+
if (lane_markdown === undefined) return undefined
|
|
68
|
+
|
|
69
|
+
for (const rules_path of rules_sources(cwd)) {
|
|
70
|
+
const text = composed_from(lane_markdown, rules_path)
|
|
71
|
+
|
|
72
|
+
if (text !== undefined) return { text, rules_path }
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
return undefined
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
function composed_in(
|
|
79
|
+
cwd: string,
|
|
80
|
+
package_directory: string = PACKAGE_DIRECTORY,
|
|
81
|
+
): string | undefined {
|
|
82
|
+
return composed_set(cwd, package_directory)?.text
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
// Named by its content and this account, so concurrent launches of the same text share one file and a
|
|
86
|
+
// changed `CLAUDE.md` never reuses a stale one. The name is predictable in a temp directory other
|
|
87
|
+
// accounts may write to, so an existing file is reused only when it is this account's own regular file
|
|
88
|
+
// holding exactly this text; anything else is replaced through `stamp_file`, which refuses another
|
|
89
|
+
// account's file and renames a new one into place whole. A file rather than the text itself, because
|
|
90
|
+
// the launcher refuses a line break in any argv element but the prompt.
|
|
91
|
+
function written(text: string): string | undefined {
|
|
92
|
+
const file_path = stamp_file.stamp_path(PROMPT_FILE_PREFIX, text, PROMPT_FILE_SUFFIX)
|
|
93
|
+
|
|
94
|
+
if (stamp_file.read_stamp_text(file_path) === text) return file_path
|
|
95
|
+
|
|
96
|
+
try {
|
|
97
|
+
return stamp_file.replace_text_stamp(file_path, text)
|
|
98
|
+
} catch {
|
|
99
|
+
return undefined
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
function excluded_settings(rules_path: string, cwd: string): string {
|
|
104
|
+
return JSON.stringify({ claudeMdExcludes: [rules_path, path.join(cwd, POINTER_FILE)] })
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
// No flags when the set cannot be composed or safely written: the child then loads `CLAUDE.md` as
|
|
108
|
+
// before, which costs tokens but never loses a rule.
|
|
109
|
+
function flags(cwd: string = process.cwd()): ReadonlyArray<string> {
|
|
110
|
+
const set = composed_set(cwd, PACKAGE_DIRECTORY)
|
|
111
|
+
const prompt_file = set === undefined ? undefined : written(set.text)
|
|
112
|
+
|
|
113
|
+
if (set === undefined || prompt_file === undefined) return []
|
|
114
|
+
|
|
115
|
+
return [PROMPT_FILE_FLAG, prompt_file, SETTINGS_FLAG, excluded_settings(set.rules_path, cwd)]
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
function tokens_of(text: string): string {
|
|
119
|
+
return cost_tokens.estimate(text).toLocaleString('en-US')
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
// `read:set lane-child`'s resident line: what each lane request carries in place of the rules file.
|
|
123
|
+
function rules_tokens(rules_path: string): string {
|
|
124
|
+
return tokens_of(document_section.read_optional(rules_path) ?? '')
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
function resident_line(cwd: string = process.cwd()): string {
|
|
128
|
+
const set = composed_set(cwd, PACKAGE_DIRECTORY)
|
|
129
|
+
const rules = rules_tokens(set?.rules_path ?? path.join(cwd, RULES_FILE))
|
|
130
|
+
const lane_tokens = set === undefined ? 'not composed' : `${tokens_of(set.text)} tok`
|
|
131
|
+
|
|
132
|
+
return ` resident: lane instructions ${lane_tokens} in place of ${RULES_FILE} ${rules} tok`
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
const lane_instructions = {
|
|
136
|
+
PROMPT_FILE_FLAG,
|
|
137
|
+
SETTINGS_FLAG,
|
|
138
|
+
compose,
|
|
139
|
+
composed_in,
|
|
140
|
+
flags,
|
|
141
|
+
resident_line,
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
export { lane_instructions }
|
|
@@ -42,7 +42,7 @@ const WATCH_VERDICT = 'watch'
|
|
|
42
42
|
const STOP_VERDICT = 'stop'
|
|
43
43
|
const TRIAGE_VERDICT = 'triage'
|
|
44
44
|
|
|
45
|
-
// `run` — start what `backlog:next` offered. `watch` — sleep the
|
|
45
|
+
// `run` — start what `backlog:next` offered. `watch` — sleep the re-ask floor and ask again,
|
|
46
46
|
// except while something of the run's own is in flight, where the wake is the progress watcher's exit
|
|
47
47
|
// and the interval is only a floor (`docs/maintainers/backlogrun-driver.md` → "Waiting while
|
|
48
48
|
// something is in flight").
|
|
@@ -87,7 +87,7 @@ interface BudgetDecision {
|
|
|
87
87
|
}
|
|
88
88
|
|
|
89
89
|
const BLOCKED_REASON =
|
|
90
|
-
'Everything opted in is blocked or already running, so waiting can still change the answer.
|
|
90
|
+
'Everything opted in is blocked or already running, so waiting can still change the answer. Ask again at the re-ask floor (60 s).'
|
|
91
91
|
|
|
92
92
|
const TRIAGE_REASON =
|
|
93
93
|
'An opted-in candidate carries neither `run:solo` nor `run:lane`, so nothing starts until it is judged. Read each issue named above, record its order and one of the two labels with a comment, then ask again.'
|
|
@@ -121,7 +121,7 @@ function max_reached_reason(merged: number, max_issues: number): string {
|
|
|
121
121
|
}
|
|
122
122
|
|
|
123
123
|
function draining_reason(running: number, reason: string): string {
|
|
124
|
-
return `Start nothing more and
|
|
124
|
+
return `Start nothing more and wait on \`pnpm josh lane:await\` until the running children merge (running: ${String(running)}). Then report and finish — ${reason}`
|
|
125
125
|
}
|
|
126
126
|
|
|
127
127
|
function idle_expired_reason(answer: BacklogAnswer, idle_budget_ms: number): string {
|
|
@@ -140,7 +140,7 @@ function idle_watch_reason(answer: BacklogAnswer, left_ms: number): string {
|
|
|
140
140
|
// **The watch runs down while they drain, and restarts at each one collected.** A collected child is
|
|
141
141
|
// work, so it moves `active_at`; an ask that offered nothing never does.
|
|
142
142
|
function running_watch_reason(answer: BacklogAnswer, running: number, left_ms: number): string {
|
|
143
|
-
return `${waiting_on(answer)} and the idle watch has about ${to_minutes(left_ms)} minutes left, with ${String(running)} still running. Keep the working tree
|
|
143
|
+
return `${waiting_on(answer)} and the idle watch has about ${to_minutes(left_ms)} minutes left, with ${String(running)} still running. Keep the working tree and wait on \`pnpm josh lane:await\` until they merge — the watch is running down meanwhile.`
|
|
144
144
|
}
|
|
145
145
|
|
|
146
146
|
function watching_reason(input: BudgetInput, left_ms: number): string {
|
|
@@ -12,7 +12,7 @@ const { MERGE_TOKEN } = run_merge_token
|
|
|
12
12
|
|
|
13
13
|
const NO_RETRIES = 0
|
|
14
14
|
const FIRST = 0
|
|
15
|
-
const TERMINAL_OUTCOMES: ReadonlySet<string> = new Set(['merged', 'parked', 'split'])
|
|
15
|
+
const TERMINAL_OUTCOMES: ReadonlySet<string> = new Set(['merged', 'parked', 'split', 'withdrawn'])
|
|
16
16
|
|
|
17
17
|
// The skip follows the declared order alone: an issue `run:add` put in was
|
|
18
18
|
// asked for on its own, so its failure skips nothing and a declared failure does not skip it.
|