@joshuafolkken/kit 1.1010.0 → 1.1050.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 +1 -5
- package/.claude/skills/dependency-update/SKILL.md +6 -3
- package/.claude/skills/epic-commands/SKILL.md +46 -17
- package/.claude/skills/verify-ui/SKILL.md +1 -1
- package/.claude/skills/workflow-commands/SKILL.md +10 -25
- package/.claude/skills/workflow-commands/background-commands.md +1 -1
- package/.claude/skills/workflow-commands/backlogrun-child.md +6 -4
- package/.claude/skills/workflow-commands/backlogrun-lanes.md +1 -1
- package/.claude/skills/workflow-commands/backlogrun-park.md +6 -0
- package/.claude/skills/workflow-commands/backlogrun-progress.md +30 -36
- package/.claude/skills/workflow-commands/backlogrun-steps.md +16 -16
- package/.claude/skills/workflow-commands/backlogrun.md +3 -12
- package/.claude/skills/workflow-commands/chain-rule.md +2 -2
- package/.claude/skills/workflow-commands/followup-reference.md +2 -2
- package/.claude/skills/workflow-commands/fullrun-steps.md +4 -3
- package/.claude/skills/workflow-commands/fullrun.md +1 -1
- package/.claude/skills/workflow-commands/halfrun.md +2 -1
- package/.claude/skills/workflow-commands/into-target.md +1 -1
- package/.claude/skills/workflow-commands/issue-comments.md +1 -1
- package/.claude/skills/workflow-commands/observation-filing.md +80 -126
- package/.claude/skills/workflow-commands/pre-gate-cut.md +33 -36
- package/.claude/skills/workflow-commands/prerequisite.md +1 -1
- package/.claude/skills/workflow-commands/working-tree-hold.md +2 -2
- package/.codex/hooks.json +1 -5
- package/.github/workflows/auto-tag.yml +21 -0
- package/.github/workflows/publish.yml +9 -10
- package/.github/workflows/sonar-qube.yml +1 -1
- package/AGENTS.md +4 -25
- package/CLAUDE.md +24 -38
- package/GEMINI.md +4 -24
- package/README.md +22 -56
- package/dist/CLAUDE.md +24 -38
- package/dist/hooks/batch-guard.js +4 -4
- package/dist/hooks/{chunk-6PSTVOLH.js → chunk-5BK3K7V4.js} +114 -90
- package/dist/hooks/{chunk-PAXMLGSS.js → chunk-7VPDUBRN.js} +93 -175
- package/dist/hooks/{chunk-KPZN42RT.js → chunk-DMQN45FN.js} +3 -3
- package/dist/hooks/{chunk-WMIR5SNX.js → chunk-EJDX2TOB.js} +13 -3
- package/dist/hooks/{chunk-A2H5I2VD.js → chunk-GQ7KP45M.js} +2 -2
- package/dist/hooks/{chunk-4GDR2YTV.js → chunk-HJIISEAC.js} +4 -4
- package/dist/hooks/{chunk-AWTFKKFW.js → chunk-NAS4C7GP.js} +6 -33
- package/dist/hooks/{chunk-56LGMJJV.js → chunk-NJQOBE6L.js} +2 -2
- package/dist/hooks/{chunk-EZLHXKJ6.js → chunk-RLDX5SOC.js} +1222 -941
- package/dist/hooks/{chunk-GBJUERYK.js → chunk-WQ4OSBSS.js} +10 -3
- package/dist/hooks/codex-hook-adapter.js +10 -10
- package/dist/hooks/duplicate-read-guard.js +5 -5
- package/dist/hooks/format-edited.js +15 -34
- package/dist/hooks/investigation-guard.js +5 -5
- package/dist/hooks/pretool-guard.js +23 -16
- package/dist/hooks/stop-guard.js +21 -200
- package/dist/josh.js +42 -125
- package/dist/version/index.js +466 -42
- package/eslint/base.js +2 -11
- package/package.json +36 -10
- package/prompts/coding-standards.md +15 -290
- package/prompts/collaboration-workflow/cross-repo-epic.md +2 -51
- package/prompts/collaboration-workflow/epic-audit.md +2 -53
- package/prompts/collaboration-workflow/epic-bundle.md +2 -106
- package/prompts/collaboration-workflow/file-edits.md +6 -53
- package/prompts/collaboration-workflow/issue-template.md +4 -4
- package/prompts/collaboration-workflow/operating-rules.md +18 -75
- package/prompts/collaboration-workflow/principles.md +74 -0
- package/prompts/collaboration-workflow/residency.md +55 -15
- package/prompts/collaboration-workflow/rule-delivery.md +5 -46
- package/prompts/collaboration-workflow/shell-body.md +7 -9
- package/prompts/collaboration-workflow/turn-batching.md +21 -37
- package/prompts/collaboration-workflow/upstream-interrupt.md +1 -1
- package/prompts/collaboration-workflow/wip-cap.md +15 -18
- package/prompts/collaboration-workflow.md +22 -28
- package/prompts/sonar-hotspot-handling.md +1 -1
- package/scripts/adopt/adopt-logic.ts +2 -2
- package/scripts/adopt/adopt-toolkits.ts +3 -1
- package/scripts/adopt/adopt.ts +2 -2
- package/scripts/agent/agent-event.ts +2 -9
- package/scripts/agent/agent-exit-record.ts +2 -9
- package/scripts/auto-ok/auto-ok-cli.ts +9 -9
- package/scripts/backlog/backlog-budget-cli.ts +2 -6
- package/scripts/backlog/backlog-drive-cli.ts +20 -10
- package/scripts/backlog/backlog-drive.ts +50 -16
- package/scripts/backlog/backlog-next.ts +24 -8
- package/scripts/backlog/backlog-offer-cli.ts +6 -5
- package/scripts/backlog/backlog-plan-cli.ts +5 -1
- package/scripts/backlog/backlog-plan.ts +1 -1
- package/scripts/backlog/backlog-pool.ts +31 -11
- package/scripts/backlog/backlog-rank.ts +106 -0
- package/scripts/backlog/backlog-waves.ts +42 -29
- package/scripts/build/build-bin.ts +2 -1
- package/scripts/build/build-config-merge.ts +2 -1
- package/scripts/build/build-hooks.ts +31 -6
- package/scripts/build/build-managed-marker.ts +2 -1
- package/scripts/build/build-self-sync-guard.ts +2 -1
- package/scripts/build/build-version.ts +2 -1
- package/scripts/cases/cases-logic.ts +2 -6
- package/scripts/ci/pr-classification.ts +13 -11
- package/scripts/cost-runtime/cost-transcript.ts +4 -11
- package/scripts/delegation/delegation-policy.ts +11 -0
- package/scripts/doctor/doctor-consumer.ts +5 -12
- package/scripts/doctor/doctor-ports.ts +2 -2
- package/scripts/document/document-byte-budget.ts +13 -21
- package/scripts/document/document-section.ts +2 -6
- package/scripts/document/entry-read-budget.ts +6 -5
- package/scripts/{git/git-epic-add-body.ts → epic/epic-add-body.ts} +23 -23
- package/scripts/{git/git-epic-add-plan.ts → epic/epic-add-plan.ts} +23 -26
- package/scripts/{git/git-epic-add.ts → epic/epic-add.ts} +17 -17
- package/scripts/epic/epic-audit-cli.ts +4 -4
- package/scripts/epic/epic-audit-orphans.ts +2 -2
- package/scripts/epic/epic-audit-rationale.ts +1 -1
- package/scripts/epic/epic-audit.ts +1 -1
- package/scripts/{git/git-epic-body.ts → epic/epic-body.ts} +6 -6
- package/scripts/epic/epic-bundle-cli.ts +1 -1
- package/scripts/epic/epic-bundle.ts +2 -2
- package/scripts/epic/epic-candidate-confirm.ts +2 -1
- package/scripts/{git/git-epic-chains.ts → epic/epic-chains.ts} +4 -4
- package/{scripts-ai → scripts/epic}/epic-check.ts +5 -4
- package/{scripts-ai → scripts/epic}/epic-cli.ts +4 -4
- package/scripts/{git/git-epic-close-comment.ts → epic/epic-close-comment.ts} +9 -9
- package/scripts/{git/git-epic-close.ts → epic/epic-close.ts} +23 -19
- package/scripts/epic/epic-cross-repo.ts +21 -38
- package/scripts/{git/git-epic-decision.ts → epic/epic-decision.ts} +12 -14
- package/scripts/epic/epic-fetch.ts +3 -3
- package/scripts/epic/epic-graph.ts +3 -3
- package/scripts/epic/epic-index.ts +2 -2
- package/scripts/epic/epic-issue.ts +1 -1
- package/scripts/epic/epic-lane-offer.ts +11 -22
- package/scripts/epic/epic-next.ts +3 -3
- package/scripts/{git/git-epic-parse.ts → epic/epic-parse.ts} +3 -3
- package/scripts/{git/git-epic-promote.ts → epic/epic-promote.ts} +10 -10
- package/scripts/epic/epic-rank.ts +37 -0
- package/scripts/{git/git-epic-read.ts → epic/epic-read.ts} +9 -9
- package/scripts/{git/git-epic-reconcile.ts → epic/epic-reconcile.ts} +18 -20
- package/scripts/{git/git-epic-reference.ts → epic/epic-reference.ts} +4 -23
- package/scripts/epic/epic-relation-recheck.ts +4 -3
- package/scripts/{git/git-epic-relations.ts → epic/epic-relations.ts} +5 -5
- package/scripts/{git/git-epic-remove-plan.ts → epic/epic-remove-plan.ts} +17 -19
- package/scripts/{git/git-epic-remove.ts → epic/epic-remove.ts} +13 -13
- package/scripts/{git/git-epic-run.ts → epic/epic-run.ts} +27 -27
- package/scripts/{git/git-epic-sections.ts → epic/epic-sections.ts} +6 -6
- package/scripts/{git/git-epic-shape.ts → epic/epic-shape.ts} +7 -7
- package/scripts/epic/epic-solo.ts +29 -28
- package/scripts/{git/git-epic-validate.ts → epic/epic-validate.ts} +13 -13
- package/{scripts-ai → scripts/epic}/epic.ts +17 -16
- package/scripts/gh/fix-gh-packages.ts +2 -1
- package/scripts/gh/gh-spawn.ts +12 -10
- package/scripts/git/constants.ts +1 -1
- package/scripts/git/git-branch.ts +10 -1
- package/scripts/git/git-command.ts +2 -2
- package/scripts/git/git-error.ts +2 -5
- package/scripts/git/git-execa-error.ts +6 -1
- package/{scripts-ai → scripts/git}/git-followup-finish.ts +7 -7
- package/scripts/git/git-followup-flush.ts +74 -48
- package/scripts/git/git-followup-issue-close.ts +2 -2
- package/scripts/git/git-followup-label.ts +1 -1
- package/scripts/git/git-followup-pending.ts +1 -1
- package/{scripts-ai → scripts/git}/git-followup-workflow.ts +23 -11
- package/scripts/git/git-gh-exec.ts +85 -26
- package/scripts/git/git-gh-issue-read.ts +2 -2
- package/scripts/git/git-gh-issue-rest.ts +2 -2
- package/scripts/git/git-gh-issue-write.ts +6 -6
- package/scripts/git/git-gh-pr-read.ts +0 -21
- package/scripts/git/git-gh-pr.ts +2 -23
- package/scripts/git/git-next-issues.ts +34 -5
- package/scripts/git/git-pr-followup-wrapup.ts +2 -6
- package/scripts/git/git-preflight.ts +25 -10
- package/scripts/git/git-push-transport.ts +12 -69
- package/scripts/git/git-spawn.ts +79 -3
- package/scripts/git/git-ssh-keepalive.ts +74 -0
- package/scripts/git/git-staging.ts +21 -9
- package/{scripts-ai → scripts/git}/git-workflow.ts +11 -11
- package/scripts/git/github-issue-url.ts +1 -1
- package/scripts/git/gone-branch.ts +3 -2
- package/scripts/git/issue-labels.ts +13 -1
- package/scripts/git/issue-rank.ts +89 -0
- package/scripts/git/issue-reference.ts +19 -0
- package/scripts/git/main-merge.ts +2 -1
- package/scripts/git/main-sync.ts +3 -2
- package/scripts/git/schemas.ts +1 -1
- package/scripts/git/stash-pop-args.ts +7 -13
- package/scripts/git/stash-pop-cli.ts +2 -1
- package/{scripts-ai → scripts/git}/telegram-test-logic.ts +2 -2
- package/{scripts-ai → scripts/git}/telegram-test.ts +7 -7
- package/scripts/hooks/format-edited-cli.ts +15 -0
- package/scripts/hooks/format-edited-file.ts +2 -9
- package/scripts/hooks/format-rewrite-notice.ts +2 -8
- package/scripts/hooks/pretool-guard-cli.ts +21 -0
- package/scripts/hooks/pretool-guard.ts +14 -15
- package/scripts/init/init-ai-copy.ts +2 -5
- package/scripts/init/init.ts +8 -4
- package/scripts/init/start-exec.ts +7 -1
- package/scripts/init/start-plan.ts +18 -5
- package/scripts/init/start-setup-pr.ts +46 -19
- package/scripts/init/start-steps.ts +43 -10
- package/scripts/init/start.ts +42 -11
- package/scripts/issue/issue-cite.ts +19 -1
- package/scripts/issue/issue-comment-cli.ts +2 -1
- package/scripts/issue/issue-file-cli.ts +2 -1
- package/scripts/issue/issue-fold-cli.ts +9 -16
- package/scripts/issue/issue-scout-cli.ts +8 -13
- package/scripts/issue/measure-rerun-cli.ts +7 -6
- package/scripts/josh/josh-command-map.ts +0 -102
- package/scripts/josh/josh-commands-ai.ts +2 -2
- package/scripts/josh/josh-commands-backlog.ts +1 -1
- package/scripts/josh/josh-commands-development.ts +3 -3
- package/scripts/josh/josh-commands-project.ts +1 -1
- package/scripts/josh/josh-commands-workflow.ts +4 -4
- package/scripts/lane/lane-child-marker.ts +16 -2
- package/scripts/lane/lane-cli.ts +34 -15
- package/scripts/lane/lane-open.ts +3 -2
- package/scripts/lane/lane-registry.ts +3 -6
- package/scripts/lane/lane-report.ts +10 -4
- package/scripts/lib/cli-flags.ts +20 -2
- package/scripts/lib/error-message.ts +9 -0
- package/scripts/lib/read-file.ts +26 -2
- package/scripts/observations/observation-ledger-home.ts +98 -26
- package/scripts/observations/observation-ledger-migrate.ts +26 -21
- package/scripts/observations/observation-ledger-prepare.ts +10 -20
- package/scripts/observations/observation-ledger.ts +58 -25
- package/scripts/observations/observations-flush-cli.ts +6 -23
- package/scripts/observations/observations-flush-landing.ts +2 -1
- package/scripts/observations/observations-flush.ts +29 -20
- package/scripts/package/package-scout-cli.ts +8 -16
- package/scripts/ports/port-command.ts +2 -1
- package/scripts/propagate/propagate-plan.ts +185 -0
- package/scripts/propagate/propagate-publish.ts +6 -7
- package/scripts/propagate/propagate-run.ts +5 -1
- package/scripts/propagate/propagate-steps.ts +18 -2
- package/scripts/propagate/propagate-targets.ts +13 -2
- package/scripts/propagate/propagate.ts +63 -44
- package/scripts/refactor/import-graph.ts +2 -6
- package/scripts/registry-migration/migrate-logic.ts +1 -0
- package/scripts/registry-migration/migrate.ts +3 -2
- package/scripts/registry-migration/user-npmrc.ts +2 -2
- package/scripts/release/github-release.ts +1 -1
- package/scripts/release/release-cli.ts +2 -1
- package/scripts/repo/repo-setting.ts +10 -8
- package/scripts/review/review-attest.ts +44 -0
- package/scripts/review/review-brief-cli.ts +49 -2
- package/scripts/review/review-brief.ts +46 -1
- package/scripts/review/review-finding-ledger.ts +3 -3
- package/scripts/review/review-findings-cli.ts +4 -11
- package/scripts/review/review-record-cli.ts +14 -11
- package/scripts/review/review-record.ts +6 -16
- package/scripts/review/review-stamps.ts +7 -0
- package/scripts/rules/decision-oracle.ts +2 -2
- package/scripts/rules/implementation-cut.ts +1 -1
- package/scripts/rules/issue-fold.ts +1 -2
- package/scripts/rules/lane-carry-conflict.ts +1 -2
- package/scripts/rules/lane-park.ts +1 -2
- package/scripts/rules/pre-gate-cut.ts +2 -2
- package/scripts/rules/rule-body-guard.ts +2 -2
- package/scripts/rules/rule-value-cache.ts +99 -0
- package/scripts/rules/rule-value-cli.ts +10 -9
- package/scripts/rules/rule-value.ts +1 -1
- package/scripts/run/run-carry-args.ts +2 -6
- package/scripts/run/run-carry-cli.ts +5 -7
- package/scripts/run/run-cut-args.ts +7 -13
- package/scripts/run/run-ending-cli.ts +4 -7
- package/scripts/run/run-event-stream.ts +2 -5
- package/scripts/run/run-headless.ts +22 -6
- package/scripts/run/run-liveness-cli.ts +4 -7
- package/scripts/run/run-parent-cut-hook.ts +144 -0
- package/scripts/run/run-progress-args.ts +2 -6
- package/scripts/run/run-progress-cli.ts +2 -1
- package/scripts/run/run-progress-read.ts +2 -1
- package/scripts/run/run-progress.ts +21 -8
- package/scripts/run/run-ship-cli.ts +6 -74
- package/scripts/run/run-ship-preflight.ts +87 -0
- package/scripts/run/run-ship-review-steps.ts +5 -26
- package/scripts/run/run-ship-review.ts +9 -0
- package/scripts/run/run-ship-scoped.ts +44 -0
- package/scripts/run/run-ship-stage.ts +5 -1
- package/scripts/run/run-ship-steps.ts +105 -0
- package/scripts/run/run-ship.ts +2 -0
- package/scripts/run/run-tail-cli.ts +3 -3
- package/scripts/run/run-tidy-stashes.ts +7 -14
- package/scripts/run/run-wake-cli.ts +4 -8
- package/scripts/run/run-wake-describe.ts +19 -1
- package/scripts/run/run-wake-driver.ts +15 -2
- package/scripts/run/run-wake-session.ts +14 -4
- package/scripts/run/run-watcher-hook.ts +1 -1
- package/scripts/safe-chain/project-config.ts +3 -6
- package/scripts/security/security-audit-provision.ts +6 -7
- package/scripts/sonar/sonar-hotspots-cli.ts +2 -1
- package/scripts/sync/directory-copy-guard.ts +2 -1
- package/scripts/sync/sync-ai-files.ts +3 -3
- package/scripts/sync/template-source-logic.ts +3 -4
- package/scripts/test/e2e-retry-check.ts +3 -6
- package/scripts/time-runtime/time-bundle-call.ts +2 -2
- package/scripts/ui/ui-routes-cli.ts +3 -6
- package/scripts/version/bump-version.ts +2 -1
- package/scripts/version/npm-registry.ts +97 -0
- package/scripts/version/version-commands.ts +5 -4
- package/scripts/version/version-remote.ts +38 -33
- package/scripts/version/version-targets.ts +3 -3
- package/.claude/skills/workflow-commands/rule-residency.md +0 -267
- package/evals/scenarios/consult-not-execute.json +0 -20
- package/evals/scenarios/no-implicit-workflow.json +0 -26
- package/evals/scenarios/overrides-both-locations.json +0 -29
- package/evals/scenarios/upstream-interrupt.json +0 -23
- package/evals/scenarios/workflow-skill-loaded.json +0 -16
- package/prompts/agent-lint-protocol.json +0 -79
- package/prompts/agent-rules.md +0 -63
- package/prompts/collaboration-workflow/consultation-vs-execution.md +0 -8
- package/prompts/collaboration-workflow/distributed-docs.md +0 -7
- package/prompts/collaboration-workflow/durable-rules.md +0 -24
- package/prompts/collaboration-workflow/latest-first.md +0 -14
- package/prompts/collaboration-workflow/no-clones.md +0 -11
- package/prompts/collaboration-workflow/simplicity-first.md +0 -9
- package/prompts/collaboration-workflow/single-source-rules.md +0 -13
- package/scripts/build/shim-shell.ts +0 -25
- package/scripts/claude/skill-meta.ts +0 -37
- package/scripts/document/document-scan.ts +0 -108
- package/scripts/document/generate-catalog.ts +0 -69
- package/scripts/document/retired-phrases.ts +0 -143
- package/scripts/document/review-agent-type.ts +0 -46
- package/scripts/document/single-source.ts +0 -73
- package/scripts/git/git-fixture-workspace.ts +0 -88
- package/scripts/hooks/format-edited-file-fixtures.ts +0 -109
- package/scripts/hooks/prompt-hooks.ts +0 -38
- package/scripts/init/hook-command-bootstrap.ts +0 -83
- package/scripts/issue/issue-logic.ts +0 -50
- package/scripts/josh/__snapshots__/josh-command-map.test.ts.snap +0 -1227
- package/scripts/josh/josh-script-reader.ts +0 -11
- package/scripts/package/production-dependency-graph.ts +0 -68
- package/scripts/retrospective/retrospective-cli.ts +0 -97
- package/scripts/retrospective/retrospective.ts +0 -127
- package/scripts/rules/delivered-rules-harness.ts +0 -72
- package/scripts/rules/top-level-scripts-guard.ts +0 -47
- package/scripts/run/run-carry-flush.ts +0 -58
- package/scripts/run/run-transcript.golden.txt +0 -57
- package/scripts/run/run-wake-work.ts +0 -108
- package/scripts/sync/dependabot-pin-logic.ts +0 -135
- package/scripts/test/classify-isolation.ts +0 -149
- package/scripts/test/http-fault-injection.ts +0 -79
- package/scripts/test/josh-harness-environment.ts +0 -260
- package/scripts/test/josh-harness.ts +0 -96
- package/scripts/test/pilot-files.ts +0 -452
- package/scripts/test/pilot-measure.ts +0 -78
- package/scripts/test/test-network-guard.ts +0 -453
- package/scripts/test/test-repository-guard.ts +0 -317
- package/scripts/test/test-state-guard.ts +0 -85
- package/scripts/test/test-stdout-guard.ts +0 -54
- package/scripts/test/test-telegram-guard.ts +0 -90
- package/scripts/test/unit-guard-environment.ts +0 -12
- package/scripts/test/unit-projects.ts +0 -139
- package/scripts/test/vitest-include-globs.ts +0 -12
- package/scripts/time-runtime/time-batch-guard-fixtures.ts +0 -66
- package/scripts-ai/completion-gate.ts +0 -13
- package/scripts-ai/environment-loader.ts +0 -9
- /package/{scripts-ai → scripts/epic}/epic-cli-argv.ts +0 -0
- /package/{scripts-ai → scripts/epic}/epic-cli-remove.ts +0 -0
- /package/{scripts-ai → scripts/git}/followup-issue-number.ts +0 -0
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
import { CONTEXT_CUT_THRESHOLD } from '#scripts/cost-runtime/context-cut-threshold'
|
|
2
|
+
import { cost_cli, type CostVerdict } from '#scripts/cost-runtime/cost-cli'
|
|
3
|
+
import { cost_format } from '#scripts/cost-runtime/cost-format'
|
|
4
|
+
import { cost_verdict } from '#scripts/cost-runtime/cost-verdict'
|
|
5
|
+
import { hook_decision } from '#scripts/josh/hook-decision'
|
|
6
|
+
import { implementation_cut_verdict } from '#scripts/rules/implementation-cut-verdict'
|
|
7
|
+
import { run_carry, type RunCarry } from './run-carry'
|
|
8
|
+
import { run_headless } from './run-headless'
|
|
9
|
+
import { run_watcher_hook } from './run-watcher-hook'
|
|
10
|
+
|
|
11
|
+
// The `backlogrun` parent's hand-off, delivered at the call it binds on (joshuafolkken/kit#2947).
|
|
12
|
+
//
|
|
13
|
+
// **The parent's hand-off was asked only at a merge, and the context grew between merges.** The
|
|
14
|
+
// orchestrator never implements, yet on 2026-10-02 parent 259fb6d5 sent 111 of its 170 requests past
|
|
15
|
+
// the shared threshold, peaking at 207,081 tokens: `pnpm josh cost --cut` is read after each child's
|
|
16
|
+
// merge (`backlogrun-progress.md` → "The hand-off"), and a long wait or a lane-failure investigation
|
|
17
|
+
// between two merges is exactly where it went unread. This is the lesson `implementation-cut.ts` records
|
|
18
|
+
// for a lane child, one session up — a check carried as a step is taken late, a `PreToolUse` refusal is
|
|
19
|
+
// taken at the next call.
|
|
20
|
+
//
|
|
21
|
+
// **The same statistic and the same threshold, never a second measurement.** The verdict is
|
|
22
|
+
// `cost_cli.session_verdict` — what `pnpm josh cost --cut` prints — read through the implementation cut's
|
|
23
|
+
// short reuse window so a burst of calls is priced once.
|
|
24
|
+
//
|
|
25
|
+
// **It is async and composed after the synchronous guards, like the watcher guard.** Telling the parent
|
|
26
|
+
// apart reads the carry record through `run_headless.driving_carry`, which is asynchronous; reusing it is
|
|
27
|
+
// what keeps a second, synchronous spelling of the parent test from existing.
|
|
28
|
+
|
|
29
|
+
const SWITCH_ENV_KEY = 'JOSH_PARENT_CUT_GUARD'
|
|
30
|
+
const STAMP_PREFIX = 'josh-parent-cut-guard-'
|
|
31
|
+
|
|
32
|
+
// **One refusal, then a quiet window long enough to finish the hand-off in.** The hand-off is several
|
|
33
|
+
// calls — `lane:list`, `lane:output`, `run:carry --cut`, `run:wake`, the progress comment — and a guard
|
|
34
|
+
// refusing each of them would wedge the very procedure it asks for. A parent that could not cut (a lane
|
|
35
|
+
// nobody can poll) is asked again once the window has passed, which is the "next opportunity" the merge
|
|
36
|
+
// seam used to be.
|
|
37
|
+
const QUIET_WINDOW_MS = 600_000
|
|
38
|
+
|
|
39
|
+
// What the decision has to know about the world, passed in so it is testable without a carry record or
|
|
40
|
+
// a session transcript on disk.
|
|
41
|
+
interface ParentCutState {
|
|
42
|
+
carry: () => Promise<RunCarry | undefined>
|
|
43
|
+
verdict: () => CostVerdict
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function current_state(): ParentCutState {
|
|
47
|
+
return {
|
|
48
|
+
carry: async (): Promise<RunCarry | undefined> => await run_headless.driving_carry(),
|
|
49
|
+
verdict: (): CostVerdict =>
|
|
50
|
+
implementation_cut_verdict.reused_verdict(() => cost_cli.session_verdict()),
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
const THRESHOLD_TEXT = `${cost_format.format_tokens(CONTEXT_CUT_THRESHOLD)}-token`
|
|
55
|
+
|
|
56
|
+
const PARENT_CUT_REASON =
|
|
57
|
+
'⛔ backlogrun parent hand-off: this session drives a `backlogrun` (it owns the live carry record) and ' +
|
|
58
|
+
`its recent-context cost has crossed the shared ${THRESHOLD_TEXT} threshold between merges, so every ` +
|
|
59
|
+
'later request re-reads the accumulated context (joshuafolkken/kit#2947). Take the hand-off now rather ' +
|
|
60
|
+
'than at the next merge: follow `.claude/skills/workflow-commands/backlogrun-progress.md` → "The ' +
|
|
61
|
+
'hand-off" from its `over` branch — open no new lane and take no new child, read `pnpm josh lane:list`, ' +
|
|
62
|
+
'make sure every lane in flight records an output path, then run `pnpm josh run:carry --cut --owner ' +
|
|
63
|
+
'"$PPID"` and, unless it answers `capped`, `pnpm josh run:wake`, post the progress comment and end the ' +
|
|
64
|
+
'turn. A lane nobody can poll means the cut does not happen; carry on and this asks again later. ' +
|
|
65
|
+
'Reissue this call when the hand-off is done or cannot be taken — the guard stays quiet for a while ' +
|
|
66
|
+
'after it refuses, so the hand-off calls themselves are not refused.'
|
|
67
|
+
|
|
68
|
+
// Neither an uncapped parent's own record nor a measured `over` is optional: a capped record has no cut
|
|
69
|
+
// left to take (`run:carry --cut` answers `capped`), and an unmeasurable session is left alone here —
|
|
70
|
+
// refusing every call of a session that cannot be priced would wedge it, while the merge-seam check
|
|
71
|
+
// still takes `over`'s branch for it.
|
|
72
|
+
async function is_due(state: ParentCutState): Promise<boolean> {
|
|
73
|
+
const carry = await state.carry()
|
|
74
|
+
|
|
75
|
+
if (carry === undefined || run_carry.is_at_cut_cap(carry)) return false
|
|
76
|
+
|
|
77
|
+
return state.verdict() === cost_verdict.OVER_VERDICT
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
function is_quiet(target: string, now_ms: number): boolean {
|
|
81
|
+
const last_ms = hook_decision.create_refusal_stamp(STAMP_PREFIX).last_ms(target)
|
|
82
|
+
|
|
83
|
+
return last_ms !== hook_decision.NEVER_MS && now_ms - last_ms < QUIET_WINDOW_MS
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
// The quiet window is read before the carry record and the transcript price, so a refused parent's
|
|
87
|
+
// following calls cost one small file read each.
|
|
88
|
+
async function refusal_for(
|
|
89
|
+
transcript: string,
|
|
90
|
+
now_ms: number,
|
|
91
|
+
state: ParentCutState,
|
|
92
|
+
): Promise<string | undefined> {
|
|
93
|
+
const stamp = hook_decision.create_refusal_stamp(STAMP_PREFIX)
|
|
94
|
+
const target = stamp.path(transcript)
|
|
95
|
+
|
|
96
|
+
if (is_quiet(target, now_ms) || !(await is_due(state))) return undefined
|
|
97
|
+
|
|
98
|
+
return stamp.record(target, now_ms) ? PARENT_CUT_REASON : undefined
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
// A delegated unit's call names the *parent's* transcript and runs under the parent's process, so the
|
|
102
|
+
// carry record reads as owned here; only `agent_id` tells it apart (`hook-decision.ts`). Refusing it
|
|
103
|
+
// would hand the unit a hand-off it cannot take and spend the parent's quiet window on it.
|
|
104
|
+
function is_unit_call(raw_payload: string): boolean {
|
|
105
|
+
try {
|
|
106
|
+
return (hook_decision.parse_hook_payload(raw_payload)?.agent_id ?? '') !== ''
|
|
107
|
+
} catch {
|
|
108
|
+
return false
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
function is_exempt(raw_payload: string): boolean {
|
|
113
|
+
return !hook_decision.is_switch_enabled(SWITCH_ENV_KEY) || is_unit_call(raw_payload)
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
/**
|
|
117
|
+
* The deny reason for a call a `backlogrun` parent makes past the shared threshold, or `undefined` when
|
|
118
|
+
* the call proceeds — a lane child, a delegated unit, a person's session, a handed-off or capped
|
|
119
|
+
* record, an `under` or unmeasurable verdict, or a call inside the quiet window after a refusal.
|
|
120
|
+
*/
|
|
121
|
+
async function parent_cut_reason(
|
|
122
|
+
raw_payload: string,
|
|
123
|
+
now_ms: number = Date.now(),
|
|
124
|
+
state: ParentCutState = current_state(),
|
|
125
|
+
): Promise<string | undefined> {
|
|
126
|
+
if (is_exempt(raw_payload)) return undefined
|
|
127
|
+
|
|
128
|
+
const transcript = run_watcher_hook.transcript_of(raw_payload)
|
|
129
|
+
|
|
130
|
+
if (transcript === undefined) return undefined
|
|
131
|
+
|
|
132
|
+
return await refusal_for(transcript, now_ms, state)
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
const run_parent_cut_hook = {
|
|
136
|
+
PARENT_CUT_REASON,
|
|
137
|
+
QUIET_WINDOW_MS,
|
|
138
|
+
STAMP_PREFIX,
|
|
139
|
+
SWITCH_ENV_KEY,
|
|
140
|
+
parent_cut_reason,
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
export type { ParentCutState }
|
|
144
|
+
export { run_parent_cut_hook }
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { cli_flags } from '#scripts/lib/cli-flags'
|
|
2
2
|
import { run_progress_config } from './run-progress-config'
|
|
3
3
|
|
|
4
4
|
// The argument half of `josh run:progress`, kept apart from the watcher so the CLI stays inside the file
|
|
@@ -37,11 +37,7 @@ interface ParsedValues {
|
|
|
37
37
|
}
|
|
38
38
|
|
|
39
39
|
function read_arguments(argv: ReadonlyArray<string>): ParsedValues | undefined {
|
|
40
|
-
|
|
41
|
-
return parseArgs({ args: [...argv], options: OPTIONS }).values
|
|
42
|
-
} catch {
|
|
43
|
-
return undefined
|
|
44
|
-
}
|
|
40
|
+
return cli_flags.parse_or_undefined({ args: [...argv], options: OPTIONS })?.values
|
|
45
41
|
}
|
|
46
42
|
|
|
47
43
|
// A hand-typed `--interval` outranks the environment, the environment outranks the interval the
|
|
@@ -5,6 +5,7 @@ import { backlog_arrival, type ArrivalProbe } from '#scripts/backlog/backlog-arr
|
|
|
5
5
|
import { backlog_ready } from '#scripts/backlog/backlog-ready'
|
|
6
6
|
import { gh_spawn } from '#scripts/gh/gh-spawn'
|
|
7
7
|
import { lane_child_marker } from '#scripts/lane/lane-child-marker'
|
|
8
|
+
import { error_text } from '#scripts/lib/error-message'
|
|
8
9
|
import { run_event_stream_emit } from './run-event-stream-emit'
|
|
9
10
|
import { run_progress, type ProgressState } from './run-progress'
|
|
10
11
|
import { run_progress_args, type ParsedValues } from './run-progress-args'
|
|
@@ -229,7 +230,7 @@ function decline(loop: WatchLoop, now_ms: number, notice: string): WatchLoop {
|
|
|
229
230
|
// under load — the very load this command exists to report — would otherwise take the reporting down
|
|
230
231
|
// silently for the rest of an unattended run, with the parent not waiting on it and nothing saying why.
|
|
231
232
|
function to_notice(error: unknown): string {
|
|
232
|
-
return `${FAILED_TICK_PREFIX} ${
|
|
233
|
+
return `${FAILED_TICK_PREFIX} ${error_text.message_of(error)}`
|
|
233
234
|
}
|
|
234
235
|
|
|
235
236
|
async function attempt(
|
|
@@ -78,7 +78,7 @@ async function to_child(issue: OpenIssueData): Promise<ChildObservation> {
|
|
|
78
78
|
const number = String(issue.number)
|
|
79
79
|
const child = await run_preflight.read_child_state(number)
|
|
80
80
|
|
|
81
|
-
return { issue: number, labels: to_labels(issue), pr_state: child.pr_state }
|
|
81
|
+
return { issue: number, title: issue.title, labels: to_labels(issue), pr_state: child.pr_state }
|
|
82
82
|
}
|
|
83
83
|
|
|
84
84
|
async function read_children(repo: string): Promise<ReadonlyArray<ChildObservation> | undefined> {
|
|
@@ -171,6 +171,7 @@ async function read_observations(request: ObservationRequest): Promise<Observati
|
|
|
171
171
|
if (children.length === 0 && !(await has_run_started(lanes))) return IDLE_READ
|
|
172
172
|
|
|
173
173
|
const observations: Observations = {
|
|
174
|
+
repo: request.repo,
|
|
174
175
|
children,
|
|
175
176
|
lanes,
|
|
176
177
|
load_average: loadavg()[FIRST_LOAD_AVERAGE] ?? 0,
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import { issue_cite, type IssueCiter } from '#scripts/issue/issue-cite'
|
|
2
|
+
|
|
1
3
|
// `josh run:progress` — the one line an unattended run prints when it has gone quiet, and the clock
|
|
2
4
|
// that decides when that is (joshuafolkken/kit#1520).
|
|
3
5
|
//
|
|
@@ -56,6 +58,9 @@ const DEFAULT_INTERVAL_MS = DEFAULT_INTERVAL_MINUTES * MS_PER_MINUTE
|
|
|
56
58
|
// a second spelling of it — `none` / `open` / `merged` / `closed`.
|
|
57
59
|
interface ChildObservation {
|
|
58
60
|
issue: string
|
|
61
|
+
// Kept from the `in-progress` listing the child was read from, so the line cites it in full
|
|
62
|
+
// (joshuafolkken/kit#2943) without a read per child.
|
|
63
|
+
title: string
|
|
59
64
|
labels: ReadonlyArray<string>
|
|
60
65
|
pr_state: string
|
|
61
66
|
}
|
|
@@ -66,6 +71,8 @@ interface LaneObservation {
|
|
|
66
71
|
}
|
|
67
72
|
|
|
68
73
|
interface Observations {
|
|
74
|
+
// `owner/repo`, the repository the listing was read from — what every printed issue links into.
|
|
75
|
+
repo: string
|
|
69
76
|
children: ReadonlyArray<ChildObservation>
|
|
70
77
|
lanes: ReadonlyArray<LaneObservation>
|
|
71
78
|
load_average: number
|
|
@@ -195,26 +202,32 @@ function next_state(
|
|
|
195
202
|
return { key, unchanged_since_ms: now_ms }
|
|
196
203
|
}
|
|
197
204
|
|
|
198
|
-
|
|
205
|
+
// The issue leads as a citation rather than a bare `#N` (joshuafolkken/kit#2943): the parent relays these
|
|
206
|
+
// lines verbatim, and a bare number copied into a reply is what the Stop guard sends back.
|
|
207
|
+
function format_child(child: ChildObservation, cite: IssueCiter): string {
|
|
199
208
|
const labels = child.labels.length > 0 ? child.labels.join(',') : '(no labels)'
|
|
200
209
|
|
|
201
|
-
return
|
|
210
|
+
return `${cite(child.issue)} ${labels} PR:${child.pr_state}`
|
|
202
211
|
}
|
|
203
212
|
|
|
204
213
|
// An empty set is the pre-label stage, said as such rather than left as a blank slot between the
|
|
205
214
|
// separators (joshuafolkken/kit#1900). After that change a `read_observations` result carries no
|
|
206
215
|
// children exactly when a run has started and none has reached `in-progress` yet, so this is the one
|
|
207
216
|
// place the two cases are told apart on the line.
|
|
208
|
-
function format_children(children: ReadonlyArray<ChildObservation
|
|
217
|
+
function format_children(children: ReadonlyArray<ChildObservation>, repo: string): string {
|
|
209
218
|
if (children.length === 0) return NO_CHILD_YET
|
|
219
|
+
const cite = issue_cite.citer(repo, new Map(children.map((child) => [child.issue, child.title])))
|
|
210
220
|
|
|
211
|
-
return children.map((child) => format_child(child)).join(' · ')
|
|
221
|
+
return children.map((child) => format_child(child, cite)).join(' · ')
|
|
212
222
|
}
|
|
213
223
|
|
|
214
|
-
|
|
224
|
+
// A lane is linked without its title: a lane in flight is a child the line above already cited in full,
|
|
225
|
+
// so repeating the title here would only lengthen the line.
|
|
226
|
+
function format_lanes(lanes: ReadonlyArray<LaneObservation>, repo: string): string {
|
|
215
227
|
if (lanes.length === 0) return NO_LANES
|
|
228
|
+
const cite = issue_cite.citer(repo, new Map())
|
|
216
229
|
|
|
217
|
-
return lanes.map((lane) => `${lane.issue}:${lane.state}`).join(' ')
|
|
230
|
+
return lanes.map((lane) => `${cite(lane.issue)}:${lane.state}`).join(' ')
|
|
218
231
|
}
|
|
219
232
|
|
|
220
233
|
// Absent is said, never filled in. A run started without a transcript path prints `unread` here, which
|
|
@@ -303,8 +316,8 @@ function format_line(observations: Observations, timing: LineTiming): string {
|
|
|
303
316
|
observed_at: format_observed_at(timing.now_ms),
|
|
304
317
|
quiet: format_minutes(timing.now_ms - timing.quiet_since_ms),
|
|
305
318
|
unchanged: format_minutes(timing.now_ms - timing.unchanged_since_ms),
|
|
306
|
-
children: format_children(observations.children),
|
|
307
|
-
lanes: format_lanes(observations.lanes),
|
|
319
|
+
children: format_children(observations.children, observations.repo),
|
|
320
|
+
lanes: format_lanes(observations.lanes, observations.repo),
|
|
308
321
|
load: observations.load_average.toFixed(1),
|
|
309
322
|
record: format_record(observations.record_age_ms),
|
|
310
323
|
next: format_next_report(timing.now_ms, timing.interval_ms),
|
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
#!/usr/bin/env tsx
|
|
2
2
|
import { fileURLToPath } from 'node:url'
|
|
3
3
|
import { parseArgs } from 'node:util'
|
|
4
|
-
import { josh_command, type JoshResult } from '#scripts/josh/josh-run'
|
|
5
4
|
import { lane_child_marker } from '#scripts/lane/lane-child-marker'
|
|
6
5
|
import { run_event_stream } from './run-event-stream'
|
|
7
6
|
import { run_event_stream_emit } from './run-event-stream-emit'
|
|
@@ -9,8 +8,8 @@ import { run_ship, type ShipSection } from './run-ship'
|
|
|
9
8
|
import { run_ship_detach } from './run-ship-detach'
|
|
10
9
|
import { run_ship_probe } from './run-ship-probe'
|
|
11
10
|
import { run_ship_return } from './run-ship-return'
|
|
12
|
-
import { run_ship_review_steps } from './run-ship-review-steps'
|
|
13
11
|
import { run_ship_stage, type Phase, type ShipState, type Stage } from './run-ship-stage'
|
|
12
|
+
import { run_ship_steps, type ShipArguments, type Step } from './run-ship-steps'
|
|
14
13
|
|
|
15
14
|
// `josh ship "<title> #<N>"` — one call for the fixed commit-to-report region a run ships a change on
|
|
16
15
|
// (joshuafolkken/kit#2398). The loop used to spend a round trip each on `gate`, `git -y`, `followup`
|
|
@@ -67,7 +66,6 @@ const USAGE =
|
|
|
67
66
|
const NO_LOG_NOTE = 'no detached ship supervisor log for this issue'
|
|
68
67
|
const LANE_CHILD_DETACH_NOTE =
|
|
69
68
|
'lane child: shipping through the detached supervisor (--detach implied), so the end of this turn cannot kill it'
|
|
70
|
-
const should_forward_stderr = true
|
|
71
69
|
|
|
72
70
|
type NotifyValues = Partial<Record<(typeof NOTIFY_OPTIONS)[number], string>>
|
|
73
71
|
|
|
@@ -84,24 +82,8 @@ const OPTIONS = {
|
|
|
84
82
|
cite: { type: 'string', multiple: true },
|
|
85
83
|
} as const
|
|
86
84
|
|
|
87
|
-
interface ShipArguments {
|
|
88
|
-
title: string
|
|
89
|
-
number: string
|
|
90
|
-
notify: ReadonlyArray<string>
|
|
91
|
-
body: ReadonlyArray<string>
|
|
92
|
-
cites: ReadonlyArray<string>
|
|
93
|
-
is_review: boolean
|
|
94
|
-
is_detach: boolean
|
|
95
|
-
}
|
|
96
|
-
|
|
97
85
|
type ShipCommand = { kind: 'ship'; args: ShipArguments } | { kind: 'log'; number: string }
|
|
98
86
|
|
|
99
|
-
interface Step {
|
|
100
|
-
stage: Stage
|
|
101
|
-
header: string
|
|
102
|
-
run: (args: ShipArguments, state: ShipState) => Promise<JoshResult>
|
|
103
|
-
}
|
|
104
|
-
|
|
105
87
|
// What a resumed ship knows before its first stage: the record's path (absent outside a repository),
|
|
106
88
|
// the stages it says completed, and the repository's actual state (`run-ship-stage.ts` decides from it).
|
|
107
89
|
interface ShipContext {
|
|
@@ -124,58 +106,7 @@ function body_arguments(path: string | undefined): ReadonlyArray<string> {
|
|
|
124
106
|
return path === undefined ? [] : [`--${BODY_FILE_OPTION}`, path]
|
|
125
107
|
}
|
|
126
108
|
|
|
127
|
-
const {
|
|
128
|
-
|
|
129
|
-
// The four steps in the order a change ships: the gate before the commit, the commit/push/PR before
|
|
130
|
-
// the merge, the merge before the report bookkeeping. The commit step carries the `--skip-*` flags a
|
|
131
|
-
// resumed ship needs, so an existing commit or push is never made twice.
|
|
132
|
-
async function josh(argv: ReadonlyArray<string>): Promise<JoshResult> {
|
|
133
|
-
return await josh_command.josh_run(argv, should_forward_stderr)
|
|
134
|
-
}
|
|
135
|
-
|
|
136
|
-
const COMMIT_STEPS: ReadonlyArray<Step> = [
|
|
137
|
-
{ stage: STAGE.GATE, header: run_ship.GATE_HEADER, run: async () => await josh(['gate']) },
|
|
138
|
-
{
|
|
139
|
-
stage: STAGE.COMMIT,
|
|
140
|
-
header: run_ship.COMMIT_HEADER,
|
|
141
|
-
run: async (args, state) =>
|
|
142
|
-
await josh(['git', '-y', ...run_ship_stage.commit_flags(state), ...args.body, args.title]),
|
|
143
|
-
},
|
|
144
|
-
]
|
|
145
|
-
|
|
146
|
-
const MERGE_STEPS: ReadonlyArray<Step> = [
|
|
147
|
-
{
|
|
148
|
-
stage: STAGE.FOLLOWUP,
|
|
149
|
-
header: run_ship.FOLLOWUP_HEADER,
|
|
150
|
-
run: async (args) => await josh(['followup', args.title, ...args.notify]),
|
|
151
|
-
},
|
|
152
|
-
{
|
|
153
|
-
stage: STAGE.REPORT,
|
|
154
|
-
header: run_ship.REPORT_HEADER,
|
|
155
|
-
run: async (args) => await josh(['run:tail', args.number, ...args.cites]),
|
|
156
|
-
},
|
|
157
|
-
]
|
|
158
|
-
|
|
159
|
-
// `--review` (joshuafolkken/kit#2427) puts the supervised round-1 review in front of the gate: it
|
|
160
|
-
// launches the gate itself, so the gate stage that follows reuses that tree's green record. It also
|
|
161
|
-
// puts the round-2 pass between the commit and the followup (joshuafolkken/kit#2489), so the PR opens
|
|
162
|
-
// between the rounds and round 2 runs beside CI — a no-op when round 1 left no fix delta.
|
|
163
|
-
const REVIEW_STEP: Step = {
|
|
164
|
-
stage: STAGE.REVIEW,
|
|
165
|
-
header: run_ship.REVIEW_HEADER,
|
|
166
|
-
run: async (args) => await run_ship_review_steps.review_stage(args.number),
|
|
167
|
-
}
|
|
168
|
-
const ROUND_TWO_STEP: Step = {
|
|
169
|
-
stage: STAGE.ROUND_TWO,
|
|
170
|
-
header: run_ship.ROUND_TWO_HEADER,
|
|
171
|
-
run: async (args) => await run_ship_review_steps.round_two_stage(args.number),
|
|
172
|
-
}
|
|
173
|
-
|
|
174
|
-
function steps(args: ShipArguments): ReadonlyArray<Step> {
|
|
175
|
-
if (!args.is_review) return [...COMMIT_STEPS, ...MERGE_STEPS]
|
|
176
|
-
|
|
177
|
-
return [REVIEW_STEP, ...COMMIT_STEPS, ROUND_TWO_STEP, ...MERGE_STEPS]
|
|
178
|
-
}
|
|
109
|
+
const { PHASE } = run_ship_stage
|
|
179
110
|
|
|
180
111
|
function issue_number(title: string): string | undefined {
|
|
181
112
|
return TRAILING_ISSUE_PATTERN.exec(title)?.[NUMBER_GROUP]
|
|
@@ -211,6 +142,7 @@ function ship_args(
|
|
|
211
142
|
number,
|
|
212
143
|
notify: notify_arguments(values),
|
|
213
144
|
body: body_arguments(values[BODY_FILE_OPTION]),
|
|
145
|
+
body_path: values[BODY_FILE_OPTION],
|
|
214
146
|
cites,
|
|
215
147
|
is_review: values.review === true,
|
|
216
148
|
is_detach: values.detach === true,
|
|
@@ -328,14 +260,14 @@ async function stopped(
|
|
|
328
260
|
return sections
|
|
329
261
|
}
|
|
330
262
|
|
|
331
|
-
// Run the
|
|
263
|
+
// Run the stages in order, stopping at the first that failed: a red gate never reaches the commit, so
|
|
332
264
|
// the returned sections end at the failure the report names. A ship that reached the end clears its
|
|
333
|
-
// record, so the next ship of the same issue starts from the
|
|
265
|
+
// record, so the next ship of the same issue starts from the preflight.
|
|
334
266
|
async function ship(args: ShipArguments): Promise<ReadonlyArray<ShipSection>> {
|
|
335
267
|
const context = await open_context(args)
|
|
336
268
|
const sections: Array<ShipSection> = []
|
|
337
269
|
|
|
338
|
-
for (const step of steps(args)) {
|
|
270
|
+
for (const step of run_ship_steps.steps(args)) {
|
|
339
271
|
const section = await run_stage(step, args, context)
|
|
340
272
|
|
|
341
273
|
sections.push(section)
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
import { readFile } from 'node:fs/promises'
|
|
2
|
+
import { changed_paths } from '#scripts/git/changed-paths'
|
|
3
|
+
import { git_branch } from '#scripts/git/git-branch'
|
|
4
|
+
import { git_gh_command } from '#scripts/git/git-gh-command'
|
|
5
|
+
import { git_preflight } from '#scripts/git/git-preflight'
|
|
6
|
+
import type { JoshResult } from '#scripts/josh/josh-run'
|
|
7
|
+
import { error_text } from '#scripts/lib/error-message'
|
|
8
|
+
import { live_evidence } from '#scripts/review/live-evidence'
|
|
9
|
+
import { run_ship_scoped } from './run-ship-scoped'
|
|
10
|
+
|
|
11
|
+
// `josh ship`'s first stage (joshuafolkken/kit#2946). A detached ship used to meet the pull request's
|
|
12
|
+
// preconditions only at the commit stage — `git -y`'s preflight — or at the merge — `followup`'s
|
|
13
|
+
// live-evidence refusal — each after the review and the gate had already run, so every such stop
|
|
14
|
+
// relaunched a session to repeat them. **Every precondition decidable before the review is asked here,
|
|
15
|
+
// and all of them are reported at once.** No check is new: the classification, issue and branch answers
|
|
16
|
+
// are `git_preflight.problems_of`, and the evidence answer is `live_evidence.verdict_for` over the same
|
|
17
|
+
// changed paths and the body this ship will deliver — `--body-file` when given, else the open PR's.
|
|
18
|
+
//
|
|
19
|
+
// The scoped pair runs last, met rather than stopped on (`run-ship-scoped.ts`), so the review and the
|
|
20
|
+
// gate start on a tree both checks are green on.
|
|
21
|
+
|
|
22
|
+
const SUCCESS_EXIT_CODE = 0
|
|
23
|
+
const FAILURE_EXIT_CODE = 1
|
|
24
|
+
const EVIDENCE_PROBLEM = `This change touches runtime code, but the PR body has no ${live_evidence.EVIDENCE_HEADING} section: run the acceptance criteria for real and pass the body with --body-file <path>.`
|
|
25
|
+
|
|
26
|
+
interface PreflightRequest {
|
|
27
|
+
title: string
|
|
28
|
+
body_path: string | undefined
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
async function body_text(body_path: string | undefined): Promise<string | undefined> {
|
|
32
|
+
if (body_path !== undefined) return await readFile(body_path, 'utf8')
|
|
33
|
+
|
|
34
|
+
return await git_gh_command.pr_get_body(await git_branch.current())
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
async function evidence_problems(body_path: string | undefined): Promise<Array<string>> {
|
|
38
|
+
const [paths, body] = await Promise.all([
|
|
39
|
+
changed_paths.read_changed_paths(false),
|
|
40
|
+
body_text(body_path),
|
|
41
|
+
])
|
|
42
|
+
|
|
43
|
+
return live_evidence.verdict_for(paths, body) === 'required' ? [EVIDENCE_PROBLEM] : []
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function refusal(problems: ReadonlyArray<string>): JoshResult {
|
|
47
|
+
const lines = problems.map((problem) => ` - ${problem}`)
|
|
48
|
+
const head = `${String(problems.length)} pull-request precondition(s) unmet before the review and the gate:`
|
|
49
|
+
|
|
50
|
+
return { code: FAILURE_EXIT_CODE, out: [head, ...lines].join('\n') }
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
// The reads here run in the ship's own process, where the commit stage's ran in a `git -y` child: a
|
|
54
|
+
// throw — a `--body-file` path that does not exist, a git read that failed — would escape `ship()`
|
|
55
|
+
// past the stop a supervised ship hands back on, so it is reported as one more unmet precondition.
|
|
56
|
+
// Each side is caught on its own, so one side's failed read never hides what the other side found.
|
|
57
|
+
async function caught(problems: Promise<Array<string>>): Promise<Array<string>> {
|
|
58
|
+
try {
|
|
59
|
+
return await problems
|
|
60
|
+
} catch (error) {
|
|
61
|
+
return [error_text.message_of(error)]
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
async function asked_problems(request: PreflightRequest): Promise<Array<string>> {
|
|
66
|
+
const [preflight, evidence] = await Promise.all([
|
|
67
|
+
caught(git_preflight.problems_of({ cli_input: request.title, will_open_pr: true })),
|
|
68
|
+
caught(evidence_problems(request.body_path)),
|
|
69
|
+
])
|
|
70
|
+
|
|
71
|
+
return [...preflight, ...evidence]
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
async function stage(request: PreflightRequest): Promise<JoshResult> {
|
|
75
|
+
const problems = await asked_problems(request)
|
|
76
|
+
|
|
77
|
+
if (problems.length > 0) return refusal(problems)
|
|
78
|
+
|
|
79
|
+
const scoped = await run_ship_scoped.scoped_pair()
|
|
80
|
+
|
|
81
|
+
return scoped.code === SUCCESS_EXIT_CODE ? { code: SUCCESS_EXIT_CODE, out: 'ready' } : scoped
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
const run_ship_preflight = { EVIDENCE_PROBLEM, stage }
|
|
85
|
+
|
|
86
|
+
export type { PreflightRequest }
|
|
87
|
+
export { run_ship_preflight }
|
|
@@ -1,7 +1,5 @@
|
|
|
1
1
|
import { agent_argv, type AgentArgv } from '#scripts/agent/agent-argv'
|
|
2
2
|
import { agent_role_profile, type AgentProfile } from '#scripts/agent/agent-role-profile'
|
|
3
|
-
import { gate_tree } from '#scripts/gate/gate-tree'
|
|
4
|
-
import { scoped_green } from '#scripts/gate/scoped-green'
|
|
5
3
|
import { PROJECT_ROOT } from '#scripts/init/init-paths'
|
|
6
4
|
import { josh_command, type JoshResult } from '#scripts/josh/josh-run'
|
|
7
5
|
import { stamp_file } from '#scripts/josh/stamp-file'
|
|
@@ -9,6 +7,7 @@ import { lane_child_marker } from '#scripts/lane/lane-child-marker'
|
|
|
9
7
|
import { openai_review_broker } from '#scripts/lane/openai-review-broker'
|
|
10
8
|
import { detached_launch } from './detached-launch'
|
|
11
9
|
import { run_ship_review, type RoundOutcome, type ScoredVerdict } from './run-ship-review'
|
|
10
|
+
import { run_ship_scoped, type Phase } from './run-ship-scoped'
|
|
12
11
|
|
|
13
12
|
// The side effects of `josh ship --review` (joshuafolkken/kit#2427): the round-1 review the chain used
|
|
14
13
|
// to open, launch, join, attest and record across five agent turns, run by the supervisor beside the
|
|
@@ -47,7 +46,10 @@ const FIXED_JOIN_NOTE =
|
|
|
47
46
|
'the round-1 gate read the tree before the review fixes — drained, not judged; the gate stage re-runs it.'
|
|
48
47
|
const ROUND_TWO_SKIPPED_NOTE = 'round 2 not due — nothing left to verify; shipping on.'
|
|
49
48
|
|
|
50
|
-
|
|
49
|
+
// The scoped pair's precondition, met by the supervisor itself rather than stopped on
|
|
50
|
+
// (joshuafolkken/kit#2500), single-sourced with the preflight and gate stages (joshuafolkken/kit#2946).
|
|
51
|
+
const { run_phases, scoped_pair } = run_ship_scoped
|
|
52
|
+
|
|
51
53
|
type PromptOf = (brief_path: string, findings_path: string) => string
|
|
52
54
|
|
|
53
55
|
function failure(out: string): JoshResult {
|
|
@@ -153,17 +155,6 @@ async function record_and_route(
|
|
|
153
155
|
return outcome.is_passing ? { code: SUCCESS_EXIT_CODE, out } : failure(out)
|
|
154
156
|
}
|
|
155
157
|
|
|
156
|
-
async function run_phases(phases: ReadonlyArray<Phase>): Promise<JoshResult> {
|
|
157
|
-
let last: JoshResult = { code: SUCCESS_EXIT_CODE, out: '' }
|
|
158
|
-
|
|
159
|
-
for (const phase of phases) {
|
|
160
|
-
last = await phase()
|
|
161
|
-
if (last.code !== SUCCESS_EXIT_CODE) return last
|
|
162
|
-
}
|
|
163
|
-
|
|
164
|
-
return last
|
|
165
|
-
}
|
|
166
|
-
|
|
167
158
|
// A brief-minting command, then a reviewer handed what it printed.
|
|
168
159
|
function reviewed(
|
|
169
160
|
open: ReadonlyArray<string>,
|
|
@@ -185,18 +176,6 @@ function reviewed(
|
|
|
185
176
|
]
|
|
186
177
|
}
|
|
187
178
|
|
|
188
|
-
// The scoped pair's precondition, met by the supervisor itself rather than stopped on
|
|
189
|
-
// (joshuafolkken/kit#2500): only the checks with no green record for this tree run — the same
|
|
190
|
-
// question `review:brief` and the local gate refuse on — so a fresh record costs nothing and a stale
|
|
191
|
-
// or absent one costs a scoped run instead of a stop and a relaunched session. A check that genuinely
|
|
192
|
-
// fails still stops the ship, its output forwarded to the ship log.
|
|
193
|
-
async function scoped_pair(): Promise<JoshResult> {
|
|
194
|
-
const tree = await gate_tree.read_gate_tree()
|
|
195
|
-
const scripts = scoped_green.missing_scripts(tree.files, tree.base)
|
|
196
|
-
|
|
197
|
-
return await run_phases(scripts.map((script) => async () => await josh([script])))
|
|
198
|
-
}
|
|
199
|
-
|
|
200
179
|
// Fixes made in place are counted only once the scoped pair is green on the fixed tree. The reviewer is
|
|
201
180
|
// asked to leave it green; this is the check that it did, and a fix it could not get green is routed as
|
|
202
181
|
// the finding it still is — recorded, then stopped before the gate and the commit take it in.
|
|
@@ -154,6 +154,14 @@ function round_one_outcome(verdict: ScoredVerdict): RoundOutcome {
|
|
|
154
154
|
return { is_passing: true, note: verdict.kind === VERDICT.FIXED ? FIXED_NOTE : CLEAN_NOTE }
|
|
155
155
|
}
|
|
156
156
|
|
|
157
|
+
// Whether a findings file records a clean round 1 (joshuafolkken/kit#2945). A resumed round 1 narrows to
|
|
158
|
+
// the delta only past a review that found nothing to fix. A blocking one has a finding in a file the
|
|
159
|
+
// delta may not touch. A fixed one approved its own edits, not the briefed tree the delta is measured
|
|
160
|
+
// from, so a file reverted to that tree would drop out of the delta with its finding back in it.
|
|
161
|
+
function is_clean_round_one(text: string | undefined): boolean {
|
|
162
|
+
return read_verdict(text).kind === VERDICT.CLEAN
|
|
163
|
+
}
|
|
164
|
+
|
|
157
165
|
// Round 2 is final and reads only, so it passes clean or Low-only alone — a fix it made would itself be
|
|
158
166
|
// unreviewed.
|
|
159
167
|
function round_two_outcome(verdict: ScoredVerdict): RoundOutcome {
|
|
@@ -166,6 +174,7 @@ const run_ship_review = {
|
|
|
166
174
|
FIXED_PREFIX,
|
|
167
175
|
UNVERIFIED_OUTCOME,
|
|
168
176
|
VERDICT,
|
|
177
|
+
is_clean_round_one,
|
|
169
178
|
read_verdict,
|
|
170
179
|
reviewer_prompt,
|
|
171
180
|
round_one_outcome,
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import { gate_tree } from '#scripts/gate/gate-tree'
|
|
2
|
+
import { scoped_green } from '#scripts/gate/scoped-green'
|
|
3
|
+
import { josh_command, type JoshResult } from '#scripts/josh/josh-run'
|
|
4
|
+
|
|
5
|
+
// The scoped pair `josh ship` meets itself rather than stops on (joshuafolkken/kit#2500), shared by
|
|
6
|
+
// every stage that needs a green record for this tree (joshuafolkken/kit#2946): the preflight stage at
|
|
7
|
+
// the start, the review rounds, and the gate stage — a reviewer may have edited the tree after round
|
|
8
|
+
// 1's pair ran, and `josh gate` refuses a tree with no green record.
|
|
9
|
+
|
|
10
|
+
const SUCCESS_EXIT_CODE = 0
|
|
11
|
+
const should_forward_stderr = true
|
|
12
|
+
|
|
13
|
+
type Phase = () => Promise<JoshResult>
|
|
14
|
+
|
|
15
|
+
async function run_phases(phases: ReadonlyArray<Phase>): Promise<JoshResult> {
|
|
16
|
+
let last: JoshResult = { code: SUCCESS_EXIT_CODE, out: '' }
|
|
17
|
+
|
|
18
|
+
for (const phase of phases) {
|
|
19
|
+
last = await phase()
|
|
20
|
+
if (last.code !== SUCCESS_EXIT_CODE) return last
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
return last
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
// Only the checks with no green record for this tree run — the same question `review:brief` and the
|
|
27
|
+
// local gate refuse on — so a fresh record costs nothing and a stale or absent one costs a scoped run
|
|
28
|
+
// instead of a stop and a relaunched session. A check that genuinely fails still stops the ship, its
|
|
29
|
+
// output forwarded to the ship log.
|
|
30
|
+
async function scoped_pair(): Promise<JoshResult> {
|
|
31
|
+
const tree = await gate_tree.read_gate_tree()
|
|
32
|
+
const scripts = scoped_green.missing_scripts(tree.files, tree.base)
|
|
33
|
+
|
|
34
|
+
return await run_phases(
|
|
35
|
+
scripts.map(
|
|
36
|
+
(script) => async () => await josh_command.josh_run([script], should_forward_stderr),
|
|
37
|
+
),
|
|
38
|
+
)
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
const run_ship_scoped = { run_phases, scoped_pair }
|
|
42
|
+
|
|
43
|
+
export type { Phase }
|
|
44
|
+
export { run_ship_scoped }
|
|
@@ -17,6 +17,7 @@ import { z } from 'zod'
|
|
|
17
17
|
// re-run on an unchanged tree a reuse rather than a second execution. No quality gate is folded away.
|
|
18
18
|
|
|
19
19
|
const STAGE = {
|
|
20
|
+
PREFLIGHT: 'preflight',
|
|
20
21
|
REVIEW: 'review',
|
|
21
22
|
GATE: 'gate',
|
|
22
23
|
COMMIT: 'commit',
|
|
@@ -78,7 +79,7 @@ function mark_done(target: string, stage: Stage): void {
|
|
|
78
79
|
stamp_file.replace_stamp(target, { done: [...done] })
|
|
79
80
|
}
|
|
80
81
|
|
|
81
|
-
// Removed once the report stage completes, so a later ship of the same issue starts from the
|
|
82
|
+
// Removed once the report stage completes, so a later ship of the same issue starts from the preflight.
|
|
82
83
|
function clear(target: string): void {
|
|
83
84
|
stamp_file.remove_stamp(target)
|
|
84
85
|
}
|
|
@@ -90,6 +91,9 @@ function is_shipped(state: ShipState): boolean {
|
|
|
90
91
|
}
|
|
91
92
|
|
|
92
93
|
const DONE_BY: Record<Stage, (done: ReadonlySet<string>, state: ShipState) => boolean> = {
|
|
94
|
+
// The preflight (joshuafolkken/kit#2946) asks what the commit stage would refuse on, so a commit is
|
|
95
|
+
// its output as it is the gate's; a resumed commit stage still asks `git -y`'s own preflight.
|
|
96
|
+
[STAGE.PREFLIGHT]: (_done, state) => state.is_merged || state.is_committed,
|
|
93
97
|
// The `--review` round (joshuafolkken/kit#2427) precedes the commit, so a commit is its output as it
|
|
94
98
|
// is the gate's. A record alone is not honored: before a commit the tree may have been edited since
|
|
95
99
|
// the recorded round, and only a commit pins the tree that round read.
|