@deftai/directive-core 0.93.0 → 0.95.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 (118) hide show
  1. package/dist/authz/classify.js +485 -0
  2. package/dist/cache/fetch.d.ts +29 -0
  3. package/dist/cache/fetch.js +131 -4
  4. package/dist/cache/operations.d.ts +8 -0
  5. package/dist/cache/operations.js +67 -4
  6. package/dist/check/cached-orchestrator.js +11 -0
  7. package/dist/check/consumer-gate-integrity.d.ts +68 -0
  8. package/dist/check/consumer-gate-integrity.js +265 -0
  9. package/dist/check/index.d.ts +1 -0
  10. package/dist/check/index.js +1 -0
  11. package/dist/check/orchestrator.js +10 -0
  12. package/dist/content-contracts/skills/greptile-detector.d.ts +9 -0
  13. package/dist/content-contracts/skills/greptile-detector.js +9 -2
  14. package/dist/doctor/main.js +98 -9
  15. package/dist/doctor/types.d.ts +2 -2
  16. package/dist/finish-loop/pr-finish-loop.js +1 -0
  17. package/dist/freshness/bind.d.ts +67 -0
  18. package/dist/freshness/bind.js +201 -0
  19. package/dist/freshness/cli.d.ts +30 -0
  20. package/dist/freshness/cli.js +180 -0
  21. package/dist/freshness/compare.d.ts +14 -0
  22. package/dist/freshness/compare.js +134 -0
  23. package/dist/freshness/generation.d.ts +44 -0
  24. package/dist/freshness/generation.js +172 -0
  25. package/dist/freshness/index.d.ts +13 -0
  26. package/dist/freshness/index.js +13 -0
  27. package/dist/freshness/report.d.ts +43 -0
  28. package/dist/freshness/report.js +139 -0
  29. package/dist/freshness/types.d.ts +70 -0
  30. package/dist/freshness/types.js +30 -0
  31. package/dist/handoff-evidence/index.d.ts +8 -0
  32. package/dist/handoff-evidence/index.js +7 -0
  33. package/dist/handoff-evidence/validate.d.ts +105 -0
  34. package/dist/handoff-evidence/validate.js +423 -0
  35. package/dist/hooks/dispatcher.d.ts +27 -2
  36. package/dist/hooks/dispatcher.js +134 -14
  37. package/dist/hooks/readonly.d.ts +14 -0
  38. package/dist/hooks/readonly.js +126 -0
  39. package/dist/index.d.ts +2 -0
  40. package/dist/index.js +2 -0
  41. package/dist/init-deposit/agent-hooks.d.ts +1 -1
  42. package/dist/init-deposit/agent-hooks.js +5 -2
  43. package/dist/init-deposit/init-deposit.d.ts +7 -1
  44. package/dist/init-deposit/init-deposit.js +24 -4
  45. package/dist/init-deposit/refresh.d.ts +9 -1
  46. package/dist/init-deposit/refresh.js +48 -5
  47. package/dist/intake/issue-ingest.d.ts +20 -0
  48. package/dist/intake/issue-ingest.js +58 -0
  49. package/dist/policy/index.d.ts +4 -0
  50. package/dist/policy/index.js +50 -1
  51. package/dist/policy/min-greptile-confidence.d.ts +57 -0
  52. package/dist/policy/min-greptile-confidence.js +123 -0
  53. package/dist/pr-merge-readiness/compute.d.ts +11 -1
  54. package/dist/pr-merge-readiness/compute.js +17 -4
  55. package/dist/pr-merge-readiness/evaluate.d.ts +8 -1
  56. package/dist/pr-merge-readiness/evaluate.js +7 -4
  57. package/dist/pr-merge-readiness/mergeability.d.ts +7 -1
  58. package/dist/pr-merge-readiness/mergeability.js +14 -2
  59. package/dist/pr-monitor/main.d.ts +2 -0
  60. package/dist/pr-monitor/main.js +31 -1
  61. package/dist/pr-monitor/monitor.js +5 -1
  62. package/dist/pr-monitor/readiness.js +3 -1
  63. package/dist/pr-monitor/types.d.ts +8 -0
  64. package/dist/pr-wait-mergeable/cascade.d.ts +8 -0
  65. package/dist/pr-wait-mergeable/cascade.js +28 -1
  66. package/dist/pr-wait-mergeable/main.d.ts +2 -0
  67. package/dist/pr-wait-mergeable/main.js +16 -0
  68. package/dist/pr-wait-mergeable/types.d.ts +3 -1
  69. package/dist/pr-wait-mergeable/wrappers.d.ts +6 -0
  70. package/dist/pr-wait-mergeable/wrappers.js +3 -0
  71. package/dist/pr-watch/constants.d.ts +4 -1
  72. package/dist/pr-watch/constants.js +5 -2
  73. package/dist/pr-watch/main.js +1 -0
  74. package/dist/pr-watch/probe.d.ts +1 -1
  75. package/dist/pr-watch/probe.js +4 -1
  76. package/dist/pr-watch/types.d.ts +7 -1
  77. package/dist/pr-watch/watch.js +3 -2
  78. package/dist/release-e2e/greenfield-python-free-smoke.js +7 -3
  79. package/dist/review-monitor/index.d.ts +1 -0
  80. package/dist/review-monitor/index.js +1 -0
  81. package/dist/review-monitor/l4-owner.d.ts +46 -0
  82. package/dist/review-monitor/l4-owner.js +262 -0
  83. package/dist/session/ritual-entrypoint.d.ts +6 -2
  84. package/dist/session/ritual-entrypoint.js +15 -2
  85. package/dist/session/session-ready.js +31 -14
  86. package/dist/session/session-start.d.ts +2 -1
  87. package/dist/session/session-start.js +66 -3
  88. package/dist/session/verify-session-ritual.d.ts +4 -2
  89. package/dist/session/verify-session-ritual.js +11 -11
  90. package/dist/slash/product-set.js +4 -1
  91. package/dist/swarm/verify-review-clean.d.ts +4 -3
  92. package/dist/swarm/verify-review-clean.js +28 -73
  93. package/dist/tool-events/classify.js +1 -0
  94. package/dist/triage/classify/index.d.ts +9 -0
  95. package/dist/triage/classify/index.js +34 -2
  96. package/dist/triage/classify/label-mirror.d.ts +143 -0
  97. package/dist/triage/classify/label-mirror.js +684 -0
  98. package/dist/triage/help/registry-data.d.ts +7 -7
  99. package/dist/triage/help/registry-data.js +27 -7
  100. package/dist/triage/queue/show.d.ts +1 -1
  101. package/dist/triage/queue/show.js +4 -2
  102. package/dist/triage/welcome/writers.js +16 -7
  103. package/dist/umbrella-current-shape/index.d.ts +45 -0
  104. package/dist/umbrella-current-shape/index.js +162 -8
  105. package/dist/vbrief-reconcile/index.d.ts +2 -2
  106. package/dist/vbrief-reconcile/index.js +1 -1
  107. package/dist/vbrief-reconcile/types.d.ts +14 -0
  108. package/dist/vbrief-reconcile/umbrellas.d.ts +31 -2
  109. package/dist/vbrief-reconcile/umbrellas.js +231 -19
  110. package/dist/vbrief-validate/project-definition.js +1 -1
  111. package/dist/verify-env/agent-hook-readiness.d.ts +42 -0
  112. package/dist/verify-env/agent-hook-readiness.js +150 -0
  113. package/dist/verify-env/agent-hooks-live-probe.d.ts +16 -4
  114. package/dist/verify-env/agent-hooks-live-probe.js +120 -107
  115. package/dist/verify-env/agent-hooks.js +11 -3
  116. package/dist/verify-env/index.d.ts +1 -0
  117. package/dist/verify-env/index.js +1 -0
  118. package/package.json +7 -3
@@ -1,7 +1,8 @@
1
1
  import { existsSync, readdirSync, readFileSync } from "node:fs";
2
2
  import { dirname, join, resolve } from "node:path";
3
3
  import { ContainedWriteError, containedWrite } from "../fs/contained-write.js";
4
- import { GhRestError, InvalidRepoError, restIssueView, runGhApi, } from "../scm/gh-rest.js";
4
+ import { GhRestError, InvalidRepoError, restIssueView, runGhApi, splitRepo, } from "../scm/gh-rest.js";
5
+ import { CURRENT_SHAPE_SIDECAR, isUmbrellaLikeIssue, parseCommentsFromGhStdout, RAW_ISSUE_COMMENTS_KEY, } from "../umbrella-current-shape/index.js";
5
6
  import { DEFAULT_BATCH_SIZE, DEFAULT_DELAY_MS } from "./constants.js";
6
7
  import { CacheError, CacheFetchError } from "./errors.js";
7
8
  import { atomicWriteText } from "./io.js";
@@ -54,20 +55,130 @@ export const DEFAULT_RETRY_AFTER_FALLBACK_S = 60;
54
55
  export const PROGRESS_EVERY_N = 50;
55
56
  const RATE_LIMIT_RE = /(?:HTTP\s*429|API rate limit exceeded|rate limit exceeded)/i;
56
57
  const RETRY_AFTER_RE = /Retry-After:\s*(\d+)/i;
58
+ /**
59
+ * Default REST fetch for an issue's comment thread (#1870).
60
+ * Returns raw REST comment objects (with id/body/html_url/author_association).
61
+ */
62
+ export function restIssueComments(repo, issueNumber, options = {}) {
63
+ const [owner, name] = splitRepo(repo);
64
+ const endpoint = `repos/${owner}/${name}/issues/${issueNumber}/comments?per_page=100`;
65
+ const runner = options.runGhApiFn ?? runGhApi;
66
+ const result = runner(["--paginate", endpoint]);
67
+ if (result.returncode !== 0) {
68
+ throw new GhRestError({
69
+ stderr: result.stderr.trim(),
70
+ exitCode: result.returncode,
71
+ endpoint,
72
+ payload: null,
73
+ hint: "verify repo/issue number and gh auth; comments feed the #1152 current-shape surface",
74
+ });
75
+ }
76
+ try {
77
+ // Reuse the paginated-JSON parser so multi-page comment threads stay intact.
78
+ const mapped = parseCommentsFromGhStdout(result.stdout);
79
+ return mapped.map((c) => ({
80
+ id: c.id,
81
+ body: c.body,
82
+ html_url: c.htmlUrl,
83
+ updated_at: c.updatedAt,
84
+ author_association: c.authorAssociation,
85
+ user: { login: c.authorLogin },
86
+ }));
87
+ }
88
+ catch (exc) {
89
+ throw new GhRestError({
90
+ stderr: `non-JSON comments response: ${exc instanceof Error ? exc.message : String(exc)}`,
91
+ exitCode: 0,
92
+ endpoint,
93
+ payload: null,
94
+ hint: "REST comments endpoint returned non-JSON; check gh / ghx version",
95
+ });
96
+ }
97
+ }
57
98
  let paginatedListerImpl = restIssueListPaginated;
58
99
  let sleepImpl = () => { };
59
100
  let progressWriterImpl = (line) => process.stderr.write(line);
60
101
  let progressFlusherImpl = () => { };
61
102
  let singleIssueFetcherImpl = restIssueView;
103
+ let issueCommentsFetcherImpl = restIssueComments;
62
104
  export function setSingleIssueFetcher(fn) {
63
105
  singleIssueFetcherImpl = fn;
64
106
  }
107
+ export function setIssueCommentsFetcher(fn) {
108
+ issueCommentsFetcherImpl = fn;
109
+ }
65
110
  export function setPaginatedLister(fn) {
66
111
  paginatedListerImpl = fn;
67
112
  }
68
113
  export function setSleepFn(fn) {
69
114
  sleepImpl = fn;
70
115
  }
116
+ /**
117
+ * Attach the issue comment thread onto a raw issue payload when the issue is
118
+ * umbrella/tracker-like (#1870). Non-umbrellas skip the extra REST call.
119
+ * Failures leave the payload unchanged so body-only cache still lands.
120
+ *
121
+ * When `force` is true, re-fetches even if a comments array is already present
122
+ * (closed-state refresh and shape backfill paths).
123
+ */
124
+ export function enrichRawWithCommentsIfUmbrella(repo, raw, options = {}) {
125
+ if (!isUmbrellaLikeIssue(raw)) {
126
+ return raw;
127
+ }
128
+ if (options.force !== true && Array.isArray(raw[RAW_ISSUE_COMMENTS_KEY])) {
129
+ return raw;
130
+ }
131
+ const number = raw.number;
132
+ if (typeof number !== "number" || number <= 0) {
133
+ return raw;
134
+ }
135
+ const fetcher = options.fetchComments ?? issueCommentsFetcherImpl;
136
+ try {
137
+ const comments = fetcher(repo, number);
138
+ return { ...raw, [RAW_ISSUE_COMMENTS_KEY]: [...comments] };
139
+ }
140
+ catch {
141
+ // Soft-fail: cache body still useful; agents can live-fetch via umbrella:current-shape.
142
+ // Preserve a prior comments array when force-refresh fails so we do not
143
+ // drop a previously cached current-shape surface (#1870 Greptile P1).
144
+ if (Array.isArray(raw[RAW_ISSUE_COMMENTS_KEY])) {
145
+ return raw;
146
+ }
147
+ return raw;
148
+ }
149
+ }
150
+ /**
151
+ * True when an on-disk cache entry is umbrella-like but was written before
152
+ * #1870 comment enrichment (no `comments` key on raw.json). Used to override
153
+ * the TTL-fresh skip so body-only entries get one backfill pass.
154
+ */
155
+ export function entryNeedsCurrentShapeBackfill(edir) {
156
+ const rawPath = join(edir, "raw.json");
157
+ if (!existsSync(rawPath)) {
158
+ return false;
159
+ }
160
+ try {
161
+ const raw = JSON.parse(readFileSync(rawPath, "utf8"));
162
+ if (!isUmbrellaLikeIssue(raw)) {
163
+ return false;
164
+ }
165
+ // Missing comments key → never enriched. Empty array is "attempted".
166
+ if (!Object.hasOwn(raw, RAW_ISSUE_COMMENTS_KEY)) {
167
+ return true;
168
+ }
169
+ // Has comments but no sidecar while a shape should exist: rare mid-write
170
+ // race; treat missing sidecar + non-empty comments as needing rewrite.
171
+ if (Array.isArray(raw[RAW_ISSUE_COMMENTS_KEY]) &&
172
+ raw[RAW_ISSUE_COMMENTS_KEY].length > 0 &&
173
+ !existsSync(join(edir, CURRENT_SHAPE_SIDECAR))) {
174
+ return true;
175
+ }
176
+ return false;
177
+ }
178
+ catch {
179
+ return false;
180
+ }
181
+ }
71
182
  export function setProgressWriter(fn, flusher) {
72
183
  progressWriterImpl = fn;
73
184
  progressFlusherImpl = flusher ?? (() => { });
@@ -311,7 +422,7 @@ export function runFetchAll(options) {
311
422
  }
312
423
  for (let i = 0; i < issues.length; i += 1) {
313
424
  const processed = i + 1;
314
- const raw = normaliseRestIssue(issues[i] ?? {});
425
+ let raw = normaliseRestIssue(issues[i] ?? {});
315
426
  const number = raw.number;
316
427
  if (typeof number !== "number" || number <= 0) {
317
428
  report.issuesFailed += 1;
@@ -324,12 +435,24 @@ export function runFetchAll(options) {
324
435
  const key = `${options.repo}/${number}`;
325
436
  const edir = entryDir(source, key, cacheRoot);
326
437
  const metaPath = join(edir, "meta.json");
327
- const skipFresh = !options.force && isFreshFn(metaPath) && !contentDriftNumbers.has(number);
438
+ // #1870 Greptile P1: TTL-fresh must not skip body-only umbrella entries
439
+ // forever — comment enrichment is not part of the content-drift fingerprint.
440
+ const needsShapeBackfill = entryNeedsCurrentShapeBackfill(edir);
441
+ const skipFresh = !options.force &&
442
+ isFreshFn(metaPath) &&
443
+ !contentDriftNumbers.has(number) &&
444
+ !needsShapeBackfill;
328
445
  if (skipFresh) {
329
446
  report.alreadyFresh += 1;
330
447
  }
331
448
  else {
332
449
  try {
450
+ // #1870: umbrella/tracker issues get comment threads so content.md
451
+ // carries the canonical ## Current shape surface, not body alone.
452
+ raw = enrichRawWithCommentsIfUmbrella(options.repo, raw, {
453
+ fetchComments: options.fetchComments,
454
+ force: needsShapeBackfill,
455
+ });
333
456
  doPutFn(key, raw);
334
457
  report.issuesWritten += 1;
335
458
  }
@@ -388,6 +511,7 @@ export function cacheFetchAll(options) {
388
511
  force: options.force,
389
512
  contentDriftNumbers: options.contentDriftNumbers,
390
513
  probeDriftFn: options.probeDriftFn,
514
+ fetchComments: options.fetchComments,
391
515
  });
392
516
  }
393
517
  export class StateRefreshReportImpl {
@@ -509,7 +633,10 @@ export function cacheRefreshClosed(options) {
509
633
  delayMs: options.delayMs ?? DEFAULT_DELAY_MS,
510
634
  fetchSingle: singleIssueFetcherImpl,
511
635
  doPut: (key, raw) => {
512
- cachePut(options.source, key, raw, {
636
+ // #1870 Greptile P1: closed-state rewrite still enriches umbrella comments
637
+ // so current-shape.json is not deleted by a body-only single-issue put.
638
+ const enriched = enrichRawWithCommentsIfUmbrella(options.repo, raw, { force: true });
639
+ cachePut(options.source, key, enriched, {
513
640
  ttlSeconds: options.ttlSeconds,
514
641
  cacheRoot,
515
642
  });
@@ -4,6 +4,14 @@ import { SCANNER_VERSION } from "./scanner.js";
4
4
  import { type Clock } from "./time.js";
5
5
  import type { CacheGetOptions, CachePutOptions, GetResult, PutResult } from "./types.js";
6
6
  export { ProjectionContainmentError as CacheContainmentError };
7
+ /**
8
+ * Render agent-facing content.md for a cache entry.
9
+ *
10
+ * For github-issue sources, appends the canonical `## Current shape` comment
11
+ * when the raw payload carries a comment thread with a maintainer-authored
12
+ * shape (#1870 / #1152). Body alone is never sufficient for umbrella planning.
13
+ */
14
+ export declare function renderContent(source: string, raw: Record<string, unknown>): string;
7
15
  /** Write a cache entry (mirrors `cache.cache_put`). */
8
16
  export declare function cachePut(source: string, key: string, raw: Record<string, unknown>, options?: CachePutOptions): PutResult;
9
17
  /** Read a cache entry (mirrors `cache.cache_get`). */
@@ -2,6 +2,7 @@ import { existsSync, readdirSync, readFileSync, unlinkSync } from "node:fs";
2
2
  import { dirname, join, resolve } from "node:path";
3
3
  import { assertProjectionContained, ProjectionContainmentError, } from "../fs/projection-containment.js";
4
4
  import { pyRepr } from "../scm/py-format.js";
5
+ import { appendCurrentShapeSection, buildCurrentShapeSidecar, CURRENT_SHAPE_SIDECAR, } from "../umbrella-current-shape/index.js";
5
6
  import { ALLOWED_SOURCES, REPO_RE, SOURCE_TTL_SECONDS } from "./constants.js";
6
7
  import { CacheCapBreachedError, CacheError, CacheNotFoundError, CacheValidationError, } from "./errors.js";
7
8
  import { appendAudit, atomicWriteText, fileSize, removeEntryDir, touchMtime } from "./io.js";
@@ -20,7 +21,14 @@ function assertWritableCachePath(cacheRoot, ...segments) {
20
21
  assertProjectionContained(projectDir, target);
21
22
  return target;
22
23
  }
23
- function renderContent(source, raw) {
24
+ /**
25
+ * Render agent-facing content.md for a cache entry.
26
+ *
27
+ * For github-issue sources, appends the canonical `## Current shape` comment
28
+ * when the raw payload carries a comment thread with a maintainer-authored
29
+ * shape (#1870 / #1152). Body alone is never sufficient for umbrella planning.
30
+ */
31
+ export function renderContent(source, raw) {
24
32
  if (source === "github-issue") {
25
33
  const number = raw.number;
26
34
  const title = raw.title ?? "";
@@ -28,7 +36,8 @@ function renderContent(source, raw) {
28
36
  if (typeof number !== "number") {
29
37
  throw new CacheError(`invalid github-issue raw payload: 'number' must be int (got ${typeof number})`);
30
38
  }
31
- return `# #${number}: ${title}\n\n${body}`;
39
+ const base = `# #${number}: ${title}\n\n${body}`;
40
+ return appendCurrentShapeSection(base, raw);
32
41
  }
33
42
  throw new CacheError(`unknown source '${source}': v1 supports ${JSON.stringify([...ALLOWED_SOURCES].sort())}`);
34
43
  }
@@ -96,9 +105,20 @@ export function cachePut(source, key, raw, options = {}) {
96
105
  const edir = entryDir(source, key, cacheRoot);
97
106
  const rawText = pythonJsonDump(raw);
98
107
  const rawSize = Buffer.byteLength(rawText, "utf8");
108
+ // #1870 Greptile P2: account for current-shape sidecar bytes in cap + meta
109
+ // so usage does not underreport by the cumulative sidecar size.
110
+ const plannedSidecar = buildCurrentShapeSidecar(raw);
111
+ const plannedSidecarText = plannedSidecar !== null
112
+ ? `${pythonJsonDump({
113
+ ...plannedSidecar,
114
+ body: plannedSidecar.body,
115
+ })}\n`
116
+ : "";
117
+ const plannedSidecarBytes = Buffer.byteLength(plannedSidecarText, "utf8");
118
+ const entryPayloadBytes = rawSize + plannedSidecarBytes;
99
119
  const existingSize = existingEntrySize(edir);
100
120
  const isNewEntry = existingSize === null;
101
- const incomingDelta = isNewEntry ? rawSize : rawSize - existingSize;
121
+ const incomingDelta = isNewEntry ? entryPayloadBytes : entryPayloadBytes - existingSize;
102
122
  const incomingEntries = isNewEntry ? 1 : 0;
103
123
  const enforceResult = enforceCaps(cacheRoot, {
104
124
  caps: options.caps ?? null,
@@ -132,14 +152,48 @@ export function cachePut(source, key, raw, options = {}) {
132
152
  // #3042: contain cache entry writes against project root (parent of cacheRoot).
133
153
  const projectRoot = dirname(resolve(cacheRoot));
134
154
  atomicWriteText(join(edir, "raw.json"), rawText, { projectRoot });
135
- const authoritativeSize = fileSize(join(edir, "raw.json"));
155
+ const rawFileSize = fileSize(join(edir, "raw.json"));
136
156
  const rendered = renderContent(source, raw);
137
157
  const scanResult = scan(rendered, utcIso(clock, fetched));
138
158
  const contentPath = join(edir, "content.md");
159
+ const shapeSidecarPath = join(edir, CURRENT_SHAPE_SIDECAR);
139
160
  let contentWritten = false;
161
+ let sidecarBytesWritten = 0;
140
162
  if (scanResult.passed) {
141
163
  atomicWriteText(contentPath, scanResult.transformed_content, { projectRoot });
142
164
  contentWritten = true;
165
+ // #1870: persist the selected current-shape comment + permalink beside
166
+ // content.md so triage/gates can read it without re-parsing the markdown.
167
+ const sidecar = plannedSidecar;
168
+ if (sidecar !== null) {
169
+ // Quarantine-scan the sidecar body independently so a hard-fail on the
170
+ // full content.md transform cannot leave a credential-bearing sidecar.
171
+ const shapeScan = scan(sidecar.body, utcIso(clock, fetched));
172
+ if (shapeScan.passed) {
173
+ const sidecarText = `${pythonJsonDump({
174
+ ...sidecar,
175
+ body: shapeScan.transformed_content,
176
+ })}\n`;
177
+ atomicWriteText(shapeSidecarPath, sidecarText, { projectRoot });
178
+ sidecarBytesWritten = Buffer.byteLength(sidecarText, "utf8");
179
+ }
180
+ else if (existsSync(shapeSidecarPath)) {
181
+ try {
182
+ unlinkSync(shapeSidecarPath);
183
+ }
184
+ catch {
185
+ /* ignore */
186
+ }
187
+ }
188
+ }
189
+ else if (existsSync(shapeSidecarPath)) {
190
+ try {
191
+ unlinkSync(shapeSidecarPath);
192
+ }
193
+ catch {
194
+ /* ignore */
195
+ }
196
+ }
143
197
  }
144
198
  else if (existsSync(contentPath)) {
145
199
  try {
@@ -148,7 +202,16 @@ export function cachePut(source, key, raw, options = {}) {
148
202
  catch {
149
203
  /* ignore */
150
204
  }
205
+ if (existsSync(shapeSidecarPath)) {
206
+ try {
207
+ unlinkSync(shapeSidecarPath);
208
+ }
209
+ catch {
210
+ /* ignore */
211
+ }
212
+ }
151
213
  }
214
+ const authoritativeSize = rawFileSize + sidecarBytesWritten;
152
215
  const meta = buildMeta({
153
216
  source,
154
217
  key,
@@ -2,6 +2,7 @@ import { spawnSync } from "node:child_process";
2
2
  import { join, resolve } from "node:path";
3
3
  import { lintShippedRegistry, resolveTaskContract, runWithCache, } from "../cache/task-cache/index.js";
4
4
  import { readCorePackageVersion } from "../engine-version.js";
5
+ import { evaluateConsumerGateIntegrity, formatConsumerGateIntegrityFailure, } from "./consumer-gate-integrity.js";
5
6
  import { resolveCheckTarget } from "./context.js";
6
7
  import { checkGateId, checkGateSpawnArgs, gatesForCheckTarget } from "./gate-lists.js";
7
8
  function captureSpawn(taskBin, args, opts) {
@@ -29,6 +30,16 @@ export function dispatchCachedTaskCheck(frameworkRoot, projectRoot, options = {}
29
30
  const cwd = target === "check:framework-source" ? resolvedFramework : resolvedProject;
30
31
  const gates = gatesForCheckTarget(target);
31
32
  const codeVersion = readCorePackageVersion();
33
+ // #3070: fail loud with deposit-repair guidance when consumer check-graph
34
+ // includes (e.g. tasks/verify.yml for verify:orphan-active) are missing,
35
+ // instead of opaque go-task "Task does not exist" exit 200/201.
36
+ if (target === "check:consumer") {
37
+ const integrity = evaluateConsumerGateIntegrity(resolvedFramework);
38
+ if (!integrity.ok) {
39
+ process.stderr.write(formatConsumerGateIntegrityFailure(integrity));
40
+ return 2;
41
+ }
42
+ }
32
43
  const registryLint = lintShippedRegistry();
33
44
  if (!registryLint.ok) {
34
45
  for (const finding of registryLint.findings.filter((f) => f.kind === "under-declared-input")) {
@@ -0,0 +1,68 @@
1
+ /**
2
+ * Consumer check-graph integrity (#3070).
3
+ *
4
+ * CONSUMER_CHECK_GATES lists Taskfile tasks that must resolve in a vendored
5
+ * deposit. When optional Taskfile includes silently omit `tasks/verify.yml`
6
+ * (etc.), go-task fails with opaque exit 200/201 ("Task does not exist").
7
+ *
8
+ * This module:
9
+ * 1. Maps each gate to its include namespace / root Taskfile surface
10
+ * 2. Proves shipped files define those tasks (static + runtime)
11
+ * 3. Emits a deposit-repair recovery message instead of opaque go-task errors
12
+ */
13
+ import { type CheckGateSpec } from "./gate-lists.js";
14
+ /** Include namespaces required by the consumer check graph (Taskfile includes). */
15
+ export declare const CHECK_GRAPH_REQUIRED_NAMESPACES: readonly ["verify", "toolchain", "vbrief"];
16
+ export type CheckGraphNamespace = (typeof CHECK_GRAPH_REQUIRED_NAMESPACES)[number];
17
+ export declare const CONSUMER_GATE_INTEGRITY_RECOVERY: string;
18
+ export interface ConsumerGateIntegrityFinding {
19
+ readonly gateId: string;
20
+ readonly kind: "missing-include-file" | "missing-task-definition" | "missing-root-taskfile";
21
+ readonly detail: string;
22
+ readonly expectedPath?: string;
23
+ }
24
+ export interface ConsumerGateIntegrityResult {
25
+ readonly ok: boolean;
26
+ readonly findings: readonly ConsumerGateIntegrityFinding[];
27
+ readonly recovery: string;
28
+ }
29
+ export interface ConsumerGateIntegritySeams {
30
+ readonly exists?: (path: string) => boolean;
31
+ readonly readText?: (path: string) => string | null;
32
+ /** Override gate list under test (defaults to CONSUMER_CHECK_GATES). */
33
+ readonly gates?: readonly CheckGateSpec[];
34
+ }
35
+ /** Namespace for a namespaced task (`verify:orphan-active` → `verify`); null for root tasks. */
36
+ export declare function gateNamespace(gateId: string): string | null;
37
+ /** Local task name inside an include (`verify:orphan-active` → `orphan-active`). */
38
+ export declare function gateLocalName(gateId: string): string;
39
+ /** Relative path of the include Taskfile for a namespace. */
40
+ export declare function includeTaskfileRel(namespace: string): string;
41
+ /**
42
+ * Namespaces that CONSUMER_CHECK_GATES require via `ns:task` form.
43
+ * Root-only gates (doctor, verify-strategy-output) do not add a namespace.
44
+ */
45
+ export declare function requiredNamespacesForGates(gates?: readonly CheckGateSpec[]): readonly string[];
46
+ /** True when `localName` is a top-level task key under a Taskfile `tasks:` section. */
47
+ export declare function taskDefinedInTaskfileYaml(text: string, localName: string): boolean;
48
+ /**
49
+ * Parse root Taskfile `includes:` entries: map namespace → { taskfile, optional }.
50
+ * Minimal line parser (avoids a YAML dependency); sufficient for go-task include shape.
51
+ */
52
+ export declare function parseTaskfileIncludes(text: string): ReadonlyMap<string, {
53
+ readonly taskfile: string;
54
+ readonly optional: boolean;
55
+ }>;
56
+ /**
57
+ * Assert every consumer check gate resolves against `frameworkRoot` Taskfile + includes.
58
+ * When the root Taskfile is absent, returns ok with no findings (caller/spawn handles that).
59
+ */
60
+ export declare function evaluateConsumerGateIntegrity(frameworkRoot: string, seams?: ConsumerGateIntegritySeams): ConsumerGateIntegrityResult;
61
+ /** Format integrity failure for stderr (check orchestrator / doctor). */
62
+ export declare function formatConsumerGateIntegrityFailure(result: ConsumerGateIntegrityResult): string;
63
+ /**
64
+ * Namespaces that must not be `optional: true` on the root Taskfile when the
65
+ * check graph depends on them — silent omit is the #3070 failure mode.
66
+ */
67
+ export declare function checkGraphOptionalIncludeViolations(rootTaskfileText: string, namespaces?: readonly string[]): readonly string[];
68
+ //# sourceMappingURL=consumer-gate-integrity.d.ts.map