@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
@@ -1,4 +1,4 @@
1
- import { DEFAULT_STALE_MINUTES, REVIEW_OWNER_MARKER_END, REVIEW_OWNER_MARKER_START, } from "./constants.js";
1
+ import { DEFAULT_STALE_MINUTES, PASS_MARKER_KIND, REVIEW_OWNER_MARKER_END, REVIEW_OWNER_MARKER_START, } from "./constants.js";
2
2
  export function parseIso8601Utc(value) {
3
3
  if (typeof value !== "string" || value.length === 0) {
4
4
  return null;
@@ -17,7 +17,7 @@ export function parseIso8601Utc(value) {
17
17
  }
18
18
  return parsed;
19
19
  }
20
- const FIELD_RE = /^(owner|monitor_agent_id|head_sha|started_at|expires_at|platform_primitive|ended_at):\s*(.*)$/;
20
+ const FIELD_RE = /^(kind|owner|monitor_agent_id|agent_id|pass_kind|ceiling|head_sha|started_at|expires_at|platform_primitive|ended_at):\s*(.*)$/;
21
21
  /** True when the comment body contains the review-owner marker block. */
22
22
  export function hasReviewOwnerMarker(body) {
23
23
  return body.includes(REVIEW_OWNER_MARKER_START);
@@ -42,8 +42,17 @@ function parseFieldBlock(body) {
42
42
  }
43
43
  return fields;
44
44
  }
45
+ /** Marker `kind:` value, or "" when the block declares none (ownership-lease shape). */
46
+ function markerKind(body) {
47
+ return parseFieldBlock(body).kind ?? "";
48
+ }
45
49
  export function parseReviewOwnerLease(body) {
46
50
  const fields = parseFieldBlock(body);
51
+ // An advisory pass mark shares the marker but must never satisfy the
52
+ // review-monitor ownership gate (#3607).
53
+ if ((fields.kind ?? "") === PASS_MARKER_KIND) {
54
+ return null;
55
+ }
47
56
  const owner = fields.owner ?? "";
48
57
  const monitorAgentId = fields.monitor_agent_id ?? "";
49
58
  const startedAt = fields.started_at ?? "";
@@ -128,6 +137,11 @@ export function mapCommentEntry(entry) {
128
137
  if (!hasReviewOwnerMarker(rec.body)) {
129
138
  return null;
130
139
  }
140
+ // Pass marks are a disjoint population on the same marker; excluding them here
141
+ // keeps `registerReviewMonitor` from overwriting one as its lease anchor (#3607).
142
+ if (markerKind(rec.body) === PASS_MARKER_KIND) {
143
+ return null;
144
+ }
131
145
  return {
132
146
  id: rec.id,
133
147
  body: rec.body,
@@ -149,4 +163,91 @@ export function findActiveLeaseComment(comments, options = {}) {
149
163
  }
150
164
  return selectWinningReviewOwnerComment(active);
151
165
  }
166
+ export function parsePassOpenMarker(body) {
167
+ if (!hasReviewOwnerMarker(body)) {
168
+ return null;
169
+ }
170
+ const fields = parseFieldBlock(body);
171
+ if ((fields.kind ?? "") !== PASS_MARKER_KIND) {
172
+ return null;
173
+ }
174
+ const owner = fields.owner ?? "";
175
+ const passKind = fields.pass_kind ?? "";
176
+ const startedAt = fields.started_at ?? "";
177
+ const expiresAt = fields.expires_at ?? "";
178
+ if (owner.length === 0 ||
179
+ passKind.length === 0 ||
180
+ startedAt.length === 0 ||
181
+ expiresAt.length === 0) {
182
+ return null;
183
+ }
184
+ const agentId = fields.agent_id ?? "";
185
+ const ceiling = fields.ceiling ?? "";
186
+ const endedAt = fields.ended_at ?? "";
187
+ return {
188
+ kind: PASS_MARKER_KIND,
189
+ pass_kind: passKind,
190
+ owner,
191
+ agent_id: agentId.length > 0 ? agentId : null,
192
+ ceiling: ceiling.length > 0 ? ceiling : null,
193
+ started_at: startedAt,
194
+ expires_at: expiresAt,
195
+ ended_at: endedAt.length > 0 ? endedAt : null,
196
+ };
197
+ }
198
+ export function renderPassOpenComment(marker) {
199
+ const optional = (name, value) => value !== null && value.length > 0 ? `${name}: ${value}\n` : "";
200
+ return (`${REVIEW_OWNER_MARKER_START}\n` +
201
+ `kind: ${PASS_MARKER_KIND}\n` +
202
+ `pass_kind: ${marker.pass_kind}\n` +
203
+ `owner: ${marker.owner}\n` +
204
+ optional("agent_id", marker.agent_id) +
205
+ optional("ceiling", marker.ceiling) +
206
+ `started_at: ${marker.started_at}\n` +
207
+ `expires_at: ${marker.expires_at}\n` +
208
+ optional("ended_at", marker.ended_at) +
209
+ `${REVIEW_OWNER_MARKER_END}`);
210
+ }
211
+ /** Expired or synthesis-cleared marks are ignored on read; a mark self-clears (#3607). */
212
+ export function isPassMarkerActive(marker, now = new Date()) {
213
+ if (marker.ended_at !== null) {
214
+ return false;
215
+ }
216
+ const expires = parseIso8601Utc(marker.expires_at);
217
+ if (expires === null) {
218
+ return false;
219
+ }
220
+ return now.getTime() <= expires.getTime();
221
+ }
222
+ export function mapPassCommentEntry(entry) {
223
+ if (typeof entry !== "object" || entry === null || Array.isArray(entry)) {
224
+ return null;
225
+ }
226
+ const rec = entry;
227
+ if (typeof rec.id !== "number" || typeof rec.body !== "string") {
228
+ return null;
229
+ }
230
+ const marker = parsePassOpenMarker(rec.body);
231
+ if (marker === null) {
232
+ return null;
233
+ }
234
+ return {
235
+ id: rec.id,
236
+ body: rec.body,
237
+ createdAt: typeof rec.created_at === "string" ? rec.created_at : "",
238
+ authorLogin: typeof rec.author_login === "string" ? rec.author_login : "",
239
+ marker,
240
+ };
241
+ }
242
+ /** Oldest comment id wins when two marks race, matching the lease (#2814 / #3607). */
243
+ export function selectWinningPassComment(comments) {
244
+ if (comments.length === 0) {
245
+ return null;
246
+ }
247
+ return [...comments].sort((a, b) => a.id - b.id)[0] ?? null;
248
+ }
249
+ export function findActivePassComment(comments, options = {}) {
250
+ const active = comments.filter((comment) => comment.marker !== null && isPassMarkerActive(comment.marker, options.now));
251
+ return selectWinningPassComment(active);
252
+ }
152
253
  //# sourceMappingURL=lease-comment.js.map
package/dist/scm/call.js CHANGED
@@ -1,4 +1,5 @@
1
1
  import { spawnSync } from "node:child_process";
2
+ import { resolveCaptureFailureStderr, SUBPROCESS_MAX_BUFFER } from "../subprocess/max-buffer.js";
2
3
  import { resolveBinary } from "./binary.js";
3
4
  import { SUPPORTED_CALL_SOURCES } from "./constants.js";
4
5
  import { pyRepr } from "./py-format.js";
@@ -26,18 +27,26 @@ export function call(source, verb, args = null, options = {}) {
26
27
  input: options.input,
27
28
  encoding: (options.text ?? true) ? "utf8" : undefined,
28
29
  timeout: timeoutMs,
30
+ maxBuffer: SUBPROCESS_MAX_BUFFER,
29
31
  stdio: captureOutput ? ["pipe", "pipe", "pipe"] : "inherit",
30
32
  });
33
+ // A spawn-level failure (ENOBUFS past maxBuffer, timeout kill) yields a null
34
+ // status and empty stderr; surface error.message so callers never report a
35
+ // bare exit 1 with no reason (#1867 / #3903).
36
+ const stderr = resolveCaptureFailureStderr({
37
+ captured: typeof result.stderr === "string" ? result.stderr : "",
38
+ status: result.status,
39
+ message: result.error?.message,
40
+ });
31
41
  if (options.check && result.status !== 0) {
32
- const message = typeof result.stderr === "string" ? result.stderr : "";
33
- const error = new Error(message || `Process exited with code ${result.status}`);
42
+ const error = new Error(stderr || `Process exited with code ${result.status}`);
34
43
  throw error;
35
44
  }
36
45
  return {
37
46
  args: argv,
38
47
  returncode: result.status ?? 1,
39
48
  stdout: typeof result.stdout === "string" ? result.stdout : "",
40
- stderr: typeof result.stderr === "string" ? result.stderr : "",
49
+ stderr,
41
50
  };
42
51
  }
43
52
  //# sourceMappingURL=call.js.map
@@ -7,6 +7,7 @@
7
7
  import { type DesignCritiqueCatalogChip } from "../design-critique/exclusive-chip.js";
8
8
  import type { LabelClient } from "../vbrief-reconcile/types.js";
9
9
  export declare const DESIGN_CRITIQUE_CHIP_VERB: "design-critique-chip";
10
+ export declare const CHIP_APPLY_MISS_TOKEN = "chip apply missed (non-blocking convenience)";
10
11
  export declare const DESIGN_CRITIQUE_CHIP_USAGE: string;
11
12
  export interface DesignCritiqueChipArgs {
12
13
  readonly issue: number;
@@ -7,14 +7,16 @@
7
7
  import { spawnSync } from "node:child_process";
8
8
  import { applyDesignCritiqueCatalogChip, } from "../design-critique/exclusive-chip.js";
9
9
  import { parseGithubOwnerRepo } from "../policy/sync-default.js";
10
- import { ScmLabelClient, ScmLabelError } from "../vbrief-reconcile/labels.js";
10
+ import { ScmLabelClient } from "../vbrief-reconcile/labels.js";
11
11
  import { extractFlag, extractValueFlag } from "./argv.js";
12
12
  import { InvalidRepoError, splitRepo } from "./gh-rest.js";
13
13
  import { pyRepr } from "./py-format.js";
14
14
  export const DESIGN_CRITIQUE_CHIP_VERB = "design-critique-chip";
15
+ export const CHIP_APPLY_MISS_TOKEN = "chip apply missed (non-blocking convenience)";
15
16
  export const DESIGN_CRITIQUE_CHIP_USAGE = "usage: scm issue design-critique-chip --issue N --chip triage-ready|mechanism-shaped [--repo OWNER/NAME] [--json]\n" +
16
17
  " Parent attach of design-critique:triage-ready / recut mechanism-shaped.\n" +
17
- " Closed catalog remaining-set replace. One write. Other facets stay.\n";
18
+ " Closed catalog remaining-set replace. One write. Other facets stay.\n" +
19
+ " Apply miss is non-blocking convenience; ingest is not blocked.\n";
18
20
  const CHIP_ALIASES = {
19
21
  "triage-ready": "design-critique:triage-ready",
20
22
  "mechanism-shaped": "design-critique:mechanism-shaped",
@@ -161,11 +163,25 @@ export function runDesignCritiqueChip(extra, seams = {}) {
161
163
  };
162
164
  }
163
165
  catch (err) {
164
- if (err instanceof ScmLabelError) {
165
- return { exitCode: 1, stdout: "", stderr: `error: ${err.message}\n` };
166
- }
167
166
  const message = err instanceof Error ? err.message : String(err);
168
- return { exitCode: 1, stdout: "", stderr: `error: ${message}\n` };
167
+ const payload = {
168
+ repo,
169
+ issue: args.issue,
170
+ chip: args.chip,
171
+ applied: false,
172
+ miss: true,
173
+ blocking: false,
174
+ error: message,
175
+ };
176
+ if (args.json) {
177
+ return { exitCode: 0, stdout: `${JSON.stringify(payload)}\n`, stderr: "" };
178
+ }
179
+ return {
180
+ exitCode: 0,
181
+ stdout: "",
182
+ stderr: `${CHIP_APPLY_MISS_TOKEN}: ${message}\n` +
183
+ "ingest is not blocked; remaining-set hygiene is optional for a write-capable identity\n",
184
+ };
169
185
  }
170
186
  }
171
187
  //# sourceMappingURL=design-critique-chip.js.map
@@ -53,7 +53,7 @@ export interface RestIssueListOptions {
53
53
  export declare function restIssueList(repo: string, options?: RestIssueListOptions, seams?: GhRestSeams): Record<string, unknown>[];
54
54
  export declare const REST_MAX_PER_PAGE = 100;
55
55
  export declare const REST_PAGINATION_MAX_PAGES = 100;
56
- export declare const PUBLIC_HELPERS: readonly ["restCreateIssue", "restPostComment", "restUpdateComment", "restDeleteComment", "restGetUser", "restUpdateIssue", "restCreateLabel", "restCloseIssue", "restOpenPr", "restMergePr", "restIssueView", "restPrView", "restIssueList", "restIssueListPaginated"];
56
+ export declare const PUBLIC_HELPERS: readonly ["restCreateIssue", "restPostComment", "restUpdateComment", "restDeleteComment", "restGetUser", "restUpdateIssue", "restCreateLabel", "restCloseIssue", "restOpenPr", "restMergePr", "restIssueView", "restPrView", "restIssueList", "restIssueListPaginated", "restIssueListOpenInventory"];
57
57
  export declare function restCreateIssue(repo: string, title: string, body: string, labels?: readonly string[], seams?: GhRestSeams): Record<string, unknown>;
58
58
  export declare function restPostComment(repo: string, n: number, body: string, seams?: GhRestSeams): Record<string, unknown>;
59
59
  /** `PATCH /repos/{owner}/{repo}/issues/comments/{id}` -- edit a PR/issue comment in place. */
@@ -78,5 +78,13 @@ export interface RestIssueListPaginatedOptions extends RestIssueListOptions {
78
78
  readonly limit?: number | null;
79
79
  readonly excludePulls?: boolean;
80
80
  }
81
+ /** Flatten `gh api --paginate --slurp` issue-list output (pages or single page). */
82
+ export declare function flattenOpenInventoryPayload(parsed: unknown[], endpoint: string): Record<string, unknown>[];
83
+ /**
84
+ * Complete open-issue inventory in one `gh api --paginate --slurp` subprocess (#3752).
85
+ * Fail-closed on command failure, non-array JSON, buffer exhaustion, or cap hit.
86
+ * Pull-request rows are excluded.
87
+ */
88
+ export declare function restIssueListOpenInventory(repo: string, seams?: GhRestSeams): Record<string, unknown>[];
81
89
  export declare function restIssueListPaginated(repo: string, options?: RestIssueListPaginatedOptions, seams?: GhRestSeams): Record<string, unknown>[];
82
90
  //# sourceMappingURL=gh-rest.d.ts.map
@@ -183,6 +183,7 @@ export const PUBLIC_HELPERS = [
183
183
  "restPrView",
184
184
  "restIssueList",
185
185
  "restIssueListPaginated",
186
+ "restIssueListOpenInventory",
186
187
  ];
187
188
  function writeJsonPayload(payload) {
188
189
  const dir = mkdtempSync(join(tmpdir(), "gh_rest_payload_"));
@@ -331,6 +332,108 @@ export function restPrView(repo, n, seams = {}) {
331
332
  whichFn: seams.whichFn,
332
333
  });
333
334
  }
335
+ const OPEN_INVENTORY_MAX_ISSUES = REST_PAGINATION_MAX_PAGES * REST_MAX_PER_PAGE;
336
+ function issueRowOrThrow(item, endpoint) {
337
+ if (typeof item !== "object" || item === null || Array.isArray(item)) {
338
+ throw new GhRestError({
339
+ stderr: "open-issue inventory row is not an object",
340
+ exitCode: 0,
341
+ endpoint,
342
+ payload: null,
343
+ hint: "REST issue list rows must be objects",
344
+ });
345
+ }
346
+ return item;
347
+ }
348
+ /** Flatten `gh api --paginate --slurp` issue-list output (pages or single page). */
349
+ export function flattenOpenInventoryPayload(parsed, endpoint) {
350
+ if (parsed.length === 0) {
351
+ return [];
352
+ }
353
+ const out = [];
354
+ const allPages = parsed.every((item) => Array.isArray(item));
355
+ if (allPages) {
356
+ for (const page of parsed) {
357
+ if (!Array.isArray(page)) {
358
+ continue;
359
+ }
360
+ for (const item of page) {
361
+ const row = issueRowOrThrow(item, endpoint);
362
+ if ("pull_request" in row) {
363
+ continue;
364
+ }
365
+ out.push(row);
366
+ }
367
+ }
368
+ return out;
369
+ }
370
+ for (const item of parsed) {
371
+ const row = issueRowOrThrow(item, endpoint);
372
+ if ("pull_request" in row) {
373
+ continue;
374
+ }
375
+ out.push(row);
376
+ }
377
+ return out;
378
+ }
379
+ /**
380
+ * Complete open-issue inventory in one `gh api --paginate --slurp` subprocess (#3752).
381
+ * Fail-closed on command failure, non-array JSON, buffer exhaustion, or cap hit.
382
+ * Pull-request rows are excluded.
383
+ */
384
+ export function restIssueListOpenInventory(repo, seams = {}) {
385
+ const [owner, name] = splitRepo(repo);
386
+ const endpoint = `repos/${owner}/${name}/issues?state=open&per_page=${REST_MAX_PER_PAGE}`;
387
+ const runner = seams.runGhApiFn ?? runGhApi;
388
+ const result = runner(["--paginate", "--slurp", endpoint], { timeout: 120 });
389
+ if (result.returncode !== 0) {
390
+ throw new GhRestError({
391
+ stderr: result.stderr.trim(),
392
+ exitCode: result.returncode,
393
+ endpoint,
394
+ payload: null,
395
+ hint: "verify gh auth and core REST quota; open-issue inventory must be complete",
396
+ });
397
+ }
398
+ const stdout = result.stdout.trim();
399
+ if (stdout.length === 0) {
400
+ return [];
401
+ }
402
+ let parsed;
403
+ try {
404
+ parsed = JSON.parse(stdout);
405
+ }
406
+ catch (exc) {
407
+ const message = exc instanceof Error ? exc.message : String(exc);
408
+ throw new GhRestError({
409
+ stderr: `non-JSON open-issue inventory: ${message}`,
410
+ exitCode: 0,
411
+ endpoint,
412
+ payload: null,
413
+ hint: "gh api --paginate --slurp must return a JSON array",
414
+ });
415
+ }
416
+ if (!Array.isArray(parsed)) {
417
+ throw new GhRestError({
418
+ stderr: `unexpected top-level type ${typeof parsed}`,
419
+ exitCode: 0,
420
+ endpoint,
421
+ payload: null,
422
+ hint: "open-issue inventory must be a JSON array",
423
+ });
424
+ }
425
+ const out = flattenOpenInventoryPayload(parsed, endpoint);
426
+ if (out.length >= OPEN_INVENTORY_MAX_ISSUES) {
427
+ throw new GhRestError({
428
+ stderr: `open-issue inventory reached cap ${OPEN_INVENTORY_MAX_ISSUES}; pagination may be incomplete`,
429
+ exitCode: 0,
430
+ endpoint,
431
+ payload: null,
432
+ hint: "repo may exceed supported open-issue count; fail closed rather than truncate",
433
+ });
434
+ }
435
+ return out;
436
+ }
334
437
  export function restIssueListPaginated(repo, options = {}, seams = {}) {
335
438
  const cappedPerPage = Math.min(Math.max(1, options.perPage ?? REST_MAX_PER_PAGE), REST_MAX_PER_PAGE);
336
439
  const [owner, name] = splitRepo(repo);
@@ -41,6 +41,8 @@ export interface ScmReadinessReport {
41
41
  readonly githubAuthMode: string;
42
42
  /** Runtime mode from the #1557a probe. */
43
43
  readonly runtimeMode: string;
44
+ /** Stable id naming why the runtime was classified that way (#3859). */
45
+ readonly runtimeModeReason: string | null;
44
46
  /** Whether an injected token env var is present (value never reported). */
45
47
  readonly injectedTokenPresent: boolean;
46
48
  /** Probe depth used for this report. */
@@ -21,6 +21,7 @@
21
21
  import { spawnSync } from "node:child_process";
22
22
  import { findInjectedToken, GITHUB_AUTH_MODE_HOST_GH, GITHUB_AUTH_MODE_INJECTED_TOKEN, inferGithubAuthMode, validateGithubAuthForWorker, } from "../intake/github-auth-modes.js";
23
23
  import { getPlatformCapabilities, probeRuntimeCapabilities, } from "../intake/platform-capabilities.js";
24
+ import { EXPLICIT_SELECTION_REMEDIATION, GITHUB_AUTH_MODE_ENV, RUNTIME_REASON_CURSOR_MARKER_AMBIGUOUS, } from "../platform/cursor-managed-runtime.js";
24
25
  import { defaultWhich } from "./binary.js";
25
26
  import { BINARY_PREFERENCE } from "./constants.js";
26
27
  import { ScmStubError } from "./errors.js";
@@ -138,6 +139,7 @@ export function probeScmReadiness(options = {}) {
138
139
  authState: "binary-absent",
139
140
  githubAuthMode,
140
141
  runtimeMode: runtimeReport.runtimeMode,
142
+ runtimeModeReason: runtimeReport.runtimeModeReason ?? null,
141
143
  injectedTokenPresent,
142
144
  depth,
143
145
  detail,
@@ -149,9 +151,11 @@ export function probeScmReadiness(options = {}) {
149
151
  }
150
152
  // Injected-token mode without a token is a hard not-ready even before auth status.
151
153
  if (githubAuthMode === GITHUB_AUTH_MODE_INJECTED_TOKEN && !injectedTokenPresent) {
154
+ const ambiguousRuntime = runtimeReport.runtimeModeReason === RUNTIME_REASON_CURSOR_MARKER_AMBIGUOUS;
152
155
  const detail = "injected-token mode requires GH_TOKEN, GITHUB_TOKEN, or GH_ENTERPRISE_TOKEN; " +
153
156
  `binary=${binary} present but SCM-dependent gates skipped ` +
154
- `(runtime_mode=${runtimeReport.runtimeMode})`;
157
+ `(runtime_mode=${runtimeReport.runtimeMode}` +
158
+ `${runtimeReport.runtimeModeReason ? `, reason=${runtimeReport.runtimeModeReason}` : ""})`;
155
159
  return {
156
160
  ready: false,
157
161
  binary,
@@ -159,10 +163,15 @@ export function probeScmReadiness(options = {}) {
159
163
  authState: "missing-token",
160
164
  githubAuthMode,
161
165
  runtimeMode: runtimeReport.runtimeMode,
166
+ runtimeModeReason: runtimeReport.runtimeModeReason ?? null,
162
167
  injectedTokenPresent: false,
163
168
  depth,
164
169
  detail,
165
- remediation: REMEDIATION_MISSING_TOKEN,
170
+ // An ambiguous Cursor runtime is the #3859 case: name the explicit opt-in
171
+ // here, because this is where the operator actually meets the block.
172
+ remediation: ambiguousRuntime
173
+ ? `${EXPLICIT_SELECTION_REMEDIATION}\n\n${REMEDIATION_MISSING_TOKEN}`
174
+ : REMEDIATION_MISSING_TOKEN,
166
175
  skippedGates: [...SCM_DEPENDENT_GATES],
167
176
  login: null,
168
177
  failureKind: "missing_injected_token",
@@ -209,6 +218,7 @@ export function probeScmReadiness(options = {}) {
209
218
  authState: "authenticated",
210
219
  githubAuthMode: deep.githubAuthMode,
211
220
  runtimeMode: runtimeReport.runtimeMode,
221
+ runtimeModeReason: runtimeReport.runtimeModeReason ?? null,
212
222
  injectedTokenPresent,
213
223
  depth,
214
224
  detail: `SCM ready: ${binary} present, ${deep.githubAuthMode} authenticated${loginPart} (deep)`,
@@ -226,6 +236,7 @@ export function probeScmReadiness(options = {}) {
226
236
  authState,
227
237
  githubAuthMode: deep.githubAuthMode,
228
238
  runtimeMode: runtimeReport.runtimeMode,
239
+ runtimeModeReason: runtimeReport.runtimeModeReason ?? null,
229
240
  injectedTokenPresent,
230
241
  depth,
231
242
  detail: `SCM not ready: ${deep.detail}`,
@@ -247,6 +258,7 @@ export function probeScmReadiness(options = {}) {
247
258
  authState: "unknown",
248
259
  githubAuthMode,
249
260
  runtimeMode: runtimeReport.runtimeMode,
261
+ runtimeModeReason: runtimeReport.runtimeModeReason ?? null,
250
262
  injectedTokenPresent,
251
263
  depth,
252
264
  detail,
@@ -268,6 +280,7 @@ export function probeScmReadiness(options = {}) {
268
280
  authState: "unauthenticated",
269
281
  githubAuthMode,
270
282
  runtimeMode: runtimeReport.runtimeMode,
283
+ runtimeModeReason: runtimeReport.runtimeModeReason ?? null,
271
284
  injectedTokenPresent,
272
285
  depth,
273
286
  detail,
@@ -284,6 +297,7 @@ export function probeScmReadiness(options = {}) {
284
297
  authState: "authenticated",
285
298
  githubAuthMode,
286
299
  runtimeMode: runtimeReport.runtimeMode,
300
+ runtimeModeReason: runtimeReport.runtimeModeReason ?? null,
287
301
  injectedTokenPresent,
288
302
  depth,
289
303
  detail: `SCM ready: ${binary} present, ${githubAuthMode} authenticated (shallow)`,
@@ -302,6 +316,7 @@ export function scmReadinessToDict(report) {
302
316
  auth_state: report.authState,
303
317
  github_auth_mode: report.githubAuthMode,
304
318
  runtime_mode: report.runtimeMode,
319
+ runtime_mode_reason: report.runtimeModeReason,
305
320
  injected_token_present: report.injectedTokenPresent,
306
321
  depth: report.depth,
307
322
  detail: report.detail,
@@ -324,7 +339,14 @@ export function formatScmReadinessLines(report) {
324
339
  }
325
340
  lines.push(`[deft scm] ${report.detail}`);
326
341
  if (report.skippedGates.length > 0) {
327
- lines.push(`[deft scm] skipped gates: ${report.skippedGates.join(", ")}`);
342
+ // Name the classification reason next to the skip: a silent skip is what
343
+ // makes this failure costly, because work-selection surfaces go dark (#3859).
344
+ const reason = report.runtimeModeReason ? ` (reason: ${report.runtimeModeReason})` : "";
345
+ lines.push(`[deft scm] skipped gates: ${report.skippedGates.join(", ")}${reason}`);
346
+ if (report.runtimeModeReason === RUNTIME_REASON_CURSOR_MARKER_AMBIGUOUS) {
347
+ lines.push(`[deft scm] runtime is ambiguous; set ${GITHUB_AUTH_MODE_ENV}=host-gh to authorize ` +
348
+ "the host gh credential store on a machine you control (#3859)");
349
+ }
328
350
  }
329
351
  lines.push("[deft scm] run SCM-dependent gates only after auth is ready, or from a matched env; " +
330
352
  "see content/scm/github.md § Mismatched/headless SCM readiness (#2275)");
@@ -387,13 +409,15 @@ export function requireScmReady(options = {}) {
387
409
  if (hermeticAuthSkip) {
388
410
  // Production path uses assertScmBinaryPresent (not test-only).
389
411
  const binary = assertScmBinaryPresent(options.whichFn);
412
+ const hermeticRuntime = options.runtimeReport ?? getPlatformCapabilities();
390
413
  const report = {
391
414
  ready: true,
392
415
  binary,
393
416
  binaryPath: options.whichFn?.(binary) ?? null,
394
417
  authState: "unknown",
395
418
  githubAuthMode: GITHUB_AUTH_MODE_HOST_GH,
396
- runtimeMode: (options.runtimeReport ?? getPlatformCapabilities()).runtimeMode,
419
+ runtimeMode: hermeticRuntime.runtimeMode,
420
+ runtimeModeReason: hermeticRuntime.runtimeModeReason ?? null,
397
421
  injectedTokenPresent: findInjectedToken(options.env ?? process.env) !== null,
398
422
  depth: "shallow",
399
423
  detail: `SCM binary present (${binary}); auth status not checked (hermetic)`,
@@ -1,16 +1,23 @@
1
1
  import { dirname, resolve } from "node:path";
2
- import { resolveProjectDefinitionPath } from "../layout/resolve.js";
3
- import { atomicWriteProjectDefinition, loadProjectDefinitionForMutation, projectDefinitionMutationLock, } from "../vbrief-build/project-definition-io.js";
2
+ import { withProjectDefinitionMutation } from "../vbrief-build/project-definition-mutation.js";
4
3
  import { syncRegistryArtifactAfterScopeMove } from "./registry-artifact-sync.js";
5
4
  /** Fail-closed sync of PROJECT-DEFINITION after a lifecycle move (#1527 / #2131). */
6
5
  export function syncProjectDefinitionAfterScopeMove(scopeData, oldPath, newPath, vbriefRoot, targetStatus) {
7
6
  const projectRoot = dirname(resolve(vbriefRoot));
8
7
  try {
9
- projectDefinitionMutationLock(projectRoot, () => {
10
- const projectDefPath = resolveProjectDefinitionPath(projectRoot);
11
- syncRegistryArtifactAfterScopeMove(projectDefPath, scopeData, oldPath, newPath, vbriefRoot, targetStatus, {
12
- loadForMutation: () => loadProjectDefinitionForMutation(projectRoot),
13
- persist: (path, data) => atomicWriteProjectDefinition(path, data),
8
+ withProjectDefinitionMutation(projectRoot, (mutation) => {
9
+ syncRegistryArtifactAfterScopeMove(
10
+ // The artifact the lock captured, not an independent re-resolution (#3796).
11
+ mutation.artifactPath, scopeData, oldPath, newPath, vbriefRoot, targetStatus, {
12
+ loadForMutation: () => [mutation.load(), mutation.artifactPath],
13
+ persist: (path, data) => {
14
+ // The registry sync echoes back the path it was handed, so a
15
+ // mismatch means something re-resolved the artifact mid-section.
16
+ if (resolve(path) !== resolve(mutation.artifactPath)) {
17
+ throw new Error("refusing to persist PROJECT-DEFINITION to a path other than the locked artifact");
18
+ }
19
+ mutation.persist(data);
20
+ },
14
21
  });
15
22
  });
16
23
  return null;
@@ -5,11 +5,35 @@ export interface GitRunResult {
5
5
  }
6
6
  export type GitRunner = (projectRoot: string, args: readonly string[]) => GitRunResult;
7
7
  export declare const defaultGitRunner: GitRunner;
8
+ /**
9
+ * Serve a dispatch's repeated ref reads from one `git` child (#3736).
10
+ *
11
+ * The host mutation gate resolved HEAD, the worktree root, and the branch with
12
+ * a separate spawn each, twice over; on a loaded Windows box a spawn measured a
13
+ * 2.2s p50, so every concurrent agent's hook slowed every other agent's hook.
14
+ * The probe is lazy on purpose: it only fires for a read it can actually
15
+ * answer, so a caller that never asks for context never pays for it.
16
+ */
17
+ export declare function memoizeGitRunner(runGit?: GitRunner): GitRunner;
8
18
  export declare function gitHead(projectRoot: string, runGit?: GitRunner): {
9
19
  head: string | null;
10
20
  error: string | null;
11
21
  };
12
22
  export declare function worktreePath(projectRoot: string, runGit?: GitRunner): string;
23
+ /**
24
+ * Walk from `targetPath` to the nearest existing directory. A write that
25
+ * creates a missing nested file is the common case; git cannot use a
26
+ * nonexistent cwd.
27
+ */
28
+ export declare function existingAncestorDir(targetPath: string): string | null;
29
+ /**
30
+ * Failure-expressing sibling of {@link worktreePath}. Returns null instead of
31
+ * falling back to `startDir` so a missing git tree is distinguishable from
32
+ * success (#3794).
33
+ */
34
+ export declare function worktreePathOrNull(startDir: string, runGit?: GitRunner): string | null;
35
+ /** Absolute `--git-common-dir` for `projectRoot`, or null on failure. */
36
+ export declare function gitCommonDir(projectRoot: string, runGit?: GitRunner): string | null;
13
37
  /** True when `ancestor` is reachable from `descendant` (same commit counts). */
14
38
  export declare function gitIsAncestor(projectRoot: string, ancestor: string, descendant: string, runGit?: GitRunner): boolean | null;
15
39
  /**