vouchington-tooling 0.52.0 → 0.53.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/README.md +27 -0
- package/dist/cli/commands/worktree.d.mts +2 -0
- package/dist/cli/commands/worktree.mjs +21 -0
- package/dist/cli/index.mjs +3 -0
- package/dist/cli/parse-forwarded-commands.d.mts +3 -0
- package/dist/cli/parse-forwarded-commands.mjs +2 -0
- package/dist/cli/parse.d.mts +3 -0
- package/dist/cli/usage.d.mts +1 -1
- package/dist/cli/usage.mjs +6 -0
- package/dist/git-worktrees/assess.d.mts +14 -0
- package/dist/git-worktrees/assess.mjs +68 -0
- package/dist/git-worktrees/list.d.mts +10 -0
- package/dist/git-worktrees/list.mjs +34 -0
- package/dist/git-worktrees/reclaim.d.mts +15 -0
- package/dist/git-worktrees/reclaim.mjs +80 -0
- package/dist/git-worktrees/reclaim.test-helpers.d.mts +16 -0
- package/dist/git-worktrees/reclaim.test-helpers.mjs +113 -0
- package/dist/git-worktrees/run.d.mts +13 -0
- package/dist/git-worktrees/run.mjs +50 -0
- package/package.json +1 -1
- package/skills/manifest.json +20 -20
- package/skills/stacked-prs/SKILL.md +19 -13
- package/skills/stacked-prs/references/drain.md +26 -19
- package/skills/triage-prs/SKILL.md +4 -4
package/README.md
CHANGED
|
@@ -230,6 +230,33 @@ Host-lock environment:
|
|
|
230
230
|
`git_worktree_canonical_path_hash <path>` helper resolves the physical path and prints a stable
|
|
231
231
|
`d` plus the first 12 lowercase hexadecimal characters of its SHA-256 digest.
|
|
232
232
|
|
|
233
|
+
## Reclaiming finished worktrees
|
|
234
|
+
|
|
235
|
+
`vouchington worktree reclaim [--yes] [--json]` removes git worktrees whose work is done. It is a
|
|
236
|
+
dry run unless `--yes` is passed. It prints one line per worktree (`reclaimed`, `would reclaim`, or
|
|
237
|
+
`kept: <reason>`, control characters escaped), or `{ dryRun, worktrees }` with `--json`.
|
|
238
|
+
|
|
239
|
+
It skips the main checkout, the caller's own worktree, and locked, bare, detached and missing
|
|
240
|
+
worktrees. Any other worktree is kept, with a reason, unless all of these hold:
|
|
241
|
+
|
|
242
|
+
- `git status` (untracked files and submodules included) is empty, and no index entry is
|
|
243
|
+
assume-unchanged or skip-worktree.
|
|
244
|
+
- `gh pr list --head <branch>` has a merged or closed pull request from this repository (not a
|
|
245
|
+
fork) whose `headRefOid` equals HEAD, and no open pull request from this repository. That PR also
|
|
246
|
+
proves HEAD is published, so squash merges whose branch was deleted are reclaimed. A `gh` failure,
|
|
247
|
+
or a query returning 1000 results, keeps the worktree.
|
|
248
|
+
|
|
249
|
+
With `--yes`, HEAD and cleanliness are checked again right before `git worktree remove <path>` runs.
|
|
250
|
+
It is never forced, and a refusal from git is reported as kept. Ignored files (for example `.env`, `node_modules`, build output) are deleted with the worktree, exactly as `git worktree remove` does; keep anything you need outside ignored paths or commit it. An unexpected error on one worktree is reported as `kept: error: <message>` and the run carries on. There is no `prune`, no branch
|
|
251
|
+
deletion, and git runs with `core.fsmonitor=false`.
|
|
252
|
+
|
|
253
|
+
Known limitations: git cannot check and remove a worktree atomically, so a worktree that changes in the moment between the final check and `git worktree remove` can still be removed; the final check runs immediately before removal to keep that window small.
|
|
254
|
+
|
|
255
|
+
This is NOT a security boundary. Run it as a trusted user or agent, and do not grant it as an
|
|
256
|
+
unconditional allow rule. It has no teardown hook: per-worktree resources such as databases and
|
|
257
|
+
containers are reclaimed afterwards by the consumer's own orphan cleanup, because the worktree is
|
|
258
|
+
gone.
|
|
259
|
+
|
|
233
260
|
## Library
|
|
234
261
|
|
|
235
262
|
```ts
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { formatReport, reclaimWorktrees } from "../../git-worktrees/reclaim.mjs";
|
|
2
|
+
import { commandUsage } from "../usage.mjs";
|
|
3
|
+
/** `vouchington worktree reclaim [--yes] [--json]`: exits 2 on bad usage, 1 when git cannot list. */
|
|
4
|
+
export function runWorktreeCommand(args, env = process.env, cwd = process.cwd()) {
|
|
5
|
+
const [subcommand, ...flags] = args;
|
|
6
|
+
const unknown = flags.find((flag) => flag !== '--yes' && flag !== '--json');
|
|
7
|
+
if (subcommand !== 'reclaim' || unknown !== undefined) {
|
|
8
|
+
process.stderr.write(`vouchington: expected: reclaim [--yes] [--json]\n${commandUsage('worktree')}`);
|
|
9
|
+
return 2;
|
|
10
|
+
}
|
|
11
|
+
try {
|
|
12
|
+
const report = reclaimWorktrees(flags.includes('--yes'), { cwd, env });
|
|
13
|
+
const json = flags.includes('--json');
|
|
14
|
+
process.stdout.write(json ? `${JSON.stringify(report)}\n` : formatReport(report));
|
|
15
|
+
return 0;
|
|
16
|
+
}
|
|
17
|
+
catch (error) {
|
|
18
|
+
process.stderr.write(`vouchington: ${error.message}\n`);
|
|
19
|
+
return 1;
|
|
20
|
+
}
|
|
21
|
+
}
|
package/dist/cli/index.mjs
CHANGED
|
@@ -20,6 +20,7 @@ import { runLinkSkill } from "./commands/link-skill.mjs";
|
|
|
20
20
|
import { runRetrospectiveFactsCommand } from "./commands/retrospective-facts.mjs";
|
|
21
21
|
import { runAgentBlackboardCommand } from "./commands/agent-blackboard.mjs";
|
|
22
22
|
import { runMcpCommand } from "./commands/mcp.mjs";
|
|
23
|
+
import { runWorktreeCommand } from "./commands/worktree.mjs";
|
|
23
24
|
import { runWithHostLock } from "./commands/with-host-lock.mjs";
|
|
24
25
|
import { runFreshBase } from "./commands/fresh-base.mjs";
|
|
25
26
|
import { runRequireUpToDate } from "./commands/require-up-to-date.mjs";
|
|
@@ -134,6 +135,8 @@ export function runCli(argv = process.argv) {
|
|
|
134
135
|
return runAgentBlackboardCommand(parsed.args);
|
|
135
136
|
case 'mcp':
|
|
136
137
|
return runMcpCommand(parsed.args);
|
|
138
|
+
case 'worktree':
|
|
139
|
+
return runWorktreeCommand(parsed.args);
|
|
137
140
|
case 'require-up-to-date':
|
|
138
141
|
return runRequireUpToDate(parsed);
|
|
139
142
|
case 'fresh-base':
|
|
@@ -37,6 +37,9 @@ type ForwardedCommand = {
|
|
|
37
37
|
} | {
|
|
38
38
|
kind: 'mcp';
|
|
39
39
|
args: string[];
|
|
40
|
+
} | {
|
|
41
|
+
kind: 'worktree';
|
|
42
|
+
args: string[];
|
|
40
43
|
};
|
|
41
44
|
export declare function parseForwardedCommand(command: string | undefined, args: string[]): ForwardedCommand | undefined;
|
|
42
45
|
export {};
|
package/dist/cli/parse.d.mts
CHANGED
package/dist/cli/usage.d.mts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
export declare const USAGE = "Usage: vouchington <command> [options]\n\nCommands:\n runner-port-policy Print or validate a runner port policy\n with-host-lock Run a command under a host-wide lock\n gha-runtime-audit Audit successful GitHub Actions job runtimes\n require-up-to-date Require HEAD to include a fetched remote branch\n fresh-base Classify the worktree against a remote base branch\n gitleaks-directory-scan Scan a directory with Gitleaks\n ast-grep-examples Run AST-grep rule examples\n ast-grep-pack Print shipped unconditional AST-grep pack paths as JSON\n gha-workspace-policy Check GitHub Actions workspace safety policy\n gha-output Write a collision-safe multiline GITHUB_OUTPUT record\n gha-needs-results Fail if required GitHub Actions job results failed\n download-with-diagnostics Download a URL and report HTTP status on failure\n download-optional-run-artifacts Download optional artifacts from the current run\n host-pressure-diagnostics Print a bounded host memory/OOM/PSI snapshot\n allocate-browser-safe-ports Allocate Fetch-safe localhost ports\n diagnose-port-collision Capture bounded localhost port diagnostics\n prepare-trivy-db Download the Trivy vulnerability database\n gha-artifacts-cleanup Delete classified GitHub Actions artifacts\n http-origin Validate an optional HTTP(S) origin\n vitest-blob-manifest Stamp a vitest-blob-manifest:v1 identity file\n vitest-report-attempt Write or read a Vitest report-attempt marker\n prepare-vitest-reports Validate and select Vitest report JSON files\n pnpm-install Install a pnpm workspace with retry and release-age fail-fast\n check-cache-size Measure a path and decide whether to save a GHA cache\n make-shard-matrix Emit a [1..N] GitHub Actions shard matrix\n load-runner-env Overlay a runner env file onto GITHUB_ENV with injection guards\n clean-workspace Reset a persistent-runner workspace with a fork-PR trust gate\n install-github-release Download a checksum-verified GitHub Release binary\n run-with-timeout Run a command with GNU timeout or a Perl fallback\n lint-links Two-pass lychee: internal links fail, external warn\n materialize-pr-context Dump PR title/body/files/diff/comments and #N crawl\n wait-for-apt-locks Wait until apt/dpkg lock files are free\n install-playwright-chromium-arm64 Install Playwright Chromium from browsers.json\n ghcr-package-retention Delete old GHCR package versions (KEEP_MIN or --main-history)\n harness-admission-lane Compute a GITHUB_RUN_ID admission lane for fleet fan-out\n harness-assert-gates Fail if any named HARNESS_*_ENABLED gate is enabled\n gha-collaborator-trust Classify authors as write-access collaborators or not\n nuget-central-version Validate a Directory.Packages.props PackageVersion delta\n swift-semantic-equal Compare Swift sources ignoring comments and whitespace\n post-review Post one COMMENT review from a staged payload file\n stage-review-payload Validate a review payload file into a staging directory\n retrospective-transcript Format facts from Claude-compatible, Codex, or Grok transcripts\n link-skill Link one packaged skill into an explicit consumer directory\n retrospective-facts Gather immutable facts for a retrospective\n agent-blackboard Probe and journal an Agent Blackboard deployment\n mcp Serve the agent-blackboard journal tools over stdio (MCP)\n\nOptions:\n -h, --help Show this help\n -v, --version Print the package version\n\nrunner-port-policy\n (no args) Print the shipped policy as JSON\n --file <path> Validate and print a policy file\n --reserved <port> Print true if the port is reserved\n\nwith-host-lock\n --name <family>\n [--slots <n>]\n --timeout-seconds <n>\n [--command-timeout-seconds <n>]\n [--failure-diagnostics <absolute-script>]\n [--on-acquire-timeout fail|run-unlocked]\n -- <command> [args...]\n\ngha-runtime-audit\n [--repository owner/name] Default GITHUB_REPOSITORY\n [--branch main]\n [--median-threshold-floor <seconds>] Off unless set. Must be below the ceiling\n [--median-threshold-ceiling <seconds>] Default 360. Flags a five-sample median above this\n --pr-workflow <name|/regex/> Repeatable\n --push-workflow <name|/regex/> Repeatable\n\nrequire-up-to-date --remote <name> --branch <name>\nfresh-base [--remote <name>] [--branch <name>] [--skip-fetch]\ngitleaks-directory-scan --config <path> [--directory <path>]\nast-grep-examples --rules <directory> --config <path>\nast-grep-pack\ngha-workspace-policy [--root <directory>] [--workflow-directory <directory>] [--action-directory <directory>]\n\ngha-output <name>\ngha-needs-results [label]\ndownload-with-diagnostics <url> <destination> [-- curl-args...]\ndownload-optional-run-artifacts (--name <name>... | --pattern <pattern>) --dir <directory>\nhost-pressure-diagnostics\nallocate-browser-safe-ports <count> [--policy path] [--forbidden-ports path]\ndiagnose-port-collision [--ports \"2200 2216\"] [--output-dir PATH]\nprepare-trivy-db\ngha-artifacts-cleanup run --run-id <id> [--keep-pattern glob] [--delete-pattern glob] [--patterns-file json]\ngha-artifacts-cleanup sweep --older-than-hours <n> [--keep-pattern glob] [--delete-pattern glob] [--patterns-file json]\nhttp-origin [--field NAME] [value]\nvitest-blob-manifest <suite> [reports-directory]\nvitest-report-attempt <write DIRECTORY SUITE|read ROOT>\nprepare-vitest-reports [primary-directory] [fallback-directory] [output-directory]\npnpm-install --runner-lifecycle persistent|ephemeral|ephemeral-full --install-scripts true|false\ncheck-cache-size <path> <max-bytes> <label>\nmake-shard-matrix <total>\nload-runner-env\nclean-workspace\ninstall-github-release --repo owner/name --version X --asset 'name-{platform}.tar.gz' --bin name [--tag-prefix PREFIX] [--expected-sha256 SHA256] [--no-checksum] [--checksums-asset NAME] [--version-flag FLAG] [--bin-dir DIR]\nrun-with-timeout <timeout-seconds> <kill-after-seconds> <command...>\nlint-links [--offline] [--config PATH] [--glob PATTERN] [files...]\nmaterialize-pr-context\nwait-for-apt-locks\ninstall-playwright-chromium-arm64 [name:archive...]\nghcr-package-retention [--main-history] [--apply|--dry-run] [<url-encoded-package>...]\nharness-admission-lane <lanes>\nharness-assert-gates <gate>...\ngha-collaborator-trust <owner/repo> [--allow-bot <login>]... < authors.json\nnuget-central-version <trusted-props> <candidate-props> <metadata-json> <output-props>\nswift-semantic-equal <base> <head> <file.swift>\npost-review\nstage-review-payload optional|required <source> <destination>\nretrospective-transcript [--session-id ID] [--jsonl PATH] [--projects-dir PATH] [--codex-sessions-dir PATH] [--grok-sessions-dir PATH]\nlink-skill <name> --source-root <skills-dir> --target-root <consumer-skills-dir> Link a packaged or repository-local skill\nretrospective-facts (--pr NUMBER | --branch NAME | --no-pr) [--repo OWNER/NAME] [--raw]\nagent-blackboard probe\nagent-blackboard session-start [--skip-env NAME] [--setup-hint TEXT] [--docs-path PATH] [--restore-hint TEXT] [--timeout-ms N] (SessionStart hook; payload on stdin)\nagent-blackboard journal append --mode interactive|autonomous --source-event-id ID --work-outcome OUTCOME --coverage-status STATUS --session-id UUID --agent NAME --version VERSION --file PATH --repository OWNER/NAME [--repository OWNER/NAME ...] [--parent-session-id UUID] [--timestamp ISO8601] [--coverage-source SOURCE,...] [--dropped-count N] [--outbox-directory PATH]\nagent-blackboard journal entries --session-id UUID\nagent-blackboard journal status --session-id UUID --outbox-directory PATH\nagent-blackboard journal flush --outbox-directory PATH [--session-id UUID]\nInteractive journal append requires --outbox-directory PATH.\nagent-blackboard journal is deprecated and being phased out: agents journal through the mcp server.\nagent-blackboard snapshot partition --snapshot PATH --checksum SHA256 --counts '{\"sessions\":N,\"entries\":N,\"records\":N,\"bytes\":N}'\nagent-blackboard snapshot cleanup [--snapshot PATH] [--partition-directory PATH --receipt JSON]\nmcp\n (no args) Serve journal_append, journal_entries, outbox_status, outbox_flush,\n session_ensure, retrospective_check, retrospective_save,\n pr_validate, pr_create, pr_update,\n snapshot_export, snapshot_cleanup, and session_archive over stdio.\n Needs the optional @modelcontextprotocol/sdk peer (and zod).\n Launch it from inside the repository's git worktree.\n";
|
|
1
|
+
export declare const USAGE = "Usage: vouchington <command> [options]\n\nCommands:\n runner-port-policy Print or validate a runner port policy\n with-host-lock Run a command under a host-wide lock\n gha-runtime-audit Audit successful GitHub Actions job runtimes\n require-up-to-date Require HEAD to include a fetched remote branch\n fresh-base Classify the worktree against a remote base branch\n gitleaks-directory-scan Scan a directory with Gitleaks\n ast-grep-examples Run AST-grep rule examples\n ast-grep-pack Print shipped unconditional AST-grep pack paths as JSON\n gha-workspace-policy Check GitHub Actions workspace safety policy\n gha-output Write a collision-safe multiline GITHUB_OUTPUT record\n gha-needs-results Fail if required GitHub Actions job results failed\n download-with-diagnostics Download a URL and report HTTP status on failure\n download-optional-run-artifacts Download optional artifacts from the current run\n host-pressure-diagnostics Print a bounded host memory/OOM/PSI snapshot\n allocate-browser-safe-ports Allocate Fetch-safe localhost ports\n diagnose-port-collision Capture bounded localhost port diagnostics\n prepare-trivy-db Download the Trivy vulnerability database\n gha-artifacts-cleanup Delete classified GitHub Actions artifacts\n http-origin Validate an optional HTTP(S) origin\n vitest-blob-manifest Stamp a vitest-blob-manifest:v1 identity file\n vitest-report-attempt Write or read a Vitest report-attempt marker\n prepare-vitest-reports Validate and select Vitest report JSON files\n pnpm-install Install a pnpm workspace with retry and release-age fail-fast\n check-cache-size Measure a path and decide whether to save a GHA cache\n make-shard-matrix Emit a [1..N] GitHub Actions shard matrix\n load-runner-env Overlay a runner env file onto GITHUB_ENV with injection guards\n clean-workspace Reset a persistent-runner workspace with a fork-PR trust gate\n install-github-release Download a checksum-verified GitHub Release binary\n run-with-timeout Run a command with GNU timeout or a Perl fallback\n lint-links Two-pass lychee: internal links fail, external warn\n materialize-pr-context Dump PR title/body/files/diff/comments and #N crawl\n wait-for-apt-locks Wait until apt/dpkg lock files are free\n install-playwright-chromium-arm64 Install Playwright Chromium from browsers.json\n ghcr-package-retention Delete old GHCR package versions (KEEP_MIN or --main-history)\n harness-admission-lane Compute a GITHUB_RUN_ID admission lane for fleet fan-out\n harness-assert-gates Fail if any named HARNESS_*_ENABLED gate is enabled\n gha-collaborator-trust Classify authors as write-access collaborators or not\n nuget-central-version Validate a Directory.Packages.props PackageVersion delta\n swift-semantic-equal Compare Swift sources ignoring comments and whitespace\n post-review Post one COMMENT review from a staged payload file\n stage-review-payload Validate a review payload file into a staging directory\n retrospective-transcript Format facts from Claude-compatible, Codex, or Grok transcripts\n link-skill Link one packaged skill into an explicit consumer directory\n retrospective-facts Gather immutable facts for a retrospective\n agent-blackboard Probe and journal an Agent Blackboard deployment\n mcp Serve the agent-blackboard journal tools over stdio (MCP)\n worktree Reclaim finished git worktrees (dry run unless --yes)\n\nOptions:\n -h, --help Show this help\n -v, --version Print the package version\n\nrunner-port-policy\n (no args) Print the shipped policy as JSON\n --file <path> Validate and print a policy file\n --reserved <port> Print true if the port is reserved\n\nwith-host-lock\n --name <family>\n [--slots <n>]\n --timeout-seconds <n>\n [--command-timeout-seconds <n>]\n [--failure-diagnostics <absolute-script>]\n [--on-acquire-timeout fail|run-unlocked]\n -- <command> [args...]\n\ngha-runtime-audit\n [--repository owner/name] Default GITHUB_REPOSITORY\n [--branch main]\n [--median-threshold-floor <seconds>] Off unless set. Must be below the ceiling\n [--median-threshold-ceiling <seconds>] Default 360. Flags a five-sample median above this\n --pr-workflow <name|/regex/> Repeatable\n --push-workflow <name|/regex/> Repeatable\n\nrequire-up-to-date --remote <name> --branch <name>\nfresh-base [--remote <name>] [--branch <name>] [--skip-fetch]\ngitleaks-directory-scan --config <path> [--directory <path>]\nast-grep-examples --rules <directory> --config <path>\nast-grep-pack\ngha-workspace-policy [--root <directory>] [--workflow-directory <directory>] [--action-directory <directory>]\n\ngha-output <name>\ngha-needs-results [label]\ndownload-with-diagnostics <url> <destination> [-- curl-args...]\ndownload-optional-run-artifacts (--name <name>... | --pattern <pattern>) --dir <directory>\nhost-pressure-diagnostics\nallocate-browser-safe-ports <count> [--policy path] [--forbidden-ports path]\ndiagnose-port-collision [--ports \"2200 2216\"] [--output-dir PATH]\nprepare-trivy-db\ngha-artifacts-cleanup run --run-id <id> [--keep-pattern glob] [--delete-pattern glob] [--patterns-file json]\ngha-artifacts-cleanup sweep --older-than-hours <n> [--keep-pattern glob] [--delete-pattern glob] [--patterns-file json]\nhttp-origin [--field NAME] [value]\nvitest-blob-manifest <suite> [reports-directory]\nvitest-report-attempt <write DIRECTORY SUITE|read ROOT>\nprepare-vitest-reports [primary-directory] [fallback-directory] [output-directory]\npnpm-install --runner-lifecycle persistent|ephemeral|ephemeral-full --install-scripts true|false\ncheck-cache-size <path> <max-bytes> <label>\nmake-shard-matrix <total>\nload-runner-env\nclean-workspace\ninstall-github-release --repo owner/name --version X --asset 'name-{platform}.tar.gz' --bin name [--tag-prefix PREFIX] [--expected-sha256 SHA256] [--no-checksum] [--checksums-asset NAME] [--version-flag FLAG] [--bin-dir DIR]\nrun-with-timeout <timeout-seconds> <kill-after-seconds> <command...>\nlint-links [--offline] [--config PATH] [--glob PATTERN] [files...]\nmaterialize-pr-context\nwait-for-apt-locks\ninstall-playwright-chromium-arm64 [name:archive...]\nghcr-package-retention [--main-history] [--apply|--dry-run] [<url-encoded-package>...]\nharness-admission-lane <lanes>\nharness-assert-gates <gate>...\ngha-collaborator-trust <owner/repo> [--allow-bot <login>]... < authors.json\nnuget-central-version <trusted-props> <candidate-props> <metadata-json> <output-props>\nswift-semantic-equal <base> <head> <file.swift>\npost-review\nstage-review-payload optional|required <source> <destination>\nretrospective-transcript [--session-id ID] [--jsonl PATH] [--projects-dir PATH] [--codex-sessions-dir PATH] [--grok-sessions-dir PATH]\nlink-skill <name> --source-root <skills-dir> --target-root <consumer-skills-dir> Link a packaged or repository-local skill\nretrospective-facts (--pr NUMBER | --branch NAME | --no-pr) [--repo OWNER/NAME] [--raw]\nagent-blackboard probe\nagent-blackboard session-start [--skip-env NAME] [--setup-hint TEXT] [--docs-path PATH] [--restore-hint TEXT] [--timeout-ms N] (SessionStart hook; payload on stdin)\nagent-blackboard journal append --mode interactive|autonomous --source-event-id ID --work-outcome OUTCOME --coverage-status STATUS --session-id UUID --agent NAME --version VERSION --file PATH --repository OWNER/NAME [--repository OWNER/NAME ...] [--parent-session-id UUID] [--timestamp ISO8601] [--coverage-source SOURCE,...] [--dropped-count N] [--outbox-directory PATH]\nagent-blackboard journal entries --session-id UUID\nagent-blackboard journal status --session-id UUID --outbox-directory PATH\nagent-blackboard journal flush --outbox-directory PATH [--session-id UUID]\nInteractive journal append requires --outbox-directory PATH.\nagent-blackboard journal is deprecated and being phased out: agents journal through the mcp server.\nagent-blackboard snapshot partition --snapshot PATH --checksum SHA256 --counts '{\"sessions\":N,\"entries\":N,\"records\":N,\"bytes\":N}'\nagent-blackboard snapshot cleanup [--snapshot PATH] [--partition-directory PATH --receipt JSON]\nmcp\n (no args) Serve journal_append, journal_entries, outbox_status, outbox_flush,\n session_ensure, retrospective_check, retrospective_save,\n pr_validate, pr_create, pr_update,\n snapshot_export, snapshot_cleanup, and session_archive over stdio.\n Needs the optional @modelcontextprotocol/sdk peer (and zod).\n Launch it from inside the repository's git worktree.\n\nworktree reclaim [--yes] [--json]\n Dry run by default. Removes clean, fully pushed worktrees whose same-repository PR for the\n branch is merged or closed at exactly HEAD, using git worktree remove (never forced).\n Skips the main checkout, the current worktree, and locked, bare or detached worktrees.\n";
|
|
2
2
|
export declare function printUsage(stream?: NodeJS.WritableStream): void;
|
|
3
3
|
export declare function commandNames(usage?: string): string[];
|
|
4
4
|
export declare function commandUsage(command: string, usage?: string): string;
|
package/dist/cli/usage.mjs
CHANGED
|
@@ -47,6 +47,7 @@ Commands:
|
|
|
47
47
|
retrospective-facts Gather immutable facts for a retrospective
|
|
48
48
|
agent-blackboard Probe and journal an Agent Blackboard deployment
|
|
49
49
|
mcp Serve the agent-blackboard journal tools over stdio (MCP)
|
|
50
|
+
worktree Reclaim finished git worktrees (dry run unless --yes)
|
|
50
51
|
|
|
51
52
|
Options:
|
|
52
53
|
-h, --help Show this help
|
|
@@ -134,6 +135,11 @@ mcp
|
|
|
134
135
|
snapshot_export, snapshot_cleanup, and session_archive over stdio.
|
|
135
136
|
Needs the optional @modelcontextprotocol/sdk peer (and zod).
|
|
136
137
|
Launch it from inside the repository's git worktree.
|
|
138
|
+
|
|
139
|
+
worktree reclaim [--yes] [--json]
|
|
140
|
+
Dry run by default. Removes clean, fully pushed worktrees whose same-repository PR for the
|
|
141
|
+
branch is merged or closed at exactly HEAD, using git worktree remove (never forced).
|
|
142
|
+
Skips the main checkout, the current worktree, and locked, bare or detached worktrees.
|
|
137
143
|
`;
|
|
138
144
|
export function printUsage(stream = process.stdout) {
|
|
139
145
|
stream.write(USAGE);
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { type RunOptions } from './run.mts';
|
|
2
|
+
export type Assessment = {
|
|
3
|
+
readonly head: string;
|
|
4
|
+
} | {
|
|
5
|
+
readonly reason: string;
|
|
6
|
+
};
|
|
7
|
+
export declare function readRepository(main: RunOptions): string | undefined;
|
|
8
|
+
/** Clean status and no hidden index entries. Returns the HEAD commit or a reason. */
|
|
9
|
+
export declare function assessLocal(worktree: RunOptions): Assessment;
|
|
10
|
+
/**
|
|
11
|
+
* Needs a merged or closed same-repository PR for this branch whose head is exactly HEAD. That
|
|
12
|
+
* also proves HEAD is published on GitHub, so deleted branches (squash merges) need no remote ref.
|
|
13
|
+
*/
|
|
14
|
+
export declare function assessPullRequest(branch: string, head: string, repository: string | undefined, main: RunOptions): string | undefined;
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
import { git, LOCAL_ENV, run, withoutVariables } from "./run.mjs";
|
|
2
|
+
const FINISHED = new Set(['MERGED', 'CLOSED']);
|
|
3
|
+
function parseJson(text) {
|
|
4
|
+
try {
|
|
5
|
+
return text === undefined ? undefined : JSON.parse(text);
|
|
6
|
+
}
|
|
7
|
+
catch {
|
|
8
|
+
return undefined;
|
|
9
|
+
}
|
|
10
|
+
}
|
|
11
|
+
/** Repo-local git variables, GH_REPO and GH_HOST must not redirect the lookup away from the main checkout. */
|
|
12
|
+
function runGh(args, main) {
|
|
13
|
+
const env = withoutVariables(main.env, [...LOCAL_ENV, 'GH_REPO', 'GH_HOST']);
|
|
14
|
+
return run('gh', args, { cwd: main.cwd, env });
|
|
15
|
+
}
|
|
16
|
+
export function readRepository(main) {
|
|
17
|
+
const repo = parseJson(runGh(['repo', 'view', '--json', 'nameWithOwner'], main));
|
|
18
|
+
return repo?.nameWithOwner?.toLowerCase();
|
|
19
|
+
}
|
|
20
|
+
/** Clean status and no hidden index entries. Returns the HEAD commit or a reason. */
|
|
21
|
+
export function assessLocal(worktree) {
|
|
22
|
+
const status = git(['status', '--porcelain', '--untracked-files=all', '--ignore-submodules=none'], worktree);
|
|
23
|
+
if (status === undefined)
|
|
24
|
+
return { reason: 'cannot read status' };
|
|
25
|
+
if (status !== '')
|
|
26
|
+
return { reason: 'uncommitted changes' };
|
|
27
|
+
const files = git(['ls-files', '-v', '-z'], worktree);
|
|
28
|
+
if (files === undefined)
|
|
29
|
+
return { reason: 'cannot read index' };
|
|
30
|
+
if (files.split('\0').some((entry) => /^[a-zS]/.test(entry))) {
|
|
31
|
+
return { reason: 'hidden index entries (assume-unchanged or skip-worktree)' };
|
|
32
|
+
}
|
|
33
|
+
const headOutput = git(['rev-parse', 'HEAD'], worktree);
|
|
34
|
+
if (headOutput === undefined)
|
|
35
|
+
return { reason: 'cannot read HEAD' };
|
|
36
|
+
const head = headOutput.trim();
|
|
37
|
+
return { head };
|
|
38
|
+
}
|
|
39
|
+
const PAGE = 1000;
|
|
40
|
+
function listPulls(branch, state, main) {
|
|
41
|
+
const fields = 'state,headRefOid,isCrossRepository,headRepository,headRepositoryOwner';
|
|
42
|
+
const args = ['pr', 'list', '--head', branch, '--state', state, '--limit', String(PAGE)];
|
|
43
|
+
const listed = parseJson(runGh([...args, '--json', fields], main));
|
|
44
|
+
return Array.isArray(listed) && listed.length < PAGE ? listed : undefined;
|
|
45
|
+
}
|
|
46
|
+
/**
|
|
47
|
+
* Needs a merged or closed same-repository PR for this branch whose head is exactly HEAD. That
|
|
48
|
+
* also proves HEAD is published on GitHub, so deleted branches (squash merges) need no remote ref.
|
|
49
|
+
*/
|
|
50
|
+
export function assessPullRequest(branch, head, repository, main) {
|
|
51
|
+
if (repository === undefined)
|
|
52
|
+
return 'cannot read repository';
|
|
53
|
+
// Open PRs are queried on their own so a long history cannot hide one; a full page is unreadable.
|
|
54
|
+
const open = listPulls(branch, 'open', main);
|
|
55
|
+
const all = listPulls(branch, 'all', main);
|
|
56
|
+
if (open === undefined || all === undefined)
|
|
57
|
+
return 'cannot read PR state';
|
|
58
|
+
const own = [...open, ...all].filter((pr) => pr.isCrossRepository === false &&
|
|
59
|
+
`${pr.headRepositoryOwner?.login}/${pr.headRepository?.name}`.toLowerCase() === repository);
|
|
60
|
+
if (own.some((pr) => pr.state === 'OPEN'))
|
|
61
|
+
return 'open PR';
|
|
62
|
+
const finished = own.filter((pr) => FINISHED.has(pr.state));
|
|
63
|
+
if (finished.length === 0)
|
|
64
|
+
return 'no finished PR from this repository';
|
|
65
|
+
if (!finished.some((pr) => pr.headRefOid === head))
|
|
66
|
+
return 'PR head differs from HEAD';
|
|
67
|
+
return undefined;
|
|
68
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
export interface WorktreeRecord {
|
|
2
|
+
readonly path: string;
|
|
3
|
+
readonly branch?: string;
|
|
4
|
+
readonly head?: string;
|
|
5
|
+
readonly locked: boolean;
|
|
6
|
+
readonly bare: boolean;
|
|
7
|
+
readonly detached: boolean;
|
|
8
|
+
}
|
|
9
|
+
/** Parses `git worktree list --porcelain -z` (NUL fields, an empty field ends each record). */
|
|
10
|
+
export declare function parseWorktreeList(porcelain: string | undefined): WorktreeRecord[];
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
const BRANCH_PREFIX = 'branch refs/heads/';
|
|
2
|
+
function parseRecord(fields) {
|
|
3
|
+
const path = fields.find((field) => field.startsWith('worktree '))?.slice('worktree '.length);
|
|
4
|
+
if (path === undefined)
|
|
5
|
+
return undefined;
|
|
6
|
+
const branch = fields
|
|
7
|
+
.find((field) => field.startsWith(BRANCH_PREFIX))
|
|
8
|
+
?.slice(BRANCH_PREFIX.length);
|
|
9
|
+
const head = fields.find((field) => field.startsWith('HEAD '))?.slice('HEAD '.length);
|
|
10
|
+
return {
|
|
11
|
+
path,
|
|
12
|
+
locked: fields.some((field) => field === 'locked' || field.startsWith('locked ')),
|
|
13
|
+
bare: fields.includes('bare'),
|
|
14
|
+
detached: fields.includes('detached'),
|
|
15
|
+
...(branch === undefined ? {} : { branch }),
|
|
16
|
+
...(head === undefined ? {} : { head }),
|
|
17
|
+
};
|
|
18
|
+
}
|
|
19
|
+
/** Parses `git worktree list --porcelain -z` (NUL fields, an empty field ends each record). */
|
|
20
|
+
export function parseWorktreeList(porcelain) {
|
|
21
|
+
const records = [];
|
|
22
|
+
let fields = [];
|
|
23
|
+
for (const field of [...(porcelain ?? '').split('\0'), '']) {
|
|
24
|
+
if (field !== '') {
|
|
25
|
+
fields.push(field);
|
|
26
|
+
continue;
|
|
27
|
+
}
|
|
28
|
+
const record = parseRecord(fields);
|
|
29
|
+
if (record !== undefined)
|
|
30
|
+
records.push(record);
|
|
31
|
+
fields = [];
|
|
32
|
+
}
|
|
33
|
+
return records;
|
|
34
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { type RunOptions } from './run.mts';
|
|
2
|
+
interface ReclaimEntry {
|
|
3
|
+
readonly path: string;
|
|
4
|
+
readonly branch: string;
|
|
5
|
+
readonly status: 'reclaimed' | 'would reclaim' | 'kept';
|
|
6
|
+
readonly reason?: string;
|
|
7
|
+
}
|
|
8
|
+
export interface ReclaimReport {
|
|
9
|
+
readonly dryRun: boolean;
|
|
10
|
+
readonly worktrees: ReclaimEntry[];
|
|
11
|
+
}
|
|
12
|
+
/** Lists finished worktrees and, with `yes`, removes them with a plain (never forced) `git worktree remove`. */
|
|
13
|
+
export declare function reclaimWorktrees(yes: boolean, runtime: RunOptions): ReclaimReport;
|
|
14
|
+
export declare function formatReport(report: ReclaimReport): string;
|
|
15
|
+
export {};
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
import { existsSync, realpathSync } from 'node:fs';
|
|
2
|
+
import { sep } from 'node:path';
|
|
3
|
+
import { assessLocal, assessPullRequest, readRepository } from "./assess.mjs";
|
|
4
|
+
import { parseWorktreeList } from "./list.mjs";
|
|
5
|
+
import { git } from "./run.mjs";
|
|
6
|
+
/** Assesses (and with `yes` removes) one worktree; undefined means it is not a candidate. */
|
|
7
|
+
function reclaimOne(record, context) {
|
|
8
|
+
const { path, branch, locked, bare, detached } = record;
|
|
9
|
+
if (locked || bare || detached || branch === undefined)
|
|
10
|
+
return undefined;
|
|
11
|
+
if (!existsSync(path) || realpathSync(path) === context.selfReal)
|
|
12
|
+
return undefined;
|
|
13
|
+
const keep = (reason) => ({ path, branch, status: 'kept', reason });
|
|
14
|
+
const nests = (paths) => paths.some((other) => other.startsWith(path + sep));
|
|
15
|
+
if (nests(context.registered)) {
|
|
16
|
+
return keep('contains another worktree');
|
|
17
|
+
}
|
|
18
|
+
const worktree = { cwd: path, env: context.runtime.env };
|
|
19
|
+
const local = assessLocal(worktree);
|
|
20
|
+
if ('reason' in local)
|
|
21
|
+
return keep(local.reason);
|
|
22
|
+
const reason = assessPullRequest(branch, local.head, context.repository, context.mainRun);
|
|
23
|
+
if (reason !== undefined)
|
|
24
|
+
return keep(reason);
|
|
25
|
+
if (!context.yes)
|
|
26
|
+
return { path, branch, status: 'would reclaim' };
|
|
27
|
+
const again = assessLocal(worktree);
|
|
28
|
+
const pullAgain = assessPullRequest(branch, local.head, context.repository, context.mainRun);
|
|
29
|
+
const listed = parseWorktreeList(git(['worktree', 'list', '--porcelain', '-z'], context.runtime));
|
|
30
|
+
const same = listed.some((entry) => entry.path === path && entry.branch === branch && entry.head === local.head);
|
|
31
|
+
if (nests(listed.map((entry) => entry.path)))
|
|
32
|
+
return keep('contains another worktree');
|
|
33
|
+
if ('reason' in again || again.head !== local.head || pullAgain !== undefined || !same)
|
|
34
|
+
return keep('changed since check');
|
|
35
|
+
if (git(['worktree', 'remove', path], { ...context.mainRun, timeout: 0 }) === undefined) {
|
|
36
|
+
return keep('git worktree remove refused');
|
|
37
|
+
}
|
|
38
|
+
return { path, branch, status: 'reclaimed' };
|
|
39
|
+
}
|
|
40
|
+
/** Lists finished worktrees and, with `yes`, removes them with a plain (never forced) `git worktree remove`. */
|
|
41
|
+
export function reclaimWorktrees(yes, runtime) {
|
|
42
|
+
const listing = git(['worktree', 'list', '--porcelain', '-z'], runtime);
|
|
43
|
+
const self = git(['rev-parse', '--show-toplevel'], runtime)?.replace(/\n$/, '');
|
|
44
|
+
const [main, ...others] = parseWorktreeList(listing);
|
|
45
|
+
if (main === undefined || !self)
|
|
46
|
+
throw new Error('not inside a git worktree');
|
|
47
|
+
const mainRun = { cwd: main.path, env: runtime.env };
|
|
48
|
+
const context = {
|
|
49
|
+
yes,
|
|
50
|
+
runtime,
|
|
51
|
+
mainRun,
|
|
52
|
+
selfReal: realpathSync(self),
|
|
53
|
+
registered: [main, ...others].map((record) => record.path),
|
|
54
|
+
repository: readRepository(mainRun),
|
|
55
|
+
};
|
|
56
|
+
const worktrees = [];
|
|
57
|
+
for (const record of others) {
|
|
58
|
+
try {
|
|
59
|
+
const entry = reclaimOne(record, context);
|
|
60
|
+
if (entry !== undefined)
|
|
61
|
+
worktrees.push(entry);
|
|
62
|
+
}
|
|
63
|
+
catch (error) {
|
|
64
|
+
const reason = `error: ${error.message}`;
|
|
65
|
+
worktrees.push({ path: record.path, branch: record.branch, status: 'kept', reason });
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
return { dryRun: !yes, worktrees };
|
|
69
|
+
}
|
|
70
|
+
function escape(text) {
|
|
71
|
+
return text.replace(/\p{Cc}/gu, (char) => `\\x${char.charCodeAt(0).toString(16).padStart(2, '0')}`);
|
|
72
|
+
}
|
|
73
|
+
export function formatReport(report) {
|
|
74
|
+
return report.worktrees
|
|
75
|
+
.map((entry) => {
|
|
76
|
+
const status = entry.status === 'kept' ? `kept: ${entry.reason}` : entry.status;
|
|
77
|
+
return `${escape(entry.path)}: ${escape(status)}\n`;
|
|
78
|
+
})
|
|
79
|
+
.join('');
|
|
80
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
export interface Fixture {
|
|
2
|
+
readonly root: string;
|
|
3
|
+
readonly main: string;
|
|
4
|
+
readonly caller: string;
|
|
5
|
+
readonly env: NodeJS.ProcessEnv;
|
|
6
|
+
git(cwd: string, ...args: string[]): string;
|
|
7
|
+
/** Adds a worktree on a new branch with one commit; optionally pushes it to origin. */
|
|
8
|
+
addWorktree(name: string, options?: {
|
|
9
|
+
push?: boolean;
|
|
10
|
+
dir?: string;
|
|
11
|
+
}): string;
|
|
12
|
+
/** Writes the fake `gh pr list` answer; a PR with no head defaults to the branch tip. */
|
|
13
|
+
setPrs(branch: string, prs: object[]): void;
|
|
14
|
+
cleanup(): void;
|
|
15
|
+
}
|
|
16
|
+
export declare function createFixture(): Fixture;
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
import { execFileSync } from 'node:child_process';
|
|
2
|
+
import { chmodSync, mkdirSync, mkdtempSync, realpathSync, rmSync, writeFileSync } from 'node:fs';
|
|
3
|
+
import { tmpdir } from 'node:os';
|
|
4
|
+
import { join } from 'node:path';
|
|
5
|
+
// Answers `gh repo view` and `gh pr list --head <branch>` from files, like the real CLI.
|
|
6
|
+
const FAKE_GH = `#!${process.execPath}
|
|
7
|
+
const fs = require('node:fs')
|
|
8
|
+
const args = process.argv.slice(2)
|
|
9
|
+
const env = process.env
|
|
10
|
+
if (env.FAKE_GH_FAIL) process.exit(1)
|
|
11
|
+
if (env.FAKE_GH_NO_GIT_DIR && env.GIT_DIR) process.exit(1)
|
|
12
|
+
if (args[0] === 'repo') {
|
|
13
|
+
console.log(JSON.stringify({ nameWithOwner: env.GH_REPO || 'Acme/Widgets' }))
|
|
14
|
+
process.exit(0)
|
|
15
|
+
}
|
|
16
|
+
if (env.FAKE_GH_GARBAGE) {
|
|
17
|
+
console.log('not json')
|
|
18
|
+
process.exit(0)
|
|
19
|
+
}
|
|
20
|
+
const state = args[args.indexOf('--state') + 1]
|
|
21
|
+
// Simulates a change that lands between the first assessment and the re-check.
|
|
22
|
+
if (state === 'all') {
|
|
23
|
+
const { execFileSync } = require('node:child_process')
|
|
24
|
+
const run = (cwd, ...a) => execFileSync('git', a, { cwd, stdio: 'ignore' })
|
|
25
|
+
if (env.FAKE_GH_NEST && !fs.existsSync(env.FAKE_GH_DIR + '/nested')) {
|
|
26
|
+
fs.writeFileSync(env.FAKE_GH_DIR + '/nested', '')
|
|
27
|
+
run(env.FAKE_GH_NEST, 'worktree', 'add', '-b', 'kid', 'sub/kid')
|
|
28
|
+
}
|
|
29
|
+
if (env.FAKE_GH_TOUCH) fs.writeFileSync(env.FAKE_GH_TOUCH, 'x')
|
|
30
|
+
if (env.FAKE_GH_SWITCH) run(env.FAKE_GH_SWITCH, 'checkout', '-b', 'switched')
|
|
31
|
+
if (env.FAKE_GH_ADVANCE) {
|
|
32
|
+
run(env.FAKE_GH_ADVANCE, 'commit', '--allow-empty', '-m', 'late')
|
|
33
|
+
run(env.FAKE_GH_ADVANCE, 'push', 'origin', 'HEAD')
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
const branch = args[args.indexOf('--head') + 1]
|
|
37
|
+
const file = env.FAKE_GH_DIR + '/' + branch.replaceAll('/', '_') + '.json'
|
|
38
|
+
let pulls = fs.existsSync(file) ? JSON.parse(fs.readFileSync(file, 'utf8')) : []
|
|
39
|
+
if (state === 'open') pulls = pulls.filter((pr) => pr.state === 'OPEN')
|
|
40
|
+
const pad = Number(env['FAKE_GH_PAD_' + state.toUpperCase()] || 0)
|
|
41
|
+
for (let i = 0; i < pad; i += 1) pulls.push({ state: 'CLOSED' })
|
|
42
|
+
console.log(JSON.stringify(pulls))
|
|
43
|
+
// Reopen every PR after the first full answer, as if it changed before the re-check.
|
|
44
|
+
if (state === 'all' && env.FAKE_GH_REOPEN && !fs.existsSync(file + '.reopened')) {
|
|
45
|
+
fs.writeFileSync(file, JSON.stringify(pulls.map((pr) => ({ ...pr, state: 'OPEN' }))))
|
|
46
|
+
fs.writeFileSync(file + '.reopened', '')
|
|
47
|
+
}
|
|
48
|
+
`;
|
|
49
|
+
const realGit = execFileSync('which', ['git'], { encoding: 'utf8' }).trim();
|
|
50
|
+
export function createFixture() {
|
|
51
|
+
const root = realpathSync(mkdtempSync(join(tmpdir(), 'worktree-reclaim-')));
|
|
52
|
+
const bin = join(root, 'bin');
|
|
53
|
+
const ghDir = join(root, 'gh');
|
|
54
|
+
mkdirSync(bin);
|
|
55
|
+
mkdirSync(ghDir);
|
|
56
|
+
writeFileSync(join(bin, 'gh'), FAKE_GH);
|
|
57
|
+
chmodSync(join(bin, 'gh'), 0o755);
|
|
58
|
+
// A git wrapper that fails any invocation containing the words in FAKE_GIT_FAIL, to reach error paths.
|
|
59
|
+
writeFileSync(join(bin, 'git'), `#!/bin/sh\ncase " $* " in *" $FAKE_GIT_FAIL "*) [ -n "$FAKE_GIT_FAIL" ] && exit 1;; esac\nexec '${realGit}' "$@"\n`);
|
|
60
|
+
chmodSync(join(bin, 'git'), 0o755);
|
|
61
|
+
const env = {
|
|
62
|
+
...process.env,
|
|
63
|
+
PATH: `${bin}:${process.env.PATH ?? ''}`,
|
|
64
|
+
FAKE_GH_DIR: ghDir,
|
|
65
|
+
GIT_CONFIG_GLOBAL: '/dev/null',
|
|
66
|
+
GIT_CONFIG_SYSTEM: '/dev/null',
|
|
67
|
+
GIT_AUTHOR_NAME: 'Test',
|
|
68
|
+
GIT_AUTHOR_EMAIL: 'test@example.com',
|
|
69
|
+
GIT_COMMITTER_NAME: 'Test',
|
|
70
|
+
GIT_COMMITTER_EMAIL: 'test@example.com',
|
|
71
|
+
};
|
|
72
|
+
const git = (cwd, ...args) => execFileSync('git', args, { cwd, env, encoding: 'utf8', stdio: ['ignore', 'pipe', 'pipe'] });
|
|
73
|
+
const origin = join(root, 'origin.git');
|
|
74
|
+
const main = join(root, 'main');
|
|
75
|
+
git(root, 'init', '--bare', '-b', 'main', origin);
|
|
76
|
+
git(root, 'init', '-b', 'main', main);
|
|
77
|
+
writeFileSync(join(main, 'README.md'), 'base\n');
|
|
78
|
+
git(main, 'add', '.');
|
|
79
|
+
git(main, 'commit', '-m', 'base');
|
|
80
|
+
git(main, 'remote', 'add', 'origin', origin);
|
|
81
|
+
git(main, 'push', '-u', 'origin', 'main');
|
|
82
|
+
const caller = join(root, 'worktrees', 'caller');
|
|
83
|
+
git(main, 'worktree', 'add', '-b', 'caller', caller);
|
|
84
|
+
return {
|
|
85
|
+
root,
|
|
86
|
+
main,
|
|
87
|
+
caller,
|
|
88
|
+
env,
|
|
89
|
+
git,
|
|
90
|
+
addWorktree: (name, options = {}) => {
|
|
91
|
+
const path = join(root, 'worktrees', options.dir ?? name);
|
|
92
|
+
git(main, 'worktree', 'add', '-b', name, path);
|
|
93
|
+
writeFileSync(join(path, `${name}.txt`), `${name}\n`);
|
|
94
|
+
git(path, 'add', '.');
|
|
95
|
+
git(path, 'commit', '-m', name);
|
|
96
|
+
if (options.push)
|
|
97
|
+
git(path, 'push', '-u', 'origin', name);
|
|
98
|
+
return path;
|
|
99
|
+
},
|
|
100
|
+
setPrs: (branch, prs) => {
|
|
101
|
+
const tip = git(main, 'rev-parse', `refs/heads/${branch}`).trim();
|
|
102
|
+
const filled = prs.map((pr) => ({
|
|
103
|
+
headRefOid: tip,
|
|
104
|
+
isCrossRepository: false,
|
|
105
|
+
headRepository: { name: 'Widgets' },
|
|
106
|
+
headRepositoryOwner: { login: 'acme' },
|
|
107
|
+
...pr,
|
|
108
|
+
}));
|
|
109
|
+
writeFileSync(join(ghDir, `${branch.replaceAll('/', '_')}.json`), JSON.stringify(filled));
|
|
110
|
+
},
|
|
111
|
+
cleanup: () => rmSync(root, { force: true, recursive: true }),
|
|
112
|
+
};
|
|
113
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
export interface RunOptions {
|
|
2
|
+
readonly cwd: string;
|
|
3
|
+
readonly env: NodeJS.ProcessEnv;
|
|
4
|
+
/** Milliseconds before a read-only call is killed; 0 for none (worktree removal). */
|
|
5
|
+
readonly timeout?: number;
|
|
6
|
+
}
|
|
7
|
+
/** Runs a command without a shell; returns stdout, or undefined when it fails to start or exits non-zero. */
|
|
8
|
+
export declare function run(file: string, args: readonly string[], options: RunOptions): string | undefined;
|
|
9
|
+
export declare const LOCAL_ENV: string[];
|
|
10
|
+
/** A copy of `env` without the named variables. */
|
|
11
|
+
export declare function withoutVariables(env: NodeJS.ProcessEnv, names: readonly string[]): NodeJS.ProcessEnv;
|
|
12
|
+
/** git with fsmonitor disabled so a repository config can never execute a hook. */
|
|
13
|
+
export declare function git(args: readonly string[], options: RunOptions): string | undefined;
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import { execFileSync } from 'node:child_process';
|
|
2
|
+
/** Runs a command without a shell; returns stdout, or undefined when it fails to start or exits non-zero. */
|
|
3
|
+
export function run(file, args, options) {
|
|
4
|
+
try {
|
|
5
|
+
return execFileSync(file, args, {
|
|
6
|
+
cwd: options.cwd,
|
|
7
|
+
env: options.env,
|
|
8
|
+
encoding: 'utf8',
|
|
9
|
+
maxBuffer: 64 * 1024 * 1024,
|
|
10
|
+
stdio: ['ignore', 'pipe', 'ignore'],
|
|
11
|
+
timeout: options.timeout ?? 120_000,
|
|
12
|
+
});
|
|
13
|
+
}
|
|
14
|
+
catch {
|
|
15
|
+
return undefined;
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
// Repo-local overrides (see `git rev-parse --local-env-vars`): the cwd must pick the repository.
|
|
19
|
+
export const LOCAL_ENV = [
|
|
20
|
+
'GIT_DIR',
|
|
21
|
+
'GIT_WORK_TREE',
|
|
22
|
+
'GIT_INDEX_FILE',
|
|
23
|
+
'GIT_OBJECT_DIRECTORY',
|
|
24
|
+
'GIT_ALTERNATE_OBJECT_DIRECTORIES',
|
|
25
|
+
'GIT_COMMON_DIR',
|
|
26
|
+
'GIT_PREFIX',
|
|
27
|
+
'GIT_NAMESPACE',
|
|
28
|
+
'GIT_CONFIG',
|
|
29
|
+
'GIT_CONFIG_PARAMETERS',
|
|
30
|
+
'GIT_CONFIG_COUNT',
|
|
31
|
+
'GIT_GRAFT_FILE',
|
|
32
|
+
'GIT_IMPLICIT_WORK_TREE',
|
|
33
|
+
'GIT_INTERNAL_SUPER_PREFIX',
|
|
34
|
+
'GIT_SHALLOW_FILE',
|
|
35
|
+
'GIT_REPLACE_REF_BASE',
|
|
36
|
+
'GIT_NO_REPLACE_OBJECTS',
|
|
37
|
+
'GIT_QUARANTINE_PATH',
|
|
38
|
+
];
|
|
39
|
+
/** A copy of `env` without the named variables. */
|
|
40
|
+
export function withoutVariables(env, names) {
|
|
41
|
+
const copy = { ...env };
|
|
42
|
+
for (const name of names)
|
|
43
|
+
delete copy[name];
|
|
44
|
+
return copy;
|
|
45
|
+
}
|
|
46
|
+
/** git with fsmonitor disabled so a repository config can never execute a hook. */
|
|
47
|
+
export function git(args, options) {
|
|
48
|
+
const env = withoutVariables(options.env, LOCAL_ENV);
|
|
49
|
+
return run('git', ['-c', 'core.fsmonitor=false', ...args], { ...options, env });
|
|
50
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "vouchington-tooling",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.53.0",
|
|
4
4
|
"description": "Vouchington CLI and extractable tooling libraries.",
|
|
5
5
|
"homepage": "https://github.com/vouchington/vouchington-tooling/tree/main/packages/vouchington-tooling#readme",
|
|
6
6
|
"bugs": {
|
package/skills/manifest.json
CHANGED
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
{
|
|
5
5
|
"name": "agent-workflow",
|
|
6
6
|
"plugin": "vouchington-workflow",
|
|
7
|
-
"pluginVersion": "0.13.
|
|
7
|
+
"pluginVersion": "0.13.1",
|
|
8
8
|
"path": "agent-workflow/SKILL.md",
|
|
9
9
|
"prerequisites": [
|
|
10
10
|
"blackboard",
|
|
@@ -24,7 +24,7 @@
|
|
|
24
24
|
{
|
|
25
25
|
"name": "blackboard",
|
|
26
26
|
"plugin": "vouchington-workflow",
|
|
27
|
-
"pluginVersion": "0.13.
|
|
27
|
+
"pluginVersion": "0.13.1",
|
|
28
28
|
"path": "blackboard/SKILL.md"
|
|
29
29
|
},
|
|
30
30
|
{
|
|
@@ -36,7 +36,7 @@
|
|
|
36
36
|
{
|
|
37
37
|
"name": "dependabot",
|
|
38
38
|
"plugin": "vouchington-workflow",
|
|
39
|
-
"pluginVersion": "0.13.
|
|
39
|
+
"pluginVersion": "0.13.1",
|
|
40
40
|
"path": "dependabot/SKILL.md",
|
|
41
41
|
"prerequisites": ["github-actions-checklist"]
|
|
42
42
|
},
|
|
@@ -50,26 +50,26 @@
|
|
|
50
50
|
{
|
|
51
51
|
"name": "git-commit-checklist",
|
|
52
52
|
"plugin": "vouchington-workflow",
|
|
53
|
-
"pluginVersion": "0.13.
|
|
53
|
+
"pluginVersion": "0.13.1",
|
|
54
54
|
"path": "git-commit-checklist/SKILL.md"
|
|
55
55
|
},
|
|
56
56
|
{
|
|
57
57
|
"name": "github-actions-authoring",
|
|
58
58
|
"plugin": "vouchington-workflow",
|
|
59
|
-
"pluginVersion": "0.13.
|
|
59
|
+
"pluginVersion": "0.13.1",
|
|
60
60
|
"path": "github-actions-authoring/SKILL.md",
|
|
61
61
|
"prerequisites": ["github-actions-checklist"]
|
|
62
62
|
},
|
|
63
63
|
{
|
|
64
64
|
"name": "github-actions-checklist",
|
|
65
65
|
"plugin": "vouchington-workflow",
|
|
66
|
-
"pluginVersion": "0.13.
|
|
66
|
+
"pluginVersion": "0.13.1",
|
|
67
67
|
"path": "github-actions-checklist/SKILL.md"
|
|
68
68
|
},
|
|
69
69
|
{
|
|
70
70
|
"name": "github-issue",
|
|
71
71
|
"plugin": "vouchington-workflow",
|
|
72
|
-
"pluginVersion": "0.13.
|
|
72
|
+
"pluginVersion": "0.13.1",
|
|
73
73
|
"path": "github-issue/SKILL.md"
|
|
74
74
|
},
|
|
75
75
|
{
|
|
@@ -82,26 +82,26 @@
|
|
|
82
82
|
{
|
|
83
83
|
"name": "npm-publishing",
|
|
84
84
|
"plugin": "vouchington-workflow",
|
|
85
|
-
"pluginVersion": "0.13.
|
|
85
|
+
"pluginVersion": "0.13.1",
|
|
86
86
|
"path": "npm-publishing/SKILL.md"
|
|
87
87
|
},
|
|
88
88
|
{
|
|
89
89
|
"name": "organize-github-issues",
|
|
90
90
|
"plugin": "vouchington-workflow",
|
|
91
|
-
"pluginVersion": "0.13.
|
|
91
|
+
"pluginVersion": "0.13.1",
|
|
92
92
|
"path": "organize-github-issues/SKILL.md",
|
|
93
93
|
"prerequisites": ["github-issue"]
|
|
94
94
|
},
|
|
95
95
|
{
|
|
96
96
|
"name": "package-json-checklist",
|
|
97
97
|
"plugin": "vouchington-workflow",
|
|
98
|
-
"pluginVersion": "0.13.
|
|
98
|
+
"pluginVersion": "0.13.1",
|
|
99
99
|
"path": "package-json-checklist/SKILL.md"
|
|
100
100
|
},
|
|
101
101
|
{
|
|
102
102
|
"name": "planning",
|
|
103
103
|
"plugin": "vouchington-workflow",
|
|
104
|
-
"pluginVersion": "0.13.
|
|
104
|
+
"pluginVersion": "0.13.1",
|
|
105
105
|
"path": "planning/SKILL.md",
|
|
106
106
|
"prerequisites": ["github-issue"]
|
|
107
107
|
},
|
|
@@ -133,53 +133,53 @@
|
|
|
133
133
|
{
|
|
134
134
|
"name": "pr-description",
|
|
135
135
|
"plugin": "vouchington-workflow",
|
|
136
|
-
"pluginVersion": "0.13.
|
|
136
|
+
"pluginVersion": "0.13.1",
|
|
137
137
|
"path": "pr-description/SKILL.md"
|
|
138
138
|
},
|
|
139
139
|
{
|
|
140
140
|
"name": "retrospective",
|
|
141
141
|
"plugin": "vouchington-workflow",
|
|
142
|
-
"pluginVersion": "0.13.
|
|
142
|
+
"pluginVersion": "0.13.1",
|
|
143
143
|
"path": "retrospective/SKILL.md"
|
|
144
144
|
},
|
|
145
145
|
{
|
|
146
146
|
"name": "retrospective-distill",
|
|
147
147
|
"plugin": "vouchington-workflow",
|
|
148
|
-
"pluginVersion": "0.13.
|
|
148
|
+
"pluginVersion": "0.13.1",
|
|
149
149
|
"path": "retrospective-distill/SKILL.md",
|
|
150
150
|
"prerequisites": ["github-issue"]
|
|
151
151
|
},
|
|
152
152
|
{
|
|
153
153
|
"name": "review-ci-logs",
|
|
154
154
|
"plugin": "vouchington-workflow",
|
|
155
|
-
"pluginVersion": "0.13.
|
|
155
|
+
"pluginVersion": "0.13.1",
|
|
156
156
|
"path": "review-ci-logs/SKILL.md",
|
|
157
157
|
"prerequisites": ["github-actions-checklist", "pr-description"]
|
|
158
158
|
},
|
|
159
159
|
{
|
|
160
160
|
"name": "review-github-issue-taxonomy",
|
|
161
161
|
"plugin": "vouchington-workflow",
|
|
162
|
-
"pluginVersion": "0.13.
|
|
162
|
+
"pluginVersion": "0.13.1",
|
|
163
163
|
"path": "review-github-issue-taxonomy/SKILL.md",
|
|
164
164
|
"prerequisites": ["github-issue"]
|
|
165
165
|
},
|
|
166
166
|
{
|
|
167
167
|
"name": "revisit-followups",
|
|
168
168
|
"plugin": "vouchington-workflow",
|
|
169
|
-
"pluginVersion": "0.13.
|
|
169
|
+
"pluginVersion": "0.13.1",
|
|
170
170
|
"path": "revisit-followups/SKILL.md",
|
|
171
171
|
"prerequisites": ["github-issue"]
|
|
172
172
|
},
|
|
173
173
|
{
|
|
174
174
|
"name": "stacked-prs",
|
|
175
175
|
"plugin": "vouchington-workflow",
|
|
176
|
-
"pluginVersion": "0.13.
|
|
176
|
+
"pluginVersion": "0.13.1",
|
|
177
177
|
"path": "stacked-prs/SKILL.md"
|
|
178
178
|
},
|
|
179
179
|
{
|
|
180
180
|
"name": "static-analysis-checklist",
|
|
181
181
|
"plugin": "vouchington-workflow",
|
|
182
|
-
"pluginVersion": "0.13.
|
|
182
|
+
"pluginVersion": "0.13.1",
|
|
183
183
|
"path": "static-analysis-checklist/SKILL.md",
|
|
184
184
|
"prerequisites": ["github-actions-checklist"]
|
|
185
185
|
},
|
|
@@ -206,7 +206,7 @@
|
|
|
206
206
|
{
|
|
207
207
|
"name": "triage-prs",
|
|
208
208
|
"plugin": "vouchington-workflow",
|
|
209
|
-
"pluginVersion": "0.13.
|
|
209
|
+
"pluginVersion": "0.13.1",
|
|
210
210
|
"path": "triage-prs/SKILL.md",
|
|
211
211
|
"prerequisites": [
|
|
212
212
|
"pr-description",
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: stacked-prs
|
|
3
|
-
description: Work native GitHub stacks bottom-up and
|
|
3
|
+
description: Work native GitHub stacks bottom-up and prefer merging the whole stack in one command; drain the ready lower layers first only when the upper layers will clearly take longer.
|
|
4
4
|
---
|
|
5
5
|
|
|
6
6
|
# Stacked pull requests
|
|
@@ -44,7 +44,8 @@ a stack, and who may authorize a merge.
|
|
|
44
44
|
a ready-delay, or green CI.
|
|
45
45
|
- On each shepherd result, act on the lowest owned layer that is not ready before any higher
|
|
46
46
|
layer. Put each fix on the lowest layer that owns it. A bottom layer that reaches ready early
|
|
47
|
-
|
|
47
|
+
waits for the rest of the stack unless the layers above will clearly take longer; keep readying
|
|
48
|
+
them.
|
|
48
49
|
- Before a command that adds layers to a stack, such as `gh stack add` or `gh stack link`, add the
|
|
49
50
|
forge's open-layer count, plus every locally tracked layer not yet submitted, to every layer the
|
|
50
51
|
command would add. `gh stack add` creates only a local branch, so repeated adds before a submit
|
|
@@ -54,20 +55,25 @@ a stack, and who may authorize a merge.
|
|
|
54
55
|
- A ready, mergeable, or queued lower layer blocks only an operation that would rewrite that
|
|
55
56
|
layer. Keep readying every other owned layer.
|
|
56
57
|
|
|
57
|
-
## Merge the
|
|
58
|
+
## Merge the whole stack
|
|
58
59
|
|
|
59
|
-
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
60
|
+
- Prefer merging the whole stack at once: when every layer is ready and inside the human's
|
|
61
|
+
approval, merge it with one command whose target is the top layer. Never merge one layer at a
|
|
62
|
+
time, and never merge only the lower layers when every layer is ready.
|
|
63
|
+
- Ready every layer in parallel so the stack reaches ready together. A ready lower layer waits
|
|
64
|
+
while the layers above are close behind (a poll or a ready-delay away).
|
|
65
|
+
- Drain bottom-up only when the layers above will clearly take longer: a requested change or
|
|
66
|
+
`FIX_CODE` still open, a pending human review, a blocker, or CI far longer than the wait so far.
|
|
67
|
+
Then merge the ready lower layers in one command whose target is the highest ready layer, state
|
|
68
|
+
why in the report, and merge the rest together when it is ready.
|
|
69
|
+
- Re-read every layer's readiness immediately before merging. If the whole stack is ready and
|
|
70
|
+
approved, the target is the top layer; never retarget past the approval.
|
|
65
71
|
- Nothing in the stack merges on its own. Run the merge checks in
|
|
66
|
-
[Drain and keep track](references/drain.md#merge-the-
|
|
67
|
-
- After a
|
|
68
|
-
the new bottom.
|
|
72
|
+
[Drain and keep track](references/drain.md#merge-the-whole-stack) before each merge.
|
|
73
|
+
- After a bottom-up drain, GitHub retargets the next unmerged layer onto the default branch. That
|
|
74
|
+
layer is the new bottom.
|
|
69
75
|
- On a stall, or when a layer closes without merging, stop if the bottom layers are otherwise
|
|
70
|
-
ready. Report every layer and ask before
|
|
76
|
+
ready. Report every layer and ask before draining them.
|
|
71
77
|
- A stall blocks only layers that depend on it. Ready the others. Pause the whole drain only when
|
|
72
78
|
nothing further can be readied without the blocked layer.
|
|
73
79
|
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
# Drain and keep track
|
|
2
2
|
|
|
3
3
|
Shepherding a layer to ready and merging are different acts. The first covers every pull request
|
|
4
|
-
the worker owns in the stack at once. The second lands the
|
|
5
|
-
never the agent's own decision.
|
|
4
|
+
the worker owns in the stack at once. The second lands the whole stack in one command once every
|
|
5
|
+
layer is ready, and it is never the agent's own decision.
|
|
6
6
|
|
|
7
7
|
## Establish scope
|
|
8
8
|
|
|
@@ -89,48 +89,55 @@ When the owned set settles, present the stack bottom to top: each layer's number
|
|
|
89
89
|
and whether it is ready or blocked and why. Never leave a ready bottom layer under a blocked upper
|
|
90
90
|
layer without saying so.
|
|
91
91
|
|
|
92
|
-
## Merge the
|
|
92
|
+
## Merge the whole stack
|
|
93
93
|
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
94
|
+
Prefer one merge of the whole stack: when every open layer is ready and inside the approval, the
|
|
95
|
+
target is the top layer, and one command lands it and every layer below it. A shepherd that prints
|
|
96
|
+
a merge for a lower layer does not change that; when the whole stack is ready, target the top.
|
|
97
|
+
|
|
98
|
+
Drain bottom-up only when the layers above will clearly take longer than the ready lower layers:
|
|
99
|
+
an open requested change or `FIX_CODE`, a pending human review, a blocker, or CI far longer than
|
|
100
|
+
the wait so far. A layer a poll or a ready-delay away is not a reason. Then the target is the
|
|
101
|
+
highest layer of the ready, approved run from the bottom, still one command, and the report says
|
|
102
|
+
why the upper layers were left.
|
|
98
103
|
|
|
99
104
|
Merge only on explicit human approval. The approval names what it covers: one layer, a range, or
|
|
100
|
-
the whole stack. Approving the whole stack also covers
|
|
101
|
-
|
|
102
|
-
it.
|
|
105
|
+
the whole stack. Approving the whole stack also covers a bottom-up drain under the rule above. It
|
|
106
|
+
does not cover a layer added after the approval. Ask before merging any layer outside it.
|
|
103
107
|
|
|
104
|
-
Immediately before
|
|
108
|
+
Immediately before the approved merge, run these checks in order:
|
|
105
109
|
|
|
106
110
|
1. `gh api repos/{owner}/{repo}/stacks/<target>` must return 404. `gh stack merge` resolves a bare
|
|
107
111
|
number as a stack number first, then a pull request number. A 200 response means `--yes` would
|
|
108
112
|
merge every layer of that stack. Stop and ask.
|
|
109
113
|
2. Re-read topology. Confirm every layer from the bottom-most open layer up to the target is open,
|
|
110
|
-
ready, and inside the approval, with nothing broken below.
|
|
111
|
-
|
|
114
|
+
ready, and inside the approval, with nothing broken below. Re-read every layer above the
|
|
115
|
+
target too. If those layers are all ready now and the approval covers them, retarget to the
|
|
116
|
+
highest of them and restart these checks from step 1 for the new target. Never retarget past
|
|
117
|
+
the approval; report the ready layers above it and ask.
|
|
118
|
+
3. For every layer being merged, capture `pulls/<N>.head.sha` before the final poll and after it.
|
|
112
119
|
They must match. A mismatch means a push landed inside the evaluation window: discard the
|
|
113
120
|
verdict and poll again.
|
|
114
121
|
4. `gh stack merge <target> --yes` with the repository's merge strategy. It lands the target and
|
|
115
|
-
every open layer below it
|
|
122
|
+
every open layer below it.
|
|
116
123
|
5. Do not `gh stack sync` here. Return to step 2 of [Establish scope](#establish-scope) and
|
|
117
124
|
re-read topology remotely: rebasing right after a merge can replay already-landed commits from
|
|
118
|
-
a stale local base ref.
|
|
119
|
-
the
|
|
120
|
-
layer, rerun the fetch and ahead/behind check in
|
|
125
|
+
a stale local base ref. After a bottom-up drain, keep shepherding the layers above and merge
|
|
126
|
+
the rest the same way, preferring the whole remainder at once. Before any local edit, rebase, or push
|
|
127
|
+
on a remaining layer, rerun the fetch and ahead/behind check in
|
|
121
128
|
[Import a stack](gh-stack.md#import-a-stack-before-acting-on-it): GitHub rebases the next
|
|
122
129
|
layer remotely, so local upper branches are stale until fast-forwarded. The drain is complete when no unmerged owned layer remains.
|
|
123
130
|
|
|
124
131
|
The window between the last head read and the merge cannot be closed, because `gh stack merge`
|
|
125
132
|
has no head-SHA pin. Branch protection and rulesets are evaluated when the merge runs, and stacks
|
|
126
133
|
cannot bypass merge requirements. If the merge is rejected, re-read topology instead of retrying
|
|
127
|
-
blindly. Under a merge queue, a successful `gh stack merge` only enqueues: the
|
|
134
|
+
blindly. Under a merge queue, a successful `gh stack merge` only enqueues: the layers queue
|
|
128
135
|
together, and a failing layer is ejected along with the layers above it. The next topology
|
|
129
136
|
re-read handles that.
|
|
130
137
|
|
|
131
138
|
When you stop or are blocked (an unresolved escalation, an interrupt or pause, repeated fixes with
|
|
132
139
|
no progress, or an external blocker), report each layer's number and state. If the bottom layers
|
|
133
|
-
are ready, ask whether to
|
|
140
|
+
are ready, ask whether to drain them before continuing.
|
|
134
141
|
|
|
135
142
|
## Keep track: derive the ledger
|
|
136
143
|
|
|
@@ -137,11 +137,11 @@ Classify the outcome:
|
|
|
137
137
|
- **Repository auto-merge disabled:** dispatch the shepherd, report that a manual merge is
|
|
138
138
|
required, and leave auto-merge unchanged.
|
|
139
139
|
- **Native GitHub stack:** shepherd the owned layers per
|
|
140
|
-
[stacked-prs](../stacked-prs/SKILL.md#ownership-and-workers) and merge the
|
|
141
|
-
[Merge the
|
|
140
|
+
[stacked-prs](../stacked-prs/SKILL.md#ownership-and-workers) and merge the whole stack per
|
|
141
|
+
[Merge the whole stack](../stacked-prs/SKILL.md#merge-the-whole-stack), within explicit human
|
|
142
142
|
approval and only as far as this run's decisions cover. Resolve the full stack against the Step 1
|
|
143
|
-
scope and Step 3 decisions first.
|
|
144
|
-
or not `MERGE`; never shepherd or merge a layer this run did not review. GitHub has no
|
|
143
|
+
scope and Step 3 decisions first. A bottom-up drain ends below the first layer that is out of
|
|
144
|
+
scope or not `MERGE`; never shepherd or merge a layer this run did not review. GitHub has no
|
|
145
145
|
auto-merge for stacks, and an armed auto-merge does not authorize the drain on its own.
|
|
146
146
|
|
|
147
147
|
Do not add a second pass that polls the dispatch workflow. It ends once the session is accepted or
|