@ai-sdlc/orchestrator 0.6.0 → 0.9.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.
Files changed (203) hide show
  1. package/dist/admission-composite.d.ts +67 -0
  2. package/dist/admission-composite.js +158 -0
  3. package/dist/admission-enrichment.d.ts +142 -0
  4. package/dist/admission-enrichment.js +331 -0
  5. package/dist/admission-hc.d.ts +62 -0
  6. package/dist/admission-hc.js +83 -0
  7. package/dist/admission-score.d.ts +95 -5
  8. package/dist/admission-score.js +84 -11
  9. package/dist/analysis/analyzer.js +3 -2
  10. package/dist/analysis/convention-detector.d.ts +85 -2
  11. package/dist/analysis/convention-detector.js +375 -70
  12. package/dist/analysis/diff-analyzer.d.ts +33 -0
  13. package/dist/analysis/diff-analyzer.js +122 -0
  14. package/dist/analysis/hotspot-analyzer.js +3 -1
  15. package/dist/analysis/index.d.ts +2 -1
  16. package/dist/analysis/index.js +2 -1
  17. package/dist/artifacts/index.d.ts +65 -0
  18. package/dist/artifacts/index.js +142 -0
  19. package/dist/backlog-adapter.d.ts +121 -0
  20. package/dist/backlog-adapter.js +438 -0
  21. package/dist/calibration.d.ts +43 -0
  22. package/dist/calibration.js +76 -0
  23. package/dist/cli/commands/agents.d.ts +24 -0
  24. package/dist/cli/commands/agents.js +66 -1
  25. package/dist/cli/commands/git-remote.d.ts +49 -0
  26. package/dist/cli/commands/git-remote.js +91 -0
  27. package/dist/cli/commands/health.d.ts +4 -0
  28. package/dist/cli/commands/health.js +13 -1
  29. package/dist/cli/commands/init.d.ts +26 -0
  30. package/dist/cli/commands/init.js +164 -22
  31. package/dist/cli/commands/mcp-setup.d.ts +31 -0
  32. package/dist/cli/commands/mcp-setup.js +78 -8
  33. package/dist/cli/formatters/table.js +15 -2
  34. package/dist/cli/index.d.ts +14 -1
  35. package/dist/cli/index.js +81 -20
  36. package/dist/cli/versions.d.ts +57 -0
  37. package/dist/cli/versions.js +128 -0
  38. package/dist/code-area-classifier.d.ts +21 -0
  39. package/dist/code-area-classifier.js +48 -0
  40. package/dist/config.d.ts +33 -1
  41. package/dist/config.js +78 -8
  42. package/dist/database/adapters/external.d.ts +24 -0
  43. package/dist/database/adapters/external.js +80 -0
  44. package/dist/database/adapters/neon.d.ts +41 -0
  45. package/dist/database/adapters/neon.js +98 -0
  46. package/dist/database/adapters/pg-snapshot-restore.d.ts +28 -0
  47. package/dist/database/adapters/pg-snapshot-restore.js +68 -0
  48. package/dist/database/adapters/sqlite-copy.d.ts +32 -0
  49. package/dist/database/adapters/sqlite-copy.js +145 -0
  50. package/dist/database/connection-injection.d.ts +35 -0
  51. package/dist/database/connection-injection.js +93 -0
  52. package/dist/database/index.d.ts +12 -0
  53. package/dist/database/index.js +23 -0
  54. package/dist/database/registry.d.ts +13 -0
  55. package/dist/database/registry.js +27 -0
  56. package/dist/database/topology.d.ts +52 -0
  57. package/dist/database/topology.js +44 -0
  58. package/dist/database/types.d.ts +89 -0
  59. package/dist/database/types.js +26 -0
  60. package/dist/design-authority.d.ts +40 -0
  61. package/dist/design-authority.js +71 -0
  62. package/dist/design-lookahead.d.ts +64 -0
  63. package/dist/design-lookahead.js +86 -0
  64. package/dist/design-quality-trend.d.ts +87 -0
  65. package/dist/design-quality-trend.js +190 -0
  66. package/dist/design-system-context.d.ts +46 -0
  67. package/dist/design-system-context.js +80 -0
  68. package/dist/design-system-correction-loop.d.ts +64 -0
  69. package/dist/design-system-correction-loop.js +128 -0
  70. package/dist/design-system-metrics.d.ts +61 -0
  71. package/dist/design-system-metrics.js +104 -0
  72. package/dist/design-system-stewardship.d.ts +22 -0
  73. package/dist/design-system-stewardship.js +85 -0
  74. package/dist/design-system-validation.d.ts +37 -0
  75. package/dist/design-system-validation.js +88 -0
  76. package/dist/dispatch/index.d.ts +4 -0
  77. package/dist/dispatch/index.js +4 -0
  78. package/dist/dispatch/merge-gate.d.ts +46 -0
  79. package/dist/dispatch/merge-gate.js +90 -0
  80. package/dist/dispatch/requeue.d.ts +57 -0
  81. package/dist/dispatch/requeue.js +131 -0
  82. package/dist/dispatch/worker-pool.d.ts +62 -0
  83. package/dist/dispatch/worker-pool.js +60 -0
  84. package/dist/execute.d.ts +40 -0
  85. package/dist/execute.js +185 -15
  86. package/dist/fix-ci.js +8 -1
  87. package/dist/fix-review.js +8 -1
  88. package/dist/harness/adapters/claude-code.d.ts +29 -0
  89. package/dist/harness/adapters/claude-code.js +191 -0
  90. package/dist/harness/adapters/codex.d.ts +25 -0
  91. package/dist/harness/adapters/codex.js +61 -0
  92. package/dist/harness/independence.d.ts +51 -0
  93. package/dist/harness/independence.js +67 -0
  94. package/dist/harness/index.d.ts +14 -0
  95. package/dist/harness/index.js +20 -0
  96. package/dist/harness/registry.d.ts +17 -0
  97. package/dist/harness/registry.js +31 -0
  98. package/dist/harness/types.d.ts +123 -0
  99. package/dist/harness/types.js +8 -0
  100. package/dist/harness/version-probe.d.ts +14 -0
  101. package/dist/harness/version-probe.js +123 -0
  102. package/dist/index.d.ts +30 -4
  103. package/dist/index.js +35 -2
  104. package/dist/models/classifier.d.ts +76 -0
  105. package/dist/models/classifier.js +221 -0
  106. package/dist/models/index.d.ts +3 -0
  107. package/dist/models/index.js +3 -0
  108. package/dist/models/registry.d.ts +97 -0
  109. package/dist/models/registry.js +173 -0
  110. package/dist/pillar-breakdown.d.ts +63 -0
  111. package/dist/pillar-breakdown.js +153 -0
  112. package/dist/priority.d.ts +5 -0
  113. package/dist/priority.js +18 -5
  114. package/dist/review-meta.d.ts +65 -0
  115. package/dist/review-meta.js +149 -0
  116. package/dist/review.d.ts +4 -0
  117. package/dist/review.js +12 -2
  118. package/dist/runners/claude-code-sdk.d.ts +31 -0
  119. package/dist/runners/claude-code-sdk.js +238 -0
  120. package/dist/runners/claude-code.d.ts +3 -0
  121. package/dist/runners/claude-code.js +30 -66
  122. package/dist/runners/codex.js +4 -1
  123. package/dist/runners/copilot.js +4 -1
  124. package/dist/runners/cursor.js +4 -1
  125. package/dist/runners/git-utils.d.ts +81 -0
  126. package/dist/runners/git-utils.js +201 -0
  127. package/dist/runners/index.d.ts +3 -0
  128. package/dist/runners/index.js +3 -0
  129. package/dist/runners/review-agent.d.ts +20 -0
  130. package/dist/runners/review-agent.js +171 -41
  131. package/dist/runners/runner-registry.js +10 -0
  132. package/dist/runners/sdk-review-runner.d.ts +65 -0
  133. package/dist/runners/sdk-review-runner.js +185 -0
  134. package/dist/runners/security-triage.d.ts +20 -4
  135. package/dist/runners/security-triage.js +39 -15
  136. package/dist/runners/types.d.ts +6 -0
  137. package/dist/runtime/attestations.d.ts +274 -0
  138. package/dist/runtime/attestations.js +460 -0
  139. package/dist/runtime/git-env.d.ts +53 -0
  140. package/dist/runtime/git-env.js +60 -0
  141. package/dist/runtime/index.d.ts +7 -0
  142. package/dist/runtime/index.js +7 -0
  143. package/dist/runtime/parallelism-flag.d.ts +10 -0
  144. package/dist/runtime/parallelism-flag.js +18 -0
  145. package/dist/runtime/port-allocator.d.ts +21 -0
  146. package/dist/runtime/port-allocator.js +66 -0
  147. package/dist/runtime/worktree-pool.d.ts +86 -0
  148. package/dist/runtime/worktree-pool.js +204 -0
  149. package/dist/runtime/worktree.d.ts +25 -0
  150. package/dist/runtime/worktree.js +111 -0
  151. package/dist/sa-scoring/auto-calibrate.d.ts +69 -0
  152. package/dist/sa-scoring/auto-calibrate.js +107 -0
  153. package/dist/sa-scoring/c1-sa2-computable.d.ts +42 -0
  154. package/dist/sa-scoring/c1-sa2-computable.js +59 -0
  155. package/dist/sa-scoring/composite.d.ts +107 -0
  156. package/dist/sa-scoring/composite.js +139 -0
  157. package/dist/sa-scoring/depparse-client.d.ts +79 -0
  158. package/dist/sa-scoring/depparse-client.js +187 -0
  159. package/dist/sa-scoring/did-compiler.d.ts +122 -0
  160. package/dist/sa-scoring/did-compiler.js +286 -0
  161. package/dist/sa-scoring/drift-monitor.d.ts +84 -0
  162. package/dist/sa-scoring/drift-monitor.js +186 -0
  163. package/dist/sa-scoring/exemplar-bank.d.ts +78 -0
  164. package/dist/sa-scoring/exemplar-bank.js +154 -0
  165. package/dist/sa-scoring/feedback-store.d.ts +100 -0
  166. package/dist/sa-scoring/feedback-store.js +156 -0
  167. package/dist/sa-scoring/index.d.ts +71 -0
  168. package/dist/sa-scoring/index.js +158 -0
  169. package/dist/sa-scoring/layer1-deterministic.d.ts +115 -0
  170. package/dist/sa-scoring/layer1-deterministic.js +298 -0
  171. package/dist/sa-scoring/layer2-structural.d.ts +71 -0
  172. package/dist/sa-scoring/layer2-structural.js +151 -0
  173. package/dist/sa-scoring/layer3-llm.d.ts +86 -0
  174. package/dist/sa-scoring/layer3-llm.js +282 -0
  175. package/dist/sa-scoring/rescore-orchestrator.d.ts +52 -0
  176. package/dist/sa-scoring/rescore-orchestrator.js +47 -0
  177. package/dist/scheduling/burn-down.d.ts +27 -0
  178. package/dist/scheduling/burn-down.js +43 -0
  179. package/dist/scheduling/calibration.d.ts +42 -0
  180. package/dist/scheduling/calibration.js +150 -0
  181. package/dist/scheduling/index.d.ts +8 -0
  182. package/dist/scheduling/index.js +8 -0
  183. package/dist/scheduling/ledger.d.ts +59 -0
  184. package/dist/scheduling/ledger.js +216 -0
  185. package/dist/scheduling/off-peak.d.ts +27 -0
  186. package/dist/scheduling/off-peak.js +112 -0
  187. package/dist/scheduling/schedule-decision.d.ts +39 -0
  188. package/dist/scheduling/schedule-decision.js +89 -0
  189. package/dist/scheduling/tier-analysis.d.ts +47 -0
  190. package/dist/scheduling/tier-analysis.js +81 -0
  191. package/dist/scheduling/types.d.ts +140 -0
  192. package/dist/scheduling/types.js +11 -0
  193. package/dist/shared.d.ts +13 -0
  194. package/dist/shared.js +32 -0
  195. package/dist/state/schema.d.ts +5 -1
  196. package/dist/state/schema.js +223 -1
  197. package/dist/state/store.d.ts +55 -1
  198. package/dist/state/store.js +342 -7
  199. package/dist/state/types.d.ts +139 -0
  200. package/dist/types.d.ts +1 -1
  201. package/dist/validate-agent-output.js +4 -1
  202. package/dist/watch.js +6 -0
  203. package/package.json +2 -2
