@deftai/directive-core 0.106.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 (249) hide show
  1. package/dist/architecture/sor-preflight.js +2 -0
  2. package/dist/check/cached-orchestrator.js +28 -3
  3. package/dist/check/gate-lists.js +2 -0
  4. package/dist/check/named-cause.js +18 -3
  5. package/dist/codebase/provider.js +7 -1
  6. package/dist/delivery-attempt/handoff.js +1 -1
  7. package/dist/design-critique/citation-grammar.d.ts +53 -0
  8. package/dist/design-critique/citation-grammar.js +303 -0
  9. package/dist/design-critique/completed-arc-record.d.ts +62 -0
  10. package/dist/design-critique/completed-arc-record.js +232 -0
  11. package/dist/design-critique/exclusive-chip.d.ts +33 -0
  12. package/dist/design-critique/exclusive-chip.js +68 -0
  13. package/dist/design-critique/parent-audit.d.ts +59 -0
  14. package/dist/design-critique/parent-audit.js +121 -0
  15. package/dist/doctor/checks.d.ts +6 -0
  16. package/dist/doctor/checks.js +35 -0
  17. package/dist/freshness/bind.d.ts +32 -3
  18. package/dist/freshness/bind.js +58 -12
  19. package/dist/hooks/classify/host-session-identity.d.ts +76 -0
  20. package/dist/hooks/classify/host-session-identity.js +470 -0
  21. package/dist/hooks/classify/index.d.ts +3 -2
  22. package/dist/hooks/classify/index.js +3 -2
  23. package/dist/hooks/classify/paths.d.ts +7 -0
  24. package/dist/hooks/classify/paths.js +35 -0
  25. package/dist/hooks/classify/stdin.d.ts +5 -0
  26. package/dist/hooks/classify/stdin.js +27 -0
  27. package/dist/hooks/dest-form.d.ts +46 -0
  28. package/dist/hooks/dest-form.js +575 -0
  29. package/dist/hooks/dispatcher.d.ts +69 -2
  30. package/dist/hooks/dispatcher.js +609 -45
  31. package/dist/hooks/fixtures/cases.d.ts +16 -2
  32. package/dist/hooks/fixtures/cases.js +175 -0
  33. package/dist/hooks/index.d.ts +1 -0
  34. package/dist/hooks/index.js +1 -0
  35. package/dist/hooks/scope.js +3 -1
  36. package/dist/init-deposit/agent-hooks.d.ts +2 -1
  37. package/dist/init-deposit/agent-hooks.js +8 -1
  38. package/dist/init-deposit/hook-runtime-travel.d.ts +77 -0
  39. package/dist/init-deposit/hook-runtime-travel.js +269 -0
  40. package/dist/intake/clause-derivation.js +9 -3
  41. package/dist/intake/github-auth-modes-cli.js +14 -2
  42. package/dist/intake/github-auth-modes.d.ts +54 -9
  43. package/dist/intake/github-auth-modes.js +326 -123
  44. package/dist/intake/issue-ingest.js +62 -2
  45. package/dist/intake/platform-capabilities.d.ts +9 -2
  46. package/dist/intake/platform-capabilities.js +38 -9
  47. package/dist/lifecycle/completed-tracked-on-delivery.d.ts +26 -0
  48. package/dist/lifecycle/completed-tracked-on-delivery.js +37 -14
  49. package/dist/lifecycle/completed-write-guard.d.ts +48 -0
  50. package/dist/lifecycle/completed-write-guard.js +373 -0
  51. package/dist/lifecycle/index.d.ts +1 -0
  52. package/dist/lifecycle/index.js +1 -0
  53. package/dist/literal-acceptance/capture.d.ts +16 -0
  54. package/dist/literal-acceptance/capture.js +75 -7
  55. package/dist/literal-acceptance/evaluate.js +25 -3
  56. package/dist/literal-acceptance/index.d.ts +2 -2
  57. package/dist/literal-acceptance/index.js +2 -2
  58. package/dist/literal-acceptance/run.js +9 -3
  59. package/dist/literal-acceptance/types.d.ts +6 -0
  60. package/dist/literal-acceptance/types.js +6 -0
  61. package/dist/orphan-active/evaluate.d.ts +25 -0
  62. package/dist/orphan-active/evaluate.js +193 -128
  63. package/dist/orphan-active/index.d.ts +1 -0
  64. package/dist/orphan-active/index.js +1 -0
  65. package/dist/orphan-active/issue-state.d.ts +121 -0
  66. package/dist/orphan-active/issue-state.js +278 -0
  67. package/dist/platform/cursor-managed-runtime.d.ts +95 -0
  68. package/dist/platform/cursor-managed-runtime.js +241 -0
  69. package/dist/platform/index.d.ts +1 -0
  70. package/dist/platform/index.js +1 -0
  71. package/dist/platform/platform-capabilities.d.ts +23 -1
  72. package/dist/platform/platform-capabilities.js +55 -15
  73. package/dist/policy/ceremony-dial.js +5 -9
  74. package/dist/policy/host-hooks.js +4 -9
  75. package/dist/policy/org-force-on-migration.js +5 -10
  76. package/dist/policy/plan-extensions.d.ts +14 -2
  77. package/dist/policy/plan-extensions.js +24 -3
  78. package/dist/policy/product-signal.js +5 -10
  79. package/dist/policy/require-human-merge.js +5 -9
  80. package/dist/policy/resolve.js +77 -16
  81. package/dist/policy/runtime-authority.d.ts +18 -0
  82. package/dist/policy/runtime-authority.js +11 -0
  83. package/dist/policy/value-feedback.js +9 -18
  84. package/dist/policy/write-fence.js +3 -0
  85. package/dist/pr-closeout-attestable/evaluate.d.ts +80 -0
  86. package/dist/pr-closeout-attestable/evaluate.js +306 -0
  87. package/dist/pr-closeout-attestable/index.d.ts +2 -0
  88. package/dist/pr-closeout-attestable/index.js +2 -0
  89. package/dist/pr-protected-issues/gh.d.ts +6 -2
  90. package/dist/pr-protected-issues/gh.js +7 -3
  91. package/dist/pr-wait-mergeable/cascade.d.ts +9 -1
  92. package/dist/pr-wait-mergeable/cascade.js +21 -1
  93. package/dist/pr-wait-mergeable/types.d.ts +2 -0
  94. package/dist/pr-wait-mergeable/wrappers.d.ts +6 -0
  95. package/dist/pr-wait-mergeable/wrappers.js +11 -0
  96. package/dist/preflight/evaluate.d.ts +15 -0
  97. package/dist/preflight/evaluate.js +29 -3
  98. package/dist/preflight-cache/evaluate.js +9 -3
  99. package/dist/product-first-done-gate/acceptance-resolver.d.ts +14 -2
  100. package/dist/product-first-done-gate/acceptance-resolver.js +13 -2
  101. package/dist/product-first-done-gate/evaluate.js +53 -12
  102. package/dist/release/cli-drift-report.d.ts +68 -0
  103. package/dist/release/cli-drift-report.js +189 -0
  104. package/dist/release/index.d.ts +1 -0
  105. package/dist/release/index.js +1 -0
  106. package/dist/release/issue-state-fetch.d.ts +5 -3
  107. package/dist/release/issue-state-fetch.js +86 -21
  108. package/dist/release/native-steps.js +1 -0
  109. package/dist/release/pipeline.js +19 -0
  110. package/dist/release/types.d.ts +14 -0
  111. package/dist/render/constants.d.ts +4 -0
  112. package/dist/render/constants.js +11 -8
  113. package/dist/render/export-spec.js +31 -5
  114. package/dist/render/index.d.ts +1 -1
  115. package/dist/render/index.js +1 -1
  116. package/dist/render/prd-render.d.ts +4 -1
  117. package/dist/render/prd-render.js +82 -23
  118. package/dist/render/project-render.js +14 -7
  119. package/dist/render/scope-outlook.d.ts +2 -0
  120. package/dist/render/scope-outlook.js +3 -0
  121. package/dist/render/spec-render.js +2 -2
  122. package/dist/resolution/package-manager.d.ts +33 -4
  123. package/dist/resolution/package-manager.js +210 -9
  124. package/dist/review-monitor/constants.d.ts +7 -0
  125. package/dist/review-monitor/constants.js +7 -0
  126. package/dist/review-monitor/github-lease.d.ts +98 -1
  127. package/dist/review-monitor/github-lease.js +182 -1
  128. package/dist/review-monitor/lease-comment.d.ts +36 -0
  129. package/dist/review-monitor/lease-comment.js +103 -2
  130. package/dist/scm/call.js +12 -3
  131. package/dist/scm/design-critique-chip.d.ts +38 -0
  132. package/dist/scm/design-critique-chip.js +187 -0
  133. package/dist/scm/gh-rest.d.ts +9 -1
  134. package/dist/scm/gh-rest.js +103 -0
  135. package/dist/scm/index.d.ts +1 -0
  136. package/dist/scm/index.js +1 -0
  137. package/dist/scm/main.d.ts +3 -0
  138. package/dist/scm/main.js +15 -1
  139. package/dist/scm/readiness-cli.d.ts +2 -0
  140. package/dist/scm/readiness-cli.js +42 -1
  141. package/dist/scm/readiness.d.ts +4 -1
  142. package/dist/scm/readiness.js +35 -5
  143. package/dist/scope/decompose.js +2 -1
  144. package/dist/scope/delivery-evidence.d.ts +27 -1
  145. package/dist/scope/index.d.ts +1 -0
  146. package/dist/scope/index.js +1 -0
  147. package/dist/scope/lifecycle-write.d.ts +24 -0
  148. package/dist/scope/lifecycle-write.js +64 -0
  149. package/dist/scope/main.js +3 -1
  150. package/dist/scope/project-definition-sync.js +14 -7
  151. package/dist/scope/transition.js +90 -11
  152. package/dist/scope-provenance/evaluate.js +5 -1
  153. package/dist/session/git.d.ts +42 -0
  154. package/dist/session/git.js +219 -3
  155. package/dist/session/occupancy.d.ts +239 -12
  156. package/dist/session/occupancy.js +820 -27
  157. package/dist/session/ritual-sentinel.d.ts +5 -0
  158. package/dist/session/ritual-sentinel.js +5 -0
  159. package/dist/session/session-ready.d.ts +5 -1
  160. package/dist/session/session-ready.js +98 -8
  161. package/dist/session/session-start.d.ts +17 -1
  162. package/dist/session/session-start.js +49 -17
  163. package/dist/session/toolchain-preflight.d.ts +13 -3
  164. package/dist/session/toolchain-preflight.js +93 -18
  165. package/dist/session/verify-session-ritual.d.ts +33 -3
  166. package/dist/session/verify-session-ritual.js +89 -19
  167. package/dist/slice/constants.d.ts +1 -1
  168. package/dist/spec-authority/constants.d.ts +10 -2
  169. package/dist/spec-authority/constants.js +53 -8
  170. package/dist/spec-authority/resolver.d.ts +3 -0
  171. package/dist/spec-authority/resolver.js +55 -7
  172. package/dist/subprocess/max-buffer.d.ts +14 -0
  173. package/dist/subprocess/max-buffer.js +15 -0
  174. package/dist/swarm/complete-cohort.d.ts +2 -0
  175. package/dist/swarm/complete-cohort.js +21 -13
  176. package/dist/swarm/index.d.ts +1 -0
  177. package/dist/swarm/index.js +1 -0
  178. package/dist/swarm/launch-cli.js +53 -33
  179. package/dist/swarm/launch.d.ts +77 -0
  180. package/dist/swarm/launch.js +248 -38
  181. package/dist/swarm/pre-dispatch-cli.js +2 -1
  182. package/dist/swarm/pre-dispatch.js +10 -1
  183. package/dist/swarm/subagent-status-dir.d.ts +27 -0
  184. package/dist/swarm/subagent-status-dir.js +42 -0
  185. package/dist/swarm/worktrees.d.ts +17 -3
  186. package/dist/swarm/worktrees.js +48 -4
  187. package/dist/triage/actions/index.d.ts +4 -0
  188. package/dist/triage/actions/index.js +6 -0
  189. package/dist/triage/evaluate/evaluate.d.ts +11 -0
  190. package/dist/triage/evaluate/evaluate.js +169 -0
  191. package/dist/triage/evaluate/github.d.ts +16 -0
  192. package/dist/triage/evaluate/github.js +153 -0
  193. package/dist/triage/evaluate/index.d.ts +9 -0
  194. package/dist/triage/evaluate/index.js +8 -0
  195. package/dist/triage/evaluate/paths.d.ts +7 -0
  196. package/dist/triage/evaluate/paths.js +25 -0
  197. package/dist/triage/evaluate/sink.d.ts +5 -0
  198. package/dist/triage/evaluate/sink.js +52 -0
  199. package/dist/triage/evaluate/types.d.ts +93 -0
  200. package/dist/triage/evaluate/types.js +13 -0
  201. package/dist/triage/evaluate/validity.d.ts +8 -0
  202. package/dist/triage/evaluate/validity.js +87 -0
  203. package/dist/triage/evaluate/value.d.ts +12 -0
  204. package/dist/triage/evaluate/value.js +33 -0
  205. package/dist/triage/evaluate/wip-census.d.ts +5 -0
  206. package/dist/triage/evaluate/wip-census.js +39 -0
  207. package/dist/triage/evaluate/worktrees.d.ts +8 -0
  208. package/dist/triage/evaluate/worktrees.js +68 -0
  209. package/dist/triage/evaluate/xbrief-refs.d.ts +8 -0
  210. package/dist/triage/evaluate/xbrief-refs.js +60 -0
  211. package/dist/triage/help/registry-data.d.ts +21 -7
  212. package/dist/triage/help/registry-data.js +53 -5
  213. package/dist/triage/index.d.ts +1 -0
  214. package/dist/triage/index.js +1 -0
  215. package/dist/triage/scope/mutations-core.d.ts +2 -6
  216. package/dist/triage/scope/mutations-core.js +23 -55
  217. package/dist/triage/scope-drift/add-ignore.js +5 -39
  218. package/dist/triage/subscribe/index.d.ts +2 -6
  219. package/dist/triage/subscribe/index.js +15 -65
  220. package/dist/triage/welcome/writers.js +15 -74
  221. package/dist/value/feedback-file.d.ts +17 -2
  222. package/dist/value/feedback-file.js +80 -6
  223. package/dist/vbrief-build/index.d.ts +2 -1
  224. package/dist/vbrief-build/index.js +2 -1
  225. package/dist/vbrief-build/parity-scenarios.js +5 -5
  226. package/dist/vbrief-build/project-definition-io.d.ts +63 -3
  227. package/dist/vbrief-build/project-definition-io.js +357 -31
  228. package/dist/vbrief-build/project-definition-mutation.d.ts +32 -0
  229. package/dist/vbrief-build/project-definition-mutation.js +43 -0
  230. package/dist/vbrief-reconcile/labels.d.ts +1 -0
  231. package/dist/vbrief-reconcile/labels.js +30 -0
  232. package/dist/vbrief-validate/conformance.d.ts +1 -0
  233. package/dist/vbrief-validate/conformance.js +85 -8
  234. package/dist/vbrief-validate/main.js +2 -0
  235. package/dist/vbrief-validate/precutover.js +5 -10
  236. package/dist/vbrief-validation/story-quality.d.ts +7 -0
  237. package/dist/vbrief-validation/story-quality.js +8 -1
  238. package/dist/verify-ac/clauses.d.ts +62 -2
  239. package/dist/verify-ac/clauses.js +183 -111
  240. package/dist/verify-ac/index.d.ts +1 -1
  241. package/dist/verify-ac/index.js +1 -1
  242. package/dist/verify-env/node-runtime.d.ts +8 -4
  243. package/dist/verify-env/node-runtime.js +9 -6
  244. package/dist/verify-env/toolchain-check.d.ts +35 -3
  245. package/dist/verify-env/toolchain-check.js +155 -36
  246. package/dist/verify-source/project-definition-mutation-boundary.d.ts +104 -0
  247. package/dist/verify-source/project-definition-mutation-boundary.js +326 -0
  248. package/dist/xbrief-migrate/agents-header.js +69 -7
  249. package/package.json +7 -3
@@ -1,11 +1,11 @@
1
1
  import { existsSync, mkdirSync, readdirSync, readFileSync } from "node:fs";
2
2
  import { basename, dirname, isAbsolute, join, resolve } from "node:path";
3
3
  import { containedWrite } from "../fs/contained-write.js";
4
- import { inferGithubAuthMode } from "../intake/github-auth-modes.js";
4
+ import { ENV_EXPECTED_GITHUB_LOGIN, FAILURE_INSTALLATION_IDENTITY_UNVERIFIABLE, FAILURE_MISSING_INJECTED_TOKEN, findInjectedToken, GITHUB_AUTH_MODE_HOST_GH, GITHUB_AUTH_MODE_INJECTED_TOKEN, inferGithubAuthMode, validateGithubAuthForWorker, } from "../intake/github-auth-modes.js";
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";
@@ -25,6 +25,164 @@ export const defaultRuntimeAuthProbe = () => {
25
25
  const report = getPlatformCapabilities();
26
26
  return [report.runtimeMode, inferGithubAuthMode(report)];
27
27
  };
28
+ const CREDENTIAL_VALUE_PATTERN = /^(ghp_|gho_|ghu_|ghs_|github_pat_)/i;
29
+ const INJECTION_MISSING_CREDENTIAL = "GH_TOKEN (or GITHUB_TOKEN / GH_ENTERPRISE_TOKEN)";
30
+ const INJECTION_DISPATCHER_REMEDY = "Dispatcher-side remedy: load an approved user-bearing worker credential into the dispatcher environment, then call prepareWorkerCredentialInjection before spawn. Do not fall back to the host gh token or continue under the maintainer identity. Installation credentials are not injectable; see #3693.";
31
+ const HELD_WORKER_TOKEN_VARS = [
32
+ "GH_TOKEN",
33
+ "GITHUB_TOKEN",
34
+ "GH_ENTERPRISE_TOKEN",
35
+ "GITHUB_ENTERPRISE_TOKEN",
36
+ ];
37
+ function looksLikeCredentialValue(value) {
38
+ return CREDENTIAL_VALUE_PATTERN.test(value.trim());
39
+ }
40
+ function assertNoCredentialValue(value, label) {
41
+ if (looksLikeCredentialValue(value)) {
42
+ throw new Error(`${label} must not contain a credential value`);
43
+ }
44
+ }
45
+ function sanitizeEnvelopeField(value, label) {
46
+ const cleaned = value.replace(/\r?\n/g, " ").trim();
47
+ assertNoCredentialValue(cleaned, label);
48
+ return cleaned;
49
+ }
50
+ /**
51
+ * Bind validation to the selected token. Stamp it into every gh token slot and
52
+ * drop GH_CONFIG_DIR so a host/enterprise config cannot authenticate as someone
53
+ * else. GH_HOST stays: it selects the API host, not the secret.
54
+ */
55
+ function isolateHeldWorkerToken(environ, token) {
56
+ const isolated = { ...environ };
57
+ for (const name of HELD_WORKER_TOKEN_VARS) {
58
+ isolated[name] = token;
59
+ }
60
+ delete isolated.GH_CONFIG_DIR;
61
+ return isolated;
62
+ }
63
+ function boundWorkerSpawnEnv(token, expectedLogin) {
64
+ return {
65
+ GH_TOKEN: token,
66
+ GITHUB_TOKEN: token,
67
+ GH_ENTERPRISE_TOKEN: token,
68
+ [ENV_EXPECTED_GITHUB_LOGIN]: expectedLogin,
69
+ };
70
+ }
71
+ /**
72
+ * Dispatch-envelope half of the runtime / GitHub auth-mode contract (#1557 / #1351).
73
+ * Policy labels only — never a token value.
74
+ */
75
+ export function formatDispatchAuthEnvelope(fields) {
76
+ const runtimeMode = sanitizeEnvelopeField(fields.runtimeMode, "runtime_mode");
77
+ const githubAuthMode = sanitizeEnvelopeField(fields.githubAuthMode, "github_auth_mode");
78
+ const lines = [
79
+ "## Runtime and GitHub auth mode",
80
+ "",
81
+ `- runtime_mode: ${runtimeMode}`,
82
+ `- github_auth_mode: ${githubAuthMode}`,
83
+ ];
84
+ if (fields.expectedGithubLogin !== undefined && fields.expectedGithubLogin !== null) {
85
+ const login = sanitizeEnvelopeField(fields.expectedGithubLogin, "expected_github_login");
86
+ if (login.length > 0) {
87
+ lines.push(`- expected_github_login: ${login}`);
88
+ }
89
+ }
90
+ return `${lines.join("\n")}\n`;
91
+ }
92
+ function blockedInjection(fields) {
93
+ const detail = fields.detail.startsWith("BLOCKED") ? fields.detail : `BLOCKED: ${fields.detail}`;
94
+ return {
95
+ ok: false,
96
+ blocked: true,
97
+ githubAuthMode: fields.githubAuthMode,
98
+ runtimeMode: fields.runtimeMode,
99
+ missingCredential: fields.missingCredential ?? INJECTION_MISSING_CREDENTIAL,
100
+ remedy: INJECTION_DISPATCHER_REMEDY,
101
+ detail,
102
+ failureKind: fields.failureKind,
103
+ envelopeSection: formatDispatchAuthEnvelope({
104
+ runtimeMode: fields.runtimeMode ?? "unknown",
105
+ githubAuthMode: fields.githubAuthMode,
106
+ }),
107
+ };
108
+ }
109
+ /**
110
+ * Validate a held user-bearing worker credential and prepare spawn-time injection (#1351).
111
+ *
112
+ * Comparison is opt-in on the validator. This path always stamps
113
+ * `DEFT_EXPECTED_GITHUB_LOGIN` after a successful user-principal check so the
114
+ * worker envelope does not inherit "any user token is approved."
115
+ *
116
+ * Installation credentials fail closed via the existing validator (#3693).
117
+ * Token values belong only in the returned `spawnEnv` — never in prompts,
118
+ * transcripts, or manifest entries.
119
+ */
120
+ export function prepareWorkerCredentialInjection(request) {
121
+ const environ = request.environ ?? process.env;
122
+ const runtimeMode = request.runtimeMode ?? null;
123
+ const token = findInjectedToken(environ);
124
+ const grokBuild = request.dispatchPath === "grok-build";
125
+ const effectiveMode = grokBuild || token !== null ? GITHUB_AUTH_MODE_INJECTED_TOKEN : request.githubAuthMode;
126
+ if (effectiveMode === GITHUB_AUTH_MODE_HOST_GH && token === null) {
127
+ return {
128
+ ok: true,
129
+ injected: false,
130
+ githubAuthMode: GITHUB_AUTH_MODE_HOST_GH,
131
+ runtimeMode,
132
+ expectedLogin: null,
133
+ spawnEnv: {},
134
+ envelopeSection: formatDispatchAuthEnvelope({
135
+ runtimeMode: runtimeMode ?? "local-unsandboxed",
136
+ githubAuthMode: GITHUB_AUTH_MODE_HOST_GH,
137
+ }),
138
+ };
139
+ }
140
+ if (token === null) {
141
+ return blockedInjection({
142
+ githubAuthMode: GITHUB_AUTH_MODE_INJECTED_TOKEN,
143
+ runtimeMode,
144
+ failureKind: FAILURE_MISSING_INJECTED_TOKEN,
145
+ missingCredential: INJECTION_MISSING_CREDENTIAL,
146
+ detail: "missing worker credential GH_TOKEN (or GITHUB_TOKEN / GH_ENTERPRISE_TOKEN) for a write-requiring injected-token dispatch",
147
+ });
148
+ }
149
+ const isolatedEnviron = isolateHeldWorkerToken(environ, token);
150
+ const validated = validateGithubAuthForWorker(GITHUB_AUTH_MODE_INJECTED_TOKEN, {
151
+ environ: isolatedEnviron,
152
+ runtimeReport: { runtimeMode: runtimeMode ?? "cloud-headless" },
153
+ repo: request.repo,
154
+ runGh: request.runGh,
155
+ expectedPrincipal: request.expectedPrincipal,
156
+ cwd: request.cwd,
157
+ });
158
+ if (!validated.ok || validated.login === null || validated.login.trim().length === 0) {
159
+ const installation = validated.failureKind === FAILURE_INSTALLATION_IDENTITY_UNVERIFIABLE;
160
+ return blockedInjection({
161
+ githubAuthMode: GITHUB_AUTH_MODE_INJECTED_TOKEN,
162
+ runtimeMode,
163
+ failureKind: validated.failureKind,
164
+ detail: installation
165
+ ? `${validated.detail} Installation credentials are not injectable (#3693).`
166
+ : validated.detail,
167
+ });
168
+ }
169
+ const expectedLogin = validated.login.trim();
170
+ assertNoCredentialValue(expectedLogin, "expected_github_login");
171
+ const spawnEnv = boundWorkerSpawnEnv(token, expectedLogin);
172
+ return {
173
+ ok: true,
174
+ injected: true,
175
+ githubAuthMode: GITHUB_AUTH_MODE_INJECTED_TOKEN,
176
+ runtimeMode,
177
+ expectedLogin,
178
+ spawnEnv,
179
+ envelopeSection: formatDispatchAuthEnvelope({
180
+ runtimeMode: runtimeMode ?? "cloud-headless",
181
+ githubAuthMode: GITHUB_AUTH_MODE_INJECTED_TOKEN,
182
+ expectedGithubLogin: expectedLogin,
183
+ }),
184
+ };
185
+ }
28
186
  function loadJson(path) {
29
187
  try {
30
188
  const data = JSON.parse(readFileSync(path, "utf8"));
@@ -498,6 +656,10 @@ export function buildManifest(resolved, options) {
498
656
  if (options.githubAuthMode !== undefined && options.githubAuthMode !== null) {
499
657
  entry.github_auth_mode = options.githubAuthMode;
500
658
  }
659
+ if (options.expectedGithubLogin !== undefined && options.expectedGithubLogin !== null) {
660
+ assertNoCredentialValue(options.expectedGithubLogin, "expected_github_login");
661
+ entry.expected_github_login = options.expectedGithubLogin;
662
+ }
501
663
  manifest.push(entry);
502
664
  }
503
665
  return manifest;
@@ -533,7 +695,23 @@ function splitCsv(values) {
533
695
  return out;
534
696
  }
535
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
+ }
536
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
+ }
537
715
  const tokens = [...splitCsv(args.stories ?? []), ...splitCsv(args.paths ?? [])];
538
716
  if (tokens.length === 0) {
539
717
  return {
@@ -619,17 +797,52 @@ export function swarmLaunch(args) {
619
797
  }
620
798
  const operatorApproval = args.operatorApproval ??
621
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
+ };
622
839
  let worktreeRecordMap = new Map();
623
840
  if (args.worktreeMap !== undefined && args.worktreeMap !== null) {
624
841
  const resolver = args.worktreeResolver ?? resolveWorktreeMap;
625
842
  try {
626
843
  const payload = JSON.parse(readFileSync(args.worktreeMap, "utf8"));
627
844
  if (!Array.isArray(payload)) {
628
- return {
629
- exitCode: EXIT_CONFIG_ERROR,
630
- stdout: "",
631
- stderr: `Error: --worktree-map ${args.worktreeMap} must contain a JSON array of records.\n`,
632
- };
845
+ return failAfterClaim(EXIT_CONFIG_ERROR, `Error: --worktree-map ${args.worktreeMap} must contain a JSON array of records.\n`);
633
846
  }
634
847
  const records = resolver(payload, args.baseBranch ?? DEFAULT_BASE_BRANCH, !(args.noCreateWorktrees ?? false), {
635
848
  repoRoot: projectRoot,
@@ -637,11 +850,7 @@ export function swarmLaunch(args) {
637
850
  worktreeRecordMap = new Map(records.map((r) => [r.story_id, r]));
638
851
  }
639
852
  catch (exc) {
640
- return {
641
- exitCode: EXIT_CONFIG_ERROR,
642
- stdout: "",
643
- stderr: `Error: worktree map resolution failed: ${String(exc)}\n`,
644
- };
853
+ return failAfterClaim(EXIT_CONFIG_ERROR, `Error: worktree map resolution failed: ${String(exc)}\n`);
645
854
  }
646
855
  }
647
856
  let runtimeMode;
@@ -651,7 +860,7 @@ export function swarmLaunch(args) {
651
860
  [runtimeMode, githubAuthMode] = probe();
652
861
  }
653
862
  catch (exc) {
654
- return { exitCode: EXIT_CONFIG_ERROR, stdout: "", stderr: `Error: ${String(exc)}\n` };
863
+ return failAfterClaim(EXIT_CONFIG_ERROR, `Error: ${String(exc)}\n`);
655
864
  }
656
865
  let resolvedModel = null;
657
866
  let modelSource = null;
@@ -664,11 +873,7 @@ export function swarmLaunch(args) {
664
873
  // would emit an exit-0 manifest with no model and no error to follow. Match
665
874
  // verify:routing, which treats the same state as a config error (#1739).
666
875
  if (route.source === "invalid") {
667
- return {
668
- exitCode: EXIT_CONFIG_ERROR,
669
- stdout: "",
670
- stderr: `Error: routing gate misconfigured: ${route.error ?? "invalid routing decision"}\n`,
671
- };
876
+ return failAfterClaim(EXIT_CONFIG_ERROR, `Error: routing gate misconfigured: ${route.error ?? "invalid routing decision"}\n`);
672
877
  }
673
878
  if (route.decided) {
674
879
  resolvedModel = route.model;
@@ -681,16 +886,28 @@ export function swarmLaunch(args) {
681
886
  ? dispatchProviderFor(backend.backend_id)
682
887
  : null;
683
888
  const workerRoleValue = routingFile !== null || backend !== null ? LEAF_CODING_WORKER_ROLE : null;
684
- const occupancy = applyWorktreeOccupancy(projectRoot, {
685
- env: args.environ ?? process.env,
686
- intent: "swarm",
687
- });
688
- if (occupancy.code !== 0) {
689
- return {
690
- exitCode: EXIT_GATE_FAILED,
691
- stdout: "",
692
- stderr: `${occupancy.message}\n`,
693
- };
889
+ const launchEnviron = args.environ ?? process.env;
890
+ let expectedGithubLogin = null;
891
+ // Only the injected-token launch path auto-validates (and fail-closes on a
892
+ // missing token). An ambient GH_TOKEN on a host-gh probe is often the
893
+ // maintainer workaround, not a worker credential; local-hybrid injection is
894
+ // the explicit prepareWorkerCredentialInjection call.
895
+ if (githubAuthMode === GITHUB_AUTH_MODE_INJECTED_TOKEN) {
896
+ const injection = prepareWorkerCredentialInjection({
897
+ environ: launchEnviron,
898
+ githubAuthMode,
899
+ runtimeMode,
900
+ dispatchPath: "grok-build",
901
+ runGh: args.runGh,
902
+ expectedPrincipal: args.expectedPrincipal,
903
+ cwd: projectRoot,
904
+ });
905
+ if (!injection.ok) {
906
+ return failAfterClaim(EXIT_GATE_FAILED, `${injection.detail}\n${injection.remedy}\n`);
907
+ }
908
+ if (injection.injected) {
909
+ expectedGithubLogin = injection.expectedLogin;
910
+ }
694
911
  }
695
912
  const manifest = buildManifest(ordered, {
696
913
  projectRoot,
@@ -708,6 +925,7 @@ export function swarmLaunch(args) {
708
925
  modelSource,
709
926
  runtimeMode,
710
927
  githubAuthMode,
928
+ expectedGithubLogin,
711
929
  occupancySessionId: occupancy.sessionId,
712
930
  });
713
931
  const rendered = `${JSON.stringify(manifest, null, 2)}\n`;
@@ -729,11 +947,7 @@ export function swarmLaunch(args) {
729
947
  });
730
948
  }
731
949
  catch (exc) {
732
- return {
733
- exitCode: EXIT_CONFIG_ERROR,
734
- stdout: "",
735
- stderr: `Error: could not persist launch occupancy_session_id: ${String(exc)}\n`,
736
- };
950
+ return failAfterClaim(EXIT_CONFIG_ERROR, `Error: could not persist launch occupancy_session_id: ${String(exc)}\n`);
737
951
  }
738
952
  if (args.output !== undefined && args.output !== null) {
739
953
  try {
@@ -749,11 +963,7 @@ export function swarmLaunch(args) {
749
963
  });
750
964
  }
751
965
  catch (exc) {
752
- return {
753
- exitCode: EXIT_CONFIG_ERROR,
754
- stdout: "",
755
- stderr: `Error: could not write --output ${args.output}: ${String(exc)}\n`,
756
- };
966
+ return failAfterClaim(EXIT_CONFIG_ERROR, `Error: could not write --output ${args.output}: ${String(exc)}\n`);
757
967
  }
758
968
  }
759
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;
@@ -2,6 +2,7 @@ import { mkdirSync, readFileSync } from "node:fs";
2
2
  import { isAbsolute, resolve as pathResolve } from "node:path";
3
3
  import { assertProjectionContained, ProjectionContainmentError, } from "../fs/projection-containment.js";
4
4
  import { C3_FIELDS } from "./constants.js";
5
+ import { ensureSubagentStatusDir } from "./subagent-status-dir.js";
5
6
  import { runText } from "./subprocess.js";
6
7
  export class WorktreeMapError extends Error {
7
8
  name = "WorktreeMapError";
@@ -24,6 +25,9 @@ export class WorktreeMapConfigError extends Error {
24
25
  export class WorktreePathEscapeError extends WorktreeMapError {
25
26
  name = "WorktreePathEscapeError";
26
27
  }
28
+ export class WorktreeRevisionMismatchError extends WorktreeMapError {
29
+ name = "WorktreeRevisionMismatchError";
30
+ }
27
31
  export const defaultGitRunner = (args, cwd) => runText(["git", ...args], { cwd });
28
32
  function resolvePath(raw, repoRoot) {
29
33
  const candidate = isAbsolute(raw) ? raw : pathResolve(repoRoot, raw);
@@ -44,14 +48,15 @@ function assertWorktreePathContained(repoRoot, worktreePath) {
44
48
  export function compareKey(pathStr) {
45
49
  return pathStr.replace(/\\/g, "/").toLowerCase();
46
50
  }
47
- /** Parse `git worktree list --porcelain` into `{compareKey: branch|null}`. */
51
+ /** Parse `git worktree list --porcelain` into `{compareKey: {branch, head}}`. */
48
52
  export function parseWorktreePorcelain(text) {
49
53
  const registered = new Map();
50
54
  let currentPath = null;
51
55
  let currentBranch = null;
56
+ let currentHead = null;
52
57
  const flush = () => {
53
58
  if (currentPath !== null) {
54
- registered.set(compareKey(currentPath), currentBranch);
59
+ registered.set(compareKey(currentPath), { branch: currentBranch, head: currentHead });
55
60
  }
56
61
  };
57
62
  for (const line of text.split("\n")) {
@@ -59,6 +64,11 @@ export function parseWorktreePorcelain(text) {
59
64
  flush();
60
65
  currentPath = pathResolve(line.slice("worktree ".length).trim());
61
66
  currentBranch = null;
67
+ currentHead = null;
68
+ }
69
+ else if (line.startsWith("HEAD ")) {
70
+ const oid = line.slice("HEAD ".length).trim().toLowerCase();
71
+ currentHead = oid.length > 0 ? oid : null;
62
72
  }
63
73
  else if (line.startsWith("branch ")) {
64
74
  const ref = line.slice("branch ".length).trim();
@@ -82,6 +92,24 @@ function gitWorktreeList(repoRoot, git) {
82
92
  }
83
93
  return parseWorktreePorcelain(proc.stdout);
84
94
  }
95
+ function resolveCommitOid(repoRoot, revision, git) {
96
+ let proc;
97
+ try {
98
+ proc = git(["rev-parse", "--verify", "--end-of-options", `${revision}^{commit}`], repoRoot);
99
+ }
100
+ catch (exc) {
101
+ throw new WorktreeMapConfigError(`could not resolve base '${revision}' to a commit OID in ${repoRoot}: ${String(exc)}`);
102
+ }
103
+ if (proc.returncode !== 0) {
104
+ throw new WorktreeMapConfigError(`could not resolve base '${revision}' to a commit OID in ${repoRoot} ` +
105
+ `(rc=${proc.returncode}): ${proc.stderr.trim() || "<no stderr>"}`);
106
+ }
107
+ const oid = (proc.stdout.trim().split(/\s+/)[0] ?? "").toLowerCase();
108
+ if (!/^[0-9a-f]{40,64}$/.test(oid)) {
109
+ throw new WorktreeMapConfigError(`git rev-parse for base '${revision}' did not return a commit OID: ${JSON.stringify(oid)}`);
110
+ }
111
+ return oid;
112
+ }
85
113
  function createWorktree(repoRoot, worktreePath, baseBranch, git) {
86
114
  mkdirSync(pathResolve(worktreePath, ".."), { recursive: true });
87
115
  let proc;
@@ -96,7 +124,13 @@ function createWorktree(repoRoot, worktreePath, baseBranch, git) {
96
124
  `(rc=${proc.returncode}): ${proc.stderr.trim() || "<no stderr>"}`);
97
125
  }
98
126
  }
99
- /** Resolve a story-to-worktree mapping into normalized C3 records (frozen contract). */
127
+ /**
128
+ * Resolve a story-to-worktree mapping into normalized C3 records (frozen contract).
129
+ *
130
+ * Registered paths are reused only when their porcelain HEAD OID matches the
131
+ * requested base OID. That comparison is a snapshot at resolution time — not a
132
+ * pin on the worker's start revision (`swarm:launch` emits a manifest and stops).
133
+ */
100
134
  export function resolveWorktreeMap(mapping, baseBranch, createMissing = true, options = {}) {
101
135
  if (!Array.isArray(mapping)) {
102
136
  throw new WorktreeMapConfigError(`worktree map must be a list of records, got ${typeof mapping}`);
@@ -154,14 +188,24 @@ export function resolveWorktreeMap(mapping, baseBranch, createMissing = true, op
154
188
  });
155
189
  }
156
190
  const registered = gitWorktreeList(root, git);
191
+ const requestedOid = resolveCommitOid(root, trimmedBase, git);
157
192
  for (const entry of resolved) {
158
- if (registered.has(entry._key)) {
193
+ const existing = registered.get(entry._key);
194
+ if (existing !== undefined) {
195
+ const actualOid = existing.head ?? "";
196
+ if (actualOid !== requestedOid) {
197
+ throw new WorktreeRevisionMismatchError(`registered worktree '${entry.worktree_path}' HEAD OID is ${actualOid || "(missing)"} ` +
198
+ `but requested base '${trimmedBase}' resolves to ${requestedOid}; ` +
199
+ `this is a snapshot check at resolution time, not a pin on the worker's start revision`);
200
+ }
201
+ ensureSubagentStatusDir(entry._abs);
159
202
  continue;
160
203
  }
161
204
  if (!createMissing) {
162
205
  throw new MissingWorktreeError(`story '${entry.story_id}' maps to '${entry.worktree_path}' which is not a registered git worktree and create_missing is disabled`);
163
206
  }
164
207
  createWorktree(root, entry._abs, trimmedBase, git);
208
+ ensureSubagentStatusDir(entry._abs);
165
209
  }
166
210
  return resolved.map(({ story_id, worktree_path, base_branch }) => ({
167
211
  story_id,
@@ -38,6 +38,10 @@ export declare function reset(issueNumber: number, repo: string, deps: TriageAct
38
38
  actor?: string | null;
39
39
  projectRoot?: string;
40
40
  }): string;
41
+ /** Success line for ``triage:accept``, plus the optional design-critique affordance (#3708). */
42
+ export declare function formatAcceptOutput(issueNumber: number, repo: string, decisionId: string, options?: {
43
+ autoPromote?: boolean;
44
+ }): string;
41
45
  /** Format a decision entry for CLI ``status`` / ``history`` output. */
42
46
  export declare function formatDecision(entry: AuditEntry | null): string;
43
47
  //# sourceMappingURL=index.d.ts.map
@@ -407,6 +407,12 @@ export function reset(issueNumber, repo, deps, options = {}) {
407
407
  });
408
408
  return deps.candidatesLog.append(entry, { path: logPath });
409
409
  }
410
+ /** Success line for ``triage:accept``, plus the optional design-critique affordance (#3708). */
411
+ export function formatAcceptOutput(issueNumber, repo, decisionId, options = {}) {
412
+ const promoteNote = options.autoPromote === true ? " + auto-promote" : "";
413
+ return (`accept #${issueNumber} (${repo}) -> ${decisionId}${promoteNote}\n` +
414
+ "optional: deft-directive-design-critique (declining writes nothing)");
415
+ }
410
416
  /** Format a decision entry for CLI ``status`` / ``history`` output. */
411
417
  export function formatDecision(entry) {
412
418
  if (entry === null) {