@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
@@ -16,8 +16,9 @@
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
  */
22
23
  export type PackageManager = "npm" | "pnpm";
23
24
  export declare const PACKAGE_MANAGERS: readonly PackageManager[];
@@ -32,10 +33,38 @@ export interface DetectPackageManagerInput {
32
33
  /** Whether a `pnpm-lock.yaml` is present at the project root. */
33
34
  readonly pnpmLockPresent?: boolean;
34
35
  }
36
+ export type PackageManagerResolutionSource = "env-override" | "package-manager-field" | "pnpm-lock" | "user-agent" | "default";
37
+ export type PackageManagerResolutionErrorSource = "env-override" | "package-manager-field" | "package-json" | "project-filesystem";
38
+ export type PackageManagerResolution = {
39
+ readonly ok: true;
40
+ readonly packageManager: PackageManager;
41
+ readonly source: PackageManagerResolutionSource;
42
+ } | {
43
+ readonly ok: false;
44
+ readonly source: PackageManagerResolutionErrorSource;
45
+ readonly message: string;
46
+ };
47
+ export interface ResolveProjectPackageManagerOptions {
48
+ /** Consumer project root. Defaults to the current working directory. */
49
+ readonly projectRoot?: string;
50
+ /** Environment map used for override and user-agent precedence. */
51
+ readonly env?: NodeJS.ProcessEnv;
52
+ /** Text-read seam. `null` means the file is absent. */
53
+ readonly readText?: (path: string) => string | null;
54
+ /** File-existence seam used for pnpm-lock.yaml. */
55
+ readonly isFile?: (path: string) => boolean;
56
+ }
57
+ /** Human-readable, non-executable label for a resolution source. */
58
+ export declare function packageManagerSourceLabel(source: PackageManagerResolutionSource): string;
59
+ export declare function resolvePackageManager(input?: DetectPackageManagerInput): PackageManagerResolution;
60
+ /** Render untrusted diagnostic text as one bounded terminal-safe line. */
61
+ export declare function sanitizeSingleLineDiagnostic(text: string): string;
62
+ /** Resolve a consumer project's package manager from package.json and lockfile facts. */
63
+ export declare function resolveProjectPackageManager(options?: ResolveProjectPackageManagerOptions): PackageManagerResolution;
35
64
  /**
36
65
  * Detect the active package manager. Precedence (first match wins):
37
- * 1. `DEFT_PACKAGE_MANAGER` env override
38
- * 2. `packageManager` field / Corepack shim
66
+ * 1. `packageManager` field / Corepack shim (authoritative when present)
67
+ * 2. `DEFT_PACKAGE_MANAGER` env override
39
68
  * 3. `pnpm-lock.yaml` present
40
69
  * 4. `npm_config_user_agent` (set by the manager that spawned the process)
41
70
  * 5. default: npm
@@ -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
@@ -3,7 +3,8 @@ import { resolveBinary } from "../scm/binary.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
9
  const binary = resolveBinary(seams.whichFn);
9
10
  const path = `repos/${repo}/issues/${pr}/comments?per_page=100`;
@@ -103,4 +104,184 @@ export function resolveGitHubLogin(seams = {}) {
103
104
  return null;
104
105
  }
105
106
  }
107
+ /**
108
+ * Author trust boundary for `<!-- deft:review-owner -->` comments (#3607 / #2307).
109
+ *
110
+ * Ownership leases gate. `verify:review-monitor` and `verify:l4-owner` exit 0 on a live
111
+ * lease, so a forged lease defeats a fail-closed gate. `listReviewOwnerComments` above
112
+ * therefore admits only maintainer associations -- OWNER, MEMBER, COLLABORATOR. That
113
+ * filter is the single author-association check in the lease read path and it lives in
114
+ * `isMaintainerAuthored` / `MAINTAINER_ASSOCIATIONS`
115
+ * (`packages/core/src/umbrella-current-shape/index.ts`), which is why a search for
116
+ * `author_association` in this directory finds nothing.
117
+ *
118
+ * Pass marks do not gate. They tell an arriving agent that a pass is open and never
119
+ * block a write, so a forged mark costs at most an unnecessary courtesy. Pass marks are
120
+ * therefore read from every author association, including CONTRIBUTOR, which is what
121
+ * #3607 requires ("visible regardless of the author's trust association"). The field
122
+ * instance is real: comment 5429316778 on PR #3775 carries this marker and is
123
+ * CONTRIBUTOR-authored, so the lease filter hides it while this reader sees it.
124
+ */
125
+ export function listPassMarkerComments(repo, issue, seams = {}) {
126
+ const fetcher = seams.fetchComments ?? ((r, n) => defaultFetchComments(r, n, seams));
127
+ const fetched = fetcher(repo, issue);
128
+ if (!Array.isArray(fetched)) {
129
+ return fetched;
130
+ }
131
+ const comments = [];
132
+ for (const entry of fetched) {
133
+ const mapped = mapPassCommentEntry({
134
+ id: entry.id,
135
+ body: entry.body,
136
+ created_at: entry.updatedAt,
137
+ author_login: entry.authorLogin,
138
+ });
139
+ if (mapped !== null) {
140
+ comments.push(mapped);
141
+ }
142
+ }
143
+ return comments;
144
+ }
145
+ /** Read the open mark on an issue thread; expired and cleared marks are ignored (#3607). */
146
+ export function fetchActivePassMarker(repo, issue, options = {}) {
147
+ const listed = listPassMarkerComments(repo, issue, options.seams ?? {});
148
+ if (!Array.isArray(listed)) {
149
+ return { error: listed.error };
150
+ }
151
+ const active = findActivePassComment(listed, { now: options.now });
152
+ if (active === null || active.marker === null) {
153
+ return null;
154
+ }
155
+ return { commentId: active.id, marker: active.marker };
156
+ }
157
+ function buildPassMarker(input, startedAt) {
158
+ return {
159
+ kind: PASS_MARKER_KIND,
160
+ pass_kind: input.passKind,
161
+ owner: input.owner,
162
+ agent_id: input.agentId ?? null,
163
+ ceiling: input.ceiling ?? null,
164
+ started_at: startedAt.toISOString(),
165
+ expires_at: computeExpiresAt(startedAt, input.staleMinutes ?? DEFAULT_PASS_STALE_MINUTES),
166
+ ended_at: null,
167
+ };
168
+ }
169
+ function resolvePassCreateRace(input, createdId, marker, now, seams) {
170
+ const relisted = listPassMarkerComments(input.repo, input.issue, seams);
171
+ if (!Array.isArray(relisted)) {
172
+ return { error: relisted.error };
173
+ }
174
+ const winner = findActivePassComment(relisted, { now });
175
+ if (winner === null || winner.marker === null || winner.id === createdId) {
176
+ return { status: "opened", commentId: createdId, marker };
177
+ }
178
+ const deleted = deleteReviewOwnerComment(input.repo, createdId, seams);
179
+ if ("error" in deleted) {
180
+ return { error: deleted.error };
181
+ }
182
+ return { status: "observed", commentId: winner.id, marker: winner.marker };
183
+ }
184
+ /**
185
+ * Proof, read from the thread, that a mark is the caller's own to write (#3607).
186
+ *
187
+ * Both the comment's GitHub author and the marker's `owner` must be the caller. An
188
+ * author GitHub did not report is refused rather than assumed: an unreported author is
189
+ * exactly the case where a marker body claiming the caller as `owner` is unverifiable,
190
+ * and PATCHing another user's comment is a 403 for a non-maintainer.
191
+ */
192
+ function isOwnOpenPassComment(comment, owner) {
193
+ const mark = comment.marker;
194
+ return (mark !== null &&
195
+ mark.ended_at === null &&
196
+ comment.authorLogin.length > 0 &&
197
+ comment.authorLogin === owner &&
198
+ mark.owner === owner);
199
+ }
200
+ /**
201
+ * A refresh must name the pass it refreshes (#3607). Owner login, agent id and pass kind
202
+ * identify a pass; the ceiling and the expiry window are what a refresh moves. Two runs
203
+ * agreeing on all three are one pass identity by construction, so a refresh then moves
204
+ * only that pass's own window.
205
+ */
206
+ function refreshesSamePass(marker, input) {
207
+ return marker.pass_kind === input.passKind && marker.agent_id === (input.agentId ?? null);
208
+ }
209
+ /**
210
+ * Mark a pass open on an issue thread (#3607).
211
+ *
212
+ * A marker comment belongs to the one pass that created it. An open never edits a
213
+ * comment it did not create -- it creates its own, or reports the mark already there --
214
+ * so two concurrent passes can never overwrite each other's metadata, including two
215
+ * agents sharing one GitHub login. Refreshing an existing mark requires the `commentId`
216
+ * that open handed back, and the mark on that comment is re-read and checked against the
217
+ * caller before the write: a `commentId` taken from an `observed` result names another
218
+ * pass, so it is refused and the caller opens its own mark instead of overwriting.
219
+ *
220
+ * `observed` means a mark is already open on the thread. The caller is informed and MAY
221
+ * still write; nothing is held. Concurrent creates resolve oldest-comment-id wins, and
222
+ * the loser removes its own duplicate.
223
+ */
224
+ export function openPassMarker(input) {
225
+ const seams = input.seams ?? {};
226
+ const startedAt = input.startedAt ?? new Date();
227
+ const marker = buildPassMarker(input, startedAt);
228
+ const body = renderPassOpenComment(marker);
229
+ const listed = listPassMarkerComments(input.repo, input.issue, seams);
230
+ if (!Array.isArray(listed)) {
231
+ return { error: listed.error };
232
+ }
233
+ if (input.commentId !== undefined && input.commentId !== null) {
234
+ const target = listed.find((comment) => comment.id === input.commentId);
235
+ if (target !== undefined &&
236
+ isOwnOpenPassComment(target, input.owner) &&
237
+ refreshesSamePass(target.marker, input)) {
238
+ const updated = updateReviewOwnerComment(input.repo, target.id, body, seams);
239
+ if ("error" in updated) {
240
+ return { error: updated.error };
241
+ }
242
+ return { status: "renewed", commentId: target.id, marker };
243
+ }
244
+ }
245
+ const active = findActivePassComment(listed, { now: startedAt });
246
+ if (active !== null && active.marker !== null) {
247
+ return { status: "observed", commentId: active.id, marker: active.marker };
248
+ }
249
+ const created = createReviewOwnerComment(input.repo, input.issue, body, seams);
250
+ if ("error" in created) {
251
+ return { error: created.error };
252
+ }
253
+ return resolvePassCreateRace(input, created.id, marker, startedAt, seams);
254
+ }
255
+ /**
256
+ * Clear the mark at synthesis (#3607).
257
+ *
258
+ * `ended_at` is rendered over the marker as just read from the thread, never over the
259
+ * snapshot the caller opened with, so closing does not resurrect stale pass metadata.
260
+ * A mark the thread does not prove is the caller's own -- another author's, another
261
+ * owner's, or one whose author GitHub did not report -- is reported back, never
262
+ * overwritten.
263
+ */
264
+ export function closePassMarker(input) {
265
+ const seams = input.seams ?? {};
266
+ const endedAt = input.endedAt ?? new Date();
267
+ const listed = listPassMarkerComments(input.repo, input.issue, seams);
268
+ if (!Array.isArray(listed)) {
269
+ return { error: listed.error };
270
+ }
271
+ const target = input.commentId !== undefined && input.commentId !== null
272
+ ? (listed.find((comment) => comment.id === input.commentId) ?? null)
273
+ : findActivePassComment(listed, { now: endedAt });
274
+ if (target === null || target.marker === null || target.marker.ended_at !== null) {
275
+ return { status: "not-open", commentId: null };
276
+ }
277
+ if (!isOwnOpenPassComment(target, input.owner)) {
278
+ return { status: "held-by-other", commentId: target.id };
279
+ }
280
+ const body = renderPassOpenComment({ ...target.marker, ended_at: endedAt.toISOString() });
281
+ const updated = updateReviewOwnerComment(input.repo, target.id, body, seams);
282
+ if ("error" in updated) {
283
+ return { error: updated.error };
284
+ }
285
+ return { status: "cleared", commentId: target.id };
286
+ }
106
287
  //# sourceMappingURL=github-lease.js.map