peaks-loop 4.1.2 → 4.1.3
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-plugin/marketplace.json +136 -47
- package/CHANGELOG.md +56 -0
- package/README-en.md +1 -1
- package/README.md +1 -1
- package/contracts/readonly-argv-whitelist.json +211 -0
- package/dist/cli/commands/audit-artifact-runners.d.ts +16 -0
- package/dist/cli/commands/audit-artifact-runners.js +170 -0
- package/dist/cli/commands/audit-command-shared.d.ts +97 -0
- package/dist/cli/commands/audit-command-shared.js +60 -0
- package/dist/cli/commands/audit-commands.d.ts +7 -31
- package/dist/cli/commands/audit-commands.js +64 -473
- package/dist/cli/commands/audit-goal-runners.d.ts +17 -0
- package/dist/cli/commands/audit-goal-runners.js +114 -0
- package/dist/cli/commands/audit-red-lines-runners.d.ts +11 -0
- package/dist/cli/commands/audit-red-lines-runners.js +43 -0
- package/dist/cli/commands/audit-refusal-data.d.ts +23 -0
- package/dist/cli/commands/audit-refusal-data.js +69 -0
- package/dist/cli/commands/audit-static-runners.d.ts +13 -0
- package/dist/cli/commands/audit-static-runners.js +139 -0
- package/dist/cli/commands/await-runners.d.ts +11 -0
- package/dist/cli/commands/await-runners.js +148 -0
- package/dist/cli/commands/code-auto-compact-command.d.ts +3 -0
- package/dist/cli/commands/code-auto-compact-command.js +124 -0
- package/dist/cli/commands/code-auto-compact-envelope.d.ts +22 -0
- package/dist/cli/commands/code-auto-compact-envelope.js +39 -0
- package/dist/cli/commands/code-context-audit-command.d.ts +3 -0
- package/dist/cli/commands/code-context-audit-command.js +68 -0
- package/dist/cli/commands/code-context-now-command.d.ts +3 -0
- package/dist/cli/commands/code-context-now-command.js +124 -0
- package/dist/cli/commands/code-context-now-envelope.d.ts +66 -0
- package/dist/cli/commands/code-context-now-envelope.js +136 -0
- package/dist/cli/commands/code-context-now-mapping.d.ts +27 -0
- package/dist/cli/commands/code-context-now-mapping.js +43 -0
- package/dist/cli/commands/code-emit-handoff-command.d.ts +3 -0
- package/dist/cli/commands/code-emit-handoff-command.js +99 -0
- package/dist/cli/commands/code-gate-step-08-command.d.ts +3 -0
- package/dist/cli/commands/code-gate-step-08-command.js +139 -0
- package/dist/cli/commands/code-post-compact-detect-command.d.ts +3 -0
- package/dist/cli/commands/code-post-compact-detect-command.js +50 -0
- package/dist/cli/commands/code-runtime-commands.d.ts +9 -43
- package/dist/cli/commands/code-runtime-commands.js +20 -672
- package/dist/cli/commands/code-runtime-session.d.ts +1 -0
- package/dist/cli/commands/code-runtime-session.js +15 -0
- package/dist/cli/commands/compact-command.d.ts +6 -2
- package/dist/cli/commands/compact-command.js +12 -559
- package/dist/cli/commands/compact-dry-run-command.d.ts +3 -0
- package/dist/cli/commands/compact-dry-run-command.js +87 -0
- package/dist/cli/commands/compact-force-command.d.ts +3 -0
- package/dist/cli/commands/compact-force-command.js +98 -0
- package/dist/cli/commands/compact-harness-window-command.d.ts +3 -0
- package/dist/cli/commands/compact-harness-window-command.js +174 -0
- package/dist/cli/commands/compact-history-command.d.ts +3 -0
- package/dist/cli/commands/compact-history-command.js +89 -0
- package/dist/cli/commands/compact-read-commands.d.ts +3 -0
- package/dist/cli/commands/compact-read-commands.js +123 -0
- package/dist/cli/commands/compact-settle-command.d.ts +3 -0
- package/dist/cli/commands/compact-settle-command.js +130 -0
- package/dist/cli/commands/compact-shared.d.ts +9 -0
- package/dist/cli/commands/compact-shared.js +26 -0
- package/dist/cli/commands/core/skill-command.d.ts +1 -44
- package/dist/cli/commands/core/skill-command.js +13 -541
- package/dist/cli/commands/core/skill-context-verdict.d.ts +38 -0
- package/dist/cli/commands/core/skill-context-verdict.js +63 -0
- package/dist/cli/commands/core/skill-generated-config-notice.d.ts +41 -0
- package/dist/cli/commands/core/skill-generated-config-notice.js +75 -0
- package/dist/cli/commands/core/skill-heartbeat-commands.d.ts +3 -0
- package/dist/cli/commands/core/skill-heartbeat-commands.js +50 -0
- package/dist/cli/commands/core/skill-presence-commands.d.ts +3 -0
- package/dist/cli/commands/core/skill-presence-commands.js +123 -0
- package/dist/cli/commands/core/skill-presence-housekeeping-commands.d.ts +3 -0
- package/dist/cli/commands/core/skill-presence-housekeeping-commands.js +165 -0
- package/dist/cli/commands/core/skill-registry-commands.d.ts +3 -0
- package/dist/cli/commands/core/skill-registry-commands.js +117 -0
- package/dist/cli/commands/core/skill-runbook-command.d.ts +3 -0
- package/dist/cli/commands/core/skill-runbook-command.js +29 -0
- package/dist/cli/commands/dispatch-command-spec.d.ts +2 -0
- package/dist/cli/commands/dispatch-command-spec.js +62 -0
- package/dist/cli/commands/dispatch-commands.js +159 -775
- package/dist/cli/commands/dispatch-detached-branch.d.ts +3 -0
- package/dist/cli/commands/dispatch-detached-branch.js +58 -0
- package/dist/cli/commands/dispatch-envelope.d.ts +36 -0
- package/dist/cli/commands/dispatch-envelope.js +104 -0
- package/dist/cli/commands/dispatch-isolation-lease.d.ts +17 -0
- package/dist/cli/commands/dispatch-isolation-lease.js +149 -0
- package/dist/cli/commands/dispatch-preflight.d.ts +7 -0
- package/dist/cli/commands/dispatch-preflight.js +105 -0
- package/dist/cli/commands/dispatch-prompt-blocks.d.ts +10 -0
- package/dist/cli/commands/dispatch-prompt-blocks.js +56 -0
- package/dist/cli/commands/dispatch-prompt-composition.d.ts +30 -0
- package/dist/cli/commands/dispatch-prompt-composition.js +171 -0
- package/dist/cli/commands/dispatch-record-write.d.ts +24 -0
- package/dist/cli/commands/dispatch-record-write.js +96 -0
- package/dist/cli/commands/dispatch-tool-call.d.ts +15 -0
- package/dist/cli/commands/dispatch-tool-call.js +128 -0
- package/dist/cli/commands/evolution-command-shared.d.ts +78 -0
- package/dist/cli/commands/evolution-command-shared.js +107 -0
- package/dist/cli/commands/evolution-commands.d.ts +11 -1
- package/dist/cli/commands/evolution-commands.js +19 -350
- package/dist/cli/commands/evolution-evaluate-command.d.ts +24 -0
- package/dist/cli/commands/evolution-evaluate-command.js +138 -0
- package/dist/cli/commands/evolution-propose-command.d.ts +23 -0
- package/dist/cli/commands/evolution-propose-command.js +145 -0
- package/dist/cli/commands/evolution-status-command.d.ts +17 -0
- package/dist/cli/commands/evolution-status-command.js +41 -0
- package/dist/cli/commands/evolution-verdict-commands.d.ts +30 -0
- package/dist/cli/commands/evolution-verdict-commands.js +100 -0
- package/dist/cli/commands/finalize-accumulator.d.ts +44 -0
- package/dist/cli/commands/finalize-accumulator.js +55 -0
- package/dist/cli/commands/finalize-branches.d.ts +34 -0
- package/dist/cli/commands/finalize-branches.js +119 -0
- package/dist/cli/commands/finalize-runners.d.ts +3 -0
- package/dist/cli/commands/finalize-runners.js +92 -0
- package/dist/cli/commands/finalize-selection.d.ts +65 -0
- package/dist/cli/commands/finalize-selection.js +35 -0
- package/dist/cli/commands/gate-commands.js +26 -8
- package/dist/cli/commands/job-checkpoint-commands.d.ts +3 -0
- package/dist/cli/commands/job-checkpoint-commands.js +157 -0
- package/dist/cli/commands/job-command-shared.d.ts +109 -0
- package/dist/cli/commands/job-command-shared.js +72 -0
- package/dist/cli/commands/job-commands.d.ts +4 -64
- package/dist/cli/commands/job-commands.js +42 -552
- package/dist/cli/commands/job-cost-check-command.d.ts +3 -0
- package/dist/cli/commands/job-cost-check-command.js +41 -0
- package/dist/cli/commands/job-init-status-commands.d.ts +3 -0
- package/dist/cli/commands/job-init-status-commands.js +149 -0
- package/dist/cli/commands/job-run-commands.d.ts +3 -0
- package/dist/cli/commands/job-run-commands.js +97 -0
- package/dist/cli/commands/job-state-root.d.ts +35 -0
- package/dist/cli/commands/job-state-root.js +76 -0
- package/dist/cli/commands/loop-eval-capture-index.d.ts +4 -0
- package/dist/cli/commands/loop-eval-capture-index.js +44 -0
- package/dist/cli/commands/loop-eval-commands.d.ts +8 -5
- package/dist/cli/commands/loop-eval-commands.js +12 -626
- package/dist/cli/commands/loop-eval-eval-command.d.ts +3 -0
- package/dist/cli/commands/loop-eval-eval-command.js +168 -0
- package/dist/cli/commands/loop-eval-monotonic-command.d.ts +3 -0
- package/dist/cli/commands/loop-eval-monotonic-command.js +90 -0
- package/dist/cli/commands/loop-eval-parents.d.ts +7 -0
- package/dist/cli/commands/loop-eval-parents.js +20 -0
- package/dist/cli/commands/loop-eval-run-code.d.ts +7 -0
- package/dist/cli/commands/loop-eval-run-code.js +40 -0
- package/dist/cli/commands/loop-eval-run-command.d.ts +3 -0
- package/dist/cli/commands/loop-eval-run-command.js +113 -0
- package/dist/cli/commands/loop-eval-spec-commands.d.ts +3 -0
- package/dist/cli/commands/loop-eval-spec-commands.js +165 -0
- package/dist/cli/commands/loop-eval-spec-lint-command.d.ts +3 -0
- package/dist/cli/commands/loop-eval-spec-lint-command.js +42 -0
- package/dist/cli/commands/loop-eval-workflow-commands.d.ts +3 -0
- package/dist/cli/commands/loop-eval-workflow-commands.js +141 -0
- package/dist/cli/commands/mcp-commands.d.ts +10 -0
- package/dist/cli/commands/mcp-commands.js +157 -0
- package/dist/cli/commands/request-command-options.d.ts +49 -0
- package/dist/cli/commands/request-command-options.js +1 -0
- package/dist/cli/commands/request-commands.d.ts +3 -9
- package/dist/cli/commands/request-commands.js +17 -522
- package/dist/cli/commands/request-init-command.d.ts +3 -0
- package/dist/cli/commands/request-init-command.js +100 -0
- package/dist/cli/commands/request-lint-command.d.ts +3 -0
- package/dist/cli/commands/request-lint-command.js +50 -0
- package/dist/cli/commands/request-list-command.d.ts +10 -0
- package/dist/cli/commands/request-list-command.js +53 -0
- package/dist/cli/commands/request-repair-status-command.d.ts +3 -0
- package/dist/cli/commands/request-repair-status-command.js +61 -0
- package/dist/cli/commands/request-show-command.d.ts +3 -0
- package/dist/cli/commands/request-show-command.js +56 -0
- package/dist/cli/commands/request-transition-command.d.ts +3 -0
- package/dist/cli/commands/request-transition-command.js +122 -0
- package/dist/cli/commands/request-transition-failures.d.ts +8 -0
- package/dist/cli/commands/request-transition-failures.js +90 -0
- package/dist/cli/commands/request-transition-hooks.d.ts +50 -0
- package/dist/cli/commands/request-transition-hooks.js +115 -0
- package/dist/cli/commands/request-transition-preflight.d.ts +10 -0
- package/dist/cli/commands/request-transition-preflight.js +107 -0
- package/dist/cli/commands/scan-command-shared.d.ts +96 -0
- package/dist/cli/commands/scan-command-shared.js +83 -0
- package/dist/cli/commands/scan-commands.d.ts +3 -2
- package/dist/cli/commands/scan-commands.js +23 -463
- package/dist/cli/commands/scan-inventory-commands.d.ts +3 -0
- package/dist/cli/commands/scan-inventory-commands.js +96 -0
- package/dist/cli/commands/scan-project-commands.d.ts +3 -0
- package/dist/cli/commands/scan-project-commands.js +65 -0
- package/dist/cli/commands/scan-scope-commands.d.ts +3 -0
- package/dist/cli/commands/scan-scope-commands.js +149 -0
- package/dist/cli/commands/scan-scope-helpers.d.ts +8 -0
- package/dist/cli/commands/scan-scope-helpers.js +54 -0
- package/dist/cli/commands/scan-structural-commands.d.ts +3 -0
- package/dist/cli/commands/scan-structural-commands.js +113 -0
- package/dist/cli/commands/share-commands.d.ts +12 -68
- package/dist/cli/commands/share-commands.js +47 -556
- package/dist/cli/commands/share-error-hints.d.ts +14 -0
- package/dist/cli/commands/share-error-hints.js +38 -0
- package/dist/cli/commands/share-identity.d.ts +13 -0
- package/dist/cli/commands/share-identity.js +22 -0
- package/dist/cli/commands/share-record-paths.d.ts +37 -0
- package/dist/cli/commands/share-record-paths.js +68 -0
- package/dist/cli/commands/share-runners.d.ts +3 -0
- package/dist/cli/commands/share-runners.js +158 -0
- package/dist/cli/commands/shared-read-runners.d.ts +3 -0
- package/dist/cli/commands/shared-read-runners.js +51 -0
- package/dist/cli/commands/slice-check-command.d.ts +3 -0
- package/dist/cli/commands/slice-check-command.js +54 -0
- package/dist/cli/commands/slice-commands.d.ts +2 -22
- package/dist/cli/commands/slice-commands.js +18 -517
- package/dist/cli/commands/slice-decompose-artifacts.d.ts +18 -0
- package/dist/cli/commands/slice-decompose-artifacts.js +79 -0
- package/dist/cli/commands/slice-decompose-command.d.ts +3 -0
- package/dist/cli/commands/slice-decompose-command.js +123 -0
- package/dist/cli/commands/slice-list-commands.d.ts +3 -0
- package/dist/cli/commands/slice-list-commands.js +182 -0
- package/dist/cli/commands/slice-pick-command.d.ts +3 -0
- package/dist/cli/commands/slice-pick-command.js +60 -0
- package/dist/cli/commands/slice-plan-command.d.ts +24 -0
- package/dist/cli/commands/slice-plan-command.js +111 -0
- package/dist/cli/commands/workflow-commands.d.ts +10 -1
- package/dist/cli/commands/workflow-commands.js +6 -594
- package/dist/cli/commands/workflow-gate-commands.d.ts +3 -0
- package/dist/cli/commands/workflow-gate-commands.js +116 -0
- package/dist/cli/commands/workflow-plan-helpers.d.ts +48 -0
- package/dist/cli/commands/workflow-plan-helpers.js +103 -0
- package/dist/cli/commands/workflow-planning-commands.d.ts +3 -0
- package/dist/cli/commands/workflow-planning-commands.js +56 -0
- package/dist/cli/commands/workflow-swarm-actions.d.ts +7 -0
- package/dist/cli/commands/workflow-swarm-actions.js +109 -0
- package/dist/cli/commands/workflow-swarm-commands.d.ts +3 -0
- package/dist/cli/commands/workflow-swarm-commands.js +138 -0
- package/dist/cli/commands/workflow-tech-actions.d.ts +10 -0
- package/dist/cli/commands/workflow-tech-actions.js +150 -0
- package/dist/cli/commands/workspace/init-actions.d.ts +10 -0
- package/dist/cli/commands/workspace/init-actions.js +199 -0
- package/dist/cli/commands/workspace/init-codegraph-stake.d.ts +10 -0
- package/dist/cli/commands/workspace/init-codegraph-stake.js +54 -0
- package/dist/cli/commands/workspace/init-command.d.ts +8 -124
- package/dist/cli/commands/workspace/init-command.js +59 -623
- package/dist/cli/commands/workspace/init-context.d.ts +58 -0
- package/dist/cli/commands/workspace/init-context.js +121 -0
- package/dist/cli/commands/workspace/init-envelope.d.ts +3 -0
- package/dist/cli/commands/workspace/init-envelope.js +34 -0
- package/dist/cli/commands/workspace/init-failures.d.ts +3 -0
- package/dist/cli/commands/workspace/init-failures.js +84 -0
- package/dist/cli/commands/workspace/init-hooks-decision.d.ts +24 -0
- package/dist/cli/commands/workspace/init-hooks-decision.js +189 -0
- package/dist/cli/commands/workspace/init-options.d.ts +102 -0
- package/dist/cli/commands/workspace/init-options.js +1 -0
- package/dist/cli/commands/worktree-lease-commands.d.ts +10 -6
- package/dist/cli/commands/worktree-lease-commands.js +42 -512
- package/dist/cli/commands/worktree-lease-gc-command.d.ts +12 -0
- package/dist/cli/commands/worktree-lease-gc-command.js +117 -0
- package/dist/cli/commands/worktree-lease-git.d.ts +22 -0
- package/dist/cli/commands/worktree-lease-git.js +48 -0
- package/dist/cli/commands/worktree-lease-list-command.d.ts +12 -0
- package/dist/cli/commands/worktree-lease-list-command.js +100 -0
- package/dist/cli/commands/worktree-lease-load.d.ts +37 -0
- package/dist/cli/commands/worktree-lease-load.js +28 -0
- package/dist/cli/commands/worktree-lease-release-command.d.ts +13 -0
- package/dist/cli/commands/worktree-lease-release-command.js +105 -0
- package/dist/cli/commands/worktree-lease-renew-command.d.ts +11 -0
- package/dist/cli/commands/worktree-lease-renew-command.js +99 -0
- package/dist/cli/commands/worktree-lease-session.d.ts +22 -0
- package/dist/cli/commands/worktree-lease-session.js +29 -0
- package/dist/cli/commands/worktree-lease-spawn-command.d.ts +11 -0
- package/dist/cli/commands/worktree-lease-spawn-command.js +119 -0
- package/dist/cli/commands/worktree-lease-status-command.d.ts +11 -0
- package/dist/cli/commands/worktree-lease-status-command.js +86 -0
- package/dist/cli/commands/worktree-lease-store.d.ts +19 -0
- package/dist/cli/commands/worktree-lease-store.js +24 -0
- package/dist/cli/program.js +5 -0
- package/dist/services/distribution/mcp-install-runner.d.ts +21 -0
- package/dist/services/distribution/mcp-install-runner.js +59 -0
- package/dist/services/distribution/mcp-install.d.ts +95 -0
- package/dist/services/distribution/mcp-install.js +179 -0
- package/dist/services/distribution/plugin-manifest.d.ts +97 -0
- package/dist/services/distribution/plugin-manifest.js +126 -0
- package/dist/services/hooks/mcp-surface-gate.d.ts +19 -0
- package/dist/services/hooks/mcp-surface-gate.js +87 -0
- package/dist/services/ide/adapters/claude-code-adapter.d.ts +4 -35
- package/dist/services/ide/adapters/claude-code-adapter.js +48 -171
- package/dist/services/ide/adapters/claude-code-local-state.d.ts +28 -0
- package/dist/services/ide/adapters/claude-code-local-state.js +112 -0
- package/dist/services/ide/adapters/claude-code-model-window.d.ts +37 -0
- package/dist/services/ide/adapters/claude-code-model-window.js +113 -0
- package/dist/services/ide/ide-mcp-install-types.d.ts +78 -0
- package/dist/services/ide/ide-mcp-install-types.js +104 -0
- package/dist/services/ide/ide-types.d.ts +45 -1
- package/dist/services/ide/mcp-tool-matcher.d.ts +34 -0
- package/dist/services/ide/mcp-tool-matcher.js +84 -0
- package/dist/services/lint/npx-resolver.js +7 -0
- package/dist/services/mcp/cli-executor.d.ts +36 -0
- package/dist/services/mcp/cli-executor.js +97 -0
- package/dist/services/mcp/json-rpc.d.ts +81 -0
- package/dist/services/mcp/json-rpc.js +116 -0
- package/dist/services/mcp/memory-tool.d.ts +3 -0
- package/dist/services/mcp/memory-tool.js +62 -0
- package/dist/services/mcp/server-main.d.ts +1 -0
- package/dist/services/mcp/server-main.js +66 -0
- package/dist/services/mcp/server.d.ts +22 -0
- package/dist/services/mcp/server.js +152 -0
- package/dist/services/mcp/surface.d.ts +62 -0
- package/dist/services/mcp/surface.js +198 -0
- package/dist/services/mcp/tool-core.d.ts +42 -0
- package/dist/services/mcp/tool-core.js +137 -0
- package/dist/services/mcp/tools.d.ts +13 -0
- package/dist/services/mcp/tools.js +58 -0
- package/dist/services/readonly-surface/argv-guard.d.ts +32 -0
- package/dist/services/readonly-surface/argv-guard.js +164 -0
- package/dist/services/readonly-surface/readonly-whitelist.d.ts +147 -0
- package/dist/services/readonly-surface/readonly-whitelist.js +107 -0
- package/dist/services/readonly-surface/whitelist-generator.d.ts +64 -0
- package/dist/services/readonly-surface/whitelist-generator.js +212 -0
- package/dist/services/skills/hooks-codegate-superpowers.d.ts +1 -0
- package/dist/services/skills/hooks-codegate-superpowers.js +26 -0
- package/dist/services/skills/hooks-settings-service.js +10 -2
- package/package.json +17 -6
- package/scripts/install-skills-bundles.mjs +233 -0
- package/scripts/install-skills-fs.mjs +148 -0
- package/scripts/install-skills-ide.mjs +279 -0
- package/scripts/install-skills-output-style-default.mjs +196 -0
- package/scripts/install-skills-output-styles.mjs +121 -0
- package/scripts/install-skills-report.mjs +42 -0
- package/scripts/install-skills-user-config.mjs +198 -0
- package/scripts/install-skills.mjs +36 -1066
- package/scripts/watch-tree.mjs +140 -0
- package/scripts/watch.mjs +4 -126
- package/skills/peaks-status/SKILL.md +31 -13
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `peaks worktree gc` — sweep released/expired leases.
|
|
3
|
+
*
|
|
4
|
+
* Extracted verbatim from `worktree-lease-commands.ts` (mechanical move of
|
|
5
|
+
* the `.action()` body). The per-lease sweep is now `sweepLease`; the
|
|
6
|
+
* `--lease-id` / `--dry-run` semantics, the expired-active transition, the
|
|
7
|
+
* best-effort git removal + prune, the per-lease metric emit and the
|
|
8
|
+
* ok/fail envelopes are unchanged.
|
|
9
|
+
*/
|
|
10
|
+
import { fail, ok } from 'peaks-loop-shared/result';
|
|
11
|
+
import { addJsonOption, getErrorMessage, printResult } from '../cli-helpers.js';
|
|
12
|
+
import { atomicWriteJson } from '../../services/ide/shared/atomic-json.js';
|
|
13
|
+
import { emitLeaseEvent } from '../../services/observability/observability-service.js';
|
|
14
|
+
import { isLeaseGcEligible, leaseFilePath, markExpired, markGc } from '../../services/worktree/worktree-lease.js';
|
|
15
|
+
import { pruneWorktreesBestEffort, removeWorktreeBestEffort } from './worktree-lease-git.js';
|
|
16
|
+
import { loadLeaseStore, malformedLeaseWarnings } from './worktree-lease-store.js';
|
|
17
|
+
import { joinPathSession, resolveProjectRoot, resolveSessionId } from './worktree-lease-session.js';
|
|
18
|
+
const GC_DESCRIPTION = "Sweep released/expired leases: remove their git worktree (if still attached), prune git's " +
|
|
19
|
+
'worktree references, and mark the lease as "gc". With --lease-id <id>, only that lease is ' +
|
|
20
|
+
"considered. With --dry-run, report what would be gc'd without mutating. Expired-active " +
|
|
21
|
+
'leases (status=active but past expiresAt) are eligible — they are first marked "expired" ' +
|
|
22
|
+
'then their worktree is removed.';
|
|
23
|
+
const GC_LEASE_ID_DESCRIPTION = 'only consider this specific lease';
|
|
24
|
+
const GC_DRY_RUN_DESCRIPTION = "report what would be gc'd without mutating";
|
|
25
|
+
function sweepLease(ctx) {
|
|
26
|
+
const { lease, dryRun, now, projectRoot, sessionId } = ctx;
|
|
27
|
+
let prevStatus = lease.status;
|
|
28
|
+
let updated = lease;
|
|
29
|
+
// If the lease is still marked active but past expiresAt, transition to expired first.
|
|
30
|
+
if (lease.status === 'active' && lease.expiresAt <= now) {
|
|
31
|
+
updated = markExpired(lease);
|
|
32
|
+
prevStatus = 'active';
|
|
33
|
+
}
|
|
34
|
+
if (dryRun) {
|
|
35
|
+
return {
|
|
36
|
+
leaseId: lease.leaseId,
|
|
37
|
+
path: lease.path,
|
|
38
|
+
prevStatus,
|
|
39
|
+
gitWorktreeRemoveFailed: false
|
|
40
|
+
};
|
|
41
|
+
}
|
|
42
|
+
// `git worktree remove --force` is best-effort — if the path is
|
|
43
|
+
// already gone we still mark the lease gc.
|
|
44
|
+
const gitWorktreeRemoveFailed = removeWorktreeBestEffort(projectRoot, updated.path);
|
|
45
|
+
pruneWorktreesBestEffort(projectRoot);
|
|
46
|
+
const finalLease = markGc(updated);
|
|
47
|
+
atomicWriteJson(leaseFilePath(joinPathSession(projectRoot, sessionId), lease.leaseId), finalLease);
|
|
48
|
+
// Part 4.A: emit gc metric (per swept lease).
|
|
49
|
+
emitLeaseEvent({
|
|
50
|
+
sessionId,
|
|
51
|
+
projectRoot,
|
|
52
|
+
kind: 'gc',
|
|
53
|
+
leaseId: lease.leaseId,
|
|
54
|
+
rid: lease.rid,
|
|
55
|
+
role: lease.role
|
|
56
|
+
});
|
|
57
|
+
return { leaseId: lease.leaseId, path: lease.path, prevStatus, gitWorktreeRemoveFailed };
|
|
58
|
+
}
|
|
59
|
+
function printGcStoreMissing(io, options, ctx) {
|
|
60
|
+
const { sessionId, projectRoot, storeDir } = ctx;
|
|
61
|
+
printResult(io, ok('worktree.gc', { sessionId, projectRoot, swept: 0, storeMissing: true }, [], [`No lease store at ${storeDir}; nothing to gc.`]), options.json);
|
|
62
|
+
}
|
|
63
|
+
function printGcOk(io, options, ctx) {
|
|
64
|
+
const { sessionId, projectRoot, dryRun, candidates, swept, errors } = ctx;
|
|
65
|
+
printResult(io, ok('worktree.gc', { sessionId, projectRoot, dryRun, candidates, swept, errors }, malformedLeaseWarnings(errors), [
|
|
66
|
+
dryRun
|
|
67
|
+
? `[dry-run] Would gc ${swept.length} lease(s); no filesystem changes were made.`
|
|
68
|
+
: `Gc'd ${swept.length} lease(s).`,
|
|
69
|
+
'For per-lease detail, run `peaks worktree status --lease-id <id>`.'
|
|
70
|
+
]), options.json);
|
|
71
|
+
}
|
|
72
|
+
function printGcFailed(io, options, err, sessionId) {
|
|
73
|
+
printResult(io, fail('worktree.gc', 'GC_FAILED', getErrorMessage(err), { sessionId }, [
|
|
74
|
+
'Re-run after fixing the failure (see cause in the error message).'
|
|
75
|
+
]), options.json);
|
|
76
|
+
process.exitCode = 1;
|
|
77
|
+
}
|
|
78
|
+
function runGc(options, io) {
|
|
79
|
+
const projectRoot = resolveProjectRoot(options);
|
|
80
|
+
const sessionId = resolveSessionId(options, projectRoot);
|
|
81
|
+
try {
|
|
82
|
+
const { storeDir, result } = loadLeaseStore(projectRoot, sessionId);
|
|
83
|
+
if (result.kind === 'store-missing') {
|
|
84
|
+
printGcStoreMissing(io, options, { sessionId, projectRoot, storeDir });
|
|
85
|
+
return;
|
|
86
|
+
}
|
|
87
|
+
const now = Date.now();
|
|
88
|
+
const candidates = result.leases
|
|
89
|
+
.filter((l) => (options.leaseId ? l.leaseId === options.leaseId : true))
|
|
90
|
+
.filter((l) => isLeaseGcEligible(l, now));
|
|
91
|
+
const dryRun = options.dryRun === true;
|
|
92
|
+
const swept = [];
|
|
93
|
+
for (const lease of candidates) {
|
|
94
|
+
swept.push(sweepLease({ lease, dryRun, now, projectRoot, sessionId }));
|
|
95
|
+
}
|
|
96
|
+
printGcOk(io, options, {
|
|
97
|
+
sessionId,
|
|
98
|
+
projectRoot,
|
|
99
|
+
dryRun,
|
|
100
|
+
candidates: candidates.length,
|
|
101
|
+
swept,
|
|
102
|
+
errors: result.errors
|
|
103
|
+
});
|
|
104
|
+
}
|
|
105
|
+
catch (err) {
|
|
106
|
+
printGcFailed(io, options, err, sessionId);
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
export function registerWorktreeLeaseGcCommand(auth, io) {
|
|
110
|
+
addJsonOption(auth
|
|
111
|
+
.command('gc')
|
|
112
|
+
.description(GC_DESCRIPTION)
|
|
113
|
+
.option('--lease-id <id>', GC_LEASE_ID_DESCRIPTION)
|
|
114
|
+
.option('--dry-run', GC_DRY_RUN_DESCRIPTION)
|
|
115
|
+
.option('--session <sid>', 'override session id')
|
|
116
|
+
.option('--project <path>', 'project root (default: findProjectRoot(cwd))')).action((options) => runGc(options, io));
|
|
117
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Best-effort `git worktree` shell helpers shared by `release` and `gc`.
|
|
3
|
+
*
|
|
4
|
+
* Both commands detach a worktree the same way (and `gc` additionally
|
|
5
|
+
* prunes stale admin entries). Extracted verbatim from
|
|
6
|
+
* `worktree-lease-commands.ts`: the commands, cwd, stdio/encoding flags
|
|
7
|
+
* and the swallow-on-failure posture are unchanged.
|
|
8
|
+
*/
|
|
9
|
+
/**
|
|
10
|
+
* `git worktree remove --force` is best-effort: when the path is already
|
|
11
|
+
* pruned the caller still marks the lease released / gc. Returns whether
|
|
12
|
+
* the git invocation failed.
|
|
13
|
+
*
|
|
14
|
+
* `windowsHide: true` is spelled at each call site rather than shared: the
|
|
15
|
+
* guard (`tests/unit/spawn-windows-hide-guard.test.ts`) resolves a same-file
|
|
16
|
+
* plain-`const` object literal and a spread of it, but not one declared
|
|
17
|
+
* `as const`, frozen, `let`, or imported from another file — measured: both
|
|
18
|
+
* call sites below were reported while the shared constant was `as const`.
|
|
19
|
+
*/
|
|
20
|
+
export declare function removeWorktreeBestEffort(projectRoot: string, wtPath: string): boolean;
|
|
21
|
+
/** `git worktree prune` clears stale admin entries. Best-effort. */
|
|
22
|
+
export declare function pruneWorktreesBestEffort(projectRoot: string): void;
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Best-effort `git worktree` shell helpers shared by `release` and `gc`.
|
|
3
|
+
*
|
|
4
|
+
* Both commands detach a worktree the same way (and `gc` additionally
|
|
5
|
+
* prunes stale admin entries). Extracted verbatim from
|
|
6
|
+
* `worktree-lease-commands.ts`: the commands, cwd, stdio/encoding flags
|
|
7
|
+
* and the swallow-on-failure posture are unchanged.
|
|
8
|
+
*/
|
|
9
|
+
import { execSync } from 'node:child_process';
|
|
10
|
+
/**
|
|
11
|
+
* `git worktree remove --force` is best-effort: when the path is already
|
|
12
|
+
* pruned the caller still marks the lease released / gc. Returns whether
|
|
13
|
+
* the git invocation failed.
|
|
14
|
+
*
|
|
15
|
+
* `windowsHide: true` is spelled at each call site rather than shared: the
|
|
16
|
+
* guard (`tests/unit/spawn-windows-hide-guard.test.ts`) resolves a same-file
|
|
17
|
+
* plain-`const` object literal and a spread of it, but not one declared
|
|
18
|
+
* `as const`, frozen, `let`, or imported from another file — measured: both
|
|
19
|
+
* call sites below were reported while the shared constant was `as const`.
|
|
20
|
+
*/
|
|
21
|
+
export function removeWorktreeBestEffort(projectRoot, wtPath) {
|
|
22
|
+
try {
|
|
23
|
+
execSync(`git worktree remove --force "${wtPath}"`, {
|
|
24
|
+
cwd: projectRoot,
|
|
25
|
+
stdio: 'pipe',
|
|
26
|
+
encoding: 'utf8',
|
|
27
|
+
windowsHide: true
|
|
28
|
+
});
|
|
29
|
+
return false;
|
|
30
|
+
}
|
|
31
|
+
catch {
|
|
32
|
+
return true;
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
/** `git worktree prune` clears stale admin entries. Best-effort. */
|
|
36
|
+
export function pruneWorktreesBestEffort(projectRoot) {
|
|
37
|
+
try {
|
|
38
|
+
execSync('git worktree prune', {
|
|
39
|
+
cwd: projectRoot,
|
|
40
|
+
stdio: 'pipe',
|
|
41
|
+
encoding: 'utf8',
|
|
42
|
+
windowsHide: true
|
|
43
|
+
});
|
|
44
|
+
}
|
|
45
|
+
catch {
|
|
46
|
+
// ignore — prune is idempotent
|
|
47
|
+
}
|
|
48
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `peaks worktree list` — enumerate every lease in the session store.
|
|
3
|
+
*
|
|
4
|
+
* Extracted verbatim from `worktree-lease-commands.ts` (mechanical move of
|
|
5
|
+
* the `.action()` body). The store read is now the shared
|
|
6
|
+
* `loadLeaseStore`; the live/elapsed/remaining annotation, the
|
|
7
|
+
* `--status` / `--expired-only` filters, the createdAt-desc sort and the
|
|
8
|
+
* ok/fail envelopes are unchanged.
|
|
9
|
+
*/
|
|
10
|
+
import type { Command } from 'commander';
|
|
11
|
+
import { type ProgramIO } from '../cli-helpers.js';
|
|
12
|
+
export declare function registerWorktreeLeaseListCommand(auth: Command, io: ProgramIO): void;
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `peaks worktree list` — enumerate every lease in the session store.
|
|
3
|
+
*
|
|
4
|
+
* Extracted verbatim from `worktree-lease-commands.ts` (mechanical move of
|
|
5
|
+
* the `.action()` body). The store read is now the shared
|
|
6
|
+
* `loadLeaseStore`; the live/elapsed/remaining annotation, the
|
|
7
|
+
* `--status` / `--expired-only` filters, the createdAt-desc sort and the
|
|
8
|
+
* ok/fail envelopes are unchanged.
|
|
9
|
+
*/
|
|
10
|
+
import { fail, ok } from 'peaks-loop-shared/result';
|
|
11
|
+
import { addJsonOption, getErrorMessage, printResult } from '../cli-helpers.js';
|
|
12
|
+
import { isLeaseActive } from '../../services/worktree/worktree-lease.js';
|
|
13
|
+
import { loadLeaseStore, malformedLeaseWarnings } from './worktree-lease-store.js';
|
|
14
|
+
import { resolveProjectRoot, resolveSessionId } from './worktree-lease-session.js';
|
|
15
|
+
const LIST_DESCRIPTION = "List every lease under the current session's lease store. " +
|
|
16
|
+
'Optionally filter by --status (active|released|expired|gc) and/or --expired-only. ' +
|
|
17
|
+
'Leases past their expiresAt with status=active are still listed under "active" by default ' +
|
|
18
|
+
'(`isLeaseActive` returns false for them, but the on-disk status only flips to "expired" ' +
|
|
19
|
+
'after `peaks worktree gc` runs).';
|
|
20
|
+
const LIST_STATUS_DESCRIPTION = 'filter by lease status: active | released | expired | gc';
|
|
21
|
+
const LIST_EXPIRED_ONLY_DESCRIPTION = 'only show leases whose expiresAt is in the past';
|
|
22
|
+
function annotateLeases(leases, now) {
|
|
23
|
+
return leases.map((l) => ({
|
|
24
|
+
...l,
|
|
25
|
+
live: isLeaseActive(l, now),
|
|
26
|
+
elapsedMs: now - l.createdAt,
|
|
27
|
+
remainingMs: l.expiresAt - now
|
|
28
|
+
}));
|
|
29
|
+
}
|
|
30
|
+
function filterLeases(leases, options) {
|
|
31
|
+
let filtered = leases;
|
|
32
|
+
if (options.status) {
|
|
33
|
+
filtered = filtered.filter((l) => l.status === options.status);
|
|
34
|
+
}
|
|
35
|
+
if (options.expiredOnly === true) {
|
|
36
|
+
filtered = filtered.filter((l) => !l.live);
|
|
37
|
+
}
|
|
38
|
+
// Sort by createdAt desc — most-recent first. Stable for diffs.
|
|
39
|
+
return [...filtered].sort((a, b) => b.createdAt - a.createdAt);
|
|
40
|
+
}
|
|
41
|
+
function printListStoreMissing(io, options, ctx) {
|
|
42
|
+
const { sessionId, projectRoot, storeDir } = ctx;
|
|
43
|
+
printResult(io, ok('worktree.list', { sessionId, projectRoot, leases: [], errors: [], storeMissing: true }, [], [`No lease store at ${storeDir}. Spawn a worktree first (\`peaks worktree spawn ...\`).`]), options.json);
|
|
44
|
+
}
|
|
45
|
+
function printListOk(io, options, ctx) {
|
|
46
|
+
const { sessionId, projectRoot, storeDir, totalOnDisk, errors, leases } = ctx;
|
|
47
|
+
printResult(io, ok('worktree.list', {
|
|
48
|
+
sessionId,
|
|
49
|
+
projectRoot,
|
|
50
|
+
storeDir,
|
|
51
|
+
totalOnDisk,
|
|
52
|
+
returned: leases.length,
|
|
53
|
+
errors,
|
|
54
|
+
leases
|
|
55
|
+
}, malformedLeaseWarnings(errors), [
|
|
56
|
+
`${leases.length} lease(s) matched (${totalOnDisk} on disk).`,
|
|
57
|
+
errors.length > 0
|
|
58
|
+
? 'Some lease files were malformed — see errors[]; they were skipped.'
|
|
59
|
+
: ''
|
|
60
|
+
].filter(Boolean)), options.json);
|
|
61
|
+
}
|
|
62
|
+
function printListFailed(io, options, err, sessionId) {
|
|
63
|
+
printResult(io, fail('worktree.list', 'LIST_FAILED', getErrorMessage(err), { sessionId }, [
|
|
64
|
+
'Re-run after fixing the failure (see cause in the error message).'
|
|
65
|
+
]), options.json);
|
|
66
|
+
process.exitCode = 1;
|
|
67
|
+
}
|
|
68
|
+
function runList(options, io) {
|
|
69
|
+
const projectRoot = resolveProjectRoot(options);
|
|
70
|
+
const sessionId = resolveSessionId(options, projectRoot);
|
|
71
|
+
try {
|
|
72
|
+
const { storeDir, result } = loadLeaseStore(projectRoot, sessionId);
|
|
73
|
+
if (result.kind === 'store-missing') {
|
|
74
|
+
printListStoreMissing(io, options, { sessionId, projectRoot, storeDir });
|
|
75
|
+
return;
|
|
76
|
+
}
|
|
77
|
+
const now = Date.now();
|
|
78
|
+
const filtered = filterLeases(annotateLeases(result.leases, now), options);
|
|
79
|
+
printListOk(io, options, {
|
|
80
|
+
sessionId,
|
|
81
|
+
projectRoot,
|
|
82
|
+
storeDir,
|
|
83
|
+
totalOnDisk: result.leases.length,
|
|
84
|
+
errors: result.errors,
|
|
85
|
+
leases: filtered
|
|
86
|
+
});
|
|
87
|
+
}
|
|
88
|
+
catch (err) {
|
|
89
|
+
printListFailed(io, options, err, sessionId);
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
export function registerWorktreeLeaseListCommand(auth, io) {
|
|
93
|
+
addJsonOption(auth
|
|
94
|
+
.command('list')
|
|
95
|
+
.description(LIST_DESCRIPTION)
|
|
96
|
+
.option('--status <state>', LIST_STATUS_DESCRIPTION)
|
|
97
|
+
.option('--expired-only', LIST_EXPIRED_ONLY_DESCRIPTION)
|
|
98
|
+
.option('--session <sid>', 'override session id')
|
|
99
|
+
.option('--project <path>', 'project root (default: findProjectRoot(cwd))')).action((options) => runList(options, io));
|
|
100
|
+
}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared single-lease loader for the three read-by-lease-id commands
|
|
3
|
+
* (`release`, `renew`, `lease-status`).
|
|
4
|
+
*
|
|
5
|
+
* All three used to inline the same two failure shapes — a missing lease
|
|
6
|
+
* file (`LEASE_NOT_FOUND`) and an unparseable one (`LEASE_FILE_INVALID`) —
|
|
7
|
+
* differing only in the envelope `command` string and the two hint lines.
|
|
8
|
+
* Both are parameters here. The message text, the `{ leaseId, file }`
|
|
9
|
+
* data payload and the caller's `process.exitCode = 1` are unchanged.
|
|
10
|
+
*/
|
|
11
|
+
import { type ProgramIO } from '../cli-helpers.js';
|
|
12
|
+
import { type WorktreeLease } from '../../services/worktree/worktree-lease.js';
|
|
13
|
+
/** The option surface the loader reads: the lease id and `--json`. */
|
|
14
|
+
export interface LeaseIdOptions {
|
|
15
|
+
leaseId: string;
|
|
16
|
+
json?: boolean;
|
|
17
|
+
}
|
|
18
|
+
export interface LeaseLoadHints {
|
|
19
|
+
/** nextActions printed for `LEASE_NOT_FOUND`. */
|
|
20
|
+
missing: string[];
|
|
21
|
+
/** nextActions printed for `LEASE_FILE_INVALID`. */
|
|
22
|
+
invalid: string[];
|
|
23
|
+
}
|
|
24
|
+
export interface LeaseLoadRequest {
|
|
25
|
+
/** The envelope `command` string (e.g. `worktree.renew`). */
|
|
26
|
+
command: string;
|
|
27
|
+
/** Absolute path of the lease file to read. */
|
|
28
|
+
file: string;
|
|
29
|
+
hints: LeaseLoadHints;
|
|
30
|
+
}
|
|
31
|
+
export type LeaseLoadResult = {
|
|
32
|
+
stop: true;
|
|
33
|
+
} | {
|
|
34
|
+
stop: false;
|
|
35
|
+
lease: WorktreeLease;
|
|
36
|
+
};
|
|
37
|
+
export declare function loadLeaseOrStop(io: ProgramIO, options: LeaseIdOptions, request: LeaseLoadRequest): LeaseLoadResult;
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared single-lease loader for the three read-by-lease-id commands
|
|
3
|
+
* (`release`, `renew`, `lease-status`).
|
|
4
|
+
*
|
|
5
|
+
* All three used to inline the same two failure shapes — a missing lease
|
|
6
|
+
* file (`LEASE_NOT_FOUND`) and an unparseable one (`LEASE_FILE_INVALID`) —
|
|
7
|
+
* differing only in the envelope `command` string and the two hint lines.
|
|
8
|
+
* Both are parameters here. The message text, the `{ leaseId, file }`
|
|
9
|
+
* data payload and the caller's `process.exitCode = 1` are unchanged.
|
|
10
|
+
*/
|
|
11
|
+
import { fail } from 'peaks-loop-shared/result';
|
|
12
|
+
import { existsSync, readFileSync as readFileSyncNode } from 'node:fs';
|
|
13
|
+
import { getErrorMessage, printResult } from '../cli-helpers.js';
|
|
14
|
+
import { deserializeLease } from '../../services/worktree/worktree-lease.js';
|
|
15
|
+
export function loadLeaseOrStop(io, options, request) {
|
|
16
|
+
const { command, file, hints } = request;
|
|
17
|
+
if (!existsSync(file)) {
|
|
18
|
+
printResult(io, fail(command, 'LEASE_NOT_FOUND', `no lease on disk at ${file}`, { leaseId: options.leaseId, file }, hints.missing), options.json);
|
|
19
|
+
return { stop: true };
|
|
20
|
+
}
|
|
21
|
+
try {
|
|
22
|
+
return { stop: false, lease: deserializeLease(readFileSyncNode(file, 'utf8')) };
|
|
23
|
+
}
|
|
24
|
+
catch (error) {
|
|
25
|
+
printResult(io, fail(command, 'LEASE_FILE_INVALID', getErrorMessage(error), { leaseId: options.leaseId, file }, hints.invalid), options.json);
|
|
26
|
+
return { stop: true };
|
|
27
|
+
}
|
|
28
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `peaks worktree release` — transition a lease to 'released' and run
|
|
3
|
+
* `git worktree remove`.
|
|
4
|
+
*
|
|
5
|
+
* Extracted verbatim from `worktree-lease-commands.ts` (mechanical move of
|
|
6
|
+
* the `.action()` body). The lease-file failures now route through the
|
|
7
|
+
* shared `loadLeaseOrStop` (same envelope, same hints, same exit code);
|
|
8
|
+
* the idempotent already-released branch, the best-effort git removal and
|
|
9
|
+
* the ok/fail envelopes are unchanged.
|
|
10
|
+
*/
|
|
11
|
+
import type { Command } from 'commander';
|
|
12
|
+
import { type ProgramIO } from '../cli-helpers.js';
|
|
13
|
+
export declare function registerWorktreeLeaseReleaseCommand(auth: Command, io: ProgramIO): void;
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `peaks worktree release` — transition a lease to 'released' and run
|
|
3
|
+
* `git worktree remove`.
|
|
4
|
+
*
|
|
5
|
+
* Extracted verbatim from `worktree-lease-commands.ts` (mechanical move of
|
|
6
|
+
* the `.action()` body). The lease-file failures now route through the
|
|
7
|
+
* shared `loadLeaseOrStop` (same envelope, same hints, same exit code);
|
|
8
|
+
* the idempotent already-released branch, the best-effort git removal and
|
|
9
|
+
* the ok/fail envelopes are unchanged.
|
|
10
|
+
*/
|
|
11
|
+
import { fail, ok } from 'peaks-loop-shared/result';
|
|
12
|
+
import { addJsonOption, getErrorMessage, printResult } from '../cli-helpers.js';
|
|
13
|
+
import { atomicWriteJson } from '../../services/ide/shared/atomic-json.js';
|
|
14
|
+
import { emitLeaseEvent } from '../../services/observability/observability-service.js';
|
|
15
|
+
import { leaseFilePath, markReleased } from '../../services/worktree/worktree-lease.js';
|
|
16
|
+
import { loadLeaseOrStop } from './worktree-lease-load.js';
|
|
17
|
+
import { removeWorktreeBestEffort } from './worktree-lease-git.js';
|
|
18
|
+
import { joinPathSession, resolveProjectRoot, resolveSessionId } from './worktree-lease-session.js';
|
|
19
|
+
const RELEASE_DESCRIPTION = 'Transition a lease to released and run `git worktree remove`. Idempotent on already-released leases.';
|
|
20
|
+
const RELEASE_MISSING_HINTS = [
|
|
21
|
+
'Run `peaks worktree list` to inspect active leases.',
|
|
22
|
+
'For a never-spawned lease, this is a no-op — no further action needed.'
|
|
23
|
+
];
|
|
24
|
+
const RELEASE_INVALID_HINTS = [
|
|
25
|
+
'Delete the malformed lease file manually and re-issue spawn.',
|
|
26
|
+
'For security, release never fails open on a malformed lease.'
|
|
27
|
+
];
|
|
28
|
+
function printReleaseAlreadyReleased(io, options, ctx) {
|
|
29
|
+
const { lease, sessionId, projectRoot } = ctx;
|
|
30
|
+
// Idempotent: already released, nothing to do.
|
|
31
|
+
printResult(io, ok('worktree.release', { lease, sessionId, projectRoot, alreadyReleased: true }, [], [`Lease ${lease.leaseId} already released; nothing to do.`]), options.json);
|
|
32
|
+
}
|
|
33
|
+
function printReleaseDone(io, options, ctx) {
|
|
34
|
+
const { lease, released, sessionId, projectRoot, gitWorktreeRemoveFailed } = ctx;
|
|
35
|
+
printResult(io, ok('worktree.release', { lease: released, sessionId, projectRoot, gitWorktreeRemoveFailed }, gitWorktreeRemoveFailed
|
|
36
|
+
? [
|
|
37
|
+
'git worktree remove failed (likely the path was already pruned); lease marked released.'
|
|
38
|
+
]
|
|
39
|
+
: [], [
|
|
40
|
+
`Lease ${lease.leaseId} marked released.`,
|
|
41
|
+
gitWorktreeRemoveFailed
|
|
42
|
+
? 'Manual `git worktree prune` may be needed.'
|
|
43
|
+
: `Worktree ${lease.path} removed.`
|
|
44
|
+
]), options.json);
|
|
45
|
+
}
|
|
46
|
+
function printReleaseFailed(io, options, error, sessionId) {
|
|
47
|
+
printResult(io, fail('worktree.release', 'RELEASE_FAILED', getErrorMessage(error), { leaseId: options.leaseId, sessionId }, ['Verify the lease id and re-run.', 'If the lease was never spawned, no-op.']), options.json);
|
|
48
|
+
process.exitCode = 1;
|
|
49
|
+
}
|
|
50
|
+
function runRelease(options, io) {
|
|
51
|
+
const projectRoot = resolveProjectRoot(options);
|
|
52
|
+
const sessionId = resolveSessionId(options, projectRoot);
|
|
53
|
+
try {
|
|
54
|
+
const file = leaseFilePath(joinPathSession(projectRoot, sessionId), options.leaseId);
|
|
55
|
+
const loaded = loadLeaseOrStop(io, options, {
|
|
56
|
+
command: 'worktree.release',
|
|
57
|
+
file,
|
|
58
|
+
hints: { missing: RELEASE_MISSING_HINTS, invalid: RELEASE_INVALID_HINTS }
|
|
59
|
+
});
|
|
60
|
+
if (loaded.stop) {
|
|
61
|
+
process.exitCode = 1;
|
|
62
|
+
return;
|
|
63
|
+
}
|
|
64
|
+
const lease = loaded.lease;
|
|
65
|
+
if (lease.status === 'released') {
|
|
66
|
+
printReleaseAlreadyReleased(io, options, { lease, sessionId, projectRoot });
|
|
67
|
+
return;
|
|
68
|
+
}
|
|
69
|
+
// Run `git worktree remove` from the project root. If the path is
|
|
70
|
+
// missing on disk (e.g. manually pruned), this fails; we still
|
|
71
|
+
// update the lease state so the L2 hook (Part 2) treats it as
|
|
72
|
+
// released.
|
|
73
|
+
const gitWorktreeRemoveFailed = removeWorktreeBestEffort(projectRoot, lease.path);
|
|
74
|
+
const released = markReleased(lease);
|
|
75
|
+
atomicWriteJson(file, released);
|
|
76
|
+
// Part 4.A: emit release metric (manual path; auto-release is
|
|
77
|
+
// recorded in dispatch-record-writer's tryAutoReleaseLease).
|
|
78
|
+
emitLeaseEvent({
|
|
79
|
+
sessionId,
|
|
80
|
+
projectRoot,
|
|
81
|
+
kind: 'release',
|
|
82
|
+
leaseId: released.leaseId,
|
|
83
|
+
rid: released.rid,
|
|
84
|
+
role: released.role
|
|
85
|
+
});
|
|
86
|
+
printReleaseDone(io, options, {
|
|
87
|
+
lease,
|
|
88
|
+
released,
|
|
89
|
+
sessionId,
|
|
90
|
+
projectRoot,
|
|
91
|
+
gitWorktreeRemoveFailed
|
|
92
|
+
});
|
|
93
|
+
}
|
|
94
|
+
catch (error) {
|
|
95
|
+
printReleaseFailed(io, options, error, sessionId);
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
export function registerWorktreeLeaseReleaseCommand(auth, io) {
|
|
99
|
+
addJsonOption(auth
|
|
100
|
+
.command('release')
|
|
101
|
+
.description(RELEASE_DESCRIPTION)
|
|
102
|
+
.requiredOption('--lease-id <id>', 'lease id returned by `peaks worktree spawn`')
|
|
103
|
+
.option('--session <sid>', 'override session id')
|
|
104
|
+
.option('--project <path>', 'project root (default: findProjectRoot(cwd))')).action((options) => runRelease(options, io));
|
|
105
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `peaks worktree renew` — extend an active lease's `expiresAt`.
|
|
3
|
+
*
|
|
4
|
+
* Extracted verbatim from `worktree-lease-commands.ts` (mechanical move of
|
|
5
|
+
* the `.action()` body). The lease-file failures route through the shared
|
|
6
|
+
* `loadLeaseOrStop`; the LEASE_NOT_RENEWABLE / INVALID_TTL guards, the
|
|
7
|
+
* renewal write, the metric emit and the ok/fail envelopes are unchanged.
|
|
8
|
+
*/
|
|
9
|
+
import type { Command } from 'commander';
|
|
10
|
+
import { type ProgramIO } from '../cli-helpers.js';
|
|
11
|
+
export declare function registerWorktreeLeaseRenewCommand(auth: Command, io: ProgramIO): void;
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `peaks worktree renew` — extend an active lease's `expiresAt`.
|
|
3
|
+
*
|
|
4
|
+
* Extracted verbatim from `worktree-lease-commands.ts` (mechanical move of
|
|
5
|
+
* the `.action()` body). The lease-file failures route through the shared
|
|
6
|
+
* `loadLeaseOrStop`; the LEASE_NOT_RENEWABLE / INVALID_TTL guards, the
|
|
7
|
+
* renewal write, the metric emit and the ok/fail envelopes are unchanged.
|
|
8
|
+
*/
|
|
9
|
+
import { fail, ok } from 'peaks-loop-shared/result';
|
|
10
|
+
import { addJsonOption, getErrorMessage, printResult } from '../cli-helpers.js';
|
|
11
|
+
import { atomicWriteJson } from '../../services/ide/shared/atomic-json.js';
|
|
12
|
+
import { emitLeaseEvent } from '../../services/observability/observability-service.js';
|
|
13
|
+
import { leaseFilePath, renewLease, ttlForRole } from '../../services/worktree/worktree-lease.js';
|
|
14
|
+
import { loadLeaseOrStop } from './worktree-lease-load.js';
|
|
15
|
+
import { joinPathSession, resolveProjectRoot, resolveSessionId } from './worktree-lease-session.js';
|
|
16
|
+
const RENEW_DESCRIPTION = "Extend an active lease's `expiresAt` and persist it. Idempotent for already-active leases. " +
|
|
17
|
+
'Default TTL uses `DEFAULT_TTL_BY_ROLE[<lease.role>]`; pass --ttl <ms> to override.';
|
|
18
|
+
const RENEW_MISSING_HINTS = [
|
|
19
|
+
'Run `peaks worktree list` to inspect active leases.',
|
|
20
|
+
'For a never-spawned lease, this is a no-op.'
|
|
21
|
+
];
|
|
22
|
+
const RENEW_INVALID_HINTS = [
|
|
23
|
+
'Delete the malformed lease file manually and re-spawn.',
|
|
24
|
+
'For security, renew never fails open on a malformed lease.'
|
|
25
|
+
];
|
|
26
|
+
function printRenewNotRenewable(io, options, lease) {
|
|
27
|
+
printResult(io, fail('worktree.renew', 'LEASE_NOT_RENEWABLE', `lease is in status '${lease.status}'; only active/expired leases may be renewed`, { leaseId: lease.leaseId, status: lease.status }, [
|
|
28
|
+
'Released leases cannot be renewed. Run `peaks worktree spawn` to start a new lease.',
|
|
29
|
+
'Gc-marked leases are terminal — re-spawn.'
|
|
30
|
+
]), options.json);
|
|
31
|
+
process.exitCode = 1;
|
|
32
|
+
}
|
|
33
|
+
function printRenewInvalidTtl(io, options) {
|
|
34
|
+
printResult(io, fail('worktree.renew', 'INVALID_TTL', '--ttl must be a positive integer (ms)', { ttl: options.ttl }, ['Re-run with --ttl 1800000 (30 min) or omit to use role default.']), options.json);
|
|
35
|
+
process.exitCode = 1;
|
|
36
|
+
}
|
|
37
|
+
function printRenewOk(io, options, ctx) {
|
|
38
|
+
const { lease, renewed, sessionId, projectRoot, ttlMs } = ctx;
|
|
39
|
+
printResult(io, ok('worktree.renew', { lease: renewed, sessionId, projectRoot, ttlMs, previousExpiresAt: lease.expiresAt }, [], [
|
|
40
|
+
`Lease ${renewed.leaseId} renewed; new expiresAt: ${new Date(renewed.expiresAt).toISOString()}.`,
|
|
41
|
+
`Branch ${renewed.branch} at ${renewed.path} remains intact (no ` +
|
|
42
|
+
'`git worktree` operation was performed — the worktree was always there).'
|
|
43
|
+
]), options.json);
|
|
44
|
+
}
|
|
45
|
+
function printRenewFailed(io, options, err, sessionId) {
|
|
46
|
+
printResult(io, fail('worktree.renew', 'RENEW_FAILED', getErrorMessage(err), { leaseId: options.leaseId, sessionId }, ['Re-run after fixing the failure (see cause in the error message).']), options.json);
|
|
47
|
+
process.exitCode = 1;
|
|
48
|
+
}
|
|
49
|
+
function runRenew(options, io) {
|
|
50
|
+
const projectRoot = resolveProjectRoot(options);
|
|
51
|
+
const sessionId = resolveSessionId(options, projectRoot);
|
|
52
|
+
try {
|
|
53
|
+
const file = leaseFilePath(joinPathSession(projectRoot, sessionId), options.leaseId);
|
|
54
|
+
const loaded = loadLeaseOrStop(io, options, {
|
|
55
|
+
command: 'worktree.renew',
|
|
56
|
+
file,
|
|
57
|
+
hints: { missing: RENEW_MISSING_HINTS, invalid: RENEW_INVALID_HINTS }
|
|
58
|
+
});
|
|
59
|
+
if (loaded.stop) {
|
|
60
|
+
process.exitCode = 1;
|
|
61
|
+
return;
|
|
62
|
+
}
|
|
63
|
+
const lease = loaded.lease;
|
|
64
|
+
if (lease.status === 'released' || lease.status === 'gc') {
|
|
65
|
+
printRenewNotRenewable(io, options, lease);
|
|
66
|
+
return;
|
|
67
|
+
}
|
|
68
|
+
const now = Date.now();
|
|
69
|
+
const ttlMs = options.ttl === undefined ? ttlForRole(lease.role) : Number.parseInt(options.ttl, 10);
|
|
70
|
+
if (!Number.isInteger(ttlMs) || ttlMs <= 0) {
|
|
71
|
+
printRenewInvalidTtl(io, options);
|
|
72
|
+
return;
|
|
73
|
+
}
|
|
74
|
+
const renewed = renewLease(lease, now + ttlMs);
|
|
75
|
+
atomicWriteJson(file, renewed);
|
|
76
|
+
// Part 4.A: emit renew metric.
|
|
77
|
+
emitLeaseEvent({
|
|
78
|
+
sessionId,
|
|
79
|
+
projectRoot,
|
|
80
|
+
kind: 'renew',
|
|
81
|
+
leaseId: renewed.leaseId,
|
|
82
|
+
rid: renewed.rid,
|
|
83
|
+
role: renewed.role
|
|
84
|
+
});
|
|
85
|
+
printRenewOk(io, options, { lease, renewed, sessionId, projectRoot, ttlMs });
|
|
86
|
+
}
|
|
87
|
+
catch (err) {
|
|
88
|
+
printRenewFailed(io, options, err, sessionId);
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
export function registerWorktreeLeaseRenewCommand(auth, io) {
|
|
92
|
+
addJsonOption(auth
|
|
93
|
+
.command('renew')
|
|
94
|
+
.description(RENEW_DESCRIPTION)
|
|
95
|
+
.requiredOption('--lease-id <id>', 'lease id returned by `peaks worktree spawn`')
|
|
96
|
+
.option('--ttl <ms>', 'time-to-live in ms from now (default: role-default)')
|
|
97
|
+
.option('--session <sid>', 'override session id')
|
|
98
|
+
.option('--project <path>', 'project root (default: findProjectRoot(cwd))')).action((options) => runRenew(options, io));
|
|
99
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Session / project-root resolution shared by every `peaks worktree
|
|
3
|
+
* <spawn|release|renew|list|gc|lease-status>` sub-command.
|
|
4
|
+
*
|
|
5
|
+
* Extracted verbatim from `worktree-lease-commands.ts` so each
|
|
6
|
+
* `worktree-lease-<name>-command.ts` sibling can resolve the same roots
|
|
7
|
+
* without importing the registrar module (the dependency direction stays
|
|
8
|
+
* one-way: siblings → this module → services).
|
|
9
|
+
*/
|
|
10
|
+
export declare function resolveSessionId(options: {
|
|
11
|
+
session?: string;
|
|
12
|
+
}, projectRoot: string): string;
|
|
13
|
+
export declare function resolveProjectRoot(options: {
|
|
14
|
+
project?: string;
|
|
15
|
+
}): string;
|
|
16
|
+
/**
|
|
17
|
+
* Resolve the per-session runtime directory: `<projectRoot>/.peaks/_runtime/<sessionId>`.
|
|
18
|
+
* The runtime tree is gitignored; the spawn CLI writes the lease file
|
|
19
|
+
* under `<this>/worktree-leases/<leaseId>.json` and the worktree itself
|
|
20
|
+
* under `<this>/worktrees/<leaseId>/`.
|
|
21
|
+
*/
|
|
22
|
+
export declare function joinPathSession(projectRoot: string, sessionId: string): string;
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Session / project-root resolution shared by every `peaks worktree
|
|
3
|
+
* <spawn|release|renew|list|gc|lease-status>` sub-command.
|
|
4
|
+
*
|
|
5
|
+
* Extracted verbatim from `worktree-lease-commands.ts` so each
|
|
6
|
+
* `worktree-lease-<name>-command.ts` sibling can resolve the same roots
|
|
7
|
+
* without importing the registrar module (the dependency direction stays
|
|
8
|
+
* one-way: siblings → this module → services).
|
|
9
|
+
*/
|
|
10
|
+
import { findProjectRoot } from '../../services/config/config-safety.js';
|
|
11
|
+
import { getCurrentSessionId } from '../../services/skills/skill-presence-service.js';
|
|
12
|
+
export function resolveSessionId(options, projectRoot) {
|
|
13
|
+
if (typeof options.session === 'string' && options.session.length > 0)
|
|
14
|
+
return options.session;
|
|
15
|
+
// Reuse the same precedence as the rest of peaks: explicit --session > PEAKS_SESSION_ID > active session.json
|
|
16
|
+
return process.env.PEAKS_SESSION_ID ?? getCurrentSessionId(projectRoot) ?? 'unknown-sid';
|
|
17
|
+
}
|
|
18
|
+
export function resolveProjectRoot(options) {
|
|
19
|
+
return options.project ?? findProjectRoot(process.cwd()) ?? process.cwd();
|
|
20
|
+
}
|
|
21
|
+
/**
|
|
22
|
+
* Resolve the per-session runtime directory: `<projectRoot>/.peaks/_runtime/<sessionId>`.
|
|
23
|
+
* The runtime tree is gitignored; the spawn CLI writes the lease file
|
|
24
|
+
* under `<this>/worktree-leases/<leaseId>.json` and the worktree itself
|
|
25
|
+
* under `<this>/worktrees/<leaseId>/`.
|
|
26
|
+
*/
|
|
27
|
+
export function joinPathSession(projectRoot, sessionId) {
|
|
28
|
+
return `${projectRoot.replace(/[\\/]+$/, '')}/.peaks/_runtime/${sessionId}`;
|
|
29
|
+
}
|