@deftai/directive-core 0.107.0 → 0.108.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 (167) hide show
  1. package/dist/architecture/sor-preflight.js +2 -0
  2. package/dist/check/cached-orchestrator.js +28 -3
  3. package/dist/check/named-cause.js +18 -3
  4. package/dist/codebase/provider.js +7 -1
  5. package/dist/delivery-attempt/handoff.js +1 -1
  6. package/dist/design-critique/citation-grammar.d.ts +53 -0
  7. package/dist/design-critique/citation-grammar.js +303 -0
  8. package/dist/design-critique/completed-arc-record.d.ts +62 -0
  9. package/dist/design-critique/completed-arc-record.js +232 -0
  10. package/dist/freshness/bind.d.ts +32 -3
  11. package/dist/freshness/bind.js +58 -12
  12. package/dist/hooks/classify/host-session-identity.d.ts +76 -0
  13. package/dist/hooks/classify/host-session-identity.js +470 -0
  14. package/dist/hooks/classify/index.d.ts +3 -2
  15. package/dist/hooks/classify/index.js +3 -2
  16. package/dist/hooks/classify/paths.d.ts +7 -0
  17. package/dist/hooks/classify/paths.js +35 -0
  18. package/dist/hooks/classify/stdin.d.ts +5 -0
  19. package/dist/hooks/classify/stdin.js +27 -0
  20. package/dist/hooks/dispatcher.d.ts +52 -1
  21. package/dist/hooks/dispatcher.js +538 -45
  22. package/dist/hooks/fixtures/cases.d.ts +16 -2
  23. package/dist/hooks/fixtures/cases.js +175 -0
  24. package/dist/hooks/scope.js +3 -1
  25. package/dist/init-deposit/agent-hooks.d.ts +2 -1
  26. package/dist/init-deposit/agent-hooks.js +8 -1
  27. package/dist/init-deposit/hook-runtime-travel.d.ts +77 -0
  28. package/dist/init-deposit/hook-runtime-travel.js +269 -0
  29. package/dist/intake/clause-derivation.js +9 -3
  30. package/dist/intake/issue-ingest.js +62 -2
  31. package/dist/intake/platform-capabilities.d.ts +9 -2
  32. package/dist/intake/platform-capabilities.js +38 -9
  33. package/dist/orphan-active/evaluate.d.ts +25 -0
  34. package/dist/orphan-active/evaluate.js +193 -128
  35. package/dist/orphan-active/index.d.ts +1 -0
  36. package/dist/orphan-active/index.js +1 -0
  37. package/dist/orphan-active/issue-state.d.ts +121 -0
  38. package/dist/orphan-active/issue-state.js +278 -0
  39. package/dist/platform/cursor-managed-runtime.d.ts +95 -0
  40. package/dist/platform/cursor-managed-runtime.js +241 -0
  41. package/dist/platform/index.d.ts +1 -0
  42. package/dist/platform/index.js +1 -0
  43. package/dist/platform/platform-capabilities.d.ts +23 -1
  44. package/dist/platform/platform-capabilities.js +55 -15
  45. package/dist/policy/ceremony-dial.js +5 -9
  46. package/dist/policy/host-hooks.js +4 -9
  47. package/dist/policy/org-force-on-migration.js +5 -10
  48. package/dist/policy/plan-extensions.d.ts +14 -2
  49. package/dist/policy/plan-extensions.js +24 -3
  50. package/dist/policy/product-signal.js +5 -10
  51. package/dist/policy/require-human-merge.js +5 -9
  52. package/dist/policy/resolve.js +77 -16
  53. package/dist/policy/value-feedback.js +9 -18
  54. package/dist/pr-closeout-attestable/evaluate.d.ts +80 -0
  55. package/dist/pr-closeout-attestable/evaluate.js +306 -0
  56. package/dist/pr-closeout-attestable/index.d.ts +2 -0
  57. package/dist/pr-closeout-attestable/index.js +2 -0
  58. package/dist/pr-protected-issues/gh.d.ts +6 -2
  59. package/dist/pr-protected-issues/gh.js +7 -3
  60. package/dist/pr-wait-mergeable/cascade.d.ts +9 -1
  61. package/dist/pr-wait-mergeable/cascade.js +21 -1
  62. package/dist/pr-wait-mergeable/types.d.ts +2 -0
  63. package/dist/pr-wait-mergeable/wrappers.d.ts +6 -0
  64. package/dist/pr-wait-mergeable/wrappers.js +11 -0
  65. package/dist/preflight/evaluate.d.ts +15 -0
  66. package/dist/preflight/evaluate.js +29 -3
  67. package/dist/product-first-done-gate/acceptance-resolver.d.ts +14 -2
  68. package/dist/product-first-done-gate/acceptance-resolver.js +13 -2
  69. package/dist/product-first-done-gate/evaluate.js +17 -7
  70. package/dist/release/cli-drift-report.d.ts +68 -0
  71. package/dist/release/cli-drift-report.js +189 -0
  72. package/dist/release/index.d.ts +1 -0
  73. package/dist/release/index.js +1 -0
  74. package/dist/release/issue-state-fetch.d.ts +5 -3
  75. package/dist/release/issue-state-fetch.js +86 -21
  76. package/dist/release/native-steps.js +1 -0
  77. package/dist/release/pipeline.js +19 -0
  78. package/dist/release/types.d.ts +14 -0
  79. package/dist/render/constants.d.ts +4 -0
  80. package/dist/render/constants.js +11 -8
  81. package/dist/render/export-spec.js +31 -5
  82. package/dist/render/index.d.ts +1 -1
  83. package/dist/render/index.js +1 -1
  84. package/dist/render/prd-render.d.ts +4 -1
  85. package/dist/render/prd-render.js +82 -23
  86. package/dist/render/project-render.js +14 -7
  87. package/dist/render/scope-outlook.d.ts +2 -0
  88. package/dist/render/scope-outlook.js +3 -0
  89. package/dist/render/spec-render.js +2 -2
  90. package/dist/resolution/package-manager.d.ts +33 -4
  91. package/dist/resolution/package-manager.js +210 -9
  92. package/dist/review-monitor/constants.d.ts +7 -0
  93. package/dist/review-monitor/constants.js +7 -0
  94. package/dist/review-monitor/github-lease.d.ts +98 -1
  95. package/dist/review-monitor/github-lease.js +182 -1
  96. package/dist/review-monitor/lease-comment.d.ts +36 -0
  97. package/dist/review-monitor/lease-comment.js +103 -2
  98. package/dist/scm/call.js +12 -3
  99. package/dist/scm/design-critique-chip.d.ts +1 -0
  100. package/dist/scm/design-critique-chip.js +22 -6
  101. package/dist/scm/gh-rest.d.ts +9 -1
  102. package/dist/scm/gh-rest.js +103 -0
  103. package/dist/scm/readiness.d.ts +2 -0
  104. package/dist/scm/readiness.js +28 -4
  105. package/dist/scope/project-definition-sync.js +14 -7
  106. package/dist/session/git.d.ts +24 -0
  107. package/dist/session/git.js +138 -2
  108. package/dist/session/occupancy.d.ts +238 -12
  109. package/dist/session/occupancy.js +815 -29
  110. package/dist/session/ritual-sentinel.d.ts +5 -0
  111. package/dist/session/ritual-sentinel.js +5 -0
  112. package/dist/session/session-ready.d.ts +5 -1
  113. package/dist/session/session-ready.js +98 -8
  114. package/dist/session/session-start.d.ts +5 -1
  115. package/dist/session/session-start.js +32 -17
  116. package/dist/session/toolchain-preflight.d.ts +13 -3
  117. package/dist/session/toolchain-preflight.js +93 -18
  118. package/dist/session/verify-session-ritual.d.ts +11 -0
  119. package/dist/session/verify-session-ritual.js +60 -15
  120. package/dist/slice/constants.d.ts +1 -1
  121. package/dist/spec-authority/constants.d.ts +10 -2
  122. package/dist/spec-authority/constants.js +53 -8
  123. package/dist/spec-authority/resolver.d.ts +3 -0
  124. package/dist/spec-authority/resolver.js +55 -7
  125. package/dist/subprocess/max-buffer.d.ts +14 -0
  126. package/dist/subprocess/max-buffer.js +15 -0
  127. package/dist/swarm/complete-cohort.d.ts +2 -0
  128. package/dist/swarm/complete-cohort.js +21 -13
  129. package/dist/swarm/index.d.ts +1 -0
  130. package/dist/swarm/index.js +1 -0
  131. package/dist/swarm/launch-cli.js +53 -33
  132. package/dist/swarm/launch.d.ts +8 -0
  133. package/dist/swarm/launch.js +63 -43
  134. package/dist/swarm/pre-dispatch-cli.js +2 -1
  135. package/dist/swarm/pre-dispatch.js +10 -1
  136. package/dist/swarm/subagent-status-dir.d.ts +27 -0
  137. package/dist/swarm/subagent-status-dir.js +42 -0
  138. package/dist/swarm/worktrees.d.ts +17 -3
  139. package/dist/swarm/worktrees.js +48 -4
  140. package/dist/triage/scope/mutations-core.d.ts +2 -6
  141. package/dist/triage/scope/mutations-core.js +23 -55
  142. package/dist/triage/scope-drift/add-ignore.js +5 -39
  143. package/dist/triage/subscribe/index.d.ts +2 -6
  144. package/dist/triage/subscribe/index.js +15 -65
  145. package/dist/triage/welcome/writers.js +15 -74
  146. package/dist/vbrief-build/index.d.ts +2 -1
  147. package/dist/vbrief-build/index.js +2 -1
  148. package/dist/vbrief-build/parity-scenarios.js +5 -5
  149. package/dist/vbrief-build/project-definition-io.d.ts +63 -3
  150. package/dist/vbrief-build/project-definition-io.js +357 -31
  151. package/dist/vbrief-build/project-definition-mutation.d.ts +32 -0
  152. package/dist/vbrief-build/project-definition-mutation.js +43 -0
  153. package/dist/vbrief-validate/conformance.d.ts +1 -0
  154. package/dist/vbrief-validate/conformance.js +85 -8
  155. package/dist/vbrief-validate/main.js +2 -0
  156. package/dist/vbrief-validate/precutover.js +5 -10
  157. package/dist/verify-ac/clauses.d.ts +62 -2
  158. package/dist/verify-ac/clauses.js +183 -111
  159. package/dist/verify-ac/index.d.ts +1 -1
  160. package/dist/verify-ac/index.js +1 -1
  161. package/dist/verify-env/node-runtime.d.ts +8 -4
  162. package/dist/verify-env/node-runtime.js +9 -6
  163. package/dist/verify-env/toolchain-check.d.ts +35 -3
  164. package/dist/verify-env/toolchain-check.js +155 -36
  165. package/dist/verify-source/project-definition-mutation-boundary.d.ts +104 -0
  166. package/dist/verify-source/project-definition-mutation-boundary.js +326 -0
  167. package/package.json +7 -3
