@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
@@ -5,6 +5,7 @@
5
5
  * Exit 0 when ready; exit 1 when not ready (loud diagnostic); exit 2 on bad flags.
6
6
  */
7
7
  import { fileURLToPath } from "node:url";
8
+ import { INSTALLATION_IDENTITY_ISSUE_URL, PRINCIPAL_KIND_USER, } from "../intake/github-auth-modes.js";
8
9
  import { clearScmReadyCache, formatScmReadinessLines, probeScmReadiness, scmReadinessToDict, } from "./readiness.js";
9
10
  export function parseScmReadinessArgs(argv) {
10
11
  const out = {};
@@ -30,6 +31,26 @@ export function parseScmReadinessArgs(argv) {
30
31
  }
31
32
  out.depth = value;
32
33
  }
34
+ else if (arg === "--repo") {
35
+ const value = argv[++i];
36
+ if (value === undefined || value.startsWith("-")) {
37
+ return { args: out, error: "--repo expects owner/repo" };
38
+ }
39
+ out.repo = value;
40
+ }
41
+ else if (arg === "--expected-login") {
42
+ const value = argv[++i];
43
+ if (value === undefined || value.startsWith("-")) {
44
+ return { args: out, error: "--expected-login expects a GitHub login" };
45
+ }
46
+ out.expectedLogin = value;
47
+ }
48
+ else if (arg === "--expected-app-slug" || arg === "--expected-installation-id") {
49
+ return {
50
+ args: out,
51
+ error: `${arg} is not accepted; GitHub App installation identity is deferred to ${INSTALLATION_IDENTITY_ISSUE_URL}`,
52
+ };
53
+ }
33
54
  else if (arg === "--help" || arg === "-h") {
34
55
  out.help = true;
35
56
  }
@@ -43,8 +64,19 @@ export function parseScmReadinessArgs(argv) {
43
64
  return { args: out, error: null };
44
65
  }
45
66
  const USAGE = "usage: deft scm:status [--json] [--deep|--shallow|--depth shallow|deep]\n" +
67
+ " [--repo OWNER/REPO] [--expected-login LOGIN]\n" +
46
68
  " Report SCM (gh/ghx) availability + auth state in this execution env (#2275).\n" +
69
+ " Deep probes derive the target repo (flag / GH_REPO / GITHUB_REPOSITORY / origin)\n" +
70
+ " and compare an expected user login when one is supplied (#3665).\n" +
71
+ " GitHub App installation identity is deferred to #3693.\n" +
47
72
  " Exit 0 ready / 1 not ready / 2 config error.\n";
73
+ function expectedPrincipalFromArgs(args) {
74
+ const login = args.expectedLogin?.trim() ?? "";
75
+ if (login.length > 0) {
76
+ return { kind: PRINCIPAL_KIND_USER, login };
77
+ }
78
+ return undefined;
79
+ }
48
80
  export function scmReadinessMain(args, options = {}) {
49
81
  const writeOut = options.writeOut ?? ((s) => process.stdout.write(s));
50
82
  const writeErr = options.writeErr ?? ((s) => process.stderr.write(s));
@@ -52,12 +84,21 @@ export function scmReadinessMain(args, options = {}) {
52
84
  writeOut(USAGE);
53
85
  return 0;
54
86
  }
87
+ const expectedPrincipal = expectedPrincipalFromArgs(args);
88
+ if (expectedPrincipal !== undefined && "error" in expectedPrincipal) {
89
+ writeErr(`error: ${expectedPrincipal.error}\n${USAGE}`);
90
+ return 2;
91
+ }
55
92
  const depth = args.depth ?? (args.deep ? "deep" : "shallow");
56
93
  // Explicit probe always re-evaluates (credentials may have changed since
57
94
  // session:start). Clears the process-scoped requireScmReady cache too.
58
95
  clearScmReadyCache();
59
96
  const probe = options.probe ?? probeScmReadiness;
60
- const report = probe({ depth });
97
+ const report = probe({
98
+ depth,
99
+ repo: args.repo,
100
+ expectedPrincipal,
101
+ });
61
102
  if (args.json) {
62
103
  writeOut(`${JSON.stringify(scmReadinessToDict(report), null, 2)}\n`);
63
104
  }
@@ -18,7 +18,7 @@
18
18
  * env token presence + optional short `gh auth status`). Deep API validation