@@ -0,0 +1,66 @@
1
+ import { createHash } from 'node:crypto';
2
+ import { resolve } from 'node:path';
3
+ import { createServer } from 'node:net';
4
+ export const DEFAULT_BASE_PORT = 3190;
5
+ export const PORT_RANGE_OFFSET_MIN = 100;
6
+ export const PORT_RANGE_OFFSET_MAX = 999;
7
+ const PORT_RANGE_SPAN = PORT_RANGE_OFFSET_MAX - PORT_RANGE_OFFSET_MIN + 1;
8
+ const COLLISION_PROBE_ATTEMPTS = 10;
9
+ export class PortAllocationError extends Error {
10
+ worktreePath;
11
+ attemptedPorts;
12
+ constructor(message, worktreePath, attemptedPorts) {
13
+ super(message);
14
+ this.worktreePath = worktreePath;
15
+ this.attemptedPorts = attemptedPorts;
16
+ this.name = 'PortAllocationError';
17
+ }
18
+ }
19
+ export function deterministicPort(worktreePath, basePort = DEFAULT_BASE_PORT) {
20
+ const absolute = resolve(worktreePath);
21
+ const digest = createHash('md5').update(absolute).digest();
22
+ const offset = (((digest[0] << 8) | digest[1]) % PORT_RANGE_SPAN) + PORT_RANGE_OFFSET_MIN;
23
+ return basePort + offset;
24
+ }
25
+ export async function allocatePort(worktreePath, options = {}) {
26
+ const envValue = options.envOverride;
27
+ if (envValue !== undefined && envValue !== '') {
28
+ const parsed = Number.parseInt(envValue, 10);
29
+ if (!Number.isFinite(parsed) || parsed <= 0 || parsed > 65535) {
30
+ throw new PortAllocationError(`PORT environment override is not a valid TCP port: ${envValue}`, worktreePath, []);
31
+ }
32
+ return parsed;
33
+ }
34
+ const basePort = options.basePort ?? DEFAULT_BASE_PORT;
35
+ const start = deterministicPort(worktreePath, basePort);
36
+ const probe = options.isPortFree ?? isPortFree;
37
+ const attempted = [];
38
+ for (let i = 0; i < COLLISION_PROBE_ATTEMPTS; i++) {
39
+ const candidate = start + i;
40
+ attempted.push(candidate);
41
+ if (await probe(candidate)) {
42
+ return candidate;
43
+ }
44
+ }
45
+ throw new PortAllocationError(`No free port found after ${COLLISION_PROBE_ATTEMPTS} attempts starting at ${start}`, worktreePath, attempted);
46
+ }
47
+ export async function allocateContiguousPorts(worktreePath, options) {
48
+ if (options.count < 1 || options.count > 10) {
49
+ throw new PortAllocationError(`Contiguous port count must be between 1 and 10 (got ${options.count})`, worktreePath, []);
50
+ }
51
+ const first = await allocatePort(worktreePath, options);
52
+ return Array.from({ length: options.count }, (_, i) => first + i);
53
+ }
54
+ export function isPortFree(port) {
55
+ return new Promise((resolveProbe) => {
56
+ const server = createServer();
57
+ server.once('error', () => {
58
+ resolveProbe(false);
59
+ });
60
+ server.once('listening', () => {
61
+ server.close(() => resolveProbe(true));
62
+ });
63
+ server.listen(port, '127.0.0.1');
64
+ });
65
+ }
66
+ //# 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
@@ -0,0 +1,111 @@
1
+ import { readFile, realpath, stat } from 'node:fs/promises';
2
+ import { dirname, isAbsolute, join, resolve, sep } from 'node:path';
3
+ const SLUG_INVALID = /[^a-zA-Z0-9._-]+/g;
4
+ const SLUG_DEDUPE = /-+/g;
5
+ export class WorktreeOwnershipError extends Error {
6
+ worktreePath;
7
+ expectedClone;
8
+ actualClone;
9
+ constructor(message, worktreePath, expectedClone, actualClone) {
10
+ super(message);
11
+ this.worktreePath = worktreePath;
12
+ this.expectedClone = expectedClone;
13
+ this.actualClone = actualClone;
14
+ this.name = 'WorktreeOwnershipError';
15
+ }
16
+ }
17
+ export function slugifyBranch(branch) {
18
+ if (!branch) {
19
+ throw new Error('Branch name cannot be empty');
20
+ }
21
+ const slug = branch
22
+ .replace(SLUG_INVALID, '-')
23
+ .replace(SLUG_DEDUPE, '-')
24
+ .replace(/^-+|-+$/g, '');
25
+ if (!slug) {
26
+ throw new Error(`Branch name has no slug-safe characters: ${branch}`);
27
+ }
28
+ return slug;
29
+ }
30
+ export function worktreePath(rootDir, branch) {
31
+ return join(rootDir, slugifyBranch(branch));
32
+ }
33
+ /**
34
+ * Verify a worktree at `worktreePath` was created from the clone at `expectedClonePath`.
35
+ *
36
+ * A git worktree contains a `.git` *file* (not directory) that points at the parent clone's
37
+ * `.git/worktrees/<name>/` directory. We parse the pointer and confirm it resolves under
38
+ * `expectedClonePath/.git/worktrees/`. Any mismatch returns owned=false with a structured reason.
39
+ */
40
+ export async function verifyOwnership(worktreePathArg, expectedClonePath) {
41
+ const expectedWorktreesDir = resolve(expectedClonePath, '.git', 'worktrees');
42
+ // Canonicalize the expected dir to follow symlinks (e.g., macOS /var → /private/var).
43
+ // realpath fails if the path doesn't exist; fall back to resolve() in that case.
44
+ let canonicalExpected = expectedWorktreesDir;
45
+ try {
46
+ canonicalExpected = await realpath(expectedWorktreesDir);
47
+ }
48
+ catch {
49
+ /* directory doesn't exist yet — keep the resolved (non-canonical) path */
50
+ }
51
+ const dotGitPath = join(worktreePathArg, '.git');
52
+ let pointer;
53
+ try {
54
+ const buf = await readFile(dotGitPath);
55
+ pointer = buf.toString('utf8').trim();
56
+ }
57
+ catch {
58
+ return {
59
+ owned: false,
60
+ expectedClone: canonicalExpected,
61
+ actualClone: null,
62
+ reason: 'pointer-missing',
63
+ };
64
+ }
65
+ // Pointer file format per git docs: `gitdir: <absolute-path>` or `gitdir: <relative-path>`.
66
+ const match = pointer.match(/^gitdir:\s*(.+)$/);
67
+ if (!match) {
68
+ return {
69
+ owned: false,
70
+ expectedClone: canonicalExpected,
71
+ actualClone: pointer || null,
72
+ reason: 'pointer-malformed',
73
+ };
74
+ }
75
+ const declared = match[1].trim();
76
+ const declaredAbs = isAbsolute(declared) ? declared : resolve(dirname(dotGitPath), declared);
77
+ // Canonicalize the declared path the same way; if it doesn't exist, the worktree is
78
+ // detached and we can fall back to the resolved string.
79
+ let canonicalDeclared = resolve(declaredAbs);
80
+ try {
81
+ canonicalDeclared = await realpath(declaredAbs);
82
+ }
83
+ catch {
84
+ /* declared dir doesn't exist — use the resolved string for comparison */
85
+ }
86
+ // The pointer should land inside `<expectedClone>/.git/worktrees/<name>/`.
87
+ const owned = canonicalDeclared === canonicalExpected ||
88
+ canonicalDeclared.startsWith(canonicalExpected + sep);
89
+ return {
90
+ owned,
91
+ expectedClone: canonicalExpected,
92
+ actualClone: canonicalDeclared,
93
+ reason: owned ? 'ok' : 'cross-clone',
94
+ };
95
+ }
96
+ export async function assertOwnership(worktreePathArg, expectedClonePath) {
97
+ const result = await verifyOwnership(worktreePathArg, expectedClonePath);
98
+ if (!result.owned) {
99
+ throw new WorktreeOwnershipError(`Worktree ownership verification failed for ${worktreePathArg}: ${result.reason}`, worktreePathArg, result.expectedClone, result.actualClone);
100
+ }
101
+ }
102
+ export async function isExistingWorktree(worktreePathArg) {
103
+ try {
104
+ const dotGit = await stat(join(worktreePathArg, '.git'));
105
+ return dotGit.isFile();
106
+ }
107
+ catch {
108
+ return false;
109
+ }
110
+ }
111
+ //# sourceMappingURL=worktree.js.map
@@ -0,0 +1,69 @@
1
+ /**
2
+ * Phase 3 weight auto-calibration (RFC-0008 Addendum B §B.8).
3
+ *
4
+ * Reads trailing-90-day precision from `SAFeedbackStore`, shifts the
5
+ * Phase 3 (w_structural, w_llm) pair in the direction of the better-
6
+ * performing layer, and persists the result to `sa_phase_weights`.
7
+ *
8
+ * CR-2: `w_structural ≥ 0.20` floor enforced on the output, as is a
9
+ * symmetric `w_llm ≥ 0.20` floor — extreme single-layer dominance is
10
+ * never written.
11
+ *
12
+ * Idempotent: if the computed weights round-trip to the currently
13
+ * persisted pair, the row is not written and the caller's diff is
14
+ * empty. Rolling 90-day window by default.
15
+ */
16
+ import type { SaDimension } from '../state/types.js';
17
+ import type { StateStore } from '../state/store.js';
18
+ import type { SAFeedbackStore } from './feedback-store.js';
19
+ import type { PhaseWeights } from './composite.js';
20
+ export declare const WEIGHT_FLOOR = 0.2;
21
+ export declare const WEIGHT_CEILING: number;
22
+ export declare const DEFAULT_SHIFT_SIZE = 0.05;
23
+ export declare const PRECISION_DELTA_THRESHOLD = 0.1;
24
+ /** Default calibration window — 90 days. */
25
+ export declare const DEFAULT_WINDOW_DAYS = 90;
26
+ /** Phase 2c default weights — used as starting point if none persisted. */
27
+ export declare const DEFAULT_STARTING_WEIGHTS: PhaseWeights;
28
+ export interface PrecisionPair {
29
+ structural: number;
30
+ llm: number;
31
+ }
32
+ export interface CalibrationDecision {
33
+ /** 'toward-structural' | 'toward-llm' | 'hold' */
34
+ direction: 'toward-structural' | 'toward-llm' | 'hold';
35
+ /** Magnitude of the LLM−structural precision difference. */
36
+ delta: number;
37
+ }
38
+ export declare function decideCalibrationDirection(precision: PrecisionPair): CalibrationDecision;
39
+ export interface ComputePhase3WeightsInput {
40
+ current: PhaseWeights;
41
+ precision: PrecisionPair;
42
+ shiftSize?: number;
43
+ }
44
+ export declare function computePhase3Weights(input: ComputePhase3WeightsInput): PhaseWeights;
45
+ export interface AutoCalibrateDeps {
46
+ feedback: SAFeedbackStore;
47
+ stateStore: StateStore;
48
+ /** Clock injection for deterministic tests. */
49
+ now?: () => number;
50
+ /** Override the window (default 90d). */
51
+ windowDays?: number;
52
+ /** Starting weights when none are persisted. */
53
+ startingWeights?: PhaseWeights;
54
+ /** Adjustment step size per calibration run. */
55
+ shiftSize?: number;
56
+ }
57
+ export interface DimensionDiff {
58
+ dimension: SaDimension;
59
+ precision: PrecisionPair;
60
+ previous: PhaseWeights;
61
+ next: PhaseWeights;
62
+ changed: boolean;
63
+ }
64
+ export interface AutoCalibrateResult {
65
+ diffs: DimensionDiff[];
66
+ }
67
+ export declare function autoCalibratePhaseWeights(deps: AutoCalibrateDeps): Promise<AutoCalibrateResult>;
68
+ export declare function renderCalibrationDiff(result: AutoCalibrateResult): string;
69
+ //# sourceMappingURL=auto-calibrate.d.ts.map
@@ -0,0 +1,107 @@
1
+ /**
2
+ * Phase 3 weight auto-calibration (RFC-0008 Addendum B §B.8).
3
+ *
4
+ * Reads trailing-90-day precision from `SAFeedbackStore`, shifts the
5
+ * Phase 3 (w_structural, w_llm) pair in the direction of the better-
6
+ * performing layer, and persists the result to `sa_phase_weights`.
7
+ *
8
+ * CR-2: `w_structural ≥ 0.20` floor enforced on the output, as is a
9
+ * symmetric `w_llm ≥ 0.20` floor — extreme single-layer dominance is
10
+ * never written.
11
+ *
12
+ * Idempotent: if the computed weights round-trip to the currently
13
+ * persisted pair, the row is not written and the caller's diff is
14
+ * empty. Rolling 90-day window by default.
15
+ */
16
+ import { W_STRUCTURAL_FLOOR } from './composite.js';
17
+ // ── Defaults and constants ──────────────────────────────────────────
18
+ export const WEIGHT_FLOOR = W_STRUCTURAL_FLOOR; // 0.20
19
+ export const WEIGHT_CEILING = 1 - WEIGHT_FLOOR; // 0.80
20
+ export const DEFAULT_SHIFT_SIZE = 0.05;
21
+ export const PRECISION_DELTA_THRESHOLD = 0.1;
22
+ /** Default calibration window — 90 days. */
23
+ export const DEFAULT_WINDOW_DAYS = 90;
24
+ /** Phase 2c default weights — used as starting point if none persisted. */
25
+ export const DEFAULT_STARTING_WEIGHTS = Object.freeze({
26
+ wStructural: 0.35,
27
+ wLlm: 0.65,
28
+ });
29
+ export function decideCalibrationDirection(precision) {
30
+ const delta = precision.llm - precision.structural;
31
+ if (delta > PRECISION_DELTA_THRESHOLD)
32
+ return { direction: 'toward-llm', delta };
33
+ if (delta < -PRECISION_DELTA_THRESHOLD)
34
+ return { direction: 'toward-structural', delta };
35
+ return { direction: 'hold', delta };
36
+ }
37
+ export function computePhase3Weights(input) {
38
+ const shift = input.shiftSize ?? DEFAULT_SHIFT_SIZE;
39
+ const decision = decideCalibrationDirection(input.precision);
40
+ let wStructural = input.current.wStructural;
41
+ if (decision.direction === 'toward-llm')
42
+ wStructural -= shift;
43
+ else if (decision.direction === 'toward-structural')
44
+ wStructural += shift;
45
+ wStructural = Math.max(WEIGHT_FLOOR, Math.min(WEIGHT_CEILING, wStructural));
46
+ return { wStructural, wLlm: 1 - wStructural };
47
+ }
48
+ export async function autoCalibratePhaseWeights(deps) {
49
+ const nowMs = (deps.now ?? (() => Date.now()))();
50
+ const windowDays = deps.windowDays ?? DEFAULT_WINDOW_DAYS;
51
+ const since = new Date(nowMs - windowDays * 24 * 60 * 60 * 1000).toISOString();
52
+ const startingWeights = deps.startingWeights ?? DEFAULT_STARTING_WEIGHTS;
53
+ const diffs = [];
54
+ for (const dimension of ['SA-1', 'SA-2']) {
55
+ const existing = deps.stateStore.getSaPhaseWeights(dimension);
56
+ const current = existing
57
+ ? { wStructural: existing.wStructural, wLlm: existing.wLlm }
58
+ : startingWeights;
59
+ const structural = deps.feedback.structuralPrecision({ dimension, since });
60
+ const llm = deps.feedback.llmPrecision({ dimension, since });
61
+ const precision = {
62
+ structural: structural.precision,
63
+ llm: llm.precision,
64
+ };
65
+ const next = computePhase3Weights({
66
+ current,
67
+ precision,
68
+ shiftSize: deps.shiftSize,
69
+ });
70
+ const changed = !weightsEqual(current, next) || !existing; /* first-run write */
71
+ if (changed) {
72
+ deps.stateStore.upsertSaPhaseWeights({
73
+ dimension,
74
+ wStructural: next.wStructural,
75
+ wLlm: next.wLlm,
76
+ });
77
+ }
78
+ diffs.push({
79
+ dimension,
80
+ precision,
81
+ previous: current,
82
+ next,
83
+ // `changed` is true when we actually wrote: either the weight pair
84
+ // shifted, OR this was the mandatory first-run initialisation.
85
+ changed,
86
+ });
87
+ }
88
+ return { diffs };
89
+ }
90
+ function weightsEqual(a, b) {
91
+ return Math.abs(a.wStructural - b.wStructural) < 1e-9 && Math.abs(a.wLlm - b.wLlm) < 1e-9;
92
+ }
93
+ // ── Diff rendering (for CLI) ─────────────────────────────────────────
94
+ export function renderCalibrationDiff(result) {
95
+ const lines = [];
96
+ lines.push('SA phase-weight calibration diff');
97
+ lines.push('--------------------------------');
98
+ for (const d of result.diffs) {
99
+ lines.push(`${d.dimension}:`);
100
+ lines.push(` previous : w_structural=${d.previous.wStructural.toFixed(3)} w_llm=${d.previous.wLlm.toFixed(3)}`);
101
+ lines.push(` next : w_structural=${d.next.wStructural.toFixed(3)} w_llm=${d.next.wLlm.toFixed(3)}`);
102
+ lines.push(` precision: structural=${(d.precision.structural * 100).toFixed(1)}% llm=${(d.precision.llm * 100).toFixed(1)}%`);
103
+ lines.push(` changed : ${d.changed ? 'yes' : 'no'}`);
104
+ }
105
+ return lines.join('\n');
106
+ }
107
+ //# sourceMappingURL=auto-calibrate.js.map