@deftai/directive-core 0.107.0 → 0.109.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 (271) hide show
  1. package/dist/architecture/sor-preflight.js +2 -0
  2. package/dist/cache/operations.js +1 -1
  3. package/dist/check/cached-orchestrator.js +28 -3
  4. package/dist/check/gate-lists.d.ts +14 -0
  5. package/dist/check/gate-lists.js +26 -3
  6. package/dist/check/named-cause.js +18 -3
  7. package/dist/check/session-completed-ac.d.ts +1 -1
  8. package/dist/check/session-completed-ac.js +1 -1
  9. package/dist/codebase/provider.js +7 -1
  10. package/dist/consumer-check-contract/evaluate.d.ts +47 -0
  11. package/dist/consumer-check-contract/evaluate.js +181 -15
  12. package/dist/delivery-attempt/evaluate.d.ts +9 -1
  13. package/dist/delivery-attempt/evaluate.js +69 -0
  14. package/dist/delivery-attempt/handoff.js +1 -1
  15. package/dist/delivery-attempt/index.d.ts +1 -1
  16. package/dist/delivery-attempt/index.js +1 -1
  17. package/dist/deposit/live-procedure-exclusions.d.ts +18 -0
  18. package/dist/deposit/live-procedure-exclusions.js +110 -0
  19. package/dist/deposit/live-procedure-targets.d.ts +45 -0
  20. package/dist/deposit/live-procedure-targets.js +274 -0
  21. package/dist/deposit/python-free.d.ts +6 -0
  22. package/dist/deposit/python-free.js +15 -0
  23. package/dist/deposit/rewrite-deposit-links.d.ts +42 -0
  24. package/dist/deposit/rewrite-deposit-links.js +148 -0
  25. package/dist/deposit/run-stage-content-pack.d.ts +2 -0
  26. package/dist/deposit/run-stage-content-pack.js +3 -0
  27. package/dist/deposit/stage-content-pack.d.ts +17 -0
  28. package/dist/deposit/stage-content-pack.js +91 -0
  29. package/dist/design-critique/citation-grammar.d.ts +60 -0
  30. package/dist/design-critique/citation-grammar.js +303 -0
  31. package/dist/design-critique/completed-arc-record.d.ts +62 -0
  32. package/dist/design-critique/completed-arc-record.js +277 -0
  33. package/dist/freshness/bind.d.ts +32 -3
  34. package/dist/freshness/bind.js +58 -12
  35. package/dist/hooks/classify/host-session-identity.d.ts +89 -0
  36. package/dist/hooks/classify/host-session-identity.js +467 -0
  37. package/dist/hooks/classify/index.d.ts +3 -2
  38. package/dist/hooks/classify/index.js +3 -2
  39. package/dist/hooks/classify/paths.d.ts +9 -0
  40. package/dist/hooks/classify/paths.js +39 -0
  41. package/dist/hooks/classify/stdin.d.ts +5 -0
  42. package/dist/hooks/classify/stdin.js +94 -1
  43. package/dist/hooks/dest-form.d.ts +20 -1
  44. package/dist/hooks/dest-form.js +158 -21
  45. package/dist/hooks/dispatcher.d.ts +58 -3
  46. package/dist/hooks/dispatcher.js +762 -73
  47. package/dist/hooks/fixtures/cases.d.ts +22 -2
  48. package/dist/hooks/fixtures/cases.js +228 -0
  49. package/dist/hooks/git-destructive-log.d.ts +32 -0
  50. package/dist/hooks/git-destructive-log.js +46 -0
  51. package/dist/hooks/index.d.ts +2 -0
  52. package/dist/hooks/index.js +2 -0
  53. package/dist/hooks/scope.js +3 -1
  54. package/dist/hooks/shell-write-targets.d.ts +10 -0
  55. package/dist/hooks/shell-write-targets.js +274 -0
  56. package/dist/hooks/tools.d.ts +20 -2
  57. package/dist/hooks/tools.js +30 -1
  58. package/dist/init-deposit/agent-hooks.d.ts +2 -1
  59. package/dist/init-deposit/agent-hooks.js +8 -1
  60. package/dist/init-deposit/gitignore.d.ts +7 -0
  61. package/dist/init-deposit/gitignore.js +24 -0
  62. package/dist/init-deposit/hook-runtime-travel.d.ts +77 -0
  63. package/dist/init-deposit/hook-runtime-travel.js +269 -0
  64. package/dist/init-deposit/init-deposit.js +3 -0
  65. package/dist/init-deposit/refresh.js +7 -0
  66. package/dist/init-deposit/runtime-writers.d.ts +14 -0
  67. package/dist/init-deposit/runtime-writers.js +33 -0
  68. package/dist/intake/clause-derivation.js +9 -3
  69. package/dist/intake/issue-ingest.d.ts +6 -1
  70. package/dist/intake/issue-ingest.js +83 -4
  71. package/dist/intake/platform-capabilities.d.ts +9 -2
  72. package/dist/intake/platform-capabilities.js +38 -9
  73. package/dist/intake/reconcile-issues.js +13 -13
  74. package/dist/lifecycle/brief-envelope.d.ts +25 -0
  75. package/dist/lifecycle/brief-envelope.js +42 -0
  76. package/dist/lifecycle/index.d.ts +1 -0
  77. package/dist/lifecycle/index.js +1 -0
  78. package/dist/literal-acceptance/evaluate.js +14 -5
  79. package/dist/literal-acceptance/index.d.ts +1 -1
  80. package/dist/literal-acceptance/index.js +1 -1
  81. package/dist/literal-acceptance/run.d.ts +2 -0
  82. package/dist/literal-acceptance/run.js +19 -1
  83. package/dist/orphan-active/candidate-scope.d.ts +53 -0
  84. package/dist/orphan-active/candidate-scope.js +157 -0
  85. package/dist/orphan-active/evaluate.d.ts +52 -0
  86. package/dist/orphan-active/evaluate.js +250 -128
  87. package/dist/orphan-active/index.d.ts +2 -0
  88. package/dist/orphan-active/index.js +2 -0
  89. package/dist/orphan-active/issue-state.d.ts +121 -0
  90. package/dist/orphan-active/issue-state.js +278 -0
  91. package/dist/platform/cursor-managed-runtime.d.ts +95 -0
  92. package/dist/platform/cursor-managed-runtime.js +241 -0
  93. package/dist/platform/index.d.ts +1 -0
  94. package/dist/platform/index.js +1 -0
  95. package/dist/platform/platform-capabilities.d.ts +23 -1
  96. package/dist/platform/platform-capabilities.js +55 -15
  97. package/dist/policy/ceremony-dial.js +5 -9
  98. package/dist/policy/host-hooks.js +4 -9
  99. package/dist/policy/merge-approval-head.js +7 -6
  100. package/dist/policy/org-force-on-migration.js +5 -10
  101. package/dist/policy/plan-extensions.d.ts +14 -2
  102. package/dist/policy/plan-extensions.js +24 -3
  103. package/dist/policy/product-signal.js +5 -10
  104. package/dist/policy/require-human-merge.js +5 -9
  105. package/dist/policy/resolve.js +77 -16
  106. package/dist/policy/value-feedback.js +9 -18
  107. package/dist/pr-closeout-attestable/evaluate.d.ts +80 -0
  108. package/dist/pr-closeout-attestable/evaluate.js +306 -0
  109. package/dist/pr-closeout-attestable/index.d.ts +2 -0
  110. package/dist/pr-closeout-attestable/index.js +2 -0
  111. package/dist/pr-closing-keywords/gh.js +32 -9
  112. package/dist/pr-closing-keywords/main.d.ts +1 -0
  113. package/dist/pr-closing-keywords/main.js +68 -3
  114. package/dist/pr-closing-keywords/types.d.ts +2 -0
  115. package/dist/pr-merge-readiness/gh.js +32 -9
  116. package/dist/pr-protected-issues/gh.d.ts +6 -2
  117. package/dist/pr-protected-issues/gh.js +42 -11
  118. package/dist/pr-wait-mergeable/cascade.d.ts +9 -1
  119. package/dist/pr-wait-mergeable/cascade.js +21 -1
  120. package/dist/pr-wait-mergeable/types.d.ts +2 -0
  121. package/dist/pr-wait-mergeable/wrappers.d.ts +6 -0
  122. package/dist/pr-wait-mergeable/wrappers.js +14 -3
  123. package/dist/preflight/evaluate.d.ts +15 -0
  124. package/dist/preflight/evaluate.js +29 -3
  125. package/dist/product-first-done-gate/acceptance-resolver.d.ts +14 -2
  126. package/dist/product-first-done-gate/acceptance-resolver.js +13 -2
  127. package/dist/product-first-done-gate/evaluate.js +23 -9
  128. package/dist/product-first-done-gate/types.js +2 -0
  129. package/dist/release/cli-drift-report.d.ts +68 -0
  130. package/dist/release/cli-drift-report.js +189 -0
  131. package/dist/release/consumer-hard-stops.d.ts +48 -0
  132. package/dist/release/consumer-hard-stops.js +140 -0
  133. package/dist/release/consumer-readiness-disclosure.d.ts +18 -0
  134. package/dist/release/consumer-readiness-disclosure.js +51 -0
  135. package/dist/release/index.d.ts +3 -0
  136. package/dist/release/index.js +3 -0
  137. package/dist/release/issue-state-fetch.d.ts +5 -3
  138. package/dist/release/issue-state-fetch.js +86 -21
  139. package/dist/release/native-steps.js +1 -0
  140. package/dist/release/pipeline.js +74 -0
  141. package/dist/release/run-consumer-readiness.d.ts +15 -0
  142. package/dist/release/run-consumer-readiness.js +24 -0
  143. package/dist/release/types.d.ts +20 -0
  144. package/dist/render/constants.d.ts +4 -0
  145. package/dist/render/constants.js +11 -8
  146. package/dist/render/export-spec.js +31 -5
  147. package/dist/render/index.d.ts +1 -1
  148. package/dist/render/index.js +1 -1
  149. package/dist/render/prd-render.d.ts +4 -1
  150. package/dist/render/prd-render.js +82 -23
  151. package/dist/render/project-render.js +14 -7
  152. package/dist/render/scope-outlook.d.ts +2 -0
  153. package/dist/render/scope-outlook.js +3 -0
  154. package/dist/render/spec-render.js +2 -2
  155. package/dist/resolution/package-manager.d.ts +33 -4
  156. package/dist/resolution/package-manager.js +210 -9
  157. package/dist/review-monitor/constants.d.ts +7 -0
  158. package/dist/review-monitor/constants.js +7 -0
  159. package/dist/review-monitor/github-lease.d.ts +98 -1
  160. package/dist/review-monitor/github-lease.js +186 -4
  161. package/dist/review-monitor/lease-comment.d.ts +36 -0
  162. package/dist/review-monitor/lease-comment.js +103 -2
  163. package/dist/run-summary/types.d.ts +2 -2
  164. package/dist/scm/build-command.d.ts +2 -2
  165. package/dist/scm/build-command.js +2 -2
  166. package/dist/scm/call-shape.d.ts +25 -0
  167. package/dist/scm/call-shape.js +59 -0
  168. package/dist/scm/call.d.ts +7 -4
  169. package/dist/scm/call.js +48 -9
  170. package/dist/scm/design-critique-chip.d.ts +1 -0
  171. package/dist/scm/design-critique-chip.js +22 -6
  172. package/dist/scm/gh-rest.d.ts +34 -13
  173. package/dist/scm/gh-rest.js +168 -15
  174. package/dist/scm/index.d.ts +2 -0
  175. package/dist/scm/index.js +2 -0
  176. package/dist/scm/readiness.d.ts +2 -0
  177. package/dist/scm/readiness.js +28 -4
  178. package/dist/scm/spawn-status.d.ts +33 -0
  179. package/dist/scm/spawn-status.js +53 -0
  180. package/dist/scope/acceptance-evidence.d.ts +1 -1
  181. package/dist/scope/project-definition-sync.js +14 -7
  182. package/dist/scope/transition.js +4 -14
  183. package/dist/session/ac-pass-banking.d.ts +2 -2
  184. package/dist/session/ac-pass-banking.js +2 -2
  185. package/dist/session/git.d.ts +24 -0
  186. package/dist/session/git.js +138 -2
  187. package/dist/session/host-session-owner.d.ts +53 -0
  188. package/dist/session/host-session-owner.js +89 -0
  189. package/dist/session/index.d.ts +1 -0
  190. package/dist/session/index.js +1 -0
  191. package/dist/session/occupancy.d.ts +259 -13
  192. package/dist/session/occupancy.js +877 -33
  193. package/dist/session/ritual-sentinel.d.ts +5 -0
  194. package/dist/session/ritual-sentinel.js +5 -0
  195. package/dist/session/session-ready.d.ts +5 -1
  196. package/dist/session/session-ready.js +98 -8
  197. package/dist/session/session-start.d.ts +5 -1
  198. package/dist/session/session-start.js +32 -17
  199. package/dist/session/toolchain-preflight.d.ts +13 -3
  200. package/dist/session/toolchain-preflight.js +93 -18
  201. package/dist/session/verify-ac-session-cache.d.ts +2 -2
  202. package/dist/session/verify-ac-session-cache.js +2 -2
  203. package/dist/session/verify-session-ritual.d.ts +11 -0
  204. package/dist/session/verify-session-ritual.js +60 -15
  205. package/dist/slice/constants.d.ts +1 -1
  206. package/dist/spec-authority/constants.d.ts +10 -2
  207. package/dist/spec-authority/constants.js +53 -8
  208. package/dist/spec-authority/resolver.d.ts +3 -0
  209. package/dist/spec-authority/resolver.js +55 -7
  210. package/dist/subprocess/max-buffer.d.ts +14 -0
  211. package/dist/subprocess/max-buffer.js +15 -0
  212. package/dist/swarm/complete-cohort.d.ts +2 -0
  213. package/dist/swarm/complete-cohort.js +21 -13
  214. package/dist/swarm/index.d.ts +1 -0
  215. package/dist/swarm/index.js +1 -0
  216. package/dist/swarm/launch-cli.js +53 -33
  217. package/dist/swarm/launch.d.ts +8 -0
  218. package/dist/swarm/launch.js +63 -43
  219. package/dist/swarm/pre-dispatch-cli.js +2 -1
  220. package/dist/swarm/pre-dispatch.js +10 -1
  221. package/dist/swarm/subagent-status-dir.d.ts +27 -0
  222. package/dist/swarm/subagent-status-dir.js +42 -0
  223. package/dist/swarm/worktrees.d.ts +17 -3
  224. package/dist/swarm/worktrees.js +48 -4
  225. package/dist/triage/evaluate/worktrees.js +153 -6
  226. package/dist/triage/scope/mutations-core.d.ts +2 -6
  227. package/dist/triage/scope/mutations-core.js +23 -55
  228. package/dist/triage/scope-drift/add-ignore.js +5 -39
  229. package/dist/triage/subscribe/index.d.ts +2 -6
  230. package/dist/triage/subscribe/index.js +15 -65
  231. package/dist/triage/welcome/writers.js +15 -74
  232. package/dist/umbrella-current-shape/index.d.ts +51 -3
  233. package/dist/umbrella-current-shape/index.js +106 -18
  234. package/dist/validate-content/deposit-required.d.ts +39 -0
  235. package/dist/validate-content/deposit-required.js +147 -0
  236. package/dist/validate-content/index.d.ts +1 -0
  237. package/dist/validate-content/index.js +1 -0
  238. package/dist/validate-content/validate-links.d.ts +2 -3
  239. package/dist/validate-content/validate-links.js +29 -2
  240. package/dist/vbrief-activate/activate.d.ts +7 -2
  241. package/dist/vbrief-activate/activate.js +26 -13
  242. package/dist/vbrief-build/index.d.ts +2 -1
  243. package/dist/vbrief-build/index.js +2 -1
  244. package/dist/vbrief-build/parity-scenarios.js +5 -5
  245. package/dist/vbrief-build/project-definition-io.d.ts +63 -3
  246. package/dist/vbrief-build/project-definition-io.js +357 -31
  247. package/dist/vbrief-build/project-definition-mutation.d.ts +32 -0
  248. package/dist/vbrief-build/project-definition-mutation.js +43 -0
  249. package/dist/vbrief-validate/conformance.d.ts +1 -0
  250. package/dist/vbrief-validate/conformance.js +85 -8
  251. package/dist/vbrief-validate/main.js +2 -0
  252. package/dist/vbrief-validate/precutover.js +5 -10
  253. package/dist/verify-ac/clauses.d.ts +62 -2
  254. package/dist/verify-ac/clauses.js +183 -111
  255. package/dist/verify-ac/evaluate.d.ts +9 -0
  256. package/dist/verify-ac/evaluate.js +32 -9
  257. package/dist/verify-ac/index.d.ts +1 -1
  258. package/dist/verify-ac/index.js +1 -1
  259. package/dist/verify-env/node-runtime.d.ts +8 -4
  260. package/dist/verify-env/node-runtime.js +9 -6
  261. package/dist/verify-env/toolchain-check.d.ts +35 -3
  262. package/dist/verify-env/toolchain-check.js +155 -36
  263. package/dist/verify-source/deposit-closure.d.ts +23 -0
  264. package/dist/verify-source/deposit-closure.js +162 -0
  265. package/dist/verify-source/index.d.ts +2 -0
  266. package/dist/verify-source/index.js +2 -0
  267. package/dist/verify-source/project-definition-mutation-boundary.d.ts +104 -0
  268. package/dist/verify-source/project-definition-mutation-boundary.js +326 -0
  269. package/dist/verify-source/semantic-single-source.d.ts +36 -0
  270. package/dist/verify-source/semantic-single-source.js +349 -0
  271. package/package.json +7 -3
@@ -16,13 +16,17 @@
16
16
  * gitignored / package-manager-invisible). Only the global, project-local,
17
17
  * ephemeral, and upgrade command forms vary by package manager.
18
18
  *
19
- * Every function here is PURE (no I/O): detection consumes an injected fact-set
20
- * so the whole surface is unit-testable without touching the filesystem or env.
19
+ * Detection from an injected fact-set remains pure. The project resolver is the
20
+ * single filesystem boundary used by consumer gates so package.json and lockfile
21
+ * precedence cannot drift across callers (#3610).
21
22
  */
23
+ import { readFileSync, statSync } from "node:fs";
24
+ import { join, resolve } from "node:path";
22
25
  export const PACKAGE_MANAGERS = ["npm", "pnpm"];
23
26
  export const DEFAULT_PACKAGE_MANAGER = "npm";
24
27
  /** Canonical published engine package name. */
25
28
  export const ENGINE_PACKAGE = "@deftai/directive";
29
+ const EXPLICIT_PACKAGE_MANAGER_RE = /^(npm|pnpm)(?:@[0-9A-Za-z][0-9A-Za-z._+/=-]*)?$/i;
26
30
  function normalizePackageManager(value) {
27
31
  const v = value.trim().toLowerCase();
28
32
  if (v.startsWith("pnpm"))
@@ -31,24 +35,221 @@ function normalizePackageManager(value) {
31
35
  return "npm";
32
36
  return null;
33
37
  }
38
+ function normalizeExplicitPackageManager(value) {
39
+ const match = EXPLICIT_PACKAGE_MANAGER_RE.exec(value.trim());
40
+ const name = match?.[1]?.toLowerCase();
41
+ return name === "npm" || name === "pnpm" ? name : null;
42
+ }
43
+ function unsupportedPackageManager(value, source) {
44
+ if (source === "env-override") {
45
+ return {
46
+ ok: false,
47
+ source,
48
+ message: "Unsupported DEFT_PACKAGE_MANAGER value; supported managers are npm and pnpm.",
49
+ };
50
+ }
51
+ const safeValue = sanitizeSingleLineDiagnostic(JSON.stringify(value));
52
+ return {
53
+ ok: false,
54
+ source,
55
+ message: `Unsupported package manager ${safeValue} from package.json#packageManager; supported managers are npm and pnpm.`,
56
+ };
57
+ }
58
+ /** Human-readable, non-executable label for a resolution source. */
59
+ export function packageManagerSourceLabel(source) {
60
+ switch (source) {
61
+ case "env-override":
62
+ return "DEFT_PACKAGE_MANAGER override";
63
+ case "package-manager-field":
64
+ return "packageManager field";
65
+ case "pnpm-lock":
66
+ return "pnpm-lock.yaml";
67
+ case "user-agent":
68
+ return "npm_config_user_agent";
69
+ case "default":
70
+ return "default";
71
+ }
72
+ }
73
+ /**
74
+ * Strict package-manager resolution for execution boundaries.
75
+ *
76
+ * Explicit override/declaration values fail closed when unsupported. The
77
+ * returned manager is a closed union and callers map it to fixed argv; raw
78
+ * package.json or environment text is never executable (#2761/#2765/#3610).
79
+ */
80
+ function packageManagerConflictResolution() {
81
+ return {
82
+ ok: false,
83
+ source: "env-override",
84
+ message: "DEFT_PACKAGE_MANAGER conflicts with package.json#packageManager; the declaration is authoritative.",
85
+ };
86
+ }
87
+ function resolveExplicitOverride(override) {
88
+ if (override.trim() === "")
89
+ return null;
90
+ const packageManager = normalizeExplicitPackageManager(override);
91
+ if (packageManager === null)
92
+ return unsupportedPackageManager(override, "env-override");
93
+ return { ok: true, packageManager, source: "env-override" };
94
+ }
95
+ export function resolvePackageManager(input = {}) {
96
+ const env = input.env ?? {};
97
+ const override = env.DEFT_PACKAGE_MANAGER;
98
+ const overrideText = typeof override === "string" ? override : "";
99
+ const field = input.packageManagerField;
100
+ if (field != null && field.trim() !== "") {
101
+ const declared = normalizeExplicitPackageManager(field);
102
+ if (declared === null)
103
+ return unsupportedPackageManager(field, "package-manager-field");
104
+ if (overrideText.trim() !== "") {
105
+ const overridden = normalizeExplicitPackageManager(overrideText);
106
+ if (overridden !== null && overridden !== declared) {
107
+ return packageManagerConflictResolution();
108
+ }
109
+ }
110
+ return { ok: true, packageManager: declared, source: "package-manager-field" };
111
+ }
112
+ const overrideResolution = resolveExplicitOverride(overrideText);
113
+ if (overrideResolution !== null)
114
+ return overrideResolution;
115
+ if (input.pnpmLockPresent) {
116
+ return { ok: true, packageManager: "pnpm", source: "pnpm-lock" };
117
+ }
118
+ const userAgent = env.npm_config_user_agent;
119
+ if (typeof userAgent === "string" && userAgent.trim() !== "") {
120
+ const packageManager = normalizePackageManager(userAgent);
121
+ if (packageManager !== null) {
122
+ return { ok: true, packageManager, source: "user-agent" };
123
+ }
124
+ }
125
+ return { ok: true, packageManager: DEFAULT_PACKAGE_MANAGER, source: "default" };
126
+ }
127
+ function defaultReadText(path) {
128
+ try {
129
+ return readFileSync(path, "utf8");
130
+ }
131
+ catch (error) {
132
+ if (error.code === "ENOENT")
133
+ return null;
134
+ throw error;
135
+ }
136
+ }
137
+ function defaultIsFile(path) {
138
+ try {
139
+ return statSync(path).isFile();
140
+ }
141
+ catch (error) {
142
+ if (error.code === "ENOENT")
143
+ return false;
144
+ throw error;
145
+ }
146
+ }
147
+ const UNSAFE_DIAGNOSTIC_CHARACTER = /[\p{Cc}\p{Cf}\p{Zl}\p{Zp}]/u;
148
+ /** Render untrusted diagnostic text as one bounded terminal-safe line. */
149
+ export function sanitizeSingleLineDiagnostic(text) {
150
+ const printable = [...text]
151
+ .map((character) => (UNSAFE_DIAGNOSTIC_CHARACTER.test(character) ? " " : character))
152
+ .join("")
153
+ .replace(/\s+/g, " ")
154
+ .trim();
155
+ const codePoints = [...printable];
156
+ return codePoints.length > 240 ? `${codePoints.slice(0, 237).join("")}...` : printable;
157
+ }
158
+ function singleLineErrorDetail(error) {
159
+ const raw = error instanceof Error ? error.message : String(error);
160
+ return sanitizeSingleLineDiagnostic(raw) || "unknown error";
161
+ }
162
+ /** Resolve a consumer project's package manager from package.json and lockfile facts. */
163
+ export function resolveProjectPackageManager(options = {}) {
164
+ const projectRoot = resolve(options.projectRoot ?? process.cwd());
165
+ const env = options.env ?? process.env;
166
+ const readText = options.readText ?? defaultReadText;
167
+ const isFile = options.isFile ?? defaultIsFile;
168
+ const packageJsonPath = join(projectRoot, "package.json");
169
+ let packageManagerField = null;
170
+ let packageJsonText;
171
+ try {
172
+ packageJsonText = readText(packageJsonPath);
173
+ }
174
+ catch (error) {
175
+ const detail = singleLineErrorDetail(error);
176
+ return {
177
+ ok: false,
178
+ source: "package-json",
179
+ message: `Could not read package.json for package-manager selection: ${detail}`,
180
+ };
181
+ }
182
+ if (packageJsonText !== null) {
183
+ let parsed;
184
+ try {
185
+ parsed = JSON.parse(packageJsonText);
186
+ }
187
+ catch (error) {
188
+ const detail = singleLineErrorDetail(error);
189
+ return {
190
+ ok: false,
191
+ source: "package-json",
192
+ message: `Could not parse package.json for package-manager selection: ${detail}`,
193
+ };
194
+ }
195
+ if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) {
196
+ return {
197
+ ok: false,
198
+ source: "package-json",
199
+ message: "Could not parse package.json for package-manager selection: root must be an object.",
200
+ };
201
+ }
202
+ const packageRecord = parsed;
203
+ if (Object.hasOwn(packageRecord, "packageManager")) {
204
+ const field = packageRecord.packageManager;
205
+ if (typeof field !== "string" || field.trim() === "") {
206
+ return {
207
+ ok: false,
208
+ source: "package-manager-field",
209
+ message: "package.json#packageManager must be a non-empty string naming npm or pnpm.",
210
+ };
211
+ }
212
+ packageManagerField = field;
213
+ }
214
+ }
215
+ if (packageManagerField !== null) {
216
+ return resolvePackageManager({ env, packageManagerField });
217
+ }
218
+ let pnpmLockPresent;
219
+ try {
220
+ pnpmLockPresent = isFile(join(projectRoot, "pnpm-lock.yaml"));
221
+ }
222
+ catch (error) {
223
+ const detail = singleLineErrorDetail(error);
224
+ return {
225
+ ok: false,
226
+ source: "project-filesystem",
227
+ message: `Could not inspect pnpm-lock.yaml for package-manager selection: ${detail}`,
228
+ };
229
+ }
230
+ return resolvePackageManager({
231
+ env,
232
+ pnpmLockPresent,
233
+ });
234
+ }
34
235
  /**
35
236
  * Detect the active package manager. Precedence (first match wins):
36
- * 1. `DEFT_PACKAGE_MANAGER` env override
37
- * 2. `packageManager` field / Corepack shim
237
+ * 1. `packageManager` field / Corepack shim (authoritative when present)
238
+ * 2. `DEFT_PACKAGE_MANAGER` env override
38
239
  * 3. `pnpm-lock.yaml` present
39
240
  * 4. `npm_config_user_agent` (set by the manager that spawned the process)
40
241
  * 5. default: npm
41
242
  */
42
243
  export function detectPackageManager(input = {}) {
43
244
  const env = input.env ?? {};
44
- const override = env.DEFT_PACKAGE_MANAGER;
45
- if (typeof override === "string" && override.trim() !== "") {
46
- const pm = normalizePackageManager(override);
245
+ if (input.packageManagerField != null && input.packageManagerField.trim() !== "") {
246
+ const pm = normalizePackageManager(input.packageManagerField);
47
247
  if (pm)
48
248
  return pm;
49
249
  }
50
- if (input.packageManagerField != null && input.packageManagerField.trim() !== "") {
51
- const pm = normalizePackageManager(input.packageManagerField);
250
+ const override = env.DEFT_PACKAGE_MANAGER;
251
+ if (typeof override === "string" && override.trim() !== "") {
252
+ const pm = normalizePackageManager(override);
52
253
  if (pm)
53
254
  return pm;
54
255
  }
@@ -14,6 +14,13 @@ export declare const MONITORING_TIER_1 = 1;
14
14
  export declare const MONITORING_TIER_2 = 2;
15
15
  export declare const MONITORING_TIER_3 = 3;
16
16
  export declare const DEFAULT_STALE_MINUTES = 30;
17
+ /** `kind:` value marking an advisory pass-open mark rather than an ownership lease (#3607). */
18
+ export declare const PASS_MARKER_KIND = "pass";
19
+ /**
20
+ * Advisory pass marks expire on read (#3607). Passes run in minutes, so an hour
21
+ * bounds an abandoned mark without needing a heartbeat.
22
+ */
23
+ export declare const DEFAULT_PASS_STALE_MINUTES = 60;
17
24
  export declare const REVIEW_MONITOR_HELP: string;
18
25
  export declare const REGISTER_HELP: string;
19
26
  export declare const RELEASE_HELP: string;
@@ -14,6 +14,13 @@ export const MONITORING_TIER_1 = 1;
14
14
  export const MONITORING_TIER_2 = 2;
15
15
  export const MONITORING_TIER_3 = 3;
16
16
  export const DEFAULT_STALE_MINUTES = 30;
17
+ /** `kind:` value marking an advisory pass-open mark rather than an ownership lease (#3607). */
18
+ export const PASS_MARKER_KIND = "pass";
19
+ /**
20
+ * Advisory pass marks expire on read (#3607). Passes run in minutes, so an hour
21
+ * bounds an abandoned mark without needing a heartbeat.
22
+ */
23
+ export const DEFAULT_PASS_STALE_MINUTES = 60;
17
24
  export const REVIEW_MONITOR_HELP = "usage: task verify:review-monitor -- --pr <N> [options]\n" +
18
25
  "\n" +
19
26
  "Fail-closed gate: when Tier 1 (sub-agent primitive available), require an\n" +
@@ -1,6 +1,6 @@
1
1
  import { type GhRestSeams } from "../scm/gh-rest.js";
2
2
  import { type IssueComment } from "../umbrella-current-shape/index.js";
3
- import { type ReviewOwnerComment } from "./lease-comment.js";
3
+ import { type PassOpenComment, type PassOpenMarker, type ReviewOwnerComment } from "./lease-comment.js";
4
4
  export type { IssueComment };
5
5
  export interface ReviewOwnerGithubSeams extends GhRestSeams {
6
6
  readonly fetchComments?: (repo: string, pr: number) => IssueComment[] | {
@@ -42,4 +42,101 @@ export declare function deleteReviewOwnerComment(repo: string, commentId: number
42
42
  error: string;
43
43
  };
44
44
  export declare function resolveGitHubLogin(seams?: ReviewOwnerGithubSeams): string | null;
45
+ /**
46
+ * Author trust boundary for `<!-- deft:review-owner -->` comments (#3607 / #2307).
47
+ *
48
+ * Ownership leases gate. `verify:review-monitor` and `verify:l4-owner` exit 0 on a live
49
+ * lease, so a forged lease defeats a fail-closed gate. `listReviewOwnerComments` above
50
+ * therefore admits only maintainer associations -- OWNER, MEMBER, COLLABORATOR. That
51
+ * filter is the single author-association check in the lease read path and it lives in
52
+ * `isMaintainerAuthored` / `MAINTAINER_ASSOCIATIONS`
53
+ * (`packages/core/src/umbrella-current-shape/index.ts`), which is why a search for
54
+ * `author_association` in this directory finds nothing.
55
+ *
56
+ * Pass marks do not gate. They tell an arriving agent that a pass is open and never
57
+ * block a write, so a forged mark costs at most an unnecessary courtesy. Pass marks are
58
+ * therefore read from every author association, including CONTRIBUTOR, which is what
59
+ * #3607 requires ("visible regardless of the author's trust association"). The field
60
+ * instance is real: comment 5429316778 on PR #3775 carries this marker and is
61
+ * CONTRIBUTOR-authored, so the lease filter hides it while this reader sees it.
62
+ */
63
+ export declare function listPassMarkerComments(repo: string, issue: number, seams?: ReviewOwnerGithubSeams): PassOpenComment[] | {
64
+ error: string;
65
+ };
66
+ export interface ActivePassMarker {
67
+ readonly commentId: number;
68
+ readonly marker: PassOpenMarker;
69
+ }
70
+ /** Read the open mark on an issue thread; expired and cleared marks are ignored (#3607). */
71
+ export declare function fetchActivePassMarker(repo: string, issue: number, options?: {
72
+ now?: Date;
73
+ seams?: ReviewOwnerGithubSeams;
74
+ }): ActivePassMarker | null | {
75
+ error: string;
76
+ };
77
+ export interface OpenPassMarkerInput {
78
+ readonly repo: string;
79
+ readonly issue: number;
80
+ readonly owner: string;
81
+ readonly passKind: string;
82
+ readonly agentId?: string | null;
83
+ readonly ceiling?: string | null;
84
+ /**
85
+ * Comment id returned by a previous open, to refresh that same mark in place.
86
+ * Only the pass that created a comment ever writes it again, which is what keeps
87
+ * two concurrent passes from overwriting one another (#3607).
88
+ */
89
+ readonly commentId?: number | null;
90
+ readonly startedAt?: Date;
91
+ readonly staleMinutes?: number;
92
+ readonly seams?: ReviewOwnerGithubSeams;
93
+ }
94
+ export type OpenPassMarkerResult = {
95
+ readonly status: "opened" | "renewed" | "observed";
96
+ readonly commentId: number;
97
+ readonly marker: PassOpenMarker;
98
+ } | {
99
+ readonly error: string;
100
+ };
101
+ /**
102
+ * Mark a pass open on an issue thread (#3607).
103
+ *
104
+ * A marker comment belongs to the one pass that created it. An open never edits a
105
+ * comment it did not create -- it creates its own, or reports the mark already there --
106
+ * so two concurrent passes can never overwrite each other's metadata, including two
107
+ * agents sharing one GitHub login. Refreshing an existing mark requires the `commentId`
108
+ * that open handed back, and the mark on that comment is re-read and checked against the
109
+ * caller before the write: a `commentId` taken from an `observed` result names another
110
+ * pass, so it is refused and the caller opens its own mark instead of overwriting.
111
+ *
112
+ * `observed` means a mark is already open on the thread. The caller is informed and MAY
113
+ * still write; nothing is held. Concurrent creates resolve oldest-comment-id wins, and
114
+ * the loser removes its own duplicate.
115
+ */
116
+ export declare function openPassMarker(input: OpenPassMarkerInput): OpenPassMarkerResult;
117
+ export interface ClosePassMarkerInput {
118
+ readonly repo: string;
119
+ readonly issue: number;
120
+ readonly owner: string;
121
+ /** Comment id from open. Without it, the oldest open mark this owner authored. */
122
+ readonly commentId?: number | null;
123
+ readonly endedAt?: Date;
124
+ readonly seams?: ReviewOwnerGithubSeams;
125
+ }
126
+ export type ClosePassMarkerResult = {
127
+ readonly status: "cleared" | "not-open" | "held-by-other";
128
+ readonly commentId: number | null;
129
+ } | {
130
+ readonly error: string;
131
+ };
132
+ /**
133
+ * Clear the mark at synthesis (#3607).
134
+ *
135
+ * `ended_at` is rendered over the marker as just read from the thread, never over the
136
+ * snapshot the caller opened with, so closing does not resurrect stale pass metadata.
137
+ * A mark the thread does not prove is the caller's own -- another author's, another
138
+ * owner's, or one whose author GitHub did not report -- is reported back, never
139
+ * overwritten.
140
+ */
141
+ export declare function closePassMarker(input: ClosePassMarkerInput): ClosePassMarkerResult;
45
142
  //# sourceMappingURL=github-lease.d.ts.map
@@ -1,13 +1,15 @@
1
1
  import { spawnSync } from "node:child_process";
2
- import { resolveBinary } from "../scm/binary.js";
2
+ import { resolveBinaryForArgv } from "../scm/call-shape.js";
3
3
  import { DEFAULT_TIMEOUT_S, restDeleteComment, restGetUser, restPostComment, restUpdateComment, } from "../scm/gh-rest.js";
4
4
  import { SUBPROCESS_MAX_BUFFER } from "../subprocess/max-buffer.js";
5
5
  import { isMaintainerAuthored, parseCommentsFromGhStdout, } from "../umbrella-current-shape/index.js";
6
- import { mapCommentEntry } from "./lease-comment.js";
6
+ import { DEFAULT_PASS_STALE_MINUTES, PASS_MARKER_KIND } from "./constants.js";
7
+ import { computeExpiresAt, findActivePassComment, mapCommentEntry, mapPassCommentEntry, renderPassOpenComment, } from "./lease-comment.js";
7
8
  function defaultFetchComments(repo, pr, seams) {
8
- const binary = resolveBinary(seams.whichFn);
9
9
  const path = `repos/${repo}/issues/${pr}/comments?per_page=100`;
10
- const proc = spawnSync(binary, ["api", "--paginate", path], {
10
+ const apiArgs = ["--paginate", path];
11
+ const binary = resolveBinaryForArgv("api", apiArgs, seams.whichFn);
12
+ const proc = spawnSync(binary, ["api", ...apiArgs], {
11
13
  encoding: "utf8",
12
14
  maxBuffer: SUBPROCESS_MAX_BUFFER,
13
15
  timeout: DEFAULT_TIMEOUT_S * 1000,
@@ -103,4 +105,184 @@ export function resolveGitHubLogin(seams = {}) {
103
105
  return null;
104
106
  }
105
107
  }
108
+ /**
109
+ * Author trust boundary for `<!-- deft:review-owner -->` comments (#3607 / #2307).
110
+ *
111
+ * Ownership leases gate. `verify:review-monitor` and `verify:l4-owner` exit 0 on a live
112
+ * lease, so a forged lease defeats a fail-closed gate. `listReviewOwnerComments` above
113
+ * therefore admits only maintainer associations -- OWNER, MEMBER, COLLABORATOR. That
114
+ * filter is the single author-association check in the lease read path and it lives in
115
+ * `isMaintainerAuthored` / `MAINTAINER_ASSOCIATIONS`
116
+ * (`packages/core/src/umbrella-current-shape/index.ts`), which is why a search for
117
+ * `author_association` in this directory finds nothing.
118
+ *
119
+ * Pass marks do not gate. They tell an arriving agent that a pass is open and never
120
+ * block a write, so a forged mark costs at most an unnecessary courtesy. Pass marks are
121
+ * therefore read from every author association, including CONTRIBUTOR, which is what
122
+ * #3607 requires ("visible regardless of the author's trust association"). The field
123
+ * instance is real: comment 5429316778 on PR #3775 carries this marker and is
124
+ * CONTRIBUTOR-authored, so the lease filter hides it while this reader sees it.
125
+ */
126
+ export function listPassMarkerComments(repo, issue, seams = {}) {
127
+ const fetcher = seams.fetchComments ?? ((r, n) => defaultFetchComments(r, n, seams));
128
+ const fetched = fetcher(repo, issue);
129
+ if (!Array.isArray(fetched)) {
130
+ return fetched;
131
+ }
132
+ const comments = [];
133
+ for (const entry of fetched) {
134
+ const mapped = mapPassCommentEntry({
135
+ id: entry.id,
136
+ body: entry.body,
137
+ created_at: entry.updatedAt,
138
+ author_login: entry.authorLogin,
139
+ });
140
+ if (mapped !== null) {
141
+ comments.push(mapped);
142
+ }
143
+ }
144
+ return comments;
145
+ }
146
+ /** Read the open mark on an issue thread; expired and cleared marks are ignored (#3607). */
147
+ export function fetchActivePassMarker(repo, issue, options = {}) {
148
+ const listed = listPassMarkerComments(repo, issue, options.seams ?? {});
149
+ if (!Array.isArray(listed)) {
150
+ return { error: listed.error };
151
+ }
152
+ const active = findActivePassComment(listed, { now: options.now });
153
+ if (active === null || active.marker === null) {
154
+ return null;
155
+ }
156
+ return { commentId: active.id, marker: active.marker };
157
+ }
158
+ function buildPassMarker(input, startedAt) {
159
+ return {
160
+ kind: PASS_MARKER_KIND,
161
+ pass_kind: input.passKind,
162
+ owner: input.owner,
163
+ agent_id: input.agentId ?? null,
164
+ ceiling: input.ceiling ?? null,
165
+ started_at: startedAt.toISOString(),
166
+ expires_at: computeExpiresAt(startedAt, input.staleMinutes ?? DEFAULT_PASS_STALE_MINUTES),
167
+ ended_at: null,
168
+ };
169
+ }
170
+ function resolvePassCreateRace(input, createdId, marker, now, seams) {
171
+ const relisted = listPassMarkerComments(input.repo, input.issue, seams);
172
+ if (!Array.isArray(relisted)) {
173
+ return { error: relisted.error };
174
+ }
175
+ const winner = findActivePassComment(relisted, { now });
176
+ if (winner === null || winner.marker === null || winner.id === createdId) {
177
+ return { status: "opened", commentId: createdId, marker };
178
+ }
179
+ const deleted = deleteReviewOwnerComment(input.repo, createdId, seams);
180
+ if ("error" in deleted) {
181
+ return { error: deleted.error };
182
+ }
183
+ return { status: "observed", commentId: winner.id, marker: winner.marker };
184
+ }
185
+ /**
186
+ * Proof, read from the thread, that a mark is the caller's own to write (#3607).
187
+ *
188
+ * Both the comment's GitHub author and the marker's `owner` must be the caller. An
189
+ * author GitHub did not report is refused rather than assumed: an unreported author is
190
+ * exactly the case where a marker body claiming the caller as `owner` is unverifiable,
191
+ * and PATCHing another user's comment is a 403 for a non-maintainer.
192
+ */
193
+ function isOwnOpenPassComment(comment, owner) {
194
+ const mark = comment.marker;
195
+ return (mark !== null &&
196
+ mark.ended_at === null &&
197
+ comment.authorLogin.length > 0 &&
198
+ comment.authorLogin === owner &&
199
+ mark.owner === owner);
200
+ }
201
+ /**
202
+ * A refresh must name the pass it refreshes (#3607). Owner login, agent id and pass kind
203
+ * identify a pass; the ceiling and the expiry window are what a refresh moves. Two runs
204
+ * agreeing on all three are one pass identity by construction, so a refresh then moves
205
+ * only that pass's own window.
206
+ */
207
+ function refreshesSamePass(marker, input) {
208
+ return marker.pass_kind === input.passKind && marker.agent_id === (input.agentId ?? null);
209
+ }
210
+ /**
211
+ * Mark a pass open on an issue thread (#3607).
212
+ *
213
+ * A marker comment belongs to the one pass that created it. An open never edits a
214
+ * comment it did not create -- it creates its own, or reports the mark already there --
215
+ * so two concurrent passes can never overwrite each other's metadata, including two
216
+ * agents sharing one GitHub login. Refreshing an existing mark requires the `commentId`
217
+ * that open handed back, and the mark on that comment is re-read and checked against the
218
+ * caller before the write: a `commentId` taken from an `observed` result names another
219
+ * pass, so it is refused and the caller opens its own mark instead of overwriting.
220
+ *
221
+ * `observed` means a mark is already open on the thread. The caller is informed and MAY
222
+ * still write; nothing is held. Concurrent creates resolve oldest-comment-id wins, and
223
+ * the loser removes its own duplicate.
224
+ */
225
+ export function openPassMarker(input) {
226
+ const seams = input.seams ?? {};
227
+ const startedAt = input.startedAt ?? new Date();
228
+ const marker = buildPassMarker(input, startedAt);
229
+ const body = renderPassOpenComment(marker);
230
+ const listed = listPassMarkerComments(input.repo, input.issue, seams);
231
+ if (!Array.isArray(listed)) {
232
+ return { error: listed.error };
233
+ }
234
+ if (input.commentId !== undefined && input.commentId !== null) {
235
+ const target = listed.find((comment) => comment.id === input.commentId);
236
+ if (target !== undefined &&
237
+ isOwnOpenPassComment(target, input.owner) &&
238
+ refreshesSamePass(target.marker, input)) {
239
+ const updated = updateReviewOwnerComment(input.repo, target.id, body, seams);
240
+ if ("error" in updated) {
241
+ return { error: updated.error };
242
+ }
243
+ return { status: "renewed", commentId: target.id, marker };
244
+ }
245
+ }
246
+ const active = findActivePassComment(listed, { now: startedAt });
247
+ if (active !== null && active.marker !== null) {
248
+ return { status: "observed", commentId: active.id, marker: active.marker };
249
+ }
250
+ const created = createReviewOwnerComment(input.repo, input.issue, body, seams);
251
+ if ("error" in created) {
252
+ return { error: created.error };
253
+ }
254
+ return resolvePassCreateRace(input, created.id, marker, startedAt, seams);
255
+ }
256
+ /**
257
+ * Clear the mark at synthesis (#3607).
258
+ *
259
+ * `ended_at` is rendered over the marker as just read from the thread, never over the
260
+ * snapshot the caller opened with, so closing does not resurrect stale pass metadata.
261
+ * A mark the thread does not prove is the caller's own -- another author's, another
262
+ * owner's, or one whose author GitHub did not report -- is reported back, never
263
+ * overwritten.
264
+ */
265
+ export function closePassMarker(input) {
266
+ const seams = input.seams ?? {};
267
+ const endedAt = input.endedAt ?? new Date();
268
+ const listed = listPassMarkerComments(input.repo, input.issue, seams);
269
+ if (!Array.isArray(listed)) {
270
+ return { error: listed.error };
271
+ }
272
+ const target = input.commentId !== undefined && input.commentId !== null
273
+ ? (listed.find((comment) => comment.id === input.commentId) ?? null)
274
+ : findActivePassComment(listed, { now: endedAt });
275
+ if (target === null || target.marker === null || target.marker.ended_at !== null) {
276
+ return { status: "not-open", commentId: null };
277
+ }
278
+ if (!isOwnOpenPassComment(target, input.owner)) {
279
+ return { status: "held-by-other", commentId: target.id };
280
+ }
281
+ const body = renderPassOpenComment({ ...target.marker, ended_at: endedAt.toISOString() });
282
+ const updated = updateReviewOwnerComment(input.repo, target.id, body, seams);
283
+ if ("error" in updated) {
284
+ return { error: updated.error };
285
+ }
286
+ return { status: "cleared", commentId: target.id };
287
+ }
106
288
  //# sourceMappingURL=github-lease.js.map
@@ -1,3 +1,4 @@
1
+ import { PASS_MARKER_KIND } from "./constants.js";
1
2
  import type { PlatformPrimitive } from "./tier-detection.js";
2
3
  export declare function parseIso8601Utc(value: string): Date | null;
3
4
  export interface ReviewOwnerLease {
@@ -33,4 +34,39 @@ export declare function findActiveLeaseComment(comments: readonly ReviewOwnerCom
33
34
  now?: Date;
34
35
  headSha?: string | null;
35
36
  }): ReviewOwnerComment | null;
37
+ /**
38
+ * Advisory pass-open mark on an issue thread (#3607).
39
+ *
40
+ * Observe-and-mark only: it records that a pass is open, who owns it, the declared
41
+ * ceiling comment, and when the mark expires. It never holds, blocks, or gates
42
+ * another actor's write -- an arriving agent reads it and decides for itself.
43
+ */
44
+ export interface PassOpenMarker {
45
+ readonly kind: typeof PASS_MARKER_KIND;
46
+ readonly pass_kind: string;
47
+ readonly owner: string;
48
+ readonly agent_id: string | null;
49
+ readonly ceiling: string | null;
50
+ readonly started_at: string;
51
+ readonly expires_at: string;
52
+ readonly ended_at: string | null;
53
+ }
54
+ export interface PassOpenComment {
55
+ readonly id: number;
56
+ readonly body: string;
57
+ readonly createdAt: string;
58
+ /** GitHub login that authored the comment; "" when the source did not report one. */
59
+ readonly authorLogin: string;
60
+ readonly marker: PassOpenMarker | null;
61
+ }
62
+ export declare function parsePassOpenMarker(body: string): PassOpenMarker | null;
63
+ export declare function renderPassOpenComment(marker: PassOpenMarker): string;
64
+ /** Expired or synthesis-cleared marks are ignored on read; a mark self-clears (#3607). */
65
+ export declare function isPassMarkerActive(marker: PassOpenMarker, now?: Date): boolean;
66
+ export declare function mapPassCommentEntry(entry: unknown): PassOpenComment | null;
67
+ /** Oldest comment id wins when two marks race, matching the lease (#2814 / #3607). */
68
+ export declare function selectWinningPassComment(comments: readonly PassOpenComment[]): PassOpenComment | null;
69
+ export declare function findActivePassComment(comments: readonly PassOpenComment[], options?: {
70
+ now?: Date;
71
+ }): PassOpenComment | null;
36
72
  //# sourceMappingURL=lease-comment.d.ts.map