19
19
  * is opt-in via `deep: true` (session:start --with-network).
20
20
  */
21
- import { findInjectedToken, type GhRunner, GITHUB_AUTH_MODE_HOST_GH, GITHUB_AUTH_MODE_INJECTED_TOKEN } from "../intake/github-auth-modes.js";
21
+ import { type ExpectedGithubWorkerPrincipal, findInjectedToken, type GhRunner, GITHUB_AUTH_MODE_HOST_GH, GITHUB_AUTH_MODE_INJECTED_TOKEN } from "../intake/github-auth-modes.js";
22
22
  import { type RuntimeCapabilityReport } from "../intake/platform-capabilities.js";
23
23
  import { type WhichFn } from "./binary.js";
24
24
  import { BINARY_PREFERENCE } from "./constants.js";
@@ -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. */
@@ -64,6 +66,7 @@ export interface ProbeScmReadinessOptions {
64
66
  readonly runtimeReport?: RuntimeCapabilityReport;
65
67
  readonly githubAuthMode?: string | null;
66
68
  readonly repo?: string;
69
+ readonly expectedPrincipal?: ExpectedGithubWorkerPrincipal | null;
67
70
  readonly runGh?: GhRunner;
68
71
  /** When false, skip even `gh auth status` on the shallow path (tests / pure PATH). */
69
72
  readonly checkAuthStatus?: boolean;
@@ -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";
@@ -105,7 +106,12 @@ function mapDeepFailureToAuthState(result) {
105
106
  return "missing-token";
106
107
  if (result.failureKind === "gh_auth_failed")
107
108
  return "unauthenticated";
108
- if (result.failureKind === "api_unreachable" || result.failureKind === "repo_access_denied") {
109
+ if (result.failureKind === "api_unreachable" ||
110
+ result.failureKind === "repo_access_denied" ||
111
+ result.failureKind === "missing_expected_principal" ||
112
+ result.failureKind === "principal_mismatch" ||
113
+ result.failureKind === "missing_target_repo" ||
114
+ result.failureKind === "installation_identity_unverifiable") {
109
115
  return "unauthenticated";
110
116
  }
111
117
  return "unknown";
@@ -133,6 +139,7 @@ export function probeScmReadiness(options = {}) {
133
139
  authState: "binary-absent",
134
140
  githubAuthMode,
135
141
  runtimeMode: runtimeReport.runtimeMode,
142
+ runtimeModeReason: runtimeReport.runtimeModeReason ?? null,
136
143
  injectedTokenPresent,
137
144
  depth,
138
145
  detail,
@@ -144,9 +151,11 @@ export function probeScmReadiness(options = {}) {
144
151
  }
145
152
  // Injected-token mode without a token is a hard not-ready even before auth status.
146
153
  if (githubAuthMode === GITHUB_AUTH_MODE_INJECTED_TOKEN && !injectedTokenPresent) {
154
+ const ambiguousRuntime = runtimeReport.runtimeModeReason === RUNTIME_REASON_CURSOR_MARKER_AMBIGUOUS;
147
155
  const detail = "injected-token mode requires GH_TOKEN, GITHUB_TOKEN, or GH_ENTERPRISE_TOKEN; " +
148
156
  `binary=${binary} present but SCM-dependent gates skipped ` +
149
- `(runtime_mode=${runtimeReport.runtimeMode})`;
157
+ `(runtime_mode=${runtimeReport.runtimeMode}` +
158
+ `${runtimeReport.runtimeModeReason ? `, reason=${runtimeReport.runtimeModeReason}` : ""})`;
150
159
  return {
151
160
  ready: false,
152
161
  binary,
@@ -154,10 +163,15 @@ export function probeScmReadiness(options = {}) {
154
163
  authState: "missing-token",
155
164
  githubAuthMode,
156
165
  runtimeMode: runtimeReport.runtimeMode,
166
+ runtimeModeReason: runtimeReport.runtimeModeReason ?? null,
157
167
  injectedTokenPresent: false,
158
168
  depth,
159
169
  detail,
160
- 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,
161
175
  skippedGates: [...SCM_DEPENDENT_GATES],
162
176
  login: null,
163
177
  failureKind: "missing_injected_token",
@@ -192,6 +206,7 @@ export function probeScmReadiness(options = {}) {
192
206
  environ: env,
193
207
  runtimeReport,
194
208
  repo: options.repo,
209
+ expectedPrincipal: options.expectedPrincipal,
195
210
  runGh: deepRunner,
196
211
  });
197
212
  if (deep.ok) {
@@ -203,6 +218,7 @@ export function probeScmReadiness(options = {}) {
203
218
  authState: "authenticated",
204
219
  githubAuthMode: deep.githubAuthMode,
205
220
  runtimeMode: runtimeReport.runtimeMode,
221
+ runtimeModeReason: runtimeReport.runtimeModeReason ?? null,
206
222
  injectedTokenPresent,
207
223
  depth,
208
224
  detail: `SCM ready: ${binary} present, ${deep.githubAuthMode} authenticated${loginPart} (deep)`,
@@ -220,6 +236,7 @@ export function probeScmReadiness(options = {}) {
220
236
  authState,
221
237
  githubAuthMode: deep.githubAuthMode,
222
238
  runtimeMode: runtimeReport.runtimeMode,
239
+ runtimeModeReason: runtimeReport.runtimeModeReason ?? null,
223
240
  injectedTokenPresent,
224
241
  depth,
225
242
  detail: `SCM not ready: ${deep.detail}`,
@@ -241,6 +258,7 @@ export function probeScmReadiness(options = {}) {
241
258
  authState: "unknown",
242
259
  githubAuthMode,
243
260
  runtimeMode: runtimeReport.runtimeMode,
261
+ runtimeModeReason: runtimeReport.runtimeModeReason ?? null,
244
262
  injectedTokenPresent,
245
263
  depth,
246
264
  detail,
@@ -262,6 +280,7 @@ export function probeScmReadiness(options = {}) {
262
280
  authState: "unauthenticated",
263
281
  githubAuthMode,
264
282
  runtimeMode: runtimeReport.runtimeMode,
283
+ runtimeModeReason: runtimeReport.runtimeModeReason ?? null,
265
284
  injectedTokenPresent,
266
285
  depth,
267
286
  detail,
@@ -278,6 +297,7 @@ export function probeScmReadiness(options = {}) {
278
297
  authState: "authenticated",
279
298
  githubAuthMode,
280
299
  runtimeMode: runtimeReport.runtimeMode,
300
+ runtimeModeReason: runtimeReport.runtimeModeReason ?? null,
281
301
  injectedTokenPresent,
282
302
  depth,
283
303
  detail: `SCM ready: ${binary} present, ${githubAuthMode} authenticated (shallow)`,
@@ -296,6 +316,7 @@ export function scmReadinessToDict(report) {
296
316
  auth_state: report.authState,
297
317
  github_auth_mode: report.githubAuthMode,
298
318
  runtime_mode: report.runtimeMode,
319
+ runtime_mode_reason: report.runtimeModeReason,
299
320
  injected_token_present: report.injectedTokenPresent,
300
321
  depth: report.depth,
301
322
  detail: report.detail,
@@ -318,7 +339,14 @@ export function formatScmReadinessLines(report) {
318
339
  }
319
340
  lines.push(`[deft scm] ${report.detail}`);
320
341
  if (report.skippedGates.length > 0) {
321
- 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
+ }
322
350
  }
323
351
  lines.push("[deft scm] run SCM-dependent gates only after auth is ready, or from a matched env; " +
324
352
  "see content/scm/github.md § Mismatched/headless SCM readiness (#2275)");
@@ -381,13 +409,15 @@ export function requireScmReady(options = {}) {
381
409
  if (hermeticAuthSkip) {
382
410
  // Production path uses assertScmBinaryPresent (not test-only).
383
411
  const binary = assertScmBinaryPresent(options.whichFn);
412
+ const hermeticRuntime = options.runtimeReport ?? getPlatformCapabilities();
384
413
  const report = {
385
414
  ready: true,
386
415
  binary,
387
416
  binaryPath: options.whichFn?.(binary) ?? null,
388
417
  authState: "unknown",
389
418
  githubAuthMode: GITHUB_AUTH_MODE_HOST_GH,
390
- runtimeMode: (options.runtimeReport ?? getPlatformCapabilities()).runtimeMode,
419
+ runtimeMode: hermeticRuntime.runtimeMode,
420
+ runtimeModeReason: hermeticRuntime.runtimeModeReason ?? null,
391
421
  injectedTokenPresent: findInjectedToken(options.env ?? process.env) !== null,
392
422
  depth: "shallow",
393
423
  detail: `SCM binary present (${binary}); auth status not checked (hermetic)`,
@@ -17,6 +17,7 @@ import { hasArtifactSuffix, LEGACY_ARTIFACT_DIR, MIGRATED_ARTIFACT_DIR, resolveL
17
17
  import { resolveRepo } from "../triage/queue/repo.js";
18
18
  import { referenceWithDefaultTrust, slugify } from "../vbrief-build/build.js";
19
19
  import { EMITTED_VBRIEF_VERSION } from "../vbrief-build/constants.js";
20
+ import { READY_REQUIRES_PARALLEL_SAFE } from "../vbrief-validation/story-quality.js";
20
21
  import { formatCoverageReportLine, validateCoverageMap } from "./coverage-map.js";
21
22
  import { buildParentLineageArtifact } from "./parent-lineage.js";
22
23
  import { formatBriefJson } from "./vbrief-json.js";
@@ -407,7 +408,7 @@ export function storyQualityIssues(opts) {
407
408
  issues.push(...fileScopeIssues(swarm));
408
409
  issues.push(...verifyCommandIssues(swarm));
409
410
  if (swarm.parallel_safe === false) {
410
- issues.push("readiness=ready requires parallel_safe=true; use readiness=sequential or needs_refinement for non-concurrent work");
411
+ issues.push(READY_REQUIRES_PARALLEL_SAFE);
411
412
  }
412
413
  if (swarm.file_scope_confidence === "low") {
413
414
  issues.push("readiness=ready requires file_scope_confidence above low");
@@ -16,6 +16,29 @@ export declare const NON_DELIVERY_DISPOSITIONS: readonly ["cancelled", "supersed
16
16
  export type NonDeliveryDisposition = (typeof NON_DELIVERY_DISPOSITIONS)[number];
17
17
  /** Delivery verification outcome for completion provenance. */
18
18
  export type DeliveryDisposition = "delivered" | "merged_to_integration" | "not_delivered" | NonDeliveryDisposition | "unknown" | "unverified";
19
+ /**
20
+ * This is a historical audit snapshot stamped onto
21
+ * `plan.metadata.completionProvenance` at `scope:complete` (#3041 / #3690).
22
+ *
23
+ * Completed xBRIEFs have full standing as a record of *what is* and zero
24
+ * authority over *what to build next* (#3383). Fields stay because they
25
+ * answer a later reconstruction question after branches are deleted and
26
+ * refs move. A field does not need a current production-code reader.
27
+ *
28
+ * Reconstruction groups:
29
+ * - Identity and location: `repository`, `implementationCommit`, `prNumber`,
30
+ * `prBase`, `deliveryBranch`
31
+ * - Delivery evidence: `mergeCommit`, `deliveryCommit`, `disposition`,
32
+ * `handoffState`
33
+ * - Verification attribution: `verifiedAt`, `verifier`
34
+ * - Explicit operator-supplied facts: `deployed`, `uatVerified` (never
35
+ * inferred from Git)
36
+ *
37
+ * `completedSessionId` is duplicated. This nested copy is stamped so the
38
+ * snapshot is self-contained. Consumers (`verify:ac` /
39
+ * `session-completed-ac`) read the sibling
40
+ * `plan.metadata.completedSessionId`, not this field.
41
+ */
19
42
  export interface CompletionProvenance {
20
43
  readonly repository: string | null;
21
44
  readonly implementationCommit: string | null;
@@ -32,7 +55,10 @@ export interface CompletionProvenance {
32
55
  readonly deployed: boolean | null;
33
56
  /** Always null unless explicitly supplied — never inferred from Git (#3041). */
34
57
  readonly uatVerified: boolean | null;
35
- /** Session that completed the brief; used by check to target xbrief/completed (#3357). */
58
+ /**
59
+ * Nested session correlation for a self-contained snapshot.
60
+ * Consumers read sibling `plan.metadata.completedSessionId`, not this field.
61
+ */
36
62
  readonly completedSessionId?: string | null;
37
63
  }
38
64
  export interface DeliveryEvidenceInput {
@@ -8,6 +8,7 @@ export * from "./decomposed-refs.js";
8
8
  export * from "./delivery-evidence.js";
9
9
  export * from "./demote.js";
10
10
  export * from "./effort-activate-gate.js";
11
+ export * from "./lifecycle-write.js";
11
12
  export * from "./main.js";
12
13
  export * from "./open-umbrella-warning.js";
13
14
  export * from "./parent-lineage.js";
@@ -8,6 +8,7 @@ export * from "./decomposed-refs.js";
8
8
  export * from "./delivery-evidence.js";
9
9
  export * from "./demote.js";
10
10
  export * from "./effort-activate-gate.js";
11
+ export * from "./lifecycle-write.js";
11
12
  export * from "./main.js";
12
13
  export * from "./open-umbrella-warning.js";
13
14
  export * from "./parent-lineage.js";
@@ -0,0 +1,24 @@
1
+ /**
2
+ * Transition-write fingerprint for completed/ artifacts (#3679).
3
+ *
4
+ * Distinct from completionProvenance (#3686): this only records that
5
+ * runTransition wrote the blob. Non-code-bearing completes and scope:fail
6
+ * stamp this and still omit delivery provenance.
7
+ */
8
+ export declare const LIFECYCLE_WRITE_KEY: "lifecycleWrite";
9
+ export type LifecycleWriteAction = "complete" | "fail";
10
+ export interface LifecycleWriteStamp {
11
+ readonly action: LifecycleWriteAction;
12
+ readonly writtenAt: string;
13
+ }
14
+ /** Worker-facing leftover-land path after a scope-provenance strip (#3476 / #3679). */
15
+ export declare const LEFTOVER_LAND_PR_REMEDIATION: string;
16
+ /** Stamp that runTransition wrote this plan into completed/. */
17
+ export declare function stampLifecycleWrite(plan: Record<string, unknown>, action: LifecycleWriteAction, writtenAt: string): void;
18
+ /**
19
+ * True when a completed/ blob shows verb evidence.
20
+ * Accepts the new stamp, legacy completedAt (pre-#3679 complete), or failed status
21
+ * (scope:fail never stamped provenance and must keep passing).
22
+ */
23
+ export declare function hasTransitionWrite(plan: Record<string, unknown>): boolean;
24
+ //# sourceMappingURL=lifecycle-write.d.ts.map
@@ -0,0 +1,64 @@
1
+ /**
2
+ * Transition-write fingerprint for completed/ artifacts (#3679).
3
+ *
4
+ * Distinct from completionProvenance (#3686): this only records that
5
+ * runTransition wrote the blob. Non-code-bearing completes and scope:fail
6
+ * stamp this and still omit delivery provenance.
7
+ */
8
+ export const LIFECYCLE_WRITE_KEY = "lifecycleWrite";
9
+ /** Worker-facing leftover-land path after a scope-provenance strip (#3476 / #3679). */
10
+ export const LEFTOVER_LAND_PR_REMEDIATION = "After a provenance-gated product PR strips active/ from the change set, " +
11
+ "the designed remainder is a leftover land PR (#3476): run `task scope:complete` " +
12
+ "after merge (it can stamp a brief already in completed/), then land that artifact. " +
13
+ "Do not git-add a completed/ husk to skip the verb.";
14
+ function asRecord(value) {
15
+ if (typeof value !== "object" || value === null || Array.isArray(value)) {
16
+ return null;
17
+ }
18
+ return value;
19
+ }
20
+ function metadataRecord(plan) {
21
+ const existing = asRecord(plan.metadata);
22
+ if (existing !== null) {
23
+ return existing;
24
+ }
25
+ const created = {};
26
+ plan.metadata = created;
27
+ return created;
28
+ }
29
+ /** Stamp that runTransition wrote this plan into completed/. */
30
+ export function stampLifecycleWrite(plan, action, writtenAt) {
31
+ const meta = metadataRecord(plan);
32
+ meta[LIFECYCLE_WRITE_KEY] = { action, writtenAt };
33
+ }
34
+ function hasLifecycleWriteStamp(plan) {
35
+ const meta = asRecord(plan.metadata);
36
+ if (meta === null) {
37
+ return false;
38
+ }
39
+ const stamp = asRecord(meta[LIFECYCLE_WRITE_KEY]);
40
+ if (stamp === null) {
41
+ return false;
42
+ }
43
+ const action = stamp.action;
44
+ const writtenAt = stamp.writtenAt;
45
+ return ((action === "complete" || action === "fail") &&
46
+ typeof writtenAt === "string" &&
47
+ writtenAt.trim().length > 0);
48
+ }
49
+ /**
50
+ * True when a completed/ blob shows verb evidence.
51
+ * Accepts the new stamp, legacy completedAt (pre-#3679 complete), or failed status
52
+ * (scope:fail never stamped provenance and must keep passing).
53
+ */
54
+ export function hasTransitionWrite(plan) {
55
+ if (hasLifecycleWriteStamp(plan)) {
56
+ return true;
57
+ }
58
+ const meta = asRecord(plan.metadata);
59
+ if (meta !== null && typeof meta.completedAt === "string" && meta.completedAt.trim().length > 0) {
60
+ return true;
61
+ }
62
+ return String(plan.status ?? "") === "failed";
63
+ }
64
+ //# sourceMappingURL=lifecycle-write.js.map
@@ -17,10 +17,12 @@ import { findByDecisionId, isAlreadyUndone, REVERSIBLE_ACTIONS, undoBatch, undoO
17
17
  const LIFECYCLE_USAGE_STDERR = "usage: deft scope:<action> [file] [--batch] [--from-issue N] [--repo OWNER/NAME]\n" +
18
18
  " [--force] [--project-root PATH] [--strict] [--force-no-cache]\n" +
19
19
  " [--path PATH]\n" +
20
+ " complete also accepts: --merge-commit SHA --pr N [--pr-base BRANCH] [--delivery-branch BRANCH]\n" +
20
21
  " actions: activate, block, cancel, complete, fail, promote, restore, unblock\n" +
21
22
  "The verb already encodes the action (e.g. deft scope:promote <file>). Do not pass the action again.\n" +
22
23
  "(promote --batch may omit file and promotes all proposed/ scopes; #3011)\n" +
23
- "(promote --from-issue=N may omit file; #1136)\n";
24
+ "(promote --from-issue=N may omit file; #1136)\n" +
25
+ "(complete --merge-commit / --pr are required for code-bearing delivery evidence; #3041 / #3721)\n";
24
26
  function parseLifecycleArgv(argv) {
25
27
  if (argv.length < 1) {
26
28
  return { args: null, error: "usage" };
@@ -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;
@@ -16,6 +16,7 @@ import { LIFECYCLE_FOLDERS, MOVE_LABELS, STATUS_PRECONDITIONS, STAY_LABELS, TRAN
16
16
  import { detectLifecycleFolder, updateDecomposedChildBackReferences, updateDecomposedParentBackReferences, } from "./decomposed-refs.js";
17
17
  import { classifyStoredDeliveryDisposition, evaluateDeliveryGate, resolveCompletionSessionId, stampDeliveryProvenance, } from "./delivery-evidence.js";
18
18
  import { evaluateEffortActivateGate } from "./effort-activate-gate.js";
19
+ import { stampLifecycleWrite } from "./lifecycle-write.js";
19
20
  import { syncProjectDefinitionAfterScopeMove } from "./project-definition-sync.js";
20
21
  import { syncSpecificationAfterScopeMove } from "./specification-sync.js";
21
22
  import { utcNowIso } from "./vbrief-json.js";
@@ -81,7 +82,9 @@ export function runTransition(action, filePath, now = new Date(), options = {})
81
82
  };
82
83
  }
83
84
  const { allowedSources, targetFolder, targetStatus } = TRANSITIONS[act];
84
- if (!allowedSources.includes(currentFolder)) {
85
+ const restampCompleted = act === "complete" && currentFolder === "completed";
86
+ if (!restampCompleted &&
87
+ !allowedSources.includes(currentFolder)) {
85
88
  const allowedStr = allowedSources.map((s) => `${s}/`).join(", ");
86
89
  return {
87
90
  ok: false,
@@ -103,15 +106,9 @@ export function runTransition(action, filePath, now = new Date(), options = {})
103
106
  const requiredStatus = STATUS_PRECONDITIONS[act];
104
107
  if (requiredStatus !== undefined) {
105
108
  if (currentStatus === targetStatus) {
106
- // Surface legacy delivery disposition on already-completed briefs (#3041).
107
- let dispositionSuffix = "";
108
- if (act === "complete" && currentFolder === "completed") {
109
- const disposition = classifyStoredDeliveryDisposition(planObj);
110
- dispositionSuffix = ` (deliveryDisposition=${disposition})`;
111
- }
112
109
  return {
113
110
  ok: true,
114
- message: `No-op: ${basename} is already ${targetStatus} in ${currentFolder}/${dispositionSuffix}`,
111
+ message: `No-op: ${basename} is already ${targetStatus} in ${currentFolder}/`,
115
112
  };
116
113
  }
117
114
  if (currentStatus !== requiredStatus) {
@@ -122,14 +119,30 @@ export function runTransition(action, filePath, now = new Date(), options = {})
122
119
  };
123
120
  }
124
121
  }
122
+ const nowIso = utcNowIso(now);
123
+ const vbriefRoot = dirname(dirname(resolvedPath));
124
+ const projectRoot = dirname(vbriefRoot);
125
+ // #3679: lift #3041 already-completed handling out of STATUS_PRECONDITIONS
126
+ // so complete can stamp a brief already in completed/ without a folder trip.
127
+ if (restampCompleted) {
128
+ return restampCompletedBrief({
129
+ resolvedPath,
130
+ basename,
131
+ data,
132
+ planObj,
133
+ projectRoot,
134
+ vbriefRoot,
135
+ now,
136
+ nowIso,
137
+ options,
138
+ });
139
+ }
125
140
  if (targetFolder !== null && targetFolder === currentFolder) {
126
141
  return {
127
142
  ok: true,
128
143
  message: `No-op: ${basename} is already in ${currentFolder}/ (status: ${currentStatus})`,
129
144
  };
130
145
  }
131
- const vbriefRoot = dirname(dirname(resolvedPath));
132
- const projectRoot = dirname(vbriefRoot);
133
146
  if (targetFolder !== null) {
134
147
  const destDir = join(vbriefRoot, targetFolder);
135
148
  try {
@@ -144,7 +157,6 @@ export function runTransition(action, filePath, now = new Date(), options = {})
144
157
  throw err;
145
158
  }
146
159
  }
147
- const nowIso = utcNowIso(now);
148
160
  // #3360: hand-authored briefs run #3323 clause derivation on activate/promote.
149
161
  // #3355: stamp is state-observed (before vs after), not derivation-notice-bound.
150
162
  const previousAcceptance = planObj.acceptance;
@@ -190,6 +202,7 @@ export function runTransition(action, filePath, now = new Date(), options = {})
190
202
  ? { ...gate.provenance, completedSessionId: sessionId }
191
203
  : gate.provenance);
192
204
  }
205
+ stampLifecycleWrite(planObj, "complete", nowIso);
193
206
  }
194
207
  // #3240: per-criterion typed evidence or human-origin disposition before auto-advance.
195
208
  let acceptanceReports;
@@ -266,6 +279,9 @@ export function runTransition(action, filePath, now = new Date(), options = {})
266
279
  };
267
280
  }
268
281
  }
282
+ if (act === "fail") {
283
+ stampLifecycleWrite(planObj, "fail", nowIso);
284
+ }
269
285
  const formatted = formatBriefJson(data);
270
286
  const crud = new InstrumentedVbriefCrud({ now: () => now });
271
287
  if (targetFolder !== null) {
@@ -370,6 +386,69 @@ export function runTransition(action, filePath, now = new Date(), options = {})
370
386
  acceptanceReports,
371
387
  };
372
388
  }
389
+ /**
390
+ * Stamp a brief already in completed/ (#3679). Runs the #3041 delivery gate
391
+ * and writes in place so recovery does not need a folder round-trip.
392
+ * The #3041 deliveryDisposition suffix now actually executes for complete.
393
+ */
394
+ function restampCompletedBrief(args) {
395
+ const { resolvedPath, basename, data, planObj, projectRoot, vbriefRoot, now, nowIso, options } = args;
396
+ const gate = evaluateDeliveryGate({
397
+ projectRoot,
398
+ plan: planObj,
399
+ nowIso,
400
+ evidence: options.deliveryEvidence,
401
+ nonDeliveryDisposition: options.nonDeliveryDisposition,
402
+ runGit: options.runGit,
403
+ verifier: options.verifier ?? "scope:complete",
404
+ assumeEvidenceValidated: options.assumeEvidenceValidated,
405
+ });
406
+ if (!gate.ok) {
407
+ return { ok: false, message: gate.message };
408
+ }
409
+ if (gate.provenance !== null) {
410
+ const sessionId = resolveCompletionSessionId(projectRoot);
411
+ stampDeliveryProvenance(planObj, sessionId !== null ? { ...gate.provenance, completedSessionId: sessionId } : gate.provenance);
412
+ }
413
+ planObj.status = "completed";
414
+ planObj.updated = nowIso;
415
+ stampEnvelopeUpdated(data, nowIso);
416
+ stampCompletionMetadata(planObj, projectRoot, nowIso, {
417
+ completedSessionId: resolveCompletionSessionId(projectRoot),
418
+ });
419
+ stampLifecycleWrite(planObj, "complete", nowIso);
420
+ const writeResult = atomicWriteBrief(resolvedPath, data, vbriefRoot, { projectRoot });
421
+ if (!writeResult.ok) {
422
+ return { ok: false, message: writeResult.message };
423
+ }
424
+ const crud = new InstrumentedVbriefCrud({ now: () => now });
425
+ crud.recordTrustedUpdate(resolvedPath, formatBriefJson(data));
426
+ try {
427
+ persistCrudMetrics(projectRoot, crud.getMetrics());
428
+ }
429
+ catch {
430
+ /* best-effort telemetry persistence */
431
+ }
432
+ const sessionId = resolveCompletionSessionId(projectRoot);
433
+ if (sessionId !== null) {
434
+ try {
435
+ writeSessionCompletedMarker(projectRoot, {
436
+ path: resolvedPath,
437
+ sessionId,
438
+ completedAt: nowIso,
439
+ });
440
+ }
441
+ catch {
442
+ return { ok: false, message: SESSION_COMPLETED_AC_REMEDIATION };
443
+ }
444
+ }
445
+ const disposition = classifyStoredDeliveryDisposition(planObj);
446
+ return {
447
+ ok: true,
448
+ message: `Restamped ${basename} in completed/ (status: completed, ` +
449
+ `deliveryDisposition=${disposition})`,
450
+ };
451
+ }
373
452
  export function recordWipCapOverride(filePath, projectRoot, check, now = new Date()) {
374
453
  try {
375
454
  const rel = resolve(filePath).startsWith(resolve(projectRoot))
@@ -209,7 +209,11 @@ function remediationForExpansion() {
209
209
  "`.deft/approved-scope/<plan-id>.json` with a humanApproval stamp). Commit that " +
210
210
  "approval on the merge base (or a prior PR) before expanding or activating the " +
211
211
  "scoped xBRIEF in the implementation change set. Editing the active xBRIEF alone " +
212
- "does not authorize new paths (#3145 / #3205). See content/docs/scope-provenance.md.");
212
+ "does not authorize new paths (#3145 / #3205). See content/docs/scope-provenance.md. " +
213
+ "If you cannot mint a digest (human TTY required), drop active/ from this product " +
214
+ "change set and land the completed artifact in a leftover land PR after merge (#3476). " +
215
+ "Run `task scope:complete` on the brief (it can stamp a file already in completed/). " +
216
+ "Do not git-add a completed/ husk to skip that PR (#3679).");
213
217
  }
214
218
  /**
215
219
  * Parse + lightly validate an approved-scope JSON blob (base-ref `git show` or disk).