@@ -15,4 +15,19 @@
15
15
  * so an overflow is never silent again.
16
16
  */
17
17
  export const SUBPROCESS_MAX_BUFFER = 64 * 1024 * 1024;
18
+ /**
19
+ * Reason text for a failed capture, so an overflow is never blank (#1867 / #3903).
20
+ *
21
+ * A spawn-level failure (ENOBUFS past the ceiling, timeout kill, missing
22
+ * binary) reports no exit status and empty stderr, whether it arrives as a
23
+ * `spawnSync` result or as a thrown `execFileSync` error. Only then does the
24
+ * error message stand in for stderr: a process that exited with a status and
25
+ * chose to say nothing keeps its empty stderr.
26
+ */
27
+ export function resolveCaptureFailureStderr(capture) {
28
+ if (capture.captured.trim().length > 0 || typeof capture.status === "number") {
29
+ return capture.captured;
30
+ }
31
+ return capture.message ?? capture.captured;
32
+ }
18
33
  //# sourceMappingURL=max-buffer.js.map
@@ -47,6 +47,8 @@ export declare function completeCohort(args: {
47
47
  delivery?: CohortDeliveryContext | null;
48
48
  /** Optional launch allocation plan id so close-out reads this cohort's occupancy slot. */
49
49
  allocationPlanId?: string | null;
50
+ /** Test seam and explicit ambient-owner source for close-out corroboration. */
51
+ env?: NodeJS.ProcessEnv;
50
52
  }): {
51
53
  exitCode: number;
52
54
  stdout: string;
@@ -471,25 +471,33 @@ export function completeCohort(args) {
471
471
  });
472
472
  result.errors.push(...errors);
473
473
  if (result.ok && args.dryRun !== true) {
474
- const envSession = process.env.DEFT_SESSION_ID?.trim() ?? "";
475
- let sessionId = envSession;
476
- if (sessionId.length === 0) {
477
- const resolved = resolveLaunchOccupancySessionId(projectRoot, {
478
- allocationPlanId: args.allocationPlanId ?? null,
479
- storyIds: paths.map(storyIdFromPath),
480
- });
481
- if (resolved.reason === "ok" && resolved.sessionId.length > 0) {
482
- sessionId = resolved.sessionId;
483
- }
484
- else if (readOccupancy(projectRoot) !== null) {
474
+ const env = args.env ?? process.env;
475
+ const envSession = env.DEFT_SESSION_ID?.trim() ?? "";
476
+ const liveOccupancy = readOccupancy(projectRoot);
477
+ const resolved = resolveLaunchOccupancySessionId(projectRoot, {
478
+ allocationPlanId: args.allocationPlanId ?? null,
479
+ storyIds: paths.map(storyIdFromPath),
480
+ });
481
+ let sessionId = "";
482
+ if (liveOccupancy !== null) {
483
+ if (resolved.reason !== "ok" || resolved.sessionId.length === 0) {
485
484
  result.ok = false;
486
485
  result.errors.push("swarm close-out occupancy record is missing or belongs to a different cohort. " +
487
- "Steal with occupancy:steal --confirm --occupant <id>.");
486
+ "Re-establish an aligned owner with session:start --steal --confirm " +
487
+ "--occupant <reported-session-id> --session-id=<your-session-id>.");
488
+ }
489
+ else if (envSession.length > 0 && envSession !== resolved.sessionId) {
490
+ result.ok = false;
491
+ result.errors.push(`swarm close-out ambient owner ${envSession} conflicts with cohort owner ` +
492
+ `${resolved.sessionId}; refusing to release a later session's live lease.`);
493
+ }
494
+ else {
495
+ sessionId = resolved.sessionId;
488
496
  }
489
497
  }
490
498
  if (result.ok && sessionId.length > 0) {
491
499
  const released = releaseSwarmOccupancy(projectRoot, {
492
- env: process.env,
500
+ env,
493
501
  sessionId,
494
502
  });
495
503
  if (released.code !== 0) {
@@ -14,6 +14,7 @@ export { routingSetMain } from "./routing-set-cli.js";
14
14
  export * from "./routing-verify.js";
15
15
  export { routingVerifyMain } from "./routing-verify-cli.js";
16
16
  export * from "./subagent-backend.js";
17
+ export * from "./subagent-status-dir.js";
17
18
  export * from "./subprocess.js";
18
19
  export * from "./verify-review-clean.js";
19
20
  export { verifyReviewCleanMain } from "./verify-review-clean-cli.js";
@@ -14,6 +14,7 @@ export { routingSetMain } from "./routing-set-cli.js";
14
14
  export * from "./routing-verify.js";
15
15
  export { routingVerifyMain } from "./routing-verify-cli.js";
16
16
  export * from "./subagent-backend.js";
17
+ export * from "./subagent-status-dir.js";
17
18
  export * from "./subprocess.js";
18
19
  export * from "./verify-review-clean.js";
19
20
  export { verifyReviewCleanMain } from "./verify-review-clean-cli.js";
@@ -17,54 +17,58 @@ export function parseLaunchArgv(argv) {
17
17
  let enforceGatesFlag = false;
18
18
  let noAudit = false;
19
19
  let projectRoot = ".";
20
+ let sessionId = null;
21
+ let parseError = null;
20
22
  for (let i = 0; i < argv.length; i += 1) {
21
23
  const arg = argv[i];
22
- const next = argv[i + 1];
23
- if (arg === "--stories" && next !== undefined) {
24
- stories.push(next);
24
+ const takeValue = () => {
25
+ const value = argv[i + 1];
26
+ if (value === undefined || value.startsWith("--")) {
27
+ parseError ??= `argument ${arg}: expected one argument`;
28
+ return null;
29
+ }
25
30
  i += 1;
31
+ return value;
32
+ };
33
+ if (arg === "--stories") {
34
+ const value = takeValue();
35
+ if (value !== null)
36
+ stories.push(value);
26
37
  }
27
- else if (arg === "--paths" && next !== undefined) {
28
- paths.push(next);
29
- i += 1;
38
+ else if (arg === "--paths") {
39
+ const value = takeValue();
40
+ if (value !== null)
41
+ paths.push(value);
30
42
  }
31
- else if (arg === "--group" && next !== undefined) {
32
- group = next;
33
- i += 1;
43
+ else if (arg === "--group") {
44
+ group = takeValue();
34
45
  }
35
- else if (arg === "--worktree-map" && next !== undefined) {
36
- worktreeMap = next;
37
- i += 1;
46
+ else if (arg === "--worktree-map") {
47
+ worktreeMap = takeValue();
38
48
  }
39
- else if (arg === "--base-branch" && next !== undefined) {
40
- baseBranch = next;
41
- i += 1;
49
+ else if (arg === "--base-branch") {
50
+ baseBranch = takeValue() ?? baseBranch;
42
51
  }
43
52
  else if (arg === "--autonomous") {
44
53
  autonomous = true;
45
54
  }
46
- else if (arg === "--allocation-plan-id" && next !== undefined) {
47
- allocationPlanId = next;
48
- i += 1;
55
+ else if (arg === "--allocation-plan-id") {
56
+ allocationPlanId = takeValue();
49
57
  }
50
- else if (arg === "--batching-rationale" && next !== undefined) {
51
- batchingRationale = next;
52
- i += 1;
58
+ else if (arg === "--batching-rationale") {
59
+ batchingRationale = takeValue();
53
60
  }
54
- else if (arg === "--operator-approval" && next !== undefined) {
55
- operatorApproval = next;
56
- i += 1;
61
+ else if (arg === "--operator-approval") {
62
+ operatorApproval = takeValue();
57
63
  }
58
64
  else if (arg === "--no-create-worktrees") {
59
65
  noCreateWorktrees = true;
60
66
  }
61
- else if (arg === "--output" && next !== undefined) {
62
- output = next;
63
- i += 1;
67
+ else if (arg === "--output") {
68
+ output = takeValue();
64
69
  }
65
- else if (arg === "--gate-clearances" && next !== undefined) {
66
- gateClearancesPath = next;
67
- i += 1;
70
+ else if (arg === "--gate-clearances") {
71
+ gateClearancesPath = takeValue();
68
72
  }
69
73
  else if (arg === "--enforce-gates") {
70
74
  enforceGatesFlag = true;
@@ -72,12 +76,27 @@ export function parseLaunchArgv(argv) {
72
76
  else if (arg === "--no-audit") {
73
77
  noAudit = true;
74
78
  }
75
- else if (arg === "--project-root" && next !== undefined) {
76
- projectRoot = next;
77
- i += 1;
79
+ else if (arg === "--project-root") {
80
+ projectRoot = takeValue() ?? projectRoot;
81
+ }
82
+ else if (arg === "--session-id") {
83
+ const value = takeValue();
84
+ if (value === null) {
85
+ // Keep an explicit invalid sentinel so swarmLaunch fails closed rather
86
+ // than falling back to ambient identity or minting a different owner.
87
+ sessionId = "";
88
+ }
89
+ else {
90
+ sessionId = value;
91
+ }
92
+ }
93
+ else if (arg?.startsWith("--session-id=") === true) {
94
+ const value = arg.slice("--session-id=".length);
95
+ sessionId = value.startsWith("--") ? "" : value;
78
96
  }
79
97
  }
80
98
  return {
99
+ parseError,
81
100
  stories,
82
101
  paths,
83
102
  group,
@@ -93,6 +112,7 @@ export function parseLaunchArgv(argv) {
93
112
  enforceGatesFlag,
94
113
  noAudit,
95
114
  projectRoot,
115
+ sessionId,
96
116
  };
97
117
  }
98
118
  export function launchMain(argv = process.argv.slice(2)) {
@@ -1,4 +1,5 @@
1
1
  import { type ExpectedGithubWorkerPrincipal, type GhRunner, GITHUB_AUTH_MODE_HOST_GH, GITHUB_AUTH_MODE_INJECTED_TOKEN } from "../intake/github-auth-modes.js";
2
+ import { releaseOccupancy } from "../session/occupancy.js";
2
3
  import { EXIT_CONFIG_ERROR, EXIT_GATE_FAILED, EXIT_OK } from "./constants.js";
3
4
  import { type WorktreeRecord } from "./worktrees.js";
4
5
  export interface ResolvedStory {
@@ -19,6 +20,7 @@ export type WorktreeResolverFn = (mapping: readonly Record<string, unknown>[], b
19
20
  repoRoot?: string;
20
21
  }) => WorktreeRecord[];
21
22
  export type RuntimeAuthProbeFn = () => [string, string];
23
+ export type OccupancyReleaseFn = typeof releaseOccupancy;
22
24
  export declare const defaultPreflightGate: PreflightGateFn;
23
25
  export declare const defaultReadinessGate: ReadinessGateFn;
24
26
  export declare const defaultRuntimeAuthProbe: RuntimeAuthProbeFn;
@@ -144,6 +146,8 @@ export declare function buildManifest(resolved: readonly ResolvedStory[], option
144
146
  }): Record<string, unknown>[];
145
147
  export declare function orderCohort(resolved: readonly ResolvedStory[], _projectRoot: string): ResolvedStory[];
146
148
  export interface LaunchArgs {
149
+ /** CLI parse failure carried into the core so later valid flags cannot erase it. */
150
+ parseError?: string | null;
147
151
  stories?: readonly string[];
148
152
  paths?: readonly string[];
149
153
  group?: string | null;
@@ -159,10 +163,14 @@ export interface LaunchArgs {
159
163
  enforceGatesFlag?: boolean;
160
164
  noAudit?: boolean;
161
165
  projectRoot?: string;
166
+ /** Explicit cooperative owner identity, including host-payload canonical IDs (#3611). */
167
+ sessionId?: string | null;
162
168
  preflightGate?: PreflightGateFn;
163
169
  readinessGate?: ReadinessGateFn;
164
170
  worktreeResolver?: WorktreeResolverFn;
165
171
  runtimeAuthProbe?: RuntimeAuthProbeFn;
172
+ /** Test seam: override occupancy release after a newly minted claim fails. */
173
+ releaseOccupancyFn?: OccupancyReleaseFn;
166
174
  /**
167
175
  * Injection seam for the routing-provider environment lookup, mirroring
168
176
  * `resolveRoutingPath`'s `environ` parameter (#1877 Greptile follow-up).
@@ -5,7 +5,7 @@ import { ENV_EXPECTED_GITHUB_LOGIN, FAILURE_INSTALLATION_IDENTITY_UNVERIFIABLE,
5
5
  import { getPlatformCapabilities } from "../intake/platform-capabilities.js";
6
6
  import { hasArtifactSuffix, resolveLifecycleFolder, stripArtifactSuffix, } from "../layout/resolve.js";
7
7
  import { evaluate as preflightEvaluate } from "../preflight/evaluate.js";
8
- import { applyWorktreeOccupancy } from "../session/occupancy.js";
8
+ import { applyWorktreeOccupancy, releaseOccupancy } from "../session/occupancy.js";
9
9
  import { issueNumbersFromPlan, scopeMetadataRank } from "../triage/queue/scope-walk.js";
10
10
  import { selectionOrderingKey } from "../triage/queue/selection.js";
11
11
  import { DEFAULT_BASE_BRANCH, EXIT_CONFIG_ERROR, EXIT_GATE_FAILED, EXIT_OK, GATE_ADVISE, GATE_ENFORCE, LEAF_CODING_WORKER_ROLE, } from "./constants.js";
@@ -695,7 +695,23 @@ function splitCsv(values) {
695
695
  return out;
696
696
  }
697
697
  export function swarmLaunch(args) {
698
+ if (args.parseError !== undefined && args.parseError !== null) {
699
+ return {
700
+ exitCode: EXIT_CONFIG_ERROR,
701
+ stdout: "",
702
+ stderr: `Error: ${args.parseError}\n`,
703
+ };
704
+ }
698
705
  const projectRoot = resolve(args.projectRoot ?? process.cwd());
706
+ if (args.sessionId !== undefined &&
707
+ args.sessionId !== null &&
708
+ (args.sessionId.trim().length === 0 || args.sessionId.trim().startsWith("--"))) {
709
+ return {
710
+ exitCode: EXIT_CONFIG_ERROR,
711
+ stdout: "",
712
+ stderr: "Error: --session-id requires a non-empty owner identity.\n",
713
+ };
714
+ }
699
715
  const tokens = [...splitCsv(args.stories ?? []), ...splitCsv(args.paths ?? [])];
700
716
  if (tokens.length === 0) {
701
717
  return {
@@ -781,17 +797,52 @@ export function swarmLaunch(args) {
781
797
  }
782
798
  const operatorApproval = args.operatorApproval ??
783
799
  `task swarm:launch (${args.autonomous ? "autonomous" : "interactive"})`;
800
+ const occupancy = applyWorktreeOccupancy(projectRoot, {
801
+ sessionId: args.sessionId ?? undefined,
802
+ env: args.environ ?? process.env,
803
+ intent: "swarm",
804
+ });
805
+ if (occupancy.code !== 0) {
806
+ return {
807
+ exitCode: EXIT_GATE_FAILED,
808
+ stdout: "",
809
+ stderr: `${occupancy.message}\n`,
810
+ };
811
+ }
812
+ // Heartbeat on an existing owner is not a new claim -- only release a lease
813
+ // this process just minted (#3649 paired failure clause).
814
+ const newlyClaimed = occupancy.action === "claimed";
815
+ const failAfterClaim = (exitCode, stderr) => {
816
+ if (newlyClaimed) {
817
+ const recovery = "Occupancy release failed after this launch error; the lease may still be live. " +
818
+ "The occupant may release (occupancy:release / session:end). A replacement owner must " +
819
+ "run session:start --steal --confirm --occupant <reported-session-id> " +
820
+ "--session-id=<your-session-id> to align lease and ritual state.";
821
+ try {
822
+ const release = args.releaseOccupancyFn ?? releaseOccupancy;
823
+ const decision = release(projectRoot, {
824
+ sessionId: occupancy.sessionId,
825
+ env: args.environ ?? process.env,
826
+ });
827
+ if (decision.code !== 0) {
828
+ stderr = `${stderr}\n${recovery} ${decision.message}\n`;
829
+ }
830
+ }
831
+ catch (exc) {
832
+ // Do not replace the original structured launch error; tell the operator
833
+ // the lease may still deny later sessions (#3649 Greptile P1).
834
+ stderr = `${stderr}\n${recovery} ${String(exc)}\n`;
835
+ }
836
+ }
837
+ return { exitCode, stdout: "", stderr };
838
+ };
784
839
  let worktreeRecordMap = new Map();
785
840
  if (args.worktreeMap !== undefined && args.worktreeMap !== null) {
786
841
  const resolver = args.worktreeResolver ?? resolveWorktreeMap;
787
842
  try {
788
843
  const payload = JSON.parse(readFileSync(args.worktreeMap, "utf8"));
789
844
  if (!Array.isArray(payload)) {
790
- return {
791
- exitCode: EXIT_CONFIG_ERROR,
792
- stdout: "",
793
- stderr: `Error: --worktree-map ${args.worktreeMap} must contain a JSON array of records.\n`,
794
- };
845
+ return failAfterClaim(EXIT_CONFIG_ERROR, `Error: --worktree-map ${args.worktreeMap} must contain a JSON array of records.\n`);
795
846
  }
796
847
  const records = resolver(payload, args.baseBranch ?? DEFAULT_BASE_BRANCH, !(args.noCreateWorktrees ?? false), {
797
848
  repoRoot: projectRoot,
@@ -799,11 +850,7 @@ export function swarmLaunch(args) {
799
850
  worktreeRecordMap = new Map(records.map((r) => [r.story_id, r]));
800
851
  }
801
852
  catch (exc) {
802
- return {
803
- exitCode: EXIT_CONFIG_ERROR,
804
- stdout: "",
805
- stderr: `Error: worktree map resolution failed: ${String(exc)}\n`,
806
- };
853
+ return failAfterClaim(EXIT_CONFIG_ERROR, `Error: worktree map resolution failed: ${String(exc)}\n`);
807
854
  }
808
855
  }
809
856
  let runtimeMode;
@@ -813,7 +860,7 @@ export function swarmLaunch(args) {
813
860
  [runtimeMode, githubAuthMode] = probe();
814
861
  }
815
862
  catch (exc) {
816
- return { exitCode: EXIT_CONFIG_ERROR, stdout: "", stderr: `Error: ${String(exc)}\n` };
863
+ return failAfterClaim(EXIT_CONFIG_ERROR, `Error: ${String(exc)}\n`);
817
864
  }
818
865
  let resolvedModel = null;
819
866
  let modelSource = null;
@@ -826,11 +873,7 @@ export function swarmLaunch(args) {
826
873
  // would emit an exit-0 manifest with no model and no error to follow. Match
827
874
  // verify:routing, which treats the same state as a config error (#1739).
828
875
  if (route.source === "invalid") {
829
- return {
830
- exitCode: EXIT_CONFIG_ERROR,
831
- stdout: "",
832
- stderr: `Error: routing gate misconfigured: ${route.error ?? "invalid routing decision"}\n`,
833
- };
876
+ return failAfterClaim(EXIT_CONFIG_ERROR, `Error: routing gate misconfigured: ${route.error ?? "invalid routing decision"}\n`);
834
877
  }
835
878
  if (route.decided) {
836
879
  resolvedModel = route.model;
@@ -843,17 +886,6 @@ export function swarmLaunch(args) {
843
886
  ? dispatchProviderFor(backend.backend_id)
844
887
  : null;
845
888
  const workerRoleValue = routingFile !== null || backend !== null ? LEAF_CODING_WORKER_ROLE : null;
846
- const occupancy = applyWorktreeOccupancy(projectRoot, {
847
- env: args.environ ?? process.env,
848
- intent: "swarm",
849
- });
850
- if (occupancy.code !== 0) {
851
- return {
852
- exitCode: EXIT_GATE_FAILED,
853
- stdout: "",
854
- stderr: `${occupancy.message}\n`,
855
- };
856
- }
857
889
  const launchEnviron = args.environ ?? process.env;
858
890
  let expectedGithubLogin = null;
859
891
  // Only the injected-token launch path auto-validates (and fail-closes on a
@@ -871,11 +903,7 @@ export function swarmLaunch(args) {
871
903
  cwd: projectRoot,
872
904
  });
873
905
  if (!injection.ok) {
874
- return {
875
- exitCode: EXIT_GATE_FAILED,
876
- stdout: "",
877
- stderr: `${injection.detail}\n${injection.remedy}\n`,
878
- };
906
+ return failAfterClaim(EXIT_GATE_FAILED, `${injection.detail}\n${injection.remedy}\n`);
879
907
  }
880
908
  if (injection.injected) {
881
909
  expectedGithubLogin = injection.expectedLogin;
@@ -919,11 +947,7 @@ export function swarmLaunch(args) {
919
947
  });
920
948
  }
921
949
  catch (exc) {
922
- return {
923
- exitCode: EXIT_CONFIG_ERROR,
924
- stdout: "",
925
- stderr: `Error: could not persist launch occupancy_session_id: ${String(exc)}\n`,
926
- };
950
+ return failAfterClaim(EXIT_CONFIG_ERROR, `Error: could not persist launch occupancy_session_id: ${String(exc)}\n`);
927
951
  }
928
952
  if (args.output !== undefined && args.output !== null) {
929
953
  try {
@@ -939,11 +963,7 @@ export function swarmLaunch(args) {
939
963
  });
940
964
  }
941
965
  catch (exc) {
942
- return {
943
- exitCode: EXIT_CONFIG_ERROR,
944
- stdout: "",
945
- stderr: `Error: could not write --output ${args.output}: ${String(exc)}\n`,
946
- };
966
+ return failAfterClaim(EXIT_CONFIG_ERROR, `Error: could not write --output ${args.output}: ${String(exc)}\n`);
947
967
  }
948
968
  }
949
969
  void args.noAudit;
@@ -85,7 +85,8 @@ Before spawning a peer implement leaf: run with default --action begin.
85
85
  exit 1 active deny / gate block (do not spawn)
86
86
  exit 2 config / usage error
87
87
 
88
- Takeover: --action cancel, then pre-dispatch begin again (not concurrent dual active).
88
+ Takeover after REDISPATCH_OK: --action cancel, then pre-dispatch begin again
89
+ (not concurrent dual active; a killed worker stays running until cancelled).
89
90
  Terminal: --action complete [--status succeeded|failed|cancelled|blocked]
90
91
 
91
92
  Options:
@@ -12,6 +12,7 @@ import { existsSync, realpathSync } from "node:fs";
12
12
  import { isAbsolute, normalize, resolve } from "node:path";
13
13
  import { activeAttempts, beginAttempt, completeAttemptOnDisk, evaluatePreDispatch, hasActiveAttempt, listUnitLedgers, loadOrCreateUnitLedger, loadUnitLedger, markBlocked, saveUnitLedger, withUnitLock, } from "../delivery-attempt/index.js";
14
14
  import { EXIT_CONFIG_ERROR, EXIT_GATE_FAILED, EXIT_OK } from "./constants.js";
15
+ import { ensureSubagentStatusDir, looksLikeWorktreeDir } from "./subagent-status-dir.js";
15
16
  import { runText } from "./subprocess.js";
16
17
  /** Default workflow id for drive-to:merge-ready implement leaves. */
17
18
  export const IMPLEMENT_LEAF_WORKFLOW_ID = "drive-to:merge-ready";
@@ -170,6 +171,14 @@ function runBegin(input) {
170
171
  const { scopeId, targetId, workflowId } = unitFields(input);
171
172
  const sourceRevision = resolveSourceRevision(input.projectRoot, input.sourceRevision);
172
173
  const trigger = input.trigger ?? "automatic";
174
+ // Arm heartbeat scratch so verify:subagent-alive can return REDISPATCH_OK
175
+ // instead of exit 2 ("scratch dir does not exist") after a host-kill (#3730).
176
+ // A bare relative worktree name (`b3730`) is opaque to the branch-vs-path
177
+ // heuristic, so accept an on-disk worktree as the second arming signal.
178
+ const armTarget = resolve(input.projectRoot, input.targetId.trim());
179
+ if (looksLikeFilesystemTarget(input.targetId) || looksLikeWorktreeDir(armTarget)) {
180
+ ensureSubagentStatusDir(armTarget);
181
+ }
173
182
  // Exclusive lock → reload → evaluate → begin+save (same decision under lock;
174
183
  // no stale preview decision on the success path).
175
184
  try {
@@ -366,7 +375,7 @@ export function formatPreDispatchReport(result) {
366
375
  lines.push(` attempt: ${result.attempt.attemptId} status=${result.attempt.status}`);
367
376
  }
368
377
  if (result.exitCode === EXIT_GATE_FAILED && result.decision === "DENY_DUPLICATE_ACTIVE") {
369
- lines.push(" hint: do not spawn; resume the live leaf, or takeover = cancel then pre-dispatch begin");
378
+ lines.push(" hint: do not spawn; a killed worker stays running until cancelled.", " Takeover: task swarm:pre-dispatch -- --scope-id <id> --target-id <target> --action cancel", " then the same command without --action (begin). REDISPATCH_OK does not lift DENY_DUPLICATE_ACTIVE.");
370
379
  }
371
380
  return lines.join("\n");
372
381
  }
@@ -0,0 +1,27 @@
1
+ /**
2
+ * Arm the shipped sub-agent heartbeat path (#3730 / #1365 / #2824).
3
+ *
4
+ * `verify:subagent-alive` exits 2 when the scratch directory is missing, which
5
+ * looks like "never started" instead of REDISPATCH_OK. Parents mkdir at
6
+ * worktree create and at pre-dispatch begin so a later missing record is
7
+ * exit 1. Does not write heartbeat records or liveness onto the C2 manifest.
8
+ */
9
+ /**
10
+ * True when a resolved path is an on-disk git worktree, i.e. it carries a `.git`
11
+ * entry (a file for `git worktree add` trees, a directory for the main clone).
12
+ *
13
+ * Arming cannot key off `looksLikeFilesystemTarget` alone: that predicate exists
14
+ * to keep branch refs opaque and so answers false for a bare relative worktree
15
+ * name like `b3730`, leaving the liveness gate at its exit-2 config error for a
16
+ * supported dispatch form. Existence alone is too loose in the other direction —
17
+ * a branch named `docs` resolves onto the real `docs/` tree and would mkdir a
18
+ * stray scratch dir in the source checkout. The `.git` marker separates the two.
19
+ */
20
+ export declare function looksLikeWorktreeDir(resolvedPath: string): boolean;
21
+ /**
22
+ * Create `<worktree>/.deft-scratch/subagent-status/` when the worktree exists.
23
+ * Returns the directory path, or null when the worktree is not on disk yet
24
+ * (do not mkdir a stray tree for a branch-name target).
25
+ */
26
+ export declare function ensureSubagentStatusDir(worktreeRoot: string): string | null;
27
+ //# sourceMappingURL=subagent-status-dir.d.ts.map
@@ -0,0 +1,42 @@
1
+ /**
2
+ * Arm the shipped sub-agent heartbeat path (#3730 / #1365 / #2824).
3
+ *
4
+ * `verify:subagent-alive` exits 2 when the scratch directory is missing, which
5
+ * looks like "never started" instead of REDISPATCH_OK. Parents mkdir at
6
+ * worktree create and at pre-dispatch begin so a later missing record is
7
+ * exit 1. Does not write heartbeat records or liveness onto the C2 manifest.
8
+ */
9
+ import { existsSync, mkdirSync } from "node:fs";
10
+ import { join } from "node:path";
11
+ import { defaultScratchDir } from "../orchestration/subagent-monitor.js";
12
+ /**
13
+ * True when a resolved path is an on-disk git worktree, i.e. it carries a `.git`
14
+ * entry (a file for `git worktree add` trees, a directory for the main clone).
15
+ *
16
+ * Arming cannot key off `looksLikeFilesystemTarget` alone: that predicate exists
17
+ * to keep branch refs opaque and so answers false for a bare relative worktree
18
+ * name like `b3730`, leaving the liveness gate at its exit-2 config error for a
19
+ * supported dispatch form. Existence alone is too loose in the other direction —
20
+ * a branch named `docs` resolves onto the real `docs/` tree and would mkdir a
21
+ * stray scratch dir in the source checkout. The `.git` marker separates the two.
22
+ */
23
+ export function looksLikeWorktreeDir(resolvedPath) {
24
+ const trimmed = resolvedPath.trim();
25
+ if (trimmed.length === 0)
26
+ return false;
27
+ return existsSync(join(trimmed, ".git"));
28
+ }
29
+ /**
30
+ * Create `<worktree>/.deft-scratch/subagent-status/` when the worktree exists.
31
+ * Returns the directory path, or null when the worktree is not on disk yet
32
+ * (do not mkdir a stray tree for a branch-name target).
33
+ */
34
+ export function ensureSubagentStatusDir(worktreeRoot) {
35
+ const trimmed = worktreeRoot.trim();
36
+ if (trimmed.length === 0 || !existsSync(trimmed))
37
+ return null;
38
+ const dir = defaultScratchDir(trimmed);
39
+ mkdirSync(dir, { recursive: true });
40
+ return dir;
41
+ }
42
+ //# sourceMappingURL=subagent-status-dir.js.map
@@ -21,6 +21,9 @@ export declare class WorktreeMapConfigError extends Error {
21
21
  export declare class WorktreePathEscapeError extends WorktreeMapError {
22
22
  name: string;
23
23
  }
24
+ export declare class WorktreeRevisionMismatchError extends WorktreeMapError {
25
+ name: string;
26
+ }
24
27
  export type GitRunner = (args: readonly string[], cwd: string) => TextCaptureResult;
25
28
  export declare const defaultGitRunner: GitRunner;
26
29
  /** Case-normalized comparison key for worktree-path equality. */
@@ -30,9 +33,20 @@ export interface WorktreeRecord {
30
33
  readonly worktree_path: string;
31
34
  readonly base_branch: string;
32
35
  }
33
- /** Parse `git worktree list --porcelain` into `{compareKey: branch|null}`. */
34
- export declare function parseWorktreePorcelain(text: string): Map<string, string | null>;
35
- /** Resolve a story-to-worktree mapping into normalized C3 records (frozen contract). */
36
+ /** One `git worktree list --porcelain` record. `head` is the commit OID or null. */
37
+ export interface WorktreePorcelainEntry {
38
+ readonly branch: string | null;
39
+ readonly head: string | null;
40
+ }
41
+ /** Parse `git worktree list --porcelain` into `{compareKey: {branch, head}}`. */
42
+ export declare function parseWorktreePorcelain(text: string): Map<string, WorktreePorcelainEntry>;
43
+ /**
44
+ * Resolve a story-to-worktree mapping into normalized C3 records (frozen contract).
45
+ *
46
+ * Registered paths are reused only when their porcelain HEAD OID matches the
47
+ * requested base OID. That comparison is a snapshot at resolution time — not a
48
+ * pin on the worker's start revision (`swarm:launch` emits a manifest and stops).
49
+ */
36
50
  export declare function resolveWorktreeMap(mapping: readonly Record<string, unknown>[], baseBranch: string, createMissing?: boolean, options?: {
37
51
  repoRoot?: string;
38
52
  git?: GitRunner;