@deftai/directive-core 0.98.1 → 0.100.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 (144) hide show
  1. package/dist/authz/actions.d.ts +6 -0
  2. package/dist/authz/actions.js +3 -0
  3. package/dist/authz/classify.js +401 -75
  4. package/dist/authz/decompose-apply.d.ts +67 -0
  5. package/dist/authz/decompose-apply.js +302 -0
  6. package/dist/authz/index.d.ts +3 -1
  7. package/dist/authz/index.js +3 -1
  8. package/dist/authz/store.d.ts +53 -0
  9. package/dist/authz/store.js +219 -1
  10. package/dist/authz/types.d.ts +19 -2
  11. package/dist/authz/types.js +5 -1
  12. package/dist/consumer-check-contract/evaluate.d.ts +40 -0
  13. package/dist/consumer-check-contract/evaluate.js +188 -3
  14. package/dist/consumer-check-contract/index.d.ts +1 -1
  15. package/dist/consumer-check-contract/index.js +1 -1
  16. package/dist/content-contracts/skills/greptile-detector.d.ts +42 -0
  17. package/dist/content-contracts/skills/greptile-detector.js +202 -4
  18. package/dist/decision/index.d.ts +17 -0
  19. package/dist/decision/index.js +35 -0
  20. package/dist/decision/list.d.ts +47 -0
  21. package/dist/decision/list.js +250 -0
  22. package/dist/decision/schema.d.ts +88 -0
  23. package/dist/decision/schema.js +293 -0
  24. package/dist/decision/write.d.ts +82 -0
  25. package/dist/decision/write.js +427 -0
  26. package/dist/doctor/checks.d.ts +44 -0
  27. package/dist/doctor/checks.js +428 -2
  28. package/dist/doctor/main.d.ts +6 -7
  29. package/dist/doctor/main.js +72 -79
  30. package/dist/doctor/which.d.ts +20 -1
  31. package/dist/doctor/which.js +67 -1
  32. package/dist/eval/report.d.ts +29 -0
  33. package/dist/eval/report.js +69 -0
  34. package/dist/eval/run.d.ts +9 -0
  35. package/dist/eval/run.js +40 -4
  36. package/dist/eval/version-pin.d.ts +99 -0
  37. package/dist/eval/version-pin.js +181 -0
  38. package/dist/finish-loop/pr-finish-loop.d.ts +13 -2
  39. package/dist/finish-loop/pr-finish-loop.js +116 -5
  40. package/dist/finish-loop/types.d.ts +1 -1
  41. package/dist/hooks/dispatcher.d.ts +9 -5
  42. package/dist/hooks/dispatcher.js +23 -15
  43. package/dist/hooks/readonly.d.ts +6 -3
  44. package/dist/hooks/readonly.js +44 -25
  45. package/dist/index.d.ts +2 -0
  46. package/dist/index.js +2 -0
  47. package/dist/init-deposit/agent-hooks.d.ts +20 -0
  48. package/dist/init-deposit/agent-hooks.js +51 -31
  49. package/dist/intake/issue-ingest.js +29 -0
  50. package/dist/lifecycle/completed-consistency.d.ts +60 -0
  51. package/dist/lifecycle/completed-consistency.js +443 -0
  52. package/dist/lifecycle/completed-tracked-on-delivery.d.ts +47 -0
  53. package/dist/lifecycle/completed-tracked-on-delivery.js +415 -0
  54. package/dist/lifecycle/index.d.ts +2 -0
  55. package/dist/lifecycle/index.js +2 -0
  56. package/dist/literal-acceptance/capture.d.ts +52 -0
  57. package/dist/literal-acceptance/capture.js +658 -0
  58. package/dist/literal-acceptance/evaluate.d.ts +47 -0
  59. package/dist/literal-acceptance/evaluate.js +193 -0
  60. package/dist/literal-acceptance/index.d.ts +12 -0
  61. package/dist/literal-acceptance/index.js +12 -0
  62. package/dist/literal-acceptance/run.d.ts +38 -0
  63. package/dist/literal-acceptance/run.js +216 -0
  64. package/dist/literal-acceptance/safety.d.ts +24 -0
  65. package/dist/literal-acceptance/safety.js +251 -0
  66. package/dist/literal-acceptance/types.d.ts +82 -0
  67. package/dist/literal-acceptance/types.js +26 -0
  68. package/dist/platform/host-content-surface.d.ts +74 -0
  69. package/dist/platform/host-content-surface.js +214 -0
  70. package/dist/platform/index.d.ts +1 -0
  71. package/dist/platform/index.js +1 -0
  72. package/dist/policy/ceremony-dial.d.ts +289 -0
  73. package/dist/policy/ceremony-dial.js +980 -0
  74. package/dist/policy/deft-directive-disable.js +12 -2
  75. package/dist/policy/index.d.ts +2 -0
  76. package/dist/policy/index.js +16 -1
  77. package/dist/policy/merge-approval-head.d.ts +129 -0
  78. package/dist/policy/merge-approval-head.js +461 -0
  79. package/dist/policy/require-human-merge.d.ts +5 -0
  80. package/dist/policy/require-human-merge.js +5 -0
  81. package/dist/pr-merge-readiness/ci-gate.d.ts +20 -3
  82. package/dist/pr-merge-readiness/ci-gate.js +65 -5
  83. package/dist/pr-merge-readiness/compute.d.ts +13 -0
  84. package/dist/pr-merge-readiness/compute.js +99 -14
  85. package/dist/pr-merge-readiness/evaluate.js +10 -0
  86. package/dist/pr-merge-readiness/gh.d.ts +49 -0
  87. package/dist/pr-merge-readiness/gh.js +268 -0
  88. package/dist/pr-merge-readiness/index.d.ts +2 -2
  89. package/dist/pr-merge-readiness/index.js +2 -2
  90. package/dist/pr-merge-readiness/mergeability.js +5 -0
  91. package/dist/pr-merge-readiness/output.js +2 -0
  92. package/dist/pr-merge-readiness/parse.js +4 -0
  93. package/dist/pr-merge-readiness/test-gh-fixtures.helpers.js +4 -0
  94. package/dist/pr-merge-readiness/types.d.ts +6 -0
  95. package/dist/pr-wait-mergeable/cascade.d.ts +11 -0
  96. package/dist/pr-wait-mergeable/cascade.js +115 -1
  97. package/dist/pr-wait-mergeable/main.d.ts +4 -0
  98. package/dist/pr-wait-mergeable/main.js +4 -0
  99. package/dist/pr-wait-mergeable/types.d.ts +3 -1
  100. package/dist/pr-wait-mergeable/wrappers.d.ts +6 -1
  101. package/dist/pr-wait-mergeable/wrappers.js +7 -1
  102. package/dist/preflight/evaluate.d.ts +15 -1
  103. package/dist/preflight/evaluate.js +33 -2
  104. package/dist/render/framework-commands.js +4 -0
  105. package/dist/scope/acceptance-evidence.d.ts +76 -0
  106. package/dist/scope/acceptance-evidence.js +348 -0
  107. package/dist/scope/coverage-map.d.ts +98 -0
  108. package/dist/scope/coverage-map.js +558 -0
  109. package/dist/scope/decompose.js +125 -11
  110. package/dist/scope/effort-activate-gate.d.ts +28 -0
  111. package/dist/scope/effort-activate-gate.js +64 -0
  112. package/dist/scope/index.d.ts +4 -0
  113. package/dist/scope/index.js +4 -0
  114. package/dist/scope/parent-lineage.d.ts +125 -0
  115. package/dist/scope/parent-lineage.js +626 -0
  116. package/dist/scope/transition.d.ts +8 -0
  117. package/dist/scope/transition.js +69 -2
  118. package/dist/session/active-cli.d.ts +79 -0
  119. package/dist/session/active-cli.js +382 -0
  120. package/dist/session/effort-budget.d.ts +130 -0
  121. package/dist/session/effort-budget.js +334 -0
  122. package/dist/session/index.d.ts +2 -0
  123. package/dist/session/index.js +2 -0
  124. package/dist/session/session-start.d.ts +30 -1
  125. package/dist/session/session-start.js +234 -26
  126. package/dist/session/verify-session-ritual.d.ts +14 -0
  127. package/dist/session/verify-session-ritual.js +33 -0
  128. package/dist/story-ready/evaluate.d.ts +11 -0
  129. package/dist/story-ready/evaluate.js +41 -3
  130. package/dist/swarm/index.d.ts +2 -0
  131. package/dist/swarm/index.js +2 -0
  132. package/dist/swarm/pre-dispatch-cli.d.ts +19 -0
  133. package/dist/swarm/pre-dispatch-cli.js +143 -0
  134. package/dist/swarm/pre-dispatch.d.ts +87 -0
  135. package/dist/swarm/pre-dispatch.js +373 -0
  136. package/dist/triage/actions/candidates-log.d.ts +1 -2
  137. package/dist/triage/actions/candidates-log.js +37 -6
  138. package/dist/vbrief-activate/activate.js +6 -0
  139. package/dist/vbrief-validate/constants.d.ts +2 -0
  140. package/dist/vbrief-validate/constants.js +2 -0
  141. package/dist/vbrief-validate/schema.js +4 -1
  142. package/dist/xbrief-migrate/migrate-project.d.ts +4 -0
  143. package/dist/xbrief-migrate/migrate-project.js +92 -3
  144. package/package.json +19 -3
@@ -4,7 +4,7 @@
4
4
  import { randomBytes } from "node:crypto";
5
5
  import { existsSync, readdirSync, readFileSync, renameSync, rmSync } from "node:fs";
6
6
  import { basename, dirname, join, resolve } from "node:path";
7
- import { containedWrite } from "../fs/contained-write.js";
7
+ import { ContainedWriteError, containedWrite } from "../fs/contained-write.js";
8
8
  import { assertWriteTargetSafe } from "../fs/projection-containment.js";
9
9
  import { isHumanOrigin } from "./origin.js";
10
10
  import { authzAuditPath, authzGrantPath, authzGrantsDir, authzStatePath } from "./paths.js";
@@ -105,6 +105,13 @@ function parseScope(raw) {
105
105
  storyIds: readStringArray(rec.storyIds ?? rec.story_ids),
106
106
  issueIds: readNumberArray(rec.issueIds ?? rec.issue_ids),
107
107
  cohortId: readString(rec, "cohortId") ?? readString(rec, "cohort_id"),
108
+ // #3239 structural decompose apply bindings (snake_case accepted on load).
109
+ contentDigest: readString(rec, "contentDigest") ??
110
+ readString(rec, "content_digest") ??
111
+ readString(rec, "draftDigest") ??
112
+ readString(rec, "draft_digest"),
113
+ parentPath: readString(rec, "parentPath") ?? readString(rec, "parent_path"),
114
+ targetPath: readString(rec, "targetPath") ?? readString(rec, "target_path"),
108
115
  };
109
116
  }
110
117
  function parseSemantics(raw) {
@@ -233,6 +240,217 @@ export function markGrantUsed(projectRoot, grantId, now = new Date()) {
233
240
  saveGrant(projectRoot, used);
234
241
  return used;
235
242
  }
243
+ function isProcessAlive(pid) {
244
+ if (!Number.isFinite(pid) || pid <= 0)
245
+ return false;
246
+ try {
247
+ process.kill(pid, 0);
248
+ return true;
249
+ }
250
+ catch (err) {
251
+ const code = err.code;
252
+ // ESRCH → dead. EPERM → exists but unsignalable — treat as alive (never reclaim).
253
+ if (code === "EPERM")
254
+ return true;
255
+ return false;
256
+ }
257
+ }
258
+ /**
259
+ * Whether a claim lock may be reclaimed after a crashed holder (#3239).
260
+ * Live owner PIDs are never reclaimed (no mtime-only steal of a live critical section).
261
+ * Corrupt / unreadable records are reclaimable; PID-reuse residual needs manual delete.
262
+ */
263
+ export function isGrantClaimLockReclaimable(rec) {
264
+ if (rec === null)
265
+ return true;
266
+ return !isProcessAlive(rec.pid);
267
+ }
268
+ function readGrantClaimLockRecord(lockPath) {
269
+ try {
270
+ const raw = readFileSync(lockPath, "utf8").trim();
271
+ // JSON form (current). Legacy: "pid\niso\n" lines from earlier #3239 revisions.
272
+ if (raw.startsWith("{")) {
273
+ const parsed = JSON.parse(raw);
274
+ if (parsed === null || typeof parsed !== "object" || Array.isArray(parsed))
275
+ return null;
276
+ const o = parsed;
277
+ const pid = typeof o.pid === "number" ? o.pid : Number(o.pid);
278
+ const startedAt = typeof o.startedAt === "string" ? o.startedAt : "";
279
+ const token = typeof o.token === "string" ? o.token : "";
280
+ if (!Number.isFinite(pid) || token.length === 0)
281
+ return null;
282
+ return { pid, startedAt, token };
283
+ }
284
+ const lines = raw.split(/\r?\n/);
285
+ const pid = Number(lines[0]);
286
+ if (!Number.isFinite(pid))
287
+ return null;
288
+ return {
289
+ pid,
290
+ startedAt: typeof lines[1] === "string" ? lines[1] : "",
291
+ token: "legacy",
292
+ };
293
+ }
294
+ catch {
295
+ return null;
296
+ }
297
+ }
298
+ /**
299
+ * Claim a single-use grant for structural apply (#3239).
300
+ *
301
+ * Concurrent-safe + failure-safe order when `apply` is provided:
302
+ * exclusive lock → re-load → revalidate → mark single-use usedAt → apply (writes)
303
+ * → on apply throw: rollback usedAt + release lock (grant reusable for retry).
304
+ * Multi-use grants run apply (if any) without mutating usedAt.
305
+ * Without `apply`, single-use is marked under the lock (claim-only / test path).
306
+ *
307
+ * Crash after mark + before rollback: grant stays spent (no double-apply); operator remints.
308
+ * Dead-PID / corrupt locks reclaim via **rename-away** of the old lock path (only one
309
+ * renamer wins) then exclusive create — never blind rmSync of a live winner's lock.
310
+ * Live PIDs are never reclaimed. PID-reuse residual: operator deletes the lock file.
311
+ */
312
+ export function claimSingleUseGrantForApply(projectRoot, grantId, options = {}) {
313
+ // Back-compat: second arg was `now: Date` in the first #3239 revision.
314
+ const opts = options instanceof Date ? { now: options } : options;
315
+ const now = opts.now ?? new Date();
316
+ const usedAtIso = utcIso(now);
317
+ const safe = grantId.replace(/[^a-zA-Z0-9._-]/g, "_");
318
+ const root = resolve(projectRoot);
319
+ const lockRel = join(".deft", "authz", "locks", `${safe}.lock`);
320
+ const lockPath = join(root, lockRel);
321
+ const lockToken = randomBytes(8).toString("hex");
322
+ const lockBody = `${JSON.stringify({
323
+ pid: process.pid,
324
+ startedAt: usedAtIso,
325
+ token: lockToken,
326
+ })}\n`;
327
+ const tryCreateLock = () => {
328
+ try {
329
+ containedWrite({
330
+ root,
331
+ target: lockPath,
332
+ data: lockBody,
333
+ mode: "create",
334
+ });
335
+ return true;
336
+ }
337
+ catch (err) {
338
+ if (err instanceof ContainedWriteError && err.code === "CONTAINED_WRITE_EXISTS") {
339
+ return false;
340
+ }
341
+ // Other containment/IO failures fail closed as reservation denial.
342
+ return false;
343
+ }
344
+ };
345
+ /** True when the on-disk lock still carries our token (not stolen mid-section). */
346
+ const stillOwnLock = () => {
347
+ const rec = readGrantClaimLockRecord(lockPath);
348
+ return rec !== null && rec.token === lockToken && rec.pid === process.pid;
349
+ };
350
+ let locked = tryCreateLock();
351
+ if (!locked) {
352
+ // Dead-PID / corrupt reclaim: rename the old lock aside (atomic contention) then create.
353
+ // Blind rmSync is forbidden — a second reclaimer must not delete a winner's new lock.
354
+ const existing = readGrantClaimLockRecord(lockPath);
355
+ if (isGrantClaimLockReclaimable(existing)) {
356
+ const side = `${lockPath}.reclaim.${randomBytes(6).toString("hex")}`;
357
+ try {
358
+ renameSync(lockPath, side);
359
+ try {
360
+ rmSync(side, { force: true });
361
+ }
362
+ catch {
363
+ /* best-effort side cleanup */
364
+ }
365
+ }
366
+ catch {
367
+ // Lost rename race or lock already gone — fall through to exclusive create.
368
+ }
369
+ locked = tryCreateLock();
370
+ }
371
+ }
372
+ if (!locked) {
373
+ return {
374
+ ok: false,
375
+ reason: `Directive denied scope:decompose apply: grant ${grantId} is already reserved ` +
376
+ "or spent by a concurrent apply. Human action required: remint if the prior apply failed " +
377
+ "(or remove a leftover `.deft/authz/locks/<id>.lock` after a dead-holder crash if reclaim fails).",
378
+ };
379
+ }
380
+ let markedUsedAt = null;
381
+ try {
382
+ const grant = loadGrant(projectRoot, grantId);
383
+ if (grant === null) {
384
+ return {
385
+ ok: false,
386
+ reason: `Directive denied scope:decompose apply: grant ${grantId} missing.`,
387
+ };
388
+ }
389
+ if (opts.revalidate !== undefined) {
390
+ const check = opts.revalidate(grant);
391
+ if (!check.ok) {
392
+ return { ok: false, reason: check.reason };
393
+ }
394
+ }
395
+ if (!grant.semantics.singleUse) {
396
+ if (opts.apply !== undefined) {
397
+ opts.apply(grant);
398
+ }
399
+ return { ok: true, grant };
400
+ }
401
+ if (grant.semantics.usedAt !== null) {
402
+ return {
403
+ ok: false,
404
+ reason: `Directive denied scope:decompose apply: single-use grant ${grantId} already spent at ` +
405
+ `${grant.semantics.usedAt}.`,
406
+ };
407
+ }
408
+ // Spend under lock before protected writes so a post-write mark failure cannot
409
+ // leave completed mutations behind an unspent single-use grant (#3239 Greptile).
410
+ // Apply throw rolls usedAt back while the lock is still held (retry-safe).
411
+ const used = {
412
+ ...grant,
413
+ semantics: {
414
+ ...grant.semantics,
415
+ usedAt: usedAtIso,
416
+ },
417
+ };
418
+ saveGrant(projectRoot, used);
419
+ markedUsedAt = usedAtIso;
420
+ if (opts.apply !== undefined) {
421
+ try {
422
+ opts.apply(used);
423
+ }
424
+ catch (applyErr) {
425
+ // Rollback spend so a failed multi-file apply does not strand the approval.
426
+ const current = loadGrant(projectRoot, grantId);
427
+ if (current?.semantics.singleUse && current.semantics.usedAt === markedUsedAt) {
428
+ const restored = {
429
+ ...current,
430
+ semantics: { ...current.semantics, usedAt: null },
431
+ };
432
+ saveGrant(projectRoot, restored);
433
+ markedUsedAt = null;
434
+ }
435
+ throw applyErr;
436
+ }
437
+ }
438
+ if (!stillOwnLock()) {
439
+ return {
440
+ ok: false,
441
+ reason: `Directive denied scope:decompose apply: grant ${grantId} lock ownership lost mid-claim. ` +
442
+ "Human action required: inspect partial outputs and remint if needed.",
443
+ };
444
+ }
445
+ return { ok: true, grant: used };
446
+ }
447
+ finally {
448
+ // Only remove the lock when we still own it — never delete a successor's claim.
449
+ if (stillOwnLock()) {
450
+ rmSync(lockPath, { force: true });
451
+ }
452
+ }
453
+ }
236
454
  export function saveAuthzState(projectRoot, state) {
237
455
  writeJsonContained(projectRoot, authzStatePath(projectRoot), state);
238
456
  }
@@ -5,8 +5,10 @@
5
5
  * agent authors must not independently satisfy implementation-approval gates.
6
6
  * Local-file forgery by a credential-compromised agent remains #983-class OOS.
7
7
  */
8
- /** Operation classes bound on a grant (Wave 1 structural binding + Wave 4 closed verbs). */
9
- export declare const AUTHZ_OPERATIONS: readonly ["edit", "push", "pr", "merge", "settings", "deployment", "issue_mutation", "release-cut", "release-publish", "release-rollback"];
8
+ /** Structural apply op for scope:decompose (#3239 / epic #3237 Q2). Always gated. */
9
+ export declare const SCOPE_DECOMPOSE_APPLY_STRUCTURAL: "scope.decompose.apply.structural";
10
+ /** Operation classes bound on a grant (Wave 1 structural binding + Wave 4 closed verbs + #3239). */
11
+ export declare const AUTHZ_OPERATIONS: readonly ["edit", "push", "pr", "merge", "settings", "deployment", "issue_mutation", "release-cut", "release-publish", "release-rollback", "scope.decompose.apply.structural"];
10
12
  export type AuthzOperation = (typeof AUTHZ_OPERATIONS)[number];
11
13
  /**
12
14
  * Human-origin kinds accepted as approval provenance.
@@ -41,6 +43,21 @@ export interface GrantScope {
41
43
  * for product mutations; approving one cohort does not clear UAT.
42
44
  */
43
45
  readonly cohortId: string | null;
46
+ /**
47
+ * SHA-256 hex of exact bound artifact bytes (decompose draft for #3239).
48
+ * Unset/null = unbound (cannot satisfy structural decompose apply).
49
+ */
50
+ readonly contentDigest?: string | null;
51
+ /**
52
+ * Project-relative parent artifact path for structural decompose apply (#3239).
53
+ * Unset/null = unbound.
54
+ */
55
+ readonly parentPath?: string | null;
56
+ /**
57
+ * Project-relative draft/target path for structural decompose apply (#3239).
58
+ * Unset/null = unbound.
59
+ */
60
+ readonly targetPath?: string | null;
44
61
  }
45
62
  export interface GrantSemantics {
46
63
  readonly expiresAt: string | null;
@@ -5,7 +5,9 @@
5
5
  * agent authors must not independently satisfy implementation-approval gates.
6
6
  * Local-file forgery by a credential-compromised agent remains #983-class OOS.
7
7
  */
8
- /** Operation classes bound on a grant (Wave 1 structural binding + Wave 4 closed verbs). */
8
+ /** Structural apply op for scope:decompose (#3239 / epic #3237 Q2). Always gated. */
9
+ export const SCOPE_DECOMPOSE_APPLY_STRUCTURAL = "scope.decompose.apply.structural";
10
+ /** Operation classes bound on a grant (Wave 1 structural binding + Wave 4 closed verbs + #3239). */
9
11
  export const AUTHZ_OPERATIONS = [
10
12
  "edit",
11
13
  "push",
@@ -18,6 +20,8 @@ export const AUTHZ_OPERATIONS = [
18
20
  "release-cut",
19
21
  "release-publish",
20
22
  "release-rollback",
23
+ /** Human-origin exact-draft-digest gate for scope:decompose apply (#3239). */
24
+ SCOPE_DECOMPOSE_APPLY_STRUCTURAL,
21
25
  ];
22
26
  /**
23
27
  * Human-origin kinds accepted as approval provenance.
@@ -38,7 +38,47 @@ export interface ConsumerCheckContractOptions {
38
38
  /** When true, missing CI references warn only (default true for migration). */
39
39
  readonly ciWarnOnly?: boolean;
40
40
  readonly enforce?: boolean;
41
+ /**
42
+ * Inject included framework Taskfile text (canonical `.deft/core/Taskfile.yml`).
43
+ * When undefined, resolve from disk via {@link resolveCanonicalDeftTaskfileInclude}.
44
+ * #3218 greenfield include-only consumers.
45
+ */
46
+ readonly includedFrameworkTaskfileText?: string | null;
47
+ /**
48
+ * Inject included framework `tasks/verify.yml` text next to the canonical include.
49
+ * When undefined, resolve from disk beside the included Taskfile.
50
+ */
51
+ readonly includedVerifyTaskfileText?: string | null;
41
52
  }
53
+ /**
54
+ * Relative path forms accepted for the deft-install canonical Taskfile include
55
+ * (init-deposit `MINIMAL_TASKFILE` / `CANONICAL_TASKFILE_INCLUDE`).
56
+ * Use only as a path-value matcher — never as a whole-file text search (Greptile P1 #3218).
57
+ */
58
+ export declare const CANONICAL_DEFT_TASKFILE_PATH_RE: RegExp;
59
+ /** @deprecated Prefer path/line parsers; kept for export stability. */
60
+ export declare const CANONICAL_DEFT_TASKFILE_INCLUDE_RE: RegExp;
61
+ /**
62
+ * Return the relative path of the canonical `.deft/core/Taskfile.yml` include when
63
+ * present as an **active, direct** go-task `includes:` entry; otherwise null.
64
+ *
65
+ * Greenfield `directive init` deposits an include-only Taskfile (#3218); composition
66
+ * lives in the included framework graph, not in root `check` deps.
67
+ *
68
+ * Greptile P1 (#3218):
69
+ * - Path text in comments / cmds / vars / non-include keys must not match.
70
+ * - Only **direct** include entries count: short form `deft: path` or object form
71
+ * `deft: { taskfile: path }` (immediate property). Nested
72
+ * `includes.some.nested.taskfile` is not an executable go-task include.
73
+ * - Namespace must be `deft` so the documented `task deft:check` entrypoint exists.
74
+ */
75
+ export declare function resolveCanonicalDeftTaskfileInclude(rootTaskfileText: string): string | null;
76
+ /**
77
+ * True when a framework (included) Taskfile composes every required gate via
78
+ * `check` / `check:consumer` / `check:framework-source` deps or an orchestrator body,
79
+ * and `tasks/verify.yml` defines the local task keys.
80
+ */
81
+ export declare function frameworkTaskfileComposesRequiredGates(frameworkTaskfileText: string, verifyTaskfileText: string | null, required?: readonly string[]): boolean;
42
82
  /** True if `text` mentions the gate as a task dep, script, or CLI invocation. */
43
83
  export declare function textReferencesGate(text: string, gateId: string): boolean;
44
84
  /**
@@ -17,6 +17,157 @@ export const REQUIRED_CONSUMER_ENFORCEMENT_GATES = [
17
17
  "verify:scope-provenance",
18
18
  "verify:consumer-check-contract",
19
19
  ];
20
+ /**
21
+ * Relative path forms accepted for the deft-install canonical Taskfile include
22
+ * (init-deposit `MINIMAL_TASKFILE` / `CANONICAL_TASKFILE_INCLUDE`).
23
+ * Use only as a path-value matcher — never as a whole-file text search (Greptile P1 #3218).
24
+ */
25
+ export const CANONICAL_DEFT_TASKFILE_PATH_RE = /^(?:\.\.?\/)?\.deft\/core\/Taskfile\.yml$/i;
26
+ /** `taskfile: ./.deft/core/Taskfile.yml` under an includes: child (object form). */
27
+ const INCLUDE_TASKFILE_KEY_RE = /^taskfile:\s*['"]?((?:\.\.?\/)?\.deft\/core\/Taskfile\.yml)['"]?\s*(?:#.*)?$/i;
28
+ /**
29
+ * Short form under includes: — **must** be namespace `deft` so `task deft:check`
30
+ * remains the documented greenfield entrypoint (Greptile conf residual #3218).
31
+ */
32
+ const INCLUDE_DEFT_SHORT_FORM_RE = /^deft:\s*['"]?((?:\.\.?\/)?\.deft\/core\/Taskfile\.yml)['"]?\s*(?:#.*)?$/i;
33
+ /** Object-form entry start for the `deft` namespace only. */
34
+ const INCLUDE_DEFT_ENTRY_START_RE = /^deft\s*:\s*(?:#.*)?$/i;
35
+ /** @deprecated Prefer path/line parsers; kept for export stability. */
36
+ export const CANONICAL_DEFT_TASKFILE_INCLUDE_RE = INCLUDE_TASKFILE_KEY_RE;
37
+ /**
38
+ * Return the relative path of the canonical `.deft/core/Taskfile.yml` include when
39
+ * present as an **active, direct** go-task `includes:` entry; otherwise null.
40
+ *
41
+ * Greenfield `directive init` deposits an include-only Taskfile (#3218); composition
42
+ * lives in the included framework graph, not in root `check` deps.
43
+ *
44
+ * Greptile P1 (#3218):
45
+ * - Path text in comments / cmds / vars / non-include keys must not match.
46
+ * - Only **direct** include entries count: short form `deft: path` or object form
47
+ * `deft: { taskfile: path }` (immediate property). Nested
48
+ * `includes.some.nested.taskfile` is not an executable go-task include.
49
+ * - Namespace must be `deft` so the documented `task deft:check` entrypoint exists.
50
+ */
51
+ export function resolveCanonicalDeftTaskfileInclude(rootTaskfileText) {
52
+ const lines = rootTaskfileText.replace(/\r\n/g, "\n").split("\n");
53
+ let inIncludes = false;
54
+ let includesIndent = 0;
55
+ /** Indent of direct children under `includes:` (include entry keys). */
56
+ let directChildIndent = null;
57
+ /** Indent of immediate properties under the current object-form include entry. */
58
+ let entryBodyIndent = null;
59
+ /** True while inside the body of the `deft:` include entry. */
60
+ let inDeftEntryBody = false;
61
+ for (const raw of lines) {
62
+ const stripped = raw.trim();
63
+ if (!stripped || stripped.startsWith("#"))
64
+ continue;
65
+ // Full-line list comment: - # ...
66
+ if (/^-\s+#/.test(stripped))
67
+ continue;
68
+ const indent = raw.length - raw.trimStart().length;
69
+ if (!inIncludes) {
70
+ // Top-level includes: only (indent 0 or document-level).
71
+ if (/^includes\s*:/.test(stripped) && indent <= 1) {
72
+ inIncludes = true;
73
+ includesIndent = indent;
74
+ directChildIndent = null;
75
+ entryBodyIndent = null;
76
+ inDeftEntryBody = false;
77
+ }
78
+ continue;
79
+ }
80
+ // Left the includes block: another key at the same or shallower indent.
81
+ if (indent <= includesIndent && /^[\w.-]+\s*:/.test(stripped)) {
82
+ if (/^includes\s*:/.test(stripped)) {
83
+ includesIndent = indent;
84
+ directChildIndent = null;
85
+ entryBodyIndent = null;
86
+ inDeftEntryBody = false;
87
+ continue;
88
+ }
89
+ inIncludes = false;
90
+ directChildIndent = null;
91
+ entryBodyIndent = null;
92
+ inDeftEntryBody = false;
93
+ continue;
94
+ }
95
+ if (indent <= includesIndent)
96
+ continue;
97
+ if (directChildIndent === null) {
98
+ directChildIndent = indent;
99
+ }
100
+ // Direct include entry line (namespace key under includes:).
101
+ if (indent === directChildIndent) {
102
+ entryBodyIndent = null;
103
+ inDeftEntryBody = false;
104
+ const short = INCLUDE_DEFT_SHORT_FORM_RE.exec(stripped);
105
+ if (short?.[1] !== undefined && CANONICAL_DEFT_TASKFILE_PATH_RE.test(short[1])) {
106
+ return short[1].replace(/\\/g, "/");
107
+ }
108
+ // Object-form `deft:` entry — only this namespace exposes task deft:check.
109
+ if (INCLUDE_DEFT_ENTRY_START_RE.test(stripped)) {
110
+ inDeftEntryBody = true;
111
+ }
112
+ continue;
113
+ }
114
+ // Nested under a direct include entry: only immediate props of `deft:` count.
115
+ if (indent > directChildIndent && inDeftEntryBody) {
116
+ if (entryBodyIndent === null) {
117
+ entryBodyIndent = indent;
118
+ }
119
+ // Deeper than one property level → not an executable include taskfile key.
120
+ if (indent !== entryBodyIndent)
121
+ continue;
122
+ const m = INCLUDE_TASKFILE_KEY_RE.exec(stripped);
123
+ if (m?.[1] !== undefined && CANONICAL_DEFT_TASKFILE_PATH_RE.test(m[1])) {
124
+ return m[1].replace(/\\/g, "/");
125
+ }
126
+ }
127
+ }
128
+ return null;
129
+ }
130
+ /**
131
+ * True when a framework (included) Taskfile composes every required gate via
132
+ * `check` / `check:consumer` / `check:framework-source` deps or an orchestrator body,
133
+ * and `tasks/verify.yml` defines the local task keys.
134
+ */
135
+ export function frameworkTaskfileComposesRequiredGates(frameworkTaskfileText, verifyTaskfileText, required = REQUIRED_CONSUMER_ENFORCEMENT_GATES) {
136
+ if (verifyTaskfileText === null || frameworkTaskfileText.trim().length === 0) {
137
+ return false;
138
+ }
139
+ for (const gateId of required) {
140
+ if (!gateId.startsWith("verify:"))
141
+ continue;
142
+ if (!taskDefinedInTaskfileYaml(verifyTaskfileText, gateId.slice("verify:".length))) {
143
+ return false;
144
+ }
145
+ }
146
+ const checkTargets = ["check", "check:consumer", "check:framework-source"];
147
+ for (const target of checkTargets) {
148
+ const deps = extractCheckDeps(frameworkTaskfileText, target);
149
+ const body = extractTaskBody(frameworkTaskfileText, target);
150
+ const defined = deps.length > 0 || body.trim().length > 0;
151
+ if (!defined)
152
+ continue;
153
+ if (taskBodyInvokesCheckOrchestrator(body)) {
154
+ return true;
155
+ }
156
+ const allListed = required.every((gateId) => {
157
+ for (const d of deps) {
158
+ const name = d.trim();
159
+ if (name === gateId)
160
+ return true;
161
+ if (name === `task ${gateId}` || name === `task:${gateId}`)
162
+ return true;
163
+ }
164
+ return false;
165
+ });
166
+ if (allListed)
167
+ return true;
168
+ }
169
+ return false;
170
+ }
20
171
  function readText(path) {
21
172
  try {
22
173
  if (!existsSync(path))
@@ -583,12 +734,46 @@ export function evaluateConsumerCheckContract(projectRoot, options = {}) {
583
734
  }
584
735
  }
585
736
  }
737
+ // Include-only greenfield (#3218): root Taskfile from `directive init` only wires
738
+ // `includes.deft → ./.deft/core/Taskfile.yml`. Composition lives in the included
739
+ // framework Taskfile + tasks/verify.yml. Trust that graph when no local check
740
+ // aggregates are defined. A partial local check aggregate still fails closed —
741
+ // the include must not conceal incomplete root deps.
742
+ let includeComposes = false;
743
+ const includeRel = resolveCanonicalDeftTaskfileInclude(rootTaskfile);
744
+ if (includeRel !== null) {
745
+ let includedTf;
746
+ if (options.includedFrameworkTaskfileText !== undefined) {
747
+ includedTf = options.includedFrameworkTaskfileText;
748
+ }
749
+ else {
750
+ const absInclude = join(root, includeRel.replace(/^\.\//, ""));
751
+ includedTf = readText(absInclude);
752
+ }
753
+ let includedVerify;
754
+ if (options.includedVerifyTaskfileText !== undefined) {
755
+ includedVerify = options.includedVerifyTaskfileText;
756
+ }
757
+ else if (includedTf !== null) {
758
+ const includeDir = join(root, includeRel.replace(/^\.\//, "").replace(/[/\\]Taskfile\.yml$/i, ""));
759
+ includedVerify = readText(join(includeDir, "tasks", "verify.yml"));
760
+ }
761
+ else {
762
+ includedVerify = null;
763
+ }
764
+ if (includedTf !== null) {
765
+ includeComposes = frameworkTaskfileComposesRequiredGates(includedTf, includedVerify, required);
766
+ }
767
+ }
586
768
  // No check aggregates defined at all — still a composition gap when enforce
587
- // (unless trust via whole-file orchestrator is impossible without a body).
769
+ // (unless trust via whole-file orchestrator, or canonical deft include composition).
588
770
  if (!anyAggregateDefined) {
589
- // Preserve prior behavior for empty/missing check tasks: fail each required gate.
590
771
  const invokesAny = taskfileInvokesCheckOrchestrator(rootTaskfile);
591
- if (!(invokesAny && verifyDefinesRequired)) {
772
+ if (includeComposes || (invokesAny && verifyDefinesRequired)) {
773
+ // Include-only greenfield or root orchestrator body — clean for check-task.
774
+ }
775
+ else {
776
+ // Preserve prior behavior for empty/missing check tasks: fail each required gate.
592
777
  for (const gateId of required) {
593
778
  findings.push({
594
779
  gateId,
@@ -1,5 +1,5 @@
1
1
  /**
2
2
  * consumer-check-contract package surface (#3145).
3
3
  */
4
- export { type ConsumerCheckContractFinding, type ConsumerCheckContractOptions, type ConsumerCheckContractResult, evaluateConsumerCheckContract, extractCheckDeps, REQUIRED_CONSUMER_ENFORCEMENT_GATES, requiredGatesFromConsumerList, taskfileInvokesCheckOrchestrator, textReferencesGate, workflowExecutesCheck, } from "./evaluate.js";
4
+ export { CANONICAL_DEFT_TASKFILE_INCLUDE_RE, CANONICAL_DEFT_TASKFILE_PATH_RE, type ConsumerCheckContractFinding, type ConsumerCheckContractOptions, type ConsumerCheckContractResult, evaluateConsumerCheckContract, extractCheckDeps, frameworkTaskfileComposesRequiredGates, REQUIRED_CONSUMER_ENFORCEMENT_GATES, requiredGatesFromConsumerList, resolveCanonicalDeftTaskfileInclude, taskfileInvokesCheckOrchestrator, textReferencesGate, workflowExecutesCheck, } from "./evaluate.js";
5
5
  //# sourceMappingURL=index.d.ts.map
@@ -1,5 +1,5 @@
1
1
  /**
2
2
  * consumer-check-contract package surface (#3145).
3
3
  */
4
- export { evaluateConsumerCheckContract, extractCheckDeps, REQUIRED_CONSUMER_ENFORCEMENT_GATES, requiredGatesFromConsumerList, taskfileInvokesCheckOrchestrator, textReferencesGate, workflowExecutesCheck, } from "./evaluate.js";
4
+ export { CANONICAL_DEFT_TASKFILE_INCLUDE_RE, CANONICAL_DEFT_TASKFILE_PATH_RE, evaluateConsumerCheckContract, extractCheckDeps, frameworkTaskfileComposesRequiredGates, REQUIRED_CONSUMER_ENFORCEMENT_GATES, requiredGatesFromConsumerList, resolveCanonicalDeftTaskfileInclude, taskfileInvokesCheckOrchestrator, textReferencesGate, workflowExecutesCheck, } from "./evaluate.js";
5
5
  //# sourceMappingURL=index.js.map
@@ -12,11 +12,53 @@ interface DetectResult {
12
12
  has_blocking: boolean;
13
13
  }
14
14
  export declare function parseConfidence(body: string): number | null;
15
+ /**
16
+ * Extract verdict-bearing text windows from a bot rolling-summary body (#3225).
17
+ * Prefers Confidence Score sections and Summary:/Decision: lines over whole-body
18
+ * Overview tables that may *describe* advisory phrases without issuing them.
19
+ */
20
+ export declare function extractAdvisoryVerdictRegions(body: string): string;
21
+ /**
22
+ * True when comment body prose records an advisory should-not-merge / not-safe
23
+ * verdict (#3225). Code-fence regions are stripped first so detector docs do
24
+ * not self-trigger (#1004).
25
+ *
26
+ * Line-anchored hybrid scan (PR #3227 residuals):
27
+ * 1. Soft + high-signal **line-anchored** phrases in Confidence / Summary /
28
+ * Decision regions (and whole body)
29
+ * 2. Same line-anchored high-signal set on whole body (standalone warnings)
30
+ * Mid-sentence residual discussion does not hard-block; conf floor still does (#3095).
31
+ */
32
+ export declare function hasShouldNotMergeProse(body: string): boolean;
33
+ /**
34
+ * Structured advisory verdict from bot comment body (#3225 / #1282 style).
35
+ * Confidence is independent of formal GitHub review state; compose with
36
+ * `minGreptileConfidence` (#3095) at the clean gate.
37
+ */
38
+ export interface AdvisoryReviewerVerdict {
39
+ readonly confidence: number | null;
40
+ readonly shouldNotMerge: boolean;
41
+ readonly hasBlocking: boolean;
42
+ }
43
+ export declare function parseAdvisoryReviewerVerdict(body: string): AdvisoryReviewerVerdict;
15
44
  export declare function detect(body: string): DetectResult;
16
45
  export declare function parseLastReviewedShaMarkdownLink(body: string): string | null;
17
46
  export declare function parseLastReviewedShaNaiveInline(body: string): string | null;
18
47
  export declare const BODY_TIER2_P1_ONLY = "Greptile review of head 1234567\n\nConfidence Score: 4/5\n\nLast reviewed commit: [fix: foo bar](https://github.com/deftai/directive/commit/abcdef1234567)\n\nComments:\n\n- **P1 -- wrong exception type for state/limit validation in populate()**\n The current code raises ValueError but the contract calls for InvalidRepoError.\n- **P2 -- minor wording in error message**\n Consider `--repo` instead of `the repo flag`.\n";
19
48
  export declare const BODY_TIER3_NOT_SAFE_ONLY = "Greptile review of head 7654321\n\nConfidence Score: 3/5\n\nLast reviewed commit: [refactor: thing](https://github.com/deftai/directive/commit/0011223344556)\n\nSummary: Not safe to merge until the mocked-import test defect and the two\npreviously filed P1s are resolved.\n";
49
+ /** #3225 live-case shape: conf 3/5 + should-not-merge prose, zero badges. */
50
+ export declare const BODY_ADVISORY_SHOULD_NOT_MERGE_CONF3 = "Greptile review of head advisory01\n\n## Confidence Score: 3/5\n\nSummary: should-not-merge \u2014 residual risk on the auth path is too high for\nthis change set. Formal review state is still Comment (not Changes Requested).\n\nLast reviewed commit: [feat: advisory](https://github.com/deftai/directive/commit/advisory01abcdef12)\n";
51
+ /** Green mechanical path trap: high conf but explicit do-not-merge prose. */
52
+ export declare const BODY_ADVISORY_DO_NOT_MERGE_HIGH_CONF = "Greptile review of head advisory02\n\nConfidence Score: 5/5\n\nNo P0 or P1 issues found via badges.\n\nSummary: Do not merge until the operator documents residual risk.\n\nLast reviewed commit: [docs: note](https://github.com/deftai/directive/commit/advisory02abcdef12)\n";
53
+ /**
54
+ * Overview prose names the detector phrase without issuing a verdict (#3225 residual).
55
+ * Confidence section is clean — must NOT set shouldNotMerge.
56
+ */
57
+ export declare const BODY_ADVISORY_DESCRIPTIVE_ONLY = "Greptile review of head advisory03\n\n<details><summary><h3>Greptile Summary</h3></summary>\n\nThis PR adds should-not-merge prose detection and Not safe to merge matching to\nmerge-ready. Descriptive overview only \u2014 no blocking residual on the product path.\n\n</details>\n\n<details open><summary><h3>Confidence Score: 5/5</h3></summary>\n\nNo P0 or P1 issues found. The change looks clean and well-tested.\n\n</details>\n\nLast reviewed commit: [feat: detector](https://github.com/deftai/directive/commit/advisory03abcdef12)\n";
58
+ /** Standalone high-signal warning outside Confidence section must still block. */
59
+ export declare const BODY_ADVISORY_STANDALONE_NOT_SAFE = "Greptile review of head advisory04\n\n<details open><summary><h3>Confidence Score: 5/5</h3></summary>\n\nLooks solid from a findings perspective.\n\n</details>\n\nNot safe to merge until the operator confirms residual risk handling.\n\nLast reviewed commit: [feat: residual](https://github.com/deftai/directive/commit/advisory04abcdef12)\n";
60
+ /** Subject-prefixed verdict Greptile commonly emits (#3225 residual). */
61
+ export declare const BODY_ADVISORY_SUBJECT_PREFIXED = "Greptile review of head advisory05\n\n## Confidence Score: 4/5\n\nThe PR is not safe to merge until residual risk is documented on the auth path.\n\nLast reviewed commit: [feat: subject](https://github.com/deftai/directive/commit/advisory05abcdef12)\n";
20
62
  export declare const BODY_TIER3_COUNT_PROSE_ONLY = "Greptile review of head deadbeef\n\nConfidence Score: 4/5\n\nLast reviewed commit: [chore: bump](https://github.com/deftai/directive/commit/deadbeefcafe123)\n\nThree P1 findings (two from prior review, one new): wrong exception type for\nstate/limit validation in populate(), misleading skip message, and an\nunguarded import that will fail on Windows.\n";
21
63
  export declare const BODY_NEGATION_GUARDED = "Greptile review of head ffffffff\n\nConfidence Score: 5/5\n\nLast reviewed commit: [feat: clean](https://github.com/deftai/directive/commit/ffffffffabc1234)\n\nSummary: No P0 findings. Zero P1 findings. The PR is ready for merge.\n";
22
64
  export declare const BODY_CLEAN = "Greptile review of head 1111111\n\nConfidence Score: 5/5\n\nLast reviewed commit: [docs: tweak](https://github.com/deftai/directive/commit/1111111aaa2222b)\n\nNo P0 or P1 issues found. The change looks clean and well-tested.\n";