@deftai/directive-core 0.103.0 → 0.104.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 (66) hide show
  1. package/dist/authz/classify.js +167 -4
  2. package/dist/check/gate-lists.d.ts +3 -2
  3. package/dist/check/gate-lists.js +5 -2
  4. package/dist/check/index.d.ts +1 -0
  5. package/dist/check/index.js +1 -0
  6. package/dist/check/session-completed-ac.d.ts +39 -0
  7. package/dist/check/session-completed-ac.js +198 -0
  8. package/dist/doctor/main.js +12 -7
  9. package/dist/fs/contained-write.js +2 -1
  10. package/dist/index.d.ts +1 -0
  11. package/dist/index.js +1 -0
  12. package/dist/intake/clause-derivation.d.ts +37 -0
  13. package/dist/intake/clause-derivation.js +207 -0
  14. package/dist/intake/index.d.ts +1 -0
  15. package/dist/intake/index.js +1 -0
  16. package/dist/intake/issue-ingest.js +2 -38
  17. package/dist/preflight/evaluate.d.ts +5 -0
  18. package/dist/preflight/evaluate.js +14 -0
  19. package/dist/preflight/index.d.ts +1 -1
  20. package/dist/product-first-done-gate/empty-resolution.d.ts +1 -1
  21. package/dist/product-first-done-gate/empty-resolution.js +1 -1
  22. package/dist/product-first-done-gate/evaluate.d.ts +17 -0
  23. package/dist/product-first-done-gate/evaluate.js +61 -10
  24. package/dist/product-first-done-gate/index.d.ts +1 -1
  25. package/dist/product-first-done-gate/index.js +1 -1
  26. package/dist/run-summary/emit.d.ts +42 -2
  27. package/dist/run-summary/emit.js +250 -25
  28. package/dist/run-summary/index.d.ts +1 -1
  29. package/dist/run-summary/index.js +1 -1
  30. package/dist/run-summary/share.d.ts +1 -1
  31. package/dist/run-summary/share.js +2 -2
  32. package/dist/run-summary/types.d.ts +10 -3
  33. package/dist/scope/acceptance-activate-gate.d.ts +1 -1
  34. package/dist/scope/acceptance-activate-gate.js +4 -4
  35. package/dist/scope/acceptance-evidence.d.ts +15 -0
  36. package/dist/scope/acceptance-evidence.js +39 -0
  37. package/dist/scope/capacity-stamp.d.ts +2 -0
  38. package/dist/scope/capacity-stamp.js +4 -0
  39. package/dist/scope/delivery-evidence.d.ts +6 -0
  40. package/dist/scope/delivery-evidence.js +17 -0
  41. package/dist/scope/transition.js +66 -4
  42. package/dist/session/ceremony-dial-evidence.d.ts +42 -0
  43. package/dist/session/ceremony-dial-evidence.js +194 -0
  44. package/dist/session/index.d.ts +1 -0
  45. package/dist/session/index.js +1 -0
  46. package/dist/session/session-start.js +21 -2
  47. package/dist/slice/existing.js +12 -1
  48. package/dist/slice/record.d.ts +8 -5
  49. package/dist/slice/record.js +21 -13
  50. package/dist/telemetry-coverage/evaluate.d.ts +42 -0
  51. package/dist/telemetry-coverage/evaluate.js +148 -0
  52. package/dist/telemetry-coverage/fake-trial.d.ts +45 -0
  53. package/dist/telemetry-coverage/fake-trial.js +194 -0
  54. package/dist/telemetry-coverage/index.d.ts +5 -0
  55. package/dist/telemetry-coverage/index.js +5 -0
  56. package/dist/telemetry-coverage/kinds.d.ts +19 -0
  57. package/dist/telemetry-coverage/kinds.js +52 -0
  58. package/dist/telemetry-coverage/scan-callers.d.ts +25 -0
  59. package/dist/telemetry-coverage/scan-callers.js +168 -0
  60. package/dist/vbrief-reconcile/index.d.ts +2 -0
  61. package/dist/vbrief-reconcile/index.js +1 -0
  62. package/dist/vbrief-reconcile/origin-freshness.d.ts +44 -0
  63. package/dist/vbrief-reconcile/origin-freshness.js +221 -0
  64. package/dist/verify-source/index.d.ts +1 -0
  65. package/dist/verify-source/index.js +1 -0
  66. package/package.json +3 -3
@@ -245,10 +245,27 @@ const DOWNLOADER_DECODER_BINS = new Set([
245
245
  "dpkg-deb",
246
246
  "hg",
247
247
  "msguniq",
248
+ // #3354 residual after #3336: nc/7zz/msgfmt peers + plant writers.
249
+ "nc",
250
+ "netcat",
251
+ "7zz",
252
+ "msgfmt",
253
+ "msgcat",
254
+ "lz4",
255
+ "lzop",
256
+ "unrar",
257
+ "rar",
258
+ "aunpack",
259
+ "atool",
260
+ "ftpget",
261
+ "tftp",
262
+ "sqlite3",
263
+ "crane",
264
+ "objcopy",
248
265
  ]);
249
266
  /**
250
267
  * Archive extractors / alt writers that can plant via pathish operands without
251
- * shell redirects (#3245 / #3288 / #3311 / #3336). Used for pathish authz/kill scans (prefer deny)
268
+ * shell redirects (#3245 / #3288 / #3311 / #3336 / #3354). Used for pathish authz/kill scans (prefer deny)
252
269
  * and write-shape residual under UAT — not bare curl-class URL fetches.
253
270
  */
254
271
  const ARCHIVE_ALT_WRITE_BINS = new Set([
@@ -304,9 +321,26 @@ const ARCHIVE_ALT_WRITE_BINS = new Set([
304
321
  "dpkg-deb",
305
322
  "hg",
306
323
  "msguniq",
324
+ // #3354 residual after #3336.
325
+ "nc",
326
+ "netcat",
327
+ "7zz",
328
+ "msgfmt",
329
+ "msgcat",
330
+ "lz4",
331
+ "lzop",
332
+ "unrar",
333
+ "rar",
334
+ "aunpack",
335
+ "atool",
336
+ "ftpget",
337
+ "tftp",
338
+ "sqlite3",
339
+ "crane",
340
+ "objcopy",
307
341
  ]);
308
342
  /**
309
- * Bins whose pathish operands are scanned for authz/kill destinations (#3213 / #3245 / #3288 / #3311 / #3336).
343
+ * Bins whose pathish operands are scanned for authz/kill destinations (#3213 / #3245 / #3288 / #3311 / #3336 / #3354).
310
344
  * Prefer a Set over a long `||` chain so coverage counts one membership check, not N branches.
311
345
  */
312
346
  const PROTECTED_POSITIONAL_BINS = new Set([
@@ -352,13 +386,35 @@ const PROTECTED_POSITIONAL_BINS = new Set([
352
386
  "dpkg-deb",
353
387
  "hg",
354
388
  "msguniq",
389
+ // #3354 residual: positional dest writers (nc/7zz/unrar/ftpget/crane/objcopy/sqlite3).
390
+ "nc",
391
+ "netcat",
392
+ "7zz",
393
+ "msgfmt",
394
+ "msgcat",
395
+ "lz4",
396
+ "lzop",
397
+ "unrar",
398
+ "rar",
399
+ "aunpack",
400
+ "atool",
401
+ "ftpget",
402
+ "tftp",
403
+ "sqlite3",
404
+ "crane",
405
+ "objcopy",
355
406
  ]);
356
407
  /** wget family (directory-prefix dest flags). */
357
408
  const WGET_FAMILY_BINS = new Set(["wget", "wget2"]);
358
409
  /** aria2 family (dir dest flags). */
359
410
  const ARIA2_FAMILY_BINS = new Set(["aria2c", "aria2"]);
360
- /** 7z family (attached -oDIR only). */
361
- const SEVEN_Z_FAMILY_BINS = new Set(["7z", "7za", "7zr"]);
411
+ /** 7z family (attached -oDIR only). Includes 7zz (#3354). */
412
+ const SEVEN_Z_FAMILY_BINS = new Set(["7z", "7za", "7zr", "7zz"]);
413
+ /** aunpack / atool extract-to dest flags (#3354). */
414
+ const ATOOL_FAMILY_BINS = new Set(["aunpack", "atool"]);
415
+ const ATOOL_DIR_DEST_FLAGS = new Set(["-x", "--extract-to"]);
416
+ /** sqlite3 meta-commands that plant a file dest (#3354). */
417
+ const SQLITE3_OUTPUT_META = [".output", ".once"];
362
418
  /** tar family (chdir -C / --directory). Includes GNU/Schily aliases (#3311). */
363
419
  const TAR_FAMILY_BINS = new Set(["tar", "bsdtar", "gtar", "star", "gnutar"]);
364
420
  /** xh / httpie family (download-dir dest flags) (#3311). */
@@ -474,6 +530,8 @@ function isDownloaderDestFlag(flag, bin, rawFlag) {
474
530
  return true;
475
531
  if (XH_FAMILY_BINS.has(bin) && XH_DIR_DEST_FLAGS.has(flag))
476
532
  return true;
533
+ if (ATOOL_FAMILY_BINS.has(bin) && ATOOL_DIR_DEST_FLAGS.has(flag))
534
+ return true;
477
535
  return false;
478
536
  }
479
537
  /**
@@ -646,6 +704,12 @@ function downloaderDecoderDestinations(tokens) {
646
704
  i++;
647
705
  continue;
648
706
  }
707
+ // aunpack / atool attached -XDIR (#3354)
708
+ if (ATOOL_FAMILY_BINS.has(bin) && n.startsWith("-x") && n.length > 2) {
709
+ dests.push(pathishToken(raw.slice(2)));
710
+ i++;
711
+ continue;
712
+ }
649
713
  // tar attached -CDIR (rare; capital C required)
650
714
  if (TAR_FAMILY_BINS.has(bin) && raw.startsWith("-C") && raw.length > 2) {
651
715
  dests.push(pathishToken(raw.slice(2)));
@@ -696,6 +760,18 @@ function downloaderDecoderDestinations(tokens) {
696
760
  }
697
761
  }
698
762
  }
763
+ // sqlite3 `.output` / `.once` dest (separate token or `.output PATH` in one token) (#3354).
764
+ if (bin === "sqlite3") {
765
+ const meta = sqlite3MetaDest(raw, tokens[i + 1]);
766
+ if (meta !== null) {
767
+ dests.push(meta.dest);
768
+ if (pathishIsAuthzDir(meta.dest) || pathishMentionsKillSwitch(meta.dest)) {
769
+ protectedPathish.push(meta.dest);
770
+ }
771
+ i += meta.consumedNext ? 2 : 1;
772
+ continue;
773
+ }
774
+ }
699
775
  // scp / certutil / rclone / archive extractors: pathish operands (quote-aware glued-op cut).
700
776
  // Under UAT: any `.deft/authz` / kill-switch pathish is fail-closed settings
701
777
  // (read vs write thrash deferred — prefer deny over dest-parser perfection; #3213 / #3245).
@@ -727,6 +803,10 @@ function downloaderDecoderDestinations(tokens) {
727
803
  dests.push(lastPositionalPath);
728
804
  }
729
805
  }
806
+ // #3354: fail-closed dest flags on unknown write-shaped bins (not named-peer only).
807
+ for (const dest of genericProtectedDests(tokens)) {
808
+ dests.push(dest);
809
+ }
730
810
  return dests;
731
811
  }
732
812
  function authzSubcommandFromToken(token) {
@@ -805,6 +885,89 @@ function hasPolicyAuthorityMutator(tokens) {
805
885
  return false;
806
886
  }
807
887
  /** True when pathish token names a kill-switch basename (quote-strip resistant). */
888
+ function pathishIsProtectedDest(pathish) {
889
+ return pathishIsAuthzDir(pathish) || pathishMentionsKillSwitch(pathish);
890
+ }
891
+ /**
892
+ * Fail-closed dest harvest for write-shaped Shell under UAT (#3354).
893
+ * Named-bin parsers above are not the only path: any token that looks like
894
+ * `-o` / `--output` / `--outfile` / `--output-file` (or attached `-oDIR`)
895
+ * whose dest is authz/kill-switch is collected even when the bin is unknown.
896
+ * scp `-o` (OpenSSH option) and cpio `-o` (copy-out) stay excluded.
897
+ */
898
+ function genericProtectedDests(tokens) {
899
+ const dests = [];
900
+ // Track the current command bin — scp `-o` is OpenSSH option, cpio `-o` is copy-out.
901
+ // Skipping only when the current token itself is `scp`/`cpio` missed later `-o` values (#3354).
902
+ let currentBin = "";
903
+ for (let i = 0; i < tokens.length; i++) {
904
+ const raw = tokens[i];
905
+ const n = normalizeToken(raw);
906
+ if (isShellSegmentBreak(raw)) {
907
+ currentBin = "";
908
+ continue;
909
+ }
910
+ if (!n.startsWith("-")) {
911
+ if (isDownloaderDecoderBin(raw)) {
912
+ currentBin = binBareName(raw);
913
+ }
914
+ else if (currentBin.length === 0 &&
915
+ !raw.includes("/") &&
916
+ !raw.includes("\\") &&
917
+ n.length > 0) {
918
+ currentBin = binBareName(raw);
919
+ }
920
+ }
921
+ if (currentBin === "scp" || currentBin === "cpio")
922
+ continue;
923
+ if (n.includes("=") && (n.startsWith("-") || n.startsWith("--"))) {
924
+ const eq = raw.indexOf("=");
925
+ const flag = normalizeToken(raw.slice(0, eq));
926
+ if (DOWNLOADER_FILE_DEST_FLAGS.has(flag)) {
927
+ const dest = pathishToken(raw.slice(eq + 1));
928
+ if (pathishIsProtectedDest(dest))
929
+ dests.push(dest);
930
+ }
931
+ continue;
932
+ }
933
+ if (n.startsWith("-") &&
934
+ !n.startsWith("--") &&
935
+ n.startsWith("-o") &&
936
+ !n.startsWith("-out") &&
937
+ n.length > 2) {
938
+ const dest = pathishToken(raw.slice(2));
939
+ if (pathishIsProtectedDest(dest))
940
+ dests.push(dest);
941
+ continue;
942
+ }
943
+ if (DOWNLOADER_FILE_DEST_FLAGS.has(n)) {
944
+ const next = tokens[i + 1];
945
+ if (next !== undefined && !String(next).startsWith("-") && !isShellSegmentBreak(next)) {
946
+ const dest = pathishToken(next);
947
+ if (pathishIsProtectedDest(dest))
948
+ dests.push(dest);
949
+ }
950
+ }
951
+ }
952
+ return dests;
953
+ }
954
+ function sqlite3MetaDest(raw, next) {
955
+ const p = pathishToken(raw);
956
+ for (const meta of SQLITE3_OUTPUT_META) {
957
+ if (p === meta) {
958
+ if (next !== undefined && !String(next).startsWith("-") && !isShellSegmentBreak(next)) {
959
+ return { dest: pathishToken(next), consumedNext: true };
960
+ }
961
+ return null;
962
+ }
963
+ if (p.startsWith(`${meta} `) || p.startsWith(`${meta}\t`)) {
964
+ const dest = p.slice(meta.length).trim();
965
+ if (dest.length > 0 && dest !== "|")
966
+ return { dest, consumedNext: false };
967
+ }
968
+ }
969
+ return null;
970
+ }
808
971
  function pathishMentionsKillSwitch(pathish) {
809
972
  for (const name of KILL_SWITCH_BASENAMES) {
810
973
  if (pathish === name || pathish.endsWith(`/${name}`) || pathish.includes(name)) {
@@ -10,8 +10,9 @@ export type CheckGateSpec = string | {
10
10
  readonly args?: readonly string[];
11
11
  };
12
12
  /**
13
- * Product-first AC gate (#3284): always first; soft-missing when no active
14
- * xBRIEF so framework self-check is not deadlocked without a story.
13
+ * Product-first AC gate (#3284): always first; `--soft-missing-xbrief` skips
14
+ * only when no story was active this session. A same-session complete targets
15
+ * xbrief/completed instead of skipping (#3357).
15
16
  */
16
17
  export declare const PRODUCT_FIRST_AC_GATE: CheckGateSpec;
17
18
  export declare function checkGateId(spec: CheckGateSpec): string;
@@ -1,8 +1,9 @@
1
1
  /** Gate execution order mirrors Taskfile.yml check targets (#1713 dogfood). */
2
2
  import { PRODUCT_AC_GATE_ID } from "../product-first-done-gate/types.js";
3
3
  /**
4
- * Product-first AC gate (#3284): always first; soft-missing when no active
5
- * xBRIEF so framework self-check is not deadlocked without a story.
4
+ * Product-first AC gate (#3284): always first; `--soft-missing-xbrief` skips
5
+ * only when no story was active this session. A same-session complete targets
6
+ * xbrief/completed instead of skipping (#3357).
6
7
  */
7
8
  export const PRODUCT_FIRST_AC_GATE = {
8
9
  task: PRODUCT_AC_GATE_ID,
@@ -81,6 +82,8 @@ export const FRAMEWORK_CHECK_GATES = [
81
82
  "verify:test-boundary",
82
83
  "verify:scope-provenance",
83
84
  "verify:consumer-check-contract",
85
+ // #3362: dead-surface detector (warn-only this release; no --enforce)
86
+ "verify:telemetry-coverage",
84
87
  "verify:vbrief-conformance",
85
88
  "verify:destructive-gh-verbs",
86
89
  "verify:scm-boundary",
@@ -6,4 +6,5 @@ export { extractGateCause, formatDegradedSkipReport, formatNamedCauseFailure, ty
6
6
  export type { CheckOrchestratorOptions, CheckOrchestratorSeams } from "./orchestrator.js";
7
7
  export { dispatchTaskCheck, isFrameworkRepoRoot, isFrameworkSourceContext, resolveCheckTarget, } from "./orchestrator.js";
8
8
  export { detectTestRunner, type RunnerDetectResult, runnerDetectionTable, type TestRunnerKind, } from "./runner-detect.js";
9
+ export { type ResolveSessionCompletedAcInput, resolveSessionCompletedVerifyAcTarget, SESSION_COMPLETED_AC_REMEDIATION, SESSION_COMPLETED_MARKER_REL, type SessionCompletedAcTarget, type SessionCompletedMarker, sessionCompletedMarkerPath, writeSessionCompletedMarker, } from "./session-completed-ac.js";
9
10
  //# sourceMappingURL=index.d.ts.map
@@ -5,4 +5,5 @@ export { CONSUMER_CHECK_GATES, checkGateId, checkGateSpawnArgs, FRAMEWORK_CHECK_
5
5
  export { extractGateCause, formatDegradedSkipReport, formatNamedCauseFailure, remedyForGate, } from "./named-cause.js";
6
6
  export { dispatchTaskCheck, isFrameworkRepoRoot, isFrameworkSourceContext, resolveCheckTarget, } from "./orchestrator.js";
7
7
  export { detectTestRunner, runnerDetectionTable, } from "./runner-detect.js";
8
+ export { resolveSessionCompletedVerifyAcTarget, SESSION_COMPLETED_AC_REMEDIATION, SESSION_COMPLETED_MARKER_REL, sessionCompletedMarkerPath, writeSessionCompletedMarker, } from "./session-completed-ac.js";
8
9
  //# sourceMappingURL=index.js.map
@@ -0,0 +1,39 @@
1
+ /**
2
+ * Check composition: verify:ac must not soft-skip after a same-session complete (#3357).
3
+ *
4
+ * "No story ever active this session" remains a legitimate skip.
5
+ * "A story completed this session" targets xbrief/completed (most recent).
6
+ * One remediation string when that target cannot be used.
7
+ */
8
+ /** One remediation when check cannot target the just-completed brief (#3357). */
9
+ export declare const SESSION_COMPLETED_AC_REMEDIATION = "Run task verify:ac -- xbrief/completed/<just-completed>.xbrief.json (a story completed this session; check must not soft-skip) (#3357)";
10
+ /** Project-relative marker written by scope:complete for this session (#3357). */
11
+ export declare const SESSION_COMPLETED_MARKER_REL: readonly [".deft", "last-completed.json"];
12
+ export interface SessionCompletedMarker {
13
+ readonly path: string;
14
+ readonly sessionId: string;
15
+ readonly completedAt: string;
16
+ }
17
+ export type SessionCompletedAcTarget = {
18
+ readonly kind: "none";
19
+ } | {
20
+ readonly kind: "target";
21
+ readonly path: string;
22
+ } | {
23
+ readonly kind: "cannot";
24
+ readonly message: string;
25
+ };
26
+ export interface ResolveSessionCompletedAcInput {
27
+ readonly projectRoot: string;
28
+ readonly sessionId?: string | null;
29
+ readonly sessionStartedAt?: Date | null;
30
+ readonly env?: NodeJS.ProcessEnv;
31
+ }
32
+ export declare function sessionCompletedMarkerPath(projectRoot: string): string;
33
+ /** Persist the just-completed brief so check can fail closed if that file is unreadable. */
34
+ export declare function writeSessionCompletedMarker(projectRoot: string, marker: SessionCompletedMarker): void;
35
+ /**
36
+ * Resolve the just-completed brief for this session, if check must target it.
37
+ */
38
+ export declare function resolveSessionCompletedVerifyAcTarget(input: ResolveSessionCompletedAcInput): SessionCompletedAcTarget;
39
+ //# sourceMappingURL=session-completed-ac.d.ts.map
@@ -0,0 +1,198 @@
1
+ /**
2
+ * Check composition: verify:ac must not soft-skip after a same-session complete (#3357).
3
+ *
4
+ * "No story ever active this session" remains a legitimate skip.
5
+ * "A story completed this session" targets xbrief/completed (most recent).
6
+ * One remediation string when that target cannot be used.
7
+ */
8
+ import { existsSync, readdirSync, readFileSync } from "node:fs";
9
+ import { join, resolve } from "node:path";
10
+ import { containedWrite } from "../fs/contained-write.js";
11
+ import { readRitualState } from "../session/ritual-sentinel.js";
12
+ import { parseTimestamp } from "../session/time.js";
13
+ /** One remediation when check cannot target the just-completed brief (#3357). */
14
+ export const SESSION_COMPLETED_AC_REMEDIATION = "Run task verify:ac -- xbrief/completed/<just-completed>.xbrief.json (a story completed this session; check must not soft-skip) (#3357)";
15
+ /** Project-relative marker written by scope:complete for this session (#3357). */
16
+ export const SESSION_COMPLETED_MARKER_REL = [".deft", "last-completed.json"];
17
+ function asRecord(value) {
18
+ if (typeof value === "object" && value !== null && !Array.isArray(value)) {
19
+ return value;
20
+ }
21
+ return null;
22
+ }
23
+ export function sessionCompletedMarkerPath(projectRoot) {
24
+ return join(resolve(projectRoot), ...SESSION_COMPLETED_MARKER_REL);
25
+ }
26
+ /** Persist the just-completed brief so check can fail closed if that file is unreadable. */
27
+ export function writeSessionCompletedMarker(projectRoot, marker) {
28
+ const root = resolve(projectRoot);
29
+ const target = sessionCompletedMarkerPath(root);
30
+ containedWrite({
31
+ root,
32
+ target,
33
+ data: `${JSON.stringify(marker, null, 2)}\n`,
34
+ mode: "replace",
35
+ mkdir: true,
36
+ });
37
+ }
38
+ function readSessionCompletedMarker(projectRoot) {
39
+ const path = sessionCompletedMarkerPath(projectRoot);
40
+ if (!existsSync(path)) {
41
+ return null;
42
+ }
43
+ try {
44
+ const parsed = JSON.parse(readFileSync(path, "utf8"));
45
+ const rec = asRecord(parsed);
46
+ const briefPath = typeof rec?.path === "string" ? rec.path.trim() : "";
47
+ const sessionId = typeof rec?.sessionId === "string" ? rec.sessionId.trim() : "";
48
+ const completedAt = typeof rec?.completedAt === "string" ? rec.completedAt.trim() : "";
49
+ if (briefPath.length === 0 || sessionId.length === 0) {
50
+ return "invalid";
51
+ }
52
+ return { path: briefPath, sessionId, completedAt };
53
+ }
54
+ catch {
55
+ return "invalid";
56
+ }
57
+ }
58
+ function resolveSession(input) {
59
+ const env = input.env ?? process.env;
60
+ const injected = input.sessionId?.trim() ?? "";
61
+ if (injected.length > 0) {
62
+ return { sessionId: injected, startedAt: input.sessionStartedAt ?? null };
63
+ }
64
+ const fromEnv = typeof env.DEFT_SESSION_ID === "string" ? env.DEFT_SESSION_ID.trim() : "";
65
+ const [state] = readRitualState(resolve(input.projectRoot));
66
+ const ritualId = state?.sessionId.trim() ?? "";
67
+ const sessionId = fromEnv.length > 0 ? fromEnv : ritualId;
68
+ // Do not pair DEFT_SESSION_ID with a different ritual-state startedAt.
69
+ const ritualStartedAt = ritualId.length > 0 && ritualId === sessionId ? (state?.startedAt ?? null) : null;
70
+ return {
71
+ sessionId: sessionId.length > 0 ? sessionId : null,
72
+ startedAt: input.sessionStartedAt ?? ritualStartedAt,
73
+ };
74
+ }
75
+ function listCompletedBriefPaths(projectRoot) {
76
+ const paths = [];
77
+ for (const dirName of ["xbrief", "vbrief"]) {
78
+ const completed = join(projectRoot, dirName, "completed");
79
+ if (!existsSync(completed))
80
+ continue;
81
+ let names = [];
82
+ try {
83
+ names = readdirSync(completed)
84
+ .filter((n) => n.endsWith(".xbrief.json") || n.endsWith(".vbrief.json"))
85
+ .sort();
86
+ }
87
+ catch {
88
+ continue;
89
+ }
90
+ for (const name of names) {
91
+ paths.push(join(completed, name));
92
+ }
93
+ }
94
+ return paths;
95
+ }
96
+ function readCompletedCandidate(path) {
97
+ if (!existsSync(path)) {
98
+ return "unreadable";
99
+ }
100
+ let parsed;
101
+ try {
102
+ parsed = JSON.parse(readFileSync(path, "utf8"));
103
+ }
104
+ catch {
105
+ return "unreadable";
106
+ }
107
+ const data = asRecord(parsed);
108
+ const plan = asRecord(data?.plan);
109
+ if (plan === null) {
110
+ return "unreadable";
111
+ }
112
+ const meta = asRecord(plan.metadata);
113
+ if (meta === null) {
114
+ // Historical completed briefs (pre-#3357) often have no plan.metadata.
115
+ // That is not an unreadable this-session target (#3375).
116
+ return { path, completedAtMs: 0, sessionId: null };
117
+ }
118
+ const sessionRaw = typeof meta.completedSessionId === "string" ? meta.completedSessionId.trim() : "";
119
+ const completedAt = parseTimestamp(meta.completedAt);
120
+ return {
121
+ path,
122
+ completedAtMs: completedAt?.getTime() ?? 0,
123
+ sessionId: sessionRaw.length > 0 ? sessionRaw : null,
124
+ };
125
+ }
126
+ function isSameSession(candidate, sessionId, startedAt) {
127
+ if (sessionId !== null && candidate.sessionId === sessionId) {
128
+ return true;
129
+ }
130
+ // Timestamp fallback only for pre-#3357 briefs that never stamped a session id.
131
+ // A brief stamped for a different session must not replace this session's target.
132
+ if (candidate.sessionId === null &&
133
+ startedAt !== null &&
134
+ candidate.completedAtMs >= startedAt.getTime()) {
135
+ return true;
136
+ }
137
+ return false;
138
+ }
139
+ /**
140
+ * Resolve the just-completed brief for this session, if check must target it.
141
+ */
142
+ export function resolveSessionCompletedVerifyAcTarget(input) {
143
+ const projectRoot = resolve(input.projectRoot);
144
+ const { sessionId, startedAt } = resolveSession(input);
145
+ const marker = readSessionCompletedMarker(projectRoot);
146
+ if (marker === "invalid" && sessionId !== null) {
147
+ return { kind: "cannot", message: SESSION_COMPLETED_AC_REMEDIATION };
148
+ }
149
+ if (marker !== null &&
150
+ marker !== "invalid" &&
151
+ sessionId !== null &&
152
+ marker.sessionId === sessionId) {
153
+ const candidate = readCompletedCandidate(marker.path);
154
+ if (candidate === "unreadable") {
155
+ return { kind: "cannot", message: SESSION_COMPLETED_AC_REMEDIATION };
156
+ }
157
+ return { kind: "target", path: candidate.path };
158
+ }
159
+ const paths = listCompletedBriefPaths(projectRoot);
160
+ if (paths.length === 0 || (sessionId === null && startedAt === null)) {
161
+ return { kind: "none" };
162
+ }
163
+ const matched = [];
164
+ let unreadableCount = 0;
165
+ for (const path of paths) {
166
+ const candidate = readCompletedCandidate(path);
167
+ if (candidate === "unreadable") {
168
+ unreadableCount += 1;
169
+ continue;
170
+ }
171
+ if (isSameSession(candidate, sessionId, startedAt)) {
172
+ matched.push(candidate);
173
+ }
174
+ }
175
+ if (matched.length === 0) {
176
+ // Any unreadable completed brief may be this session's target.
177
+ if (unreadableCount > 0) {
178
+ return { kind: "cannot", message: SESSION_COMPLETED_AC_REMEDIATION };
179
+ }
180
+ return { kind: "none" };
181
+ }
182
+ matched.sort((a, b) => {
183
+ if (a.completedAtMs !== b.completedAtMs) {
184
+ return b.completedAtMs - a.completedAtMs;
185
+ }
186
+ return a.path < b.path ? 1 : a.path > b.path ? -1 : 0;
187
+ });
188
+ const latest = matched[0];
189
+ if (latest === undefined) {
190
+ return { kind: "none" };
191
+ }
192
+ const verified = readCompletedCandidate(latest.path);
193
+ if (verified === "unreadable") {
194
+ return { kind: "cannot", message: SESSION_COMPLETED_AC_REMEDIATION };
195
+ }
196
+ return { kind: "target", path: latest.path };
197
+ }
198
+ //# sourceMappingURL=session-completed-ac.js.map
@@ -701,6 +701,17 @@ function runInstallIntegrityChecks(projectRoot, sink, addFinding, seams) {
701
701
  }
702
702
  });
703
703
  const result = runChecks(projectRoot, { isDir, isFile, readText });
704
+ // Exit-exempt check fails stay warnings so lastErrorCount stays 0
705
+ // (gated ritual / PreToolUse). Keep completed-lifecycle-consistency hard.
706
+ const warningOnFail = new Set([
707
+ "legacy-layout",
708
+ "canonical-vendored-npm-signpost",
709
+ "manifest-version-reportable",
710
+ "gitignore-coverage",
711
+ "stale-xbrief-schema-deposit",
712
+ "typescript-7-side-by-side",
713
+ "completed-open-items",
714
+ ]);
704
715
  for (const entry of result.checks ?? []) {
705
716
  const name = String(entry.name ?? "install-integrity");
706
717
  const status = String(entry.status ?? "");
@@ -718,13 +729,7 @@ function runInstallIntegrityChecks(projectRoot, sink, addFinding, seams) {
718
729
  sink.info(`${name}: skip -- ${detail}`);
719
730
  continue;
720
731
  }
721
- if ((name === "legacy-layout" ||
722
- name === "canonical-vendored-npm-signpost" ||
723
- name === "manifest-version-reportable" ||
724
- name === "gitignore-coverage" ||
725
- name === "stale-xbrief-schema-deposit" ||
726
- name === "typescript-7-side-by-side") &&
727
- status === "fail") {
732
+ if (warningOnFail.has(name) && status === "fail") {
728
733
  sink.warn(`${name}: ${detail}`);
729
734
  addFinding({
730
735
  severity: "warning",
@@ -17,7 +17,7 @@
17
17
  *
18
18
  * Refs #2951.
19
19
  */
20
- import { closeSync, constants, existsSync, lstatSync, mkdirSync, openSync, realpathSync, writeSync, } from "node:fs";
20
+ import { closeSync, constants, existsSync, fsyncSync, lstatSync, mkdirSync, openSync, realpathSync, writeSync, } from "node:fs";
21
21
  import { dirname, isAbsolute, relative, resolve } from "node:path";
22
22
  import { assertWriteTargetSafe, ProjectionContainmentError } from "./projection-containment.js";
23
23
  /** Stable machine-readable error codes for contained-write refusals. */
@@ -144,6 +144,7 @@ function writeNoFollow(targetAbs, buf, flags) {
144
144
  }
145
145
  offset += n;
146
146
  }
147
+ fsyncSync(fd);
147
148
  return offset;
148
149
  }
149
150
  finally {
package/dist/index.d.ts CHANGED
@@ -13,6 +13,7 @@ export * as authz from "./authz/index.js";
13
13
  export * as branch from "./branch/index.js";
14
14
  export * as cache from "./cache/index.js";
15
15
  export * as capacity from "./capacity/index.js";
16
+ export * as check from "./check/index.js";
16
17
  export * as codebase from "./codebase/index.js";
17
18
  export * as consumerCheckContract from "./consumer-check-contract/index.js";
18
19
  export * from "./coverage-hotspots/index.js";
package/dist/index.js CHANGED
@@ -13,6 +13,7 @@ export * as authz from "./authz/index.js";
13
13
  export * as branch from "./branch/index.js";
14
14
  export * as cache from "./cache/index.js";
15
15
  export * as capacity from "./capacity/index.js";
16
+ export * as check from "./check/index.js";
16
17
  export * as codebase from "./codebase/index.js";
17
18
  export * as consumerCheckContract from "./consumer-check-contract/index.js";
18
19
  export * from "./coverage-hotspots/index.js";
@@ -0,0 +1,37 @@
1
+ /**
2
+ * Hand-authored briefs run #3323 clause derivation on activate/promote (#3360).
3
+ *
4
+ * issue:ingest already stamps clauses. The dominant consumer path authors the
5
+ * brief by hand; #3334 only required plan.acceptance to exist. This module is
6
+ * the missing derive step.
7
+ */
8
+ import { type AcceptanceClause } from "../verify-ac/clauses.js";
9
+ /** True when activate/promote must run #3323 (absent, empty none_stated, or command-only). */
10
+ export declare function needsClauseDerivation(acceptance: unknown): boolean;
11
+ /** Task-statement text from title, narratives, and item Acceptance fields. */
12
+ export declare function collectTaskStatementFromPlan(plan: Record<string, unknown>): string;
13
+ export interface ClauseDerivationResult {
14
+ readonly applied: boolean;
15
+ readonly clauses: readonly AcceptanceClause[];
16
+ readonly notice: string;
17
+ }
18
+ /**
19
+ * Stamp derived clauses onto plan.acceptance in place. Headless: records
20
+ * chosen_reading, never blocks on a question.
21
+ */
22
+ export declare function applyClauseDerivationToPlan(plan: Record<string, unknown>, options?: {
23
+ readonly projectRoot?: string;
24
+ readonly emitStamp?: boolean;
25
+ }): ClauseDerivationResult;
26
+ /** Stable fingerprint of plan.acceptance for first-write / material-change detection (#3355). */
27
+ export declare function acceptanceFingerprint(acceptance: unknown): string | null;
28
+ /** True when next is a first write or a material change versus previous (#3355). */
29
+ export declare function isMaterialAcceptanceChange(previous: unknown, next: unknown): boolean;
30
+ /** Fail-open acceptance_stamp emission (same contract as issue:ingest / #3355). */
31
+ export declare function emitAcceptanceStampFromPlan(projectRoot: string, plan: unknown, env?: NodeJS.ProcessEnv): void;
32
+ /**
33
+ * Emit acceptance_stamp when plan.acceptance is first written or materially changed.
34
+ * State-observed: callers pass the before/after blocks from any lifecycle verb (#3355).
35
+ */
36
+ export declare function maybeEmitAcceptanceStampFromChange(projectRoot: string, previous: unknown, next: unknown, env?: NodeJS.ProcessEnv): boolean;
37
+ //# sourceMappingURL=clause-derivation.d.ts.map