@ai-sdlc/orchestrator 0.6.0 → 0.10.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/dist/__test-helpers/git-env.d.ts +40 -0
- package/dist/__test-helpers/git-env.js +73 -0
- package/dist/admission-composite.d.ts +89 -0
- package/dist/admission-composite.js +241 -0
- package/dist/admission-enrichment.d.ts +142 -0
- package/dist/admission-enrichment.js +341 -0
- package/dist/admission-hc.d.ts +81 -0
- package/dist/admission-hc.js +94 -0
- package/dist/admission-score.d.ts +109 -5
- package/dist/admission-score.js +90 -11
- package/dist/analysis/analyzer.js +3 -2
- package/dist/analysis/convention-detector.d.ts +85 -2
- package/dist/analysis/convention-detector.js +375 -70
- package/dist/analysis/diff-analyzer.d.ts +33 -0
- package/dist/analysis/diff-analyzer.js +122 -0
- package/dist/analysis/hotspot-analyzer.js +3 -1
- package/dist/analysis/index.d.ts +2 -1
- package/dist/analysis/index.js +2 -1
- package/dist/artifacts/index.d.ts +65 -0
- package/dist/artifacts/index.js +142 -0
- package/dist/backlog-adapter.d.ts +121 -0
- package/dist/backlog-adapter.js +438 -0
- package/dist/calibration.d.ts +43 -0
- package/dist/calibration.js +76 -0
- package/dist/cli/commands/agents.d.ts +24 -0
- package/dist/cli/commands/agents.js +66 -1
- package/dist/cli/commands/git-remote.d.ts +74 -0
- package/dist/cli/commands/git-remote.js +170 -0
- package/dist/cli/commands/health.d.ts +4 -0
- package/dist/cli/commands/health.js +13 -1
- package/dist/cli/commands/init-features.d.ts +208 -0
- package/dist/cli/commands/init-features.js +473 -0
- package/dist/cli/commands/init-templates.d.ts +104 -0
- package/dist/cli/commands/init-templates.js +399 -0
- package/dist/cli/commands/init.d.ts +48 -0
- package/dist/cli/commands/init.js +322 -23
- package/dist/cli/commands/mcp-setup.d.ts +31 -0
- package/dist/cli/commands/mcp-setup.js +78 -8
- package/dist/cli/formatters/table.js +15 -2
- package/dist/cli/index.d.ts +14 -1
- package/dist/cli/index.js +81 -20
- package/dist/cli/versions.d.ts +57 -0
- package/dist/cli/versions.js +128 -0
- package/dist/code-area-classifier.d.ts +21 -0
- package/dist/code-area-classifier.js +48 -0
- package/dist/config.d.ts +33 -1
- package/dist/config.js +78 -8
- package/dist/database/adapters/external.d.ts +24 -0
- package/dist/database/adapters/external.js +80 -0
- package/dist/database/adapters/neon.d.ts +41 -0
- package/dist/database/adapters/neon.js +98 -0
- package/dist/database/adapters/pg-snapshot-restore.d.ts +28 -0
- package/dist/database/adapters/pg-snapshot-restore.js +68 -0
- package/dist/database/adapters/sqlite-copy.d.ts +32 -0
- package/dist/database/adapters/sqlite-copy.js +145 -0
- package/dist/database/connection-injection.d.ts +35 -0
- package/dist/database/connection-injection.js +93 -0
- package/dist/database/index.d.ts +12 -0
- package/dist/database/index.js +23 -0
- package/dist/database/registry.d.ts +13 -0
- package/dist/database/registry.js +27 -0
- package/dist/database/topology.d.ts +52 -0
- package/dist/database/topology.js +44 -0
- package/dist/database/types.d.ts +89 -0
- package/dist/database/types.js +26 -0
- package/dist/design-authority.d.ts +53 -0
- package/dist/design-authority.js +84 -0
- package/dist/design-lookahead.d.ts +64 -0
- package/dist/design-lookahead.js +86 -0
- package/dist/design-quality-trend.d.ts +87 -0
- package/dist/design-quality-trend.js +190 -0
- package/dist/design-system-context.d.ts +46 -0
- package/dist/design-system-context.js +80 -0
- package/dist/design-system-correction-loop.d.ts +64 -0
- package/dist/design-system-correction-loop.js +128 -0
- package/dist/design-system-metrics.d.ts +61 -0
- package/dist/design-system-metrics.js +104 -0
- package/dist/design-system-stewardship.d.ts +22 -0
- package/dist/design-system-stewardship.js +85 -0
- package/dist/design-system-validation.d.ts +37 -0
- package/dist/design-system-validation.js +88 -0
- package/dist/dispatch/index.d.ts +4 -0
- package/dist/dispatch/index.js +4 -0
- package/dist/dispatch/merge-gate.d.ts +46 -0
- package/dist/dispatch/merge-gate.js +90 -0
- package/dist/dispatch/requeue.d.ts +57 -0
- package/dist/dispatch/requeue.js +131 -0
- package/dist/dispatch/worker-pool.d.ts +62 -0
- package/dist/dispatch/worker-pool.js +60 -0
- package/dist/execute.d.ts +40 -0
- package/dist/execute.js +188 -15
- package/dist/fix-ci.js +8 -1
- package/dist/fix-review.js +8 -1
- package/dist/harness/adapters/claude-code.d.ts +29 -0
- package/dist/harness/adapters/claude-code.js +191 -0
- package/dist/harness/adapters/codex.d.ts +25 -0
- package/dist/harness/adapters/codex.js +61 -0
- package/dist/harness/independence.d.ts +51 -0
- package/dist/harness/independence.js +67 -0
- package/dist/harness/index.d.ts +14 -0
- package/dist/harness/index.js +20 -0
- package/dist/harness/registry.d.ts +17 -0
- package/dist/harness/registry.js +31 -0
- package/dist/harness/types.d.ts +123 -0
- package/dist/harness/types.js +8 -0
- package/dist/harness/version-probe.d.ts +14 -0
- package/dist/harness/version-probe.js +123 -0
- package/dist/index.d.ts +30 -4
- package/dist/index.js +35 -2
- package/dist/models/classifier.d.ts +78 -0
- package/dist/models/classifier.js +277 -0
- package/dist/models/index.d.ts +3 -0
- package/dist/models/index.js +3 -0
- package/dist/models/registry.d.ts +97 -0
- package/dist/models/registry.js +173 -0
- package/dist/pillar-breakdown.d.ts +85 -0
- package/dist/pillar-breakdown.js +162 -0
- package/dist/priority.d.ts +5 -0
- package/dist/priority.js +18 -5
- package/dist/review-meta.d.ts +65 -0
- package/dist/review-meta.js +149 -0
- package/dist/review.d.ts +4 -0
- package/dist/review.js +12 -2
- package/dist/runners/claude-code-sdk.d.ts +31 -0
- package/dist/runners/claude-code-sdk.js +238 -0
- package/dist/runners/claude-code.d.ts +3 -0
- package/dist/runners/claude-code.js +30 -66
- package/dist/runners/codex.js +4 -1
- package/dist/runners/copilot.js +4 -1
- package/dist/runners/cursor.js +4 -1
- package/dist/runners/git-utils.d.ts +81 -0
- package/dist/runners/git-utils.js +201 -0
- package/dist/runners/index.d.ts +3 -0
- package/dist/runners/index.js +3 -0
- package/dist/runners/review-agent.d.ts +20 -0
- package/dist/runners/review-agent.js +171 -41
- package/dist/runners/runner-registry.js +10 -0
- package/dist/runners/sdk-review-runner.d.ts +65 -0
- package/dist/runners/sdk-review-runner.js +185 -0
- package/dist/runners/security-triage.d.ts +20 -4
- package/dist/runners/security-triage.js +39 -15
- package/dist/runners/types.d.ts +6 -0
- package/dist/runtime/attestations.d.ts +766 -0
- package/dist/runtime/attestations.js +1195 -0
- package/dist/runtime/git-env.d.ts +53 -0
- package/dist/runtime/git-env.js +60 -0
- package/dist/runtime/index.d.ts +7 -0
- package/dist/runtime/index.js +7 -0
- package/dist/runtime/parallelism-flag.d.ts +28 -0
- package/dist/runtime/parallelism-flag.js +39 -0
- package/dist/runtime/port-allocator.d.ts +32 -0
- package/dist/runtime/port-allocator.js +96 -0
- package/dist/runtime/worktree-pool.d.ts +86 -0
- package/dist/runtime/worktree-pool.js +204 -0
- package/dist/runtime/worktree.d.ts +25 -0
- package/dist/runtime/worktree.js +111 -0
- package/dist/sa-scoring/auto-calibrate.d.ts +69 -0
- package/dist/sa-scoring/auto-calibrate.js +107 -0
- package/dist/sa-scoring/c1-sa2-computable.d.ts +42 -0
- package/dist/sa-scoring/c1-sa2-computable.js +59 -0
- package/dist/sa-scoring/composite.d.ts +107 -0
- package/dist/sa-scoring/composite.js +139 -0
- package/dist/sa-scoring/depparse-client.d.ts +79 -0
- package/dist/sa-scoring/depparse-client.js +187 -0
- package/dist/sa-scoring/did-compiler.d.ts +122 -0
- package/dist/sa-scoring/did-compiler.js +286 -0
- package/dist/sa-scoring/drift-monitor.d.ts +84 -0
- package/dist/sa-scoring/drift-monitor.js +186 -0
- package/dist/sa-scoring/exemplar-bank.d.ts +78 -0
- package/dist/sa-scoring/exemplar-bank.js +154 -0
- package/dist/sa-scoring/feedback-store.d.ts +100 -0
- package/dist/sa-scoring/feedback-store.js +156 -0
- package/dist/sa-scoring/index.d.ts +71 -0
- package/dist/sa-scoring/index.js +158 -0
- package/dist/sa-scoring/layer1-deterministic.d.ts +115 -0
- package/dist/sa-scoring/layer1-deterministic.js +298 -0
- package/dist/sa-scoring/layer2-structural.d.ts +71 -0
- package/dist/sa-scoring/layer2-structural.js +151 -0
- package/dist/sa-scoring/layer3-llm.d.ts +86 -0
- package/dist/sa-scoring/layer3-llm.js +282 -0
- package/dist/sa-scoring/rescore-orchestrator.d.ts +52 -0
- package/dist/sa-scoring/rescore-orchestrator.js +47 -0
- package/dist/scheduling/burn-down.d.ts +27 -0
- package/dist/scheduling/burn-down.js +43 -0
- package/dist/scheduling/calibration.d.ts +42 -0
- package/dist/scheduling/calibration.js +150 -0
- package/dist/scheduling/index.d.ts +8 -0
- package/dist/scheduling/index.js +8 -0
- package/dist/scheduling/ledger.d.ts +59 -0
- package/dist/scheduling/ledger.js +216 -0
- package/dist/scheduling/off-peak.d.ts +27 -0
- package/dist/scheduling/off-peak.js +112 -0
- package/dist/scheduling/schedule-decision.d.ts +39 -0
- package/dist/scheduling/schedule-decision.js +89 -0
- package/dist/scheduling/tier-analysis.d.ts +47 -0
- package/dist/scheduling/tier-analysis.js +81 -0
- package/dist/scheduling/types.d.ts +140 -0
- package/dist/scheduling/types.js +11 -0
- package/dist/shared.d.ts +13 -0
- package/dist/shared.js +32 -0
- package/dist/state/schema.d.ts +5 -1
- package/dist/state/schema.js +223 -1
- package/dist/state/store.d.ts +55 -1
- package/dist/state/store.js +342 -7
- package/dist/state/types.d.ts +139 -0
- package/dist/types.d.ts +1 -1
- package/dist/validate-agent-output.js +4 -1
- package/dist/watch.js +6 -0
- package/package.json +3 -2
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Helpers for safely invoking `git` subprocesses inside isolated working
|
|
3
|
+
* directories.
|
|
4
|
+
*
|
|
5
|
+
* ## Why this exists (AISDLC-68 → AISDLC-72)
|
|
6
|
+
*
|
|
7
|
+
* When a husky pre-push hook (or any nested git invocation) runs commands
|
|
8
|
+
* from inside `.worktrees/<id>/`, git exports `GIT_DIR`, `GIT_WORK_TREE`,
|
|
9
|
+
* and `GIT_INDEX_FILE` into the child process environment so subprocesses
|
|
10
|
+
* "stay" in the calling worktree's git context. That's the right behavior
|
|
11
|
+
* for normal subprocesses — but it's catastrophic for code that creates
|
|
12
|
+
* its own throwaway repository in a temp directory.
|
|
13
|
+
*
|
|
14
|
+
* Without sanitizing the environment:
|
|
15
|
+
* - `execSync('git init', { cwd: tmpDir })` initializes the *parent*
|
|
16
|
+
* worktree's index, not tmpDir's.
|
|
17
|
+
* - `execSync('git commit', { cwd: tmpDir })` writes a commit onto the
|
|
18
|
+
* *parent's* current branch using files from tmpDir — silently
|
|
19
|
+
* corrupting the feature branch with leaked test artifacts.
|
|
20
|
+
* - The pre-push hook that triggered this whole cascade then sees
|
|
21
|
+
* unexpected commits and either fails or worse, ships them.
|
|
22
|
+
*
|
|
23
|
+
* The fix is uniform: strip `GIT_DIR` / `GIT_WORK_TREE` / `GIT_INDEX_FILE`
|
|
24
|
+
* from the inherited env at every site that runs `git` against an explicit
|
|
25
|
+
* `cwd`. PR #78 patched the tokens-studio adapter; AISDLC-72 sweeps the
|
|
26
|
+
* orchestrator + remaining test sites.
|
|
27
|
+
*/
|
|
28
|
+
/**
|
|
29
|
+
* Return a copy of `process.env` with the git-context env vars removed.
|
|
30
|
+
*
|
|
31
|
+
* Use this for any subprocess that runs `git` against an explicit `cwd`
|
|
32
|
+
* different from the calling process's CWD (temp repos, sibling repos,
|
|
33
|
+
* worktree-pool clones).
|
|
34
|
+
*/
|
|
35
|
+
export declare function cleanGitEnv(): NodeJS.ProcessEnv;
|
|
36
|
+
/**
|
|
37
|
+
* Promisified `execFile('git', args, opts)` wrapper that strips the git
|
|
38
|
+
* context env vars before invocation. Use whenever the caller passes
|
|
39
|
+
* `cwd` (i.e. running git in a directory other than the parent process's
|
|
40
|
+
* CWD).
|
|
41
|
+
*
|
|
42
|
+
* Caller-supplied `opts.env` takes precedence — it's already explicit
|
|
43
|
+
* about what to inherit. We only inject `cleanGitEnv()` when no env was
|
|
44
|
+
* provided.
|
|
45
|
+
*/
|
|
46
|
+
export declare function gitExecFile(args: string[], opts?: {
|
|
47
|
+
cwd?: string;
|
|
48
|
+
env?: NodeJS.ProcessEnv;
|
|
49
|
+
}): Promise<{
|
|
50
|
+
stdout: string;
|
|
51
|
+
stderr: string;
|
|
52
|
+
}>;
|
|
53
|
+
//# sourceMappingURL=git-env.d.ts.map
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Helpers for safely invoking `git` subprocesses inside isolated working
|
|
3
|
+
* directories.
|
|
4
|
+
*
|
|
5
|
+
* ## Why this exists (AISDLC-68 → AISDLC-72)
|
|
6
|
+
*
|
|
7
|
+
* When a husky pre-push hook (or any nested git invocation) runs commands
|
|
8
|
+
* from inside `.worktrees/<id>/`, git exports `GIT_DIR`, `GIT_WORK_TREE`,
|
|
9
|
+
* and `GIT_INDEX_FILE` into the child process environment so subprocesses
|
|
10
|
+
* "stay" in the calling worktree's git context. That's the right behavior
|
|
11
|
+
* for normal subprocesses — but it's catastrophic for code that creates
|
|
12
|
+
* its own throwaway repository in a temp directory.
|
|
13
|
+
*
|
|
14
|
+
* Without sanitizing the environment:
|
|
15
|
+
* - `execSync('git init', { cwd: tmpDir })` initializes the *parent*
|
|
16
|
+
* worktree's index, not tmpDir's.
|
|
17
|
+
* - `execSync('git commit', { cwd: tmpDir })` writes a commit onto the
|
|
18
|
+
* *parent's* current branch using files from tmpDir — silently
|
|
19
|
+
* corrupting the feature branch with leaked test artifacts.
|
|
20
|
+
* - The pre-push hook that triggered this whole cascade then sees
|
|
21
|
+
* unexpected commits and either fails or worse, ships them.
|
|
22
|
+
*
|
|
23
|
+
* The fix is uniform: strip `GIT_DIR` / `GIT_WORK_TREE` / `GIT_INDEX_FILE`
|
|
24
|
+
* from the inherited env at every site that runs `git` against an explicit
|
|
25
|
+
* `cwd`. PR #78 patched the tokens-studio adapter; AISDLC-72 sweeps the
|
|
26
|
+
* orchestrator + remaining test sites.
|
|
27
|
+
*/
|
|
28
|
+
import { execFile } from 'node:child_process';
|
|
29
|
+
import { promisify } from 'node:util';
|
|
30
|
+
const execFileAsync = promisify(execFile);
|
|
31
|
+
/**
|
|
32
|
+
* Return a copy of `process.env` with the git-context env vars removed.
|
|
33
|
+
*
|
|
34
|
+
* Use this for any subprocess that runs `git` against an explicit `cwd`
|
|
35
|
+
* different from the calling process's CWD (temp repos, sibling repos,
|
|
36
|
+
* worktree-pool clones).
|
|
37
|
+
*/
|
|
38
|
+
export function cleanGitEnv() {
|
|
39
|
+
const env = { ...process.env };
|
|
40
|
+
delete env.GIT_DIR;
|
|
41
|
+
delete env.GIT_WORK_TREE;
|
|
42
|
+
delete env.GIT_INDEX_FILE;
|
|
43
|
+
return env;
|
|
44
|
+
}
|
|
45
|
+
/**
|
|
46
|
+
* Promisified `execFile('git', args, opts)` wrapper that strips the git
|
|
47
|
+
* context env vars before invocation. Use whenever the caller passes
|
|
48
|
+
* `cwd` (i.e. running git in a directory other than the parent process's
|
|
49
|
+
* CWD).
|
|
50
|
+
*
|
|
51
|
+
* Caller-supplied `opts.env` takes precedence — it's already explicit
|
|
52
|
+
* about what to inherit. We only inject `cleanGitEnv()` when no env was
|
|
53
|
+
* provided.
|
|
54
|
+
*/
|
|
55
|
+
export async function gitExecFile(args, opts = {}) {
|
|
56
|
+
const env = opts.env ?? cleanGitEnv();
|
|
57
|
+
const { stdout, stderr } = await execFileAsync('git', args, { ...opts, env });
|
|
58
|
+
return { stdout, stderr };
|
|
59
|
+
}
|
|
60
|
+
//# sourceMappingURL=git-env.js.map
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
export { deterministicPort, allocatePort, allocateContiguousPorts, isPortFree, PortAllocationError, DEFAULT_BASE_PORT, PORT_RANGE_OFFSET_MIN, PORT_RANGE_OFFSET_MAX, type AllocatePortOptions, type AllocateContiguousOptions, } from './port-allocator.js';
|
|
2
|
+
export { slugifyBranch, worktreePath, verifyOwnership, assertOwnership, isExistingWorktree, WorktreeOwnershipError, type OwnershipResult, } from './worktree.js';
|
|
3
|
+
export { WorktreePoolManager, WorktreePoolError, DEFAULT_POOL_ROOT, DEFAULT_STALE_THRESHOLD_DAYS, type WorktreePoolSpec, type WorktreePoolManagerDeps, type AllocateOptions, type WorktreeHandle, } from './worktree-pool.js';
|
|
4
|
+
export { readParallelismMode, isParallelismEnabled, FLAG_NAME as PARALLELISM_FLAG, type ParallelismMode, } from './parallelism-flag.js';
|
|
5
|
+
export { cleanGitEnv, gitExecFile } from './git-env.js';
|
|
6
|
+
export { ACCEPTED_SCHEMA_VERSIONS, DSSE_PAYLOAD_TYPE, buildPredicate, generateSigningKeyPair, paeEncode, sha1Hex, sha256Hex, signAttestation, validateTrustedReviewers, verifyAttestation, type AttestationPredicate, type DsseEnvelope, type DsseSignature, type ReviewerEntry, type SchemaVersion, type SubjectDigest, type TrustedReviewer, type VerifyResult, } from './attestations.js';
|
|
7
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
export { deterministicPort, allocatePort, allocateContiguousPorts, isPortFree, PortAllocationError, DEFAULT_BASE_PORT, PORT_RANGE_OFFSET_MIN, PORT_RANGE_OFFSET_MAX, } from './port-allocator.js';
|
|
2
|
+
export { slugifyBranch, worktreePath, verifyOwnership, assertOwnership, isExistingWorktree, WorktreeOwnershipError, } from './worktree.js';
|
|
3
|
+
export { WorktreePoolManager, WorktreePoolError, DEFAULT_POOL_ROOT, DEFAULT_STALE_THRESHOLD_DAYS, } from './worktree-pool.js';
|
|
4
|
+
export { readParallelismMode, isParallelismEnabled, FLAG_NAME as PARALLELISM_FLAG, } from './parallelism-flag.js';
|
|
5
|
+
export { cleanGitEnv, gitExecFile } from './git-env.js';
|
|
6
|
+
export { ACCEPTED_SCHEMA_VERSIONS, DSSE_PAYLOAD_TYPE, buildPredicate, generateSigningKeyPair, paeEncode, sha1Hex, sha256Hex, signAttestation, validateTrustedReviewers, verifyAttestation, } from './attestations.js';
|
|
7
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Feature-flag gate for RFC-0010 parallelism. Phases 1-4 shipped behind
|
|
3
|
+
* AI_SDLC_PARALLELISM=experimental; Phase 5 (hardening) added the runbook,
|
|
4
|
+
* chaos plan, and stuck-heartbeat detection. Per maintainer directive
|
|
5
|
+
* 2026-05-01 (AISDLC-116), the calendar-based 1-week soak gate from RFC-0010
|
|
6
|
+
* Phase 5 was dropped in favor of substantive readiness — pre-flight scan
|
|
7
|
+
* of orchestrator/_events.jsonl + recent commit history showed zero
|
|
8
|
+
* parallelism-related incidents — and the flag now defaults to 'on'.
|
|
9
|
+
*
|
|
10
|
+
* Backwards-compat envelope (preserved on purpose):
|
|
11
|
+
* - unset → 'on' (new default)
|
|
12
|
+
* - 'on' | 'true' | '1' → 'on'
|
|
13
|
+
* - 'experimental' → 'experimental' (still honored for
|
|
14
|
+
* callers pinning the
|
|
15
|
+
* pre-promotion mode)
|
|
16
|
+
* - 'off' | 'disabled' |
|
|
17
|
+
* 'false' | '0' → 'off' (explicit opt-out)
|
|
18
|
+
* - any other string → 'on' (fail-on rather than fail-off
|
|
19
|
+
* now that 'on' is the default;
|
|
20
|
+
* prevents typos like
|
|
21
|
+
* 'enable' from silently
|
|
22
|
+
* disabling parallelism)
|
|
23
|
+
*/
|
|
24
|
+
export declare const FLAG_NAME = "AI_SDLC_PARALLELISM";
|
|
25
|
+
export type ParallelismMode = 'off' | 'experimental' | 'on';
|
|
26
|
+
export declare function readParallelismMode(env?: NodeJS.ProcessEnv): ParallelismMode;
|
|
27
|
+
export declare function isParallelismEnabled(env?: NodeJS.ProcessEnv): boolean;
|
|
28
|
+
//# sourceMappingURL=parallelism-flag.d.ts.map
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Feature-flag gate for RFC-0010 parallelism. Phases 1-4 shipped behind
|
|
3
|
+
* AI_SDLC_PARALLELISM=experimental; Phase 5 (hardening) added the runbook,
|
|
4
|
+
* chaos plan, and stuck-heartbeat detection. Per maintainer directive
|
|
5
|
+
* 2026-05-01 (AISDLC-116), the calendar-based 1-week soak gate from RFC-0010
|
|
6
|
+
* Phase 5 was dropped in favor of substantive readiness — pre-flight scan
|
|
7
|
+
* of orchestrator/_events.jsonl + recent commit history showed zero
|
|
8
|
+
* parallelism-related incidents — and the flag now defaults to 'on'.
|
|
9
|
+
*
|
|
10
|
+
* Backwards-compat envelope (preserved on purpose):
|
|
11
|
+
* - unset → 'on' (new default)
|
|
12
|
+
* - 'on' | 'true' | '1' → 'on'
|
|
13
|
+
* - 'experimental' → 'experimental' (still honored for
|
|
14
|
+
* callers pinning the
|
|
15
|
+
* pre-promotion mode)
|
|
16
|
+
* - 'off' | 'disabled' |
|
|
17
|
+
* 'false' | '0' → 'off' (explicit opt-out)
|
|
18
|
+
* - any other string → 'on' (fail-on rather than fail-off
|
|
19
|
+
* now that 'on' is the default;
|
|
20
|
+
* prevents typos like
|
|
21
|
+
* 'enable' from silently
|
|
22
|
+
* disabling parallelism)
|
|
23
|
+
*/
|
|
24
|
+
export const FLAG_NAME = 'AI_SDLC_PARALLELISM';
|
|
25
|
+
export function readParallelismMode(env = process.env) {
|
|
26
|
+
const raw = env[FLAG_NAME]?.trim().toLowerCase();
|
|
27
|
+
if (raw === undefined || raw === '')
|
|
28
|
+
return 'on';
|
|
29
|
+
if (raw === 'experimental')
|
|
30
|
+
return 'experimental';
|
|
31
|
+
if (raw === 'off' || raw === 'disabled' || raw === 'false' || raw === '0')
|
|
32
|
+
return 'off';
|
|
33
|
+
// 'on' / 'true' / '1' / any other value → 'on' (default-on, fail-on)
|
|
34
|
+
return 'on';
|
|
35
|
+
}
|
|
36
|
+
export function isParallelismEnabled(env) {
|
|
37
|
+
return readParallelismMode(env) !== 'off';
|
|
38
|
+
}
|
|
39
|
+
//# sourceMappingURL=parallelism-flag.js.map
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
export declare const DEFAULT_BASE_PORT = 3190;
|
|
2
|
+
export declare const PORT_RANGE_OFFSET_MIN = 100;
|
|
3
|
+
export declare const PORT_RANGE_OFFSET_MAX = 999;
|
|
4
|
+
export interface AllocatePortOptions {
|
|
5
|
+
basePort?: number;
|
|
6
|
+
envOverride?: string | undefined;
|
|
7
|
+
isPortFree?: (port: number) => Promise<boolean>;
|
|
8
|
+
}
|
|
9
|
+
export interface AllocateContiguousOptions extends AllocatePortOptions {
|
|
10
|
+
count: number;
|
|
11
|
+
}
|
|
12
|
+
export declare class PortAllocationError extends Error {
|
|
13
|
+
readonly worktreePath: string;
|
|
14
|
+
readonly attemptedPorts: number[];
|
|
15
|
+
constructor(message: string, worktreePath: string, attemptedPorts: number[]);
|
|
16
|
+
}
|
|
17
|
+
/**
|
|
18
|
+
* Derive a deterministic port for `worktreePath` by hashing its absolute form with MD5
|
|
19
|
+
* and mapping the first two bytes into [basePort + PORT_RANGE_OFFSET_MIN,
|
|
20
|
+
* basePort + PORT_RANGE_OFFSET_MAX].
|
|
21
|
+
*
|
|
22
|
+
* **Collision risk**: the output space is only 900 ports wide. When called with multiple
|
|
23
|
+
* independent (e.g. mkdtemp-generated) paths, two or more paths may map to the same port.
|
|
24
|
+
* For n=3 paths the birthday probability is ~0.33%. Integration tests that need to assert
|
|
25
|
+
* port distinctness MUST use pre-chosen, manually-verified paths rather than random
|
|
26
|
+
* temporaries — see the module-level JSDoc for the recommended assertion pattern.
|
|
27
|
+
*/
|
|
28
|
+
export declare function deterministicPort(worktreePath: string, basePort?: number): number;
|
|
29
|
+
export declare function allocatePort(worktreePath: string, options?: AllocatePortOptions): Promise<number>;
|
|
30
|
+
export declare function allocateContiguousPorts(worktreePath: string, options: AllocateContiguousOptions): Promise<number[]>;
|
|
31
|
+
export declare function isPortFree(port: number): Promise<boolean>;
|
|
32
|
+
//# sourceMappingURL=port-allocator.d.ts.map
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Deterministic port allocator for git worktrees.
|
|
3
|
+
*
|
|
4
|
+
* `deterministicPort` maps an absolute worktree path to a port number in a fixed
|
|
5
|
+
* 900-port range (DEFAULT_BASE_PORT + PORT_RANGE_OFFSET_MIN … PORT_RANGE_OFFSET_MAX)
|
|
6
|
+
* by hashing the path with MD5 and taking the first two bytes modulo the range span.
|
|
7
|
+
*
|
|
8
|
+
* **Hash-collision caveat**: the 900-port range is finite. When multiple worktree paths
|
|
9
|
+
* are fed through `deterministicPort` in a single test, birthday-paradox collisions are
|
|
10
|
+
* possible. For n=3 paths the probability is roughly (n*(n-1)/2) / span ≈ 0.33%. Over
|
|
11
|
+
* thousands of CI runs this causes spurious "distinct ports" assertion failures.
|
|
12
|
+
*
|
|
13
|
+
* **Integration-test guidance**: do NOT assert `new Set(ports).size === n` unless the
|
|
14
|
+
* test controls the exact path strings and has pre-verified they hash to distinct values.
|
|
15
|
+
* Instead assert:
|
|
16
|
+
* - each port is in [DEFAULT_BASE_PORT + PORT_RANGE_OFFSET_MIN, DEFAULT_BASE_PORT + PORT_RANGE_OFFSET_MAX]
|
|
17
|
+
* - calling allocatePort twice for the same path returns the same value (idempotency)
|
|
18
|
+
* - ports[i] === deterministicPort(h.path) (the core deterministic-port property)
|
|
19
|
+
*/
|
|
20
|
+
import { createHash } from 'node:crypto';
|
|
21
|
+
import { resolve } from 'node:path';
|
|
22
|
+
import { createServer } from 'node:net';
|
|
23
|
+
export const DEFAULT_BASE_PORT = 3190;
|
|
24
|
+
export const PORT_RANGE_OFFSET_MIN = 100;
|
|
25
|
+
export const PORT_RANGE_OFFSET_MAX = 999;
|
|
26
|
+
const PORT_RANGE_SPAN = PORT_RANGE_OFFSET_MAX - PORT_RANGE_OFFSET_MIN + 1;
|
|
27
|
+
const COLLISION_PROBE_ATTEMPTS = 10;
|
|
28
|
+
export class PortAllocationError extends Error {
|
|
29
|
+
worktreePath;
|
|
30
|
+
attemptedPorts;
|
|
31
|
+
constructor(message, worktreePath, attemptedPorts) {
|
|
32
|
+
super(message);
|
|
33
|
+
this.worktreePath = worktreePath;
|
|
34
|
+
this.attemptedPorts = attemptedPorts;
|
|
35
|
+
this.name = 'PortAllocationError';
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
/**
|
|
39
|
+
* Derive a deterministic port for `worktreePath` by hashing its absolute form with MD5
|
|
40
|
+
* and mapping the first two bytes into [basePort + PORT_RANGE_OFFSET_MIN,
|
|
41
|
+
* basePort + PORT_RANGE_OFFSET_MAX].
|
|
42
|
+
*
|
|
43
|
+
* **Collision risk**: the output space is only 900 ports wide. When called with multiple
|
|
44
|
+
* independent (e.g. mkdtemp-generated) paths, two or more paths may map to the same port.
|
|
45
|
+
* For n=3 paths the birthday probability is ~0.33%. Integration tests that need to assert
|
|
46
|
+
* port distinctness MUST use pre-chosen, manually-verified paths rather than random
|
|
47
|
+
* temporaries — see the module-level JSDoc for the recommended assertion pattern.
|
|
48
|
+
*/
|
|
49
|
+
export function deterministicPort(worktreePath, basePort = DEFAULT_BASE_PORT) {
|
|
50
|
+
const absolute = resolve(worktreePath);
|
|
51
|
+
const digest = createHash('md5').update(absolute).digest();
|
|
52
|
+
const offset = (((digest[0] << 8) | digest[1]) % PORT_RANGE_SPAN) + PORT_RANGE_OFFSET_MIN;
|
|
53
|
+
return basePort + offset;
|
|
54
|
+
}
|
|
55
|
+
export async function allocatePort(worktreePath, options = {}) {
|
|
56
|
+
const envValue = options.envOverride;
|
|
57
|
+
if (envValue !== undefined && envValue !== '') {
|
|
58
|
+
const parsed = Number.parseInt(envValue, 10);
|
|
59
|
+
if (!Number.isFinite(parsed) || parsed <= 0 || parsed > 65535) {
|
|
60
|
+
throw new PortAllocationError(`PORT environment override is not a valid TCP port: ${envValue}`, worktreePath, []);
|
|
61
|
+
}
|
|
62
|
+
return parsed;
|
|
63
|
+
}
|
|
64
|
+
const basePort = options.basePort ?? DEFAULT_BASE_PORT;
|
|
65
|
+
const start = deterministicPort(worktreePath, basePort);
|
|
66
|
+
const probe = options.isPortFree ?? isPortFree;
|
|
67
|
+
const attempted = [];
|
|
68
|
+
for (let i = 0; i < COLLISION_PROBE_ATTEMPTS; i++) {
|
|
69
|
+
const candidate = start + i;
|
|
70
|
+
attempted.push(candidate);
|
|
71
|
+
if (await probe(candidate)) {
|
|
72
|
+
return candidate;
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
throw new PortAllocationError(`No free port found after ${COLLISION_PROBE_ATTEMPTS} attempts starting at ${start}`, worktreePath, attempted);
|
|
76
|
+
}
|
|
77
|
+
export async function allocateContiguousPorts(worktreePath, options) {
|
|
78
|
+
if (options.count < 1 || options.count > 10) {
|
|
79
|
+
throw new PortAllocationError(`Contiguous port count must be between 1 and 10 (got ${options.count})`, worktreePath, []);
|
|
80
|
+
}
|
|
81
|
+
const first = await allocatePort(worktreePath, options);
|
|
82
|
+
return Array.from({ length: options.count }, (_, i) => first + i);
|
|
83
|
+
}
|
|
84
|
+
export function isPortFree(port) {
|
|
85
|
+
return new Promise((resolveProbe) => {
|
|
86
|
+
const server = createServer();
|
|
87
|
+
server.once('error', () => {
|
|
88
|
+
resolveProbe(false);
|
|
89
|
+
});
|
|
90
|
+
server.once('listening', () => {
|
|
91
|
+
server.close(() => resolveProbe(true));
|
|
92
|
+
});
|
|
93
|
+
server.listen(port, '127.0.0.1');
|
|
94
|
+
});
|
|
95
|
+
}
|
|
96
|
+
//# sourceMappingURL=port-allocator.js.map
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
export declare const DEFAULT_POOL_ROOT = "~/.ai-sdlc/worktrees";
|
|
2
|
+
export declare const DEFAULT_STALE_THRESHOLD_DAYS = 14;
|
|
3
|
+
export interface WorktreePoolSpec {
|
|
4
|
+
rootDir?: string;
|
|
5
|
+
staleThresholdDays?: number;
|
|
6
|
+
ownershipGuard?: 'strict' | 'advisory';
|
|
7
|
+
}
|
|
8
|
+
export interface AllocateOptions {
|
|
9
|
+
/** Base branch to create from (default 'origin/main'). */
|
|
10
|
+
baseBranch?: string;
|
|
11
|
+
}
|
|
12
|
+
export interface WorktreeHandle {
|
|
13
|
+
branch: string;
|
|
14
|
+
slug: string;
|
|
15
|
+
path: string;
|
|
16
|
+
/** True when allocate() created the worktree fresh; false when it adopted an existing one. */
|
|
17
|
+
created: boolean;
|
|
18
|
+
}
|
|
19
|
+
export declare class WorktreePoolError extends Error {
|
|
20
|
+
readonly branch: string;
|
|
21
|
+
readonly cause?: unknown | undefined;
|
|
22
|
+
constructor(message: string, branch: string, cause?: unknown | undefined);
|
|
23
|
+
}
|
|
24
|
+
export interface WorktreePoolManagerDeps {
|
|
25
|
+
/** Override `git` calls in tests. */
|
|
26
|
+
git?: (args: string[], opts?: {
|
|
27
|
+
cwd?: string;
|
|
28
|
+
}) => Promise<{
|
|
29
|
+
stdout: string;
|
|
30
|
+
stderr: string;
|
|
31
|
+
}>;
|
|
32
|
+
/** Override clock for stale-threshold tests. */
|
|
33
|
+
now?: () => Date;
|
|
34
|
+
}
|
|
35
|
+
/**
|
|
36
|
+
* Manages a pool of git worktrees rooted at `rootDir`. Each worktree is named after
|
|
37
|
+
* `slugifyBranch(branch)`. The pool enforces cross-clone ownership (RFC §7.2) and supports
|
|
38
|
+
* allocate / adopt / reclaim plus a stale-threshold sweep (RFC §7.3).
|
|
39
|
+
*/
|
|
40
|
+
export declare class WorktreePoolManager {
|
|
41
|
+
readonly rootDir: string;
|
|
42
|
+
private readonly clonePath;
|
|
43
|
+
private readonly staleThresholdDays;
|
|
44
|
+
private readonly ownershipGuard;
|
|
45
|
+
private readonly git;
|
|
46
|
+
private readonly now;
|
|
47
|
+
constructor(clonePath: string, spec?: WorktreePoolSpec, deps?: WorktreePoolManagerDeps);
|
|
48
|
+
/**
|
|
49
|
+
* Allocate a worktree for `branch`. If one already exists at the expected path:
|
|
50
|
+
* - ownership-verified → adopted (returned with `created: false`)
|
|
51
|
+
* - ownership mismatch → throws WorktreeOwnershipError (or warns when guard is advisory)
|
|
52
|
+
* Otherwise a fresh worktree is created via `git worktree add`.
|
|
53
|
+
*/
|
|
54
|
+
allocate(branch: string, options?: AllocateOptions): Promise<WorktreeHandle>;
|
|
55
|
+
/**
|
|
56
|
+
* Adopt an existing worktree at the slug-derived path. Throws if no worktree exists or
|
|
57
|
+
* if ownership verification fails (when guard is strict).
|
|
58
|
+
*/
|
|
59
|
+
adopt(branch: string): Promise<WorktreeHandle>;
|
|
60
|
+
/**
|
|
61
|
+
* Destroy the worktree for `branch`. By default refuses to remove a worktree with
|
|
62
|
+
* uncommitted changes; callers that need to force MUST pass `{ force: true }`.
|
|
63
|
+
*/
|
|
64
|
+
reclaim(branch: string, options?: {
|
|
65
|
+
force?: boolean;
|
|
66
|
+
}): Promise<void>;
|
|
67
|
+
/**
|
|
68
|
+
* Convenience hook called by the merge-gate when a PR for `branch` lands. Equivalent
|
|
69
|
+
* to `reclaim(branch)` with the default safety check.
|
|
70
|
+
*/
|
|
71
|
+
cleanupOnMerge(branch: string): Promise<void>;
|
|
72
|
+
/**
|
|
73
|
+
* Enumerate worktree slugs currently present in the pool root. Does not verify ownership.
|
|
74
|
+
*/
|
|
75
|
+
list(): Promise<string[]>;
|
|
76
|
+
/**
|
|
77
|
+
* Reclaim worktrees whose mtime is older than `staleThresholdDays`. Dry-run by default;
|
|
78
|
+
* pass `{ apply: true }` to actually remove them. Returns the slugs considered stale.
|
|
79
|
+
*/
|
|
80
|
+
reclaimStale(options?: {
|
|
81
|
+
apply?: boolean;
|
|
82
|
+
}): Promise<string[]>;
|
|
83
|
+
/** Forcibly remove a slug from the pool root without git involvement. Test/cleanup helper. */
|
|
84
|
+
_forceRemoveSlug(slug: string): Promise<void>;
|
|
85
|
+
}
|
|
86
|
+
//# sourceMappingURL=worktree-pool.d.ts.map
|
|
@@ -0,0 +1,204 @@
|
|
|
1
|
+
import { mkdir, readdir, rm, stat } from 'node:fs/promises';
|
|
2
|
+
import { homedir } from 'node:os';
|
|
3
|
+
import { join, resolve } from 'node:path';
|
|
4
|
+
import { execFileAsync } from '../shared.js';
|
|
5
|
+
import { assertOwnership, isExistingWorktree, slugifyBranch, verifyOwnership, WorktreeOwnershipError, } from './worktree.js';
|
|
6
|
+
import { cleanGitEnv } from './git-env.js';
|
|
7
|
+
export const DEFAULT_POOL_ROOT = '~/.ai-sdlc/worktrees';
|
|
8
|
+
export const DEFAULT_STALE_THRESHOLD_DAYS = 14;
|
|
9
|
+
export class WorktreePoolError extends Error {
|
|
10
|
+
branch;
|
|
11
|
+
cause;
|
|
12
|
+
constructor(message, branch, cause) {
|
|
13
|
+
super(message);
|
|
14
|
+
this.branch = branch;
|
|
15
|
+
this.cause = cause;
|
|
16
|
+
this.name = 'WorktreePoolError';
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
/**
|
|
20
|
+
* Manages a pool of git worktrees rooted at `rootDir`. Each worktree is named after
|
|
21
|
+
* `slugifyBranch(branch)`. The pool enforces cross-clone ownership (RFC §7.2) and supports
|
|
22
|
+
* allocate / adopt / reclaim plus a stale-threshold sweep (RFC §7.3).
|
|
23
|
+
*/
|
|
24
|
+
export class WorktreePoolManager {
|
|
25
|
+
rootDir;
|
|
26
|
+
clonePath;
|
|
27
|
+
staleThresholdDays;
|
|
28
|
+
ownershipGuard;
|
|
29
|
+
git;
|
|
30
|
+
now;
|
|
31
|
+
constructor(clonePath, spec = {}, deps = {}) {
|
|
32
|
+
this.clonePath = resolve(clonePath);
|
|
33
|
+
this.rootDir = expandHome(spec.rootDir ?? DEFAULT_POOL_ROOT);
|
|
34
|
+
this.staleThresholdDays = spec.staleThresholdDays ?? DEFAULT_STALE_THRESHOLD_DAYS;
|
|
35
|
+
this.ownershipGuard = spec.ownershipGuard ?? 'strict';
|
|
36
|
+
this.git =
|
|
37
|
+
deps.git ??
|
|
38
|
+
((args, opts) => {
|
|
39
|
+
// Strip leaked GIT_DIR / GIT_WORK_TREE / GIT_INDEX_FILE so worktree
|
|
40
|
+
// operations bind to `clonePath`'s repo rather than a husky hook's
|
|
41
|
+
// env (AISDLC-72).
|
|
42
|
+
const merged = { env: cleanGitEnv(), ...opts };
|
|
43
|
+
return execFileAsync('git', args, merged);
|
|
44
|
+
});
|
|
45
|
+
this.now = deps.now ?? (() => new Date());
|
|
46
|
+
}
|
|
47
|
+
/**
|
|
48
|
+
* Allocate a worktree for `branch`. If one already exists at the expected path:
|
|
49
|
+
* - ownership-verified → adopted (returned with `created: false`)
|
|
50
|
+
* - ownership mismatch → throws WorktreeOwnershipError (or warns when guard is advisory)
|
|
51
|
+
* Otherwise a fresh worktree is created via `git worktree add`.
|
|
52
|
+
*/
|
|
53
|
+
async allocate(branch, options = {}) {
|
|
54
|
+
const slug = slugifyBranch(branch);
|
|
55
|
+
const path = join(this.rootDir, slug);
|
|
56
|
+
if (await isExistingWorktree(path)) {
|
|
57
|
+
return this.adopt(branch);
|
|
58
|
+
}
|
|
59
|
+
await mkdir(this.rootDir, { recursive: true });
|
|
60
|
+
const baseBranch = options.baseBranch ?? 'origin/main';
|
|
61
|
+
try {
|
|
62
|
+
await this.git(['worktree', 'add', path, '-b', branch, baseBranch], {
|
|
63
|
+
cwd: this.clonePath,
|
|
64
|
+
});
|
|
65
|
+
}
|
|
66
|
+
catch (err) {
|
|
67
|
+
throw new WorktreePoolError(`git worktree add failed for branch ${branch}: ${err.message}`, branch, err);
|
|
68
|
+
}
|
|
69
|
+
return { branch, slug, path, created: true };
|
|
70
|
+
}
|
|
71
|
+
/**
|
|
72
|
+
* Adopt an existing worktree at the slug-derived path. Throws if no worktree exists or
|
|
73
|
+
* if ownership verification fails (when guard is strict).
|
|
74
|
+
*/
|
|
75
|
+
async adopt(branch) {
|
|
76
|
+
const slug = slugifyBranch(branch);
|
|
77
|
+
const path = join(this.rootDir, slug);
|
|
78
|
+
if (!(await isExistingWorktree(path))) {
|
|
79
|
+
throw new WorktreePoolError(`No existing worktree at ${path} for branch ${branch}`, branch);
|
|
80
|
+
}
|
|
81
|
+
if (this.ownershipGuard === 'strict') {
|
|
82
|
+
try {
|
|
83
|
+
await assertOwnership(path, this.clonePath);
|
|
84
|
+
}
|
|
85
|
+
catch (err) {
|
|
86
|
+
if (err instanceof WorktreeOwnershipError)
|
|
87
|
+
throw err;
|
|
88
|
+
throw err;
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
else {
|
|
92
|
+
const result = await verifyOwnership(path, this.clonePath);
|
|
93
|
+
if (!result.owned) {
|
|
94
|
+
// Advisory: log via console.warn so operators see drift without failing.
|
|
95
|
+
// In production this would route to the structured event stream.
|
|
96
|
+
console.warn(`[WorktreePool] advisory ownership mismatch on ${path}: expected ${result.expectedClone}, got ${result.actualClone}`);
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
return { branch, slug, path, created: false };
|
|
100
|
+
}
|
|
101
|
+
/**
|
|
102
|
+
* Destroy the worktree for `branch`. By default refuses to remove a worktree with
|
|
103
|
+
* uncommitted changes; callers that need to force MUST pass `{ force: true }`.
|
|
104
|
+
*/
|
|
105
|
+
async reclaim(branch, options = {}) {
|
|
106
|
+
const slug = slugifyBranch(branch);
|
|
107
|
+
const path = join(this.rootDir, slug);
|
|
108
|
+
if (!(await isExistingWorktree(path))) {
|
|
109
|
+
// Nothing to reclaim is not an error — idempotent.
|
|
110
|
+
return;
|
|
111
|
+
}
|
|
112
|
+
if (!options.force) {
|
|
113
|
+
// Refuse if there are uncommitted changes.
|
|
114
|
+
try {
|
|
115
|
+
const { stdout } = await this.git(['status', '--porcelain'], { cwd: path });
|
|
116
|
+
if (stdout.trim().length > 0) {
|
|
117
|
+
throw new WorktreePoolError(`Refusing to reclaim ${path}: uncommitted changes present. Use force: true to override.`, branch);
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
catch (err) {
|
|
121
|
+
if (err instanceof WorktreePoolError)
|
|
122
|
+
throw err;
|
|
123
|
+
// git status failure on a worktree we know exists is a real error.
|
|
124
|
+
throw new WorktreePoolError(`git status failed inside ${path}: ${err.message}`, branch, err);
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
const args = ['worktree', 'remove', path];
|
|
128
|
+
if (options.force)
|
|
129
|
+
args.push('--force');
|
|
130
|
+
try {
|
|
131
|
+
await this.git(args, { cwd: this.clonePath });
|
|
132
|
+
}
|
|
133
|
+
catch (err) {
|
|
134
|
+
throw new WorktreePoolError(`git worktree remove failed for ${path}: ${err.message}`, branch, err);
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
/**
|
|
138
|
+
* Convenience hook called by the merge-gate when a PR for `branch` lands. Equivalent
|
|
139
|
+
* to `reclaim(branch)` with the default safety check.
|
|
140
|
+
*/
|
|
141
|
+
cleanupOnMerge(branch) {
|
|
142
|
+
return this.reclaim(branch);
|
|
143
|
+
}
|
|
144
|
+
/**
|
|
145
|
+
* Enumerate worktree slugs currently present in the pool root. Does not verify ownership.
|
|
146
|
+
*/
|
|
147
|
+
async list() {
|
|
148
|
+
try {
|
|
149
|
+
const entries = await readdir(this.rootDir, { withFileTypes: true });
|
|
150
|
+
const result = [];
|
|
151
|
+
for (const entry of entries) {
|
|
152
|
+
if (!entry.isDirectory())
|
|
153
|
+
continue;
|
|
154
|
+
if (await isExistingWorktree(join(this.rootDir, entry.name))) {
|
|
155
|
+
result.push(entry.name);
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
return result;
|
|
159
|
+
}
|
|
160
|
+
catch (err) {
|
|
161
|
+
if (err.code === 'ENOENT')
|
|
162
|
+
return [];
|
|
163
|
+
throw err;
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
/**
|
|
167
|
+
* Reclaim worktrees whose mtime is older than `staleThresholdDays`. Dry-run by default;
|
|
168
|
+
* pass `{ apply: true }` to actually remove them. Returns the slugs considered stale.
|
|
169
|
+
*/
|
|
170
|
+
async reclaimStale(options = {}) {
|
|
171
|
+
const slugs = await this.list();
|
|
172
|
+
const cutoff = this.now().getTime() - this.staleThresholdDays * 24 * 60 * 60 * 1000;
|
|
173
|
+
const stale = [];
|
|
174
|
+
for (const slug of slugs) {
|
|
175
|
+
const path = join(this.rootDir, slug);
|
|
176
|
+
try {
|
|
177
|
+
const s = await stat(path);
|
|
178
|
+
if (s.mtimeMs < cutoff) {
|
|
179
|
+
stale.push(slug);
|
|
180
|
+
if (options.apply) {
|
|
181
|
+
// Use git worktree prune-style remove; force because branch may have moved.
|
|
182
|
+
await this.git(['worktree', 'remove', '--force', path], { cwd: this.clonePath });
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
catch {
|
|
187
|
+
// Ignore missing or stat-failed paths.
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
return stale;
|
|
191
|
+
}
|
|
192
|
+
/** Forcibly remove a slug from the pool root without git involvement. Test/cleanup helper. */
|
|
193
|
+
async _forceRemoveSlug(slug) {
|
|
194
|
+
await rm(join(this.rootDir, slug), { recursive: true, force: true });
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
function expandHome(p) {
|
|
198
|
+
if (p.startsWith('~/'))
|
|
199
|
+
return join(homedir(), p.slice(2));
|
|
200
|
+
if (p === '~')
|
|
201
|
+
return homedir();
|
|
202
|
+
return p;
|
|
203
|
+
}
|
|
204
|
+
//# sourceMappingURL=worktree-pool.js.map
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
export declare class WorktreeOwnershipError extends Error {
|
|
2
|
+
readonly worktreePath: string;
|
|
3
|
+
readonly expectedClone: string;
|
|
4
|
+
readonly actualClone: string | null;
|
|
5
|
+
constructor(message: string, worktreePath: string, expectedClone: string, actualClone: string | null);
|
|
6
|
+
}
|
|
7
|
+
export declare function slugifyBranch(branch: string): string;
|
|
8
|
+
export declare function worktreePath(rootDir: string, branch: string): string;
|
|
9
|
+
export interface OwnershipResult {
|
|
10
|
+
owned: boolean;
|
|
11
|
+
expectedClone: string;
|
|
12
|
+
actualClone: string | null;
|
|
13
|
+
reason?: 'pointer-missing' | 'pointer-malformed' | 'cross-clone' | 'ok';
|
|
14
|
+
}
|
|
15
|
+
/**
|
|
16
|
+
* Verify a worktree at `worktreePath` was created from the clone at `expectedClonePath`.
|
|
17
|
+
*
|
|
18
|
+
* A git worktree contains a `.git` *file* (not directory) that points at the parent clone's
|
|
19
|
+
* `.git/worktrees/<name>/` directory. We parse the pointer and confirm it resolves under
|
|
20
|
+
* `expectedClonePath/.git/worktrees/`. Any mismatch returns owned=false with a structured reason.
|
|
21
|
+
*/
|
|
22
|
+
export declare function verifyOwnership(worktreePathArg: string, expectedClonePath: string): Promise<OwnershipResult>;
|
|
23
|
+
export declare function assertOwnership(worktreePathArg: string, expectedClonePath: string): Promise<void>;
|
|
24
|
+
export declare function isExistingWorktree(worktreePathArg: string): Promise<boolean>;
|
|
25
|
+
//# sourceMappingURL=worktree.d.ts.map
|