@deftai/directive-core 0.99.0 → 0.101.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 (166) 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 +241 -30
  4. package/dist/authz/decompose-apply.d.ts +81 -0
  5. package/dist/authz/decompose-apply.js +377 -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/check/cached-orchestrator.d.ts +26 -5
  13. package/dist/check/cached-orchestrator.js +228 -6
  14. package/dist/check/gate-lists.d.ts +10 -4
  15. package/dist/check/gate-lists.js +19 -5
  16. package/dist/check/index.d.ts +3 -2
  17. package/dist/check/index.js +2 -1
  18. package/dist/check/named-cause.d.ts +45 -0
  19. package/dist/check/named-cause.js +121 -0
  20. package/dist/check/orchestrator.js +6 -1
  21. package/dist/doctor/checks.d.ts +44 -0
  22. package/dist/doctor/checks.js +428 -2
  23. package/dist/doctor/help.d.ts +5 -0
  24. package/dist/doctor/help.js +31 -0
  25. package/dist/doctor/index.d.ts +2 -0
  26. package/dist/doctor/index.js +2 -0
  27. package/dist/doctor/main.d.ts +6 -7
  28. package/dist/doctor/main.js +130 -79
  29. package/dist/doctor/session-coda.d.ts +82 -0
  30. package/dist/doctor/session-coda.js +151 -0
  31. package/dist/doctor/types.d.ts +16 -0
  32. package/dist/doctor/which.d.ts +20 -1
  33. package/dist/doctor/which.js +67 -1
  34. package/dist/finish-loop/pr-finish-loop.d.ts +13 -2
  35. package/dist/finish-loop/pr-finish-loop.js +116 -5
  36. package/dist/finish-loop/types.d.ts +1 -1
  37. package/dist/hooks/dispatcher.d.ts +9 -5
  38. package/dist/hooks/dispatcher.js +23 -15
  39. package/dist/hooks/readonly.d.ts +6 -3
  40. package/dist/hooks/readonly.js +44 -25
  41. package/dist/index.d.ts +3 -0
  42. package/dist/index.js +3 -0
  43. package/dist/init-deposit/agent-hooks.d.ts +20 -0
  44. package/dist/init-deposit/agent-hooks.js +51 -31
  45. package/dist/init-deposit/gitignore.js +2 -0
  46. package/dist/intake/issue-ingest.js +36 -0
  47. package/dist/lifecycle/completed-consistency.d.ts +60 -0
  48. package/dist/lifecycle/completed-consistency.js +443 -0
  49. package/dist/lifecycle/completed-tracked-on-delivery.d.ts +47 -0
  50. package/dist/lifecycle/completed-tracked-on-delivery.js +415 -0
  51. package/dist/lifecycle/index.d.ts +2 -0
  52. package/dist/lifecycle/index.js +2 -0
  53. package/dist/literal-acceptance/capture.d.ts +52 -0
  54. package/dist/literal-acceptance/capture.js +658 -0
  55. package/dist/literal-acceptance/evaluate.d.ts +47 -0
  56. package/dist/literal-acceptance/evaluate.js +193 -0
  57. package/dist/literal-acceptance/index.d.ts +12 -0
  58. package/dist/literal-acceptance/index.js +12 -0
  59. package/dist/literal-acceptance/run.d.ts +38 -0
  60. package/dist/literal-acceptance/run.js +216 -0
  61. package/dist/literal-acceptance/safety.d.ts +24 -0
  62. package/dist/literal-acceptance/safety.js +251 -0
  63. package/dist/literal-acceptance/types.d.ts +82 -0
  64. package/dist/literal-acceptance/types.js +26 -0
  65. package/dist/policy/ac-pass-banking.d.ts +58 -0
  66. package/dist/policy/ac-pass-banking.js +150 -0
  67. package/dist/policy/ceremony-dial.d.ts +96 -11
  68. package/dist/policy/ceremony-dial.js +250 -23
  69. package/dist/policy/index.d.ts +2 -0
  70. package/dist/policy/index.js +16 -1
  71. package/dist/policy/merge-approval-head.d.ts +129 -0
  72. package/dist/policy/merge-approval-head.js +461 -0
  73. package/dist/policy/require-human-merge.d.ts +5 -0
  74. package/dist/policy/require-human-merge.js +5 -0
  75. package/dist/pr-merge-readiness/ci-gate.d.ts +20 -3
  76. package/dist/pr-merge-readiness/ci-gate.js +65 -5
  77. package/dist/pr-merge-readiness/compute.d.ts +13 -0
  78. package/dist/pr-merge-readiness/compute.js +99 -14
  79. package/dist/pr-merge-readiness/gh.d.ts +49 -0
  80. package/dist/pr-merge-readiness/gh.js +268 -0
  81. package/dist/pr-merge-readiness/index.d.ts +2 -2
  82. package/dist/pr-merge-readiness/index.js +2 -2
  83. package/dist/pr-merge-readiness/test-gh-fixtures.helpers.js +4 -0
  84. package/dist/pr-wait-mergeable/cascade.d.ts +11 -0
  85. package/dist/pr-wait-mergeable/cascade.js +115 -1
  86. package/dist/pr-wait-mergeable/main.d.ts +4 -0
  87. package/dist/pr-wait-mergeable/main.js +4 -0
  88. package/dist/pr-wait-mergeable/types.d.ts +3 -1
  89. package/dist/pr-wait-mergeable/wrappers.d.ts +6 -1
  90. package/dist/pr-wait-mergeable/wrappers.js +7 -1
  91. package/dist/preflight/evaluate.d.ts +15 -1
  92. package/dist/preflight/evaluate.js +33 -2
  93. package/dist/preflight-cache/evaluate.d.ts +3 -0
  94. package/dist/preflight-cache/evaluate.js +3 -0
  95. package/dist/product-first-done-gate/acceptance.d.ts +25 -0
  96. package/dist/product-first-done-gate/acceptance.js +247 -0
  97. package/dist/product-first-done-gate/check-mode.d.ts +45 -0
  98. package/dist/product-first-done-gate/check-mode.js +148 -0
  99. package/dist/product-first-done-gate/evaluate.d.ts +50 -0
  100. package/dist/product-first-done-gate/evaluate.js +255 -0
  101. package/dist/product-first-done-gate/index.d.ts +11 -0
  102. package/dist/product-first-done-gate/index.js +11 -0
  103. package/dist/product-first-done-gate/types.d.ts +64 -0
  104. package/dist/product-first-done-gate/types.js +61 -0
  105. package/dist/render/framework-commands.js +4 -0
  106. package/dist/run-summary/emit.d.ts +56 -0
  107. package/dist/run-summary/emit.js +171 -0
  108. package/dist/run-summary/index.d.ts +4 -0
  109. package/dist/run-summary/index.js +4 -0
  110. package/dist/run-summary/path.d.ts +25 -0
  111. package/dist/run-summary/path.js +78 -0
  112. package/dist/run-summary/types.d.ts +76 -0
  113. package/dist/run-summary/types.js +21 -0
  114. package/dist/scope/acceptance-evidence.d.ts +110 -0
  115. package/dist/scope/acceptance-evidence.js +422 -0
  116. package/dist/scope/coverage-map.d.ts +98 -0
  117. package/dist/scope/coverage-map.js +558 -0
  118. package/dist/scope/decompose.d.ts +5 -0
  119. package/dist/scope/decompose.js +134 -11
  120. package/dist/scope/index.d.ts +3 -0
  121. package/dist/scope/index.js +3 -0
  122. package/dist/scope/parent-lineage.d.ts +125 -0
  123. package/dist/scope/parent-lineage.js +626 -0
  124. package/dist/scope/transition.d.ts +8 -0
  125. package/dist/scope/transition.js +61 -2
  126. package/dist/session/ac-pass-banking.d.ts +221 -0
  127. package/dist/session/ac-pass-banking.js +761 -0
  128. package/dist/session/active-cli.d.ts +79 -0
  129. package/dist/session/active-cli.js +382 -0
  130. package/dist/session/deposit-sha.d.ts +49 -0
  131. package/dist/session/deposit-sha.js +121 -0
  132. package/dist/session/effort-budget.d.ts +136 -0
  133. package/dist/session/effort-budget.js +352 -0
  134. package/dist/session/index.d.ts +7 -0
  135. package/dist/session/index.js +7 -0
  136. package/dist/session/orientation-compression.d.ts +127 -0
  137. package/dist/session/orientation-compression.js +425 -0
  138. package/dist/session/orientation-state.d.ts +29 -0
  139. package/dist/session/orientation-state.js +85 -0
  140. package/dist/session/session-start.d.ts +31 -0
  141. package/dist/session/session-start.js +228 -2
  142. package/dist/session/toolchain-preflight.d.ts +64 -0
  143. package/dist/session/toolchain-preflight.js +151 -0
  144. package/dist/session/verify-session-ritual.d.ts +14 -0
  145. package/dist/session/verify-session-ritual.js +33 -0
  146. package/dist/story-ready/evaluate.d.ts +11 -0
  147. package/dist/story-ready/evaluate.js +41 -3
  148. package/dist/triage/actions/candidates-log.d.ts +1 -2
  149. package/dist/triage/actions/candidates-log.js +37 -6
  150. package/dist/triage/bootstrap/gitignore.d.ts +1 -1
  151. package/dist/triage/bootstrap/gitignore.js +8 -2
  152. package/dist/triage/bootstrap/index.js +15 -3
  153. package/dist/triage/classify/index.d.ts +1 -0
  154. package/dist/triage/classify/index.js +2 -0
  155. package/dist/triage/classify/label-mirror.js +16 -0
  156. package/dist/triage/classify/mirror-discovery-tip.d.ts +95 -0
  157. package/dist/triage/classify/mirror-discovery-tip.js +234 -0
  158. package/dist/triage/reconcile/reconcile.js +28 -6
  159. package/dist/triage/summary/index.js +12 -4
  160. package/dist/triage/welcome/default-mode.js +16 -0
  161. package/dist/ts-check-lane/run-lane.js +21 -4
  162. package/dist/vbrief-validate/conformance.js +7 -0
  163. package/dist/vitest-runner/coverage-debt-teardown.js +16 -4
  164. package/dist/xbrief-migrate/migrate-project.d.ts +4 -0
  165. package/dist/xbrief-migrate/migrate-project.js +92 -3
  166. package/package.json +15 -3
@@ -1,25 +1,22 @@
1
1
  import { existsSync, readFileSync, statSync, writeFileSync } from "node:fs";
2
2
  import { join, resolve } from "node:path";
3
- import { VBRIEF_VERSION } from "@deftai/directive-types";
4
3
  import { evaluate as evaluateAgentsMdAdvisory } from "../agents-md-advisory/evaluate.js";
5
4
  import { evaluateConsumerGateIntegrity, formatConsumerGateIntegrityFailure, } from "../check/consumer-gate-integrity.js";
6
5
  import { contentRoot } from "../content-root.js";
7
- import { resolveProjectDefinitionPath } from "../layout/resolve.js";
8
6
  import { DEFT_DIRECTIVE_DISABLE_FLAG_NAME, DEFT_DIRECTIVE_DISABLE_STATUS, DEFT_DIRECTIVE_DISABLE_TRACKED_WARNING, detectDeftDirectiveDisable, formatDeftDirectiveDisableMessage, isDeftDirectiveDisableActive, } from "../policy/deft-directive-disable.js";
9
7
  import { detectNoDeftDirective, NO_DEFT_DIRECTIVE_DISABLED_MESSAGE, NO_DEFT_DIRECTIVE_FLAG_NAME, NO_DEFT_DIRECTIVE_INCONSISTENT_MESSAGE, NO_DEFT_DIRECTIVE_INCONSISTENT_POLICY, } from "../policy/no-deft-directive.js";
10
8
  import { describeShadowedPlanExtension, detectShadowedPlanExtensions, } from "../policy/plan-extensions.js";
11
9
  import { loadProjectDefinition } from "../policy/resolve.js";
12
10
  import { checkLocalEngineIntegrity, classify, detectPackageManager, evaluateSkew, reconcileVersions, plan as resolvePlan, } from "../resolution/index.js";
13
- import { classifyXbriefSchemaDistance } from "../staleness-tickler/probe-xbrief.js";
14
11
  import { resolveUserMdPath } from "../user-config/resolve-user-md.js";
15
12
  import { evaluateAgentHooks } from "../verify-env/agent-hooks.js";
16
13
  import { probeAgentHooksLive } from "../verify-env/agent-hooks-live-probe.js";
17
- import { readDeclaredArtifactVersion } from "../xbrief-migrate/transforms.js";
18
14
  import { agentsRefreshPlan, hasV3ManagedMarker } from "./agents-md.js";
19
- import { runChecks } from "./checks.js";
15
+ import { checkXbriefEnvelopeMajorVersion, runChecks, XBRIEF_ENVELOPE_MAJOR_CHECK, XBRIEF_ENVELOPE_MIGRATE_COMMAND, } from "./checks.js";
20
16
  import { CONSUMER_FRAMEWORK_DIRS, EXPECTED_CONTENT_DIRS, EXPECTED_FRAMEWORK_DIRS, NETWORK_DISCLOSURE_LINE, PAYLOAD_STALENESS_OFFLINE_SKIP_MESSAGE, TASKFILE_INCLUDE_SNIPPET, UV_INSTALL_URL, } from "./constants.js";
21
17
  import { decideThrottle, formatIsoZ, readState, renderDoctorStatusLine, writeState, } from "./doctor-state.js";
22
18
  import { formatAllowedFlagsHint, formatUnknownFlagsError, parseDoctorFlags } from "./flags.js";
19
+ import { formatDoctorHelp } from "./help.js";
23
20
  import { pythonJsonDump } from "./json.js";
24
21
  import { parseInstallRootFromAgentsMd } from "./manifest.js";
25
22
  import { runNpmRegistryMirrorCheck } from "./npm-registry.js";
@@ -29,12 +26,11 @@ import { runOpenClawSoftRebindCheck } from "./openclaw-soft-rebind.js";
29
26
  import { createPlainSink } from "./output.js";
30
27
  import { readTextSafe, resolveFrameworkRootForProject, resolvePath, resolveVersion, runningInsideDeftRepo, } from "./paths.js";
31
28
  import { runPayloadStalenessCheck } from "./payload-staleness.js";
29
+ import { loadSessionCodas, projectRootRealpath, resolveSessionCodaLine, SESSION_CODA_ENV, sessionCodaMode, shouldEmitSessionCodaLine, utcDateYmd, } from "./session-coda.js";
32
30
  import { runLocalSignpostChecks } from "./signpost-checks.js";
33
31
  import { classifyTaskfileInclude, formatGatesSurfaceDualRemediation, formatMissingIncludeSnippet, GATES_SURFACE_DEFT_REMEDIATION, includesBlockHasDeftTaskfile, resolveConsumerTaskfile, } from "./taskfile.js";
34
32
  import { defaultWhich } from "./which.js";
35
33
  const DEFAULT_RESOLUTION_PLATFORMS = ["linux", "darwin", "win32"];
36
- /** Next-action command for project-envelope behind-major (#2971). */
37
- const XBRIEF_ENVELOPE_MIGRATE_COMMAND = "deft migrate:xbrief";
38
34
  /**
39
35
  * Read the `packageManager` field (Corepack) from a project package.json, or
40
36
  * null when absent/unreadable. Lets the doctor detect a pnpm project that has
@@ -64,6 +60,10 @@ export function cmdDoctor(args, seams = {}) {
64
60
  sink.info(formatAllowedFlagsHint());
65
61
  return 2;
66
62
  }
63
+ if (flags.help) {
64
+ process.stdout.write(formatDoctorHelp());
65
+ return 0;
66
+ }
67
67
  const sessionMode = flags.session;
68
68
  const fixMode = flags.fix && !sessionMode;
69
69
  const jsonMode = flags.json;
@@ -495,6 +495,15 @@ export function cmdDoctor(args, seams = {}) {
495
495
  sink.blank();
496
496
  if (errorCount === 0 && warningCount === 0) {
497
497
  sink.finalSuccess("System check passed!");
498
+ emitSessionCodaAfterFinalSuccess({
499
+ projectRoot,
500
+ frameworkRoot,
501
+ jsonMode,
502
+ exitOk: true,
503
+ now: nowFn(),
504
+ seams,
505
+ sink,
506
+ });
498
507
  return 0;
499
508
  }
500
509
  if (errorCount) {
@@ -504,8 +513,51 @@ export function cmdDoctor(args, seams = {}) {
504
513
  return 1;
505
514
  }
506
515
  sink.finalWarn(`System check completed with ${warningCount} warning(s).`);
516
+ emitSessionCodaAfterFinalSuccess({
517
+ projectRoot,
518
+ frameworkRoot,
519
+ jsonMode,
520
+ exitOk: true,
521
+ now: nowFn(),
522
+ seams,
523
+ sink,
524
+ });
507
525
  return 0;
508
526
  }
527
+ /**
528
+ * After the final human success footer only (D2): optional off-hint or ✦ coda.
529
+ * Never mid-run Next command: block; never JSON; never hard-fail path (D1).
530
+ */
531
+ function emitSessionCodaAfterFinalSuccess(input) {
532
+ const stdoutIsTty = input.seams.stdoutIsTty ?? (() => process.stdout.isTTY === true);
533
+ // `in` so tests can force unset via `{ sessionCodaEnv: undefined }` without
534
+ // leaking the host process env.
535
+ const envRaw = "sessionCodaEnv" in input.seams ? input.seams.sessionCodaEnv : process.env[SESSION_CODA_ENV];
536
+ const ciRaw = "ciEnv" in input.seams ? input.seams.ciEnv : process.env.CI;
537
+ const ci = Boolean(ciRaw && ciRaw !== "0" && String(ciRaw).toLowerCase() !== "false");
538
+ const shouldEmit = shouldEmitSessionCodaLine({
539
+ tty: stdoutIsTty(),
540
+ ci,
541
+ json: input.jsonMode,
542
+ exitOk: input.exitOk,
543
+ });
544
+ const mode = sessionCodaMode(envRaw);
545
+ const codas = mode === "on"
546
+ ? (input.seams.loadSessionCodas?.(input.frameworkRoot) ??
547
+ loadSessionCodas(input.frameworkRoot, input.seams.readText))
548
+ : [];
549
+ const line = resolveSessionCodaLine({
550
+ mode,
551
+ shouldEmit,
552
+ date: utcDateYmd(input.now),
553
+ projectRoot: projectRootRealpath(input.projectRoot),
554
+ codas,
555
+ });
556
+ if (line === null)
557
+ return;
558
+ input.sink.blank();
559
+ input.sink.raw(line);
560
+ }
509
561
  export function runAgentHooksHealthCheck(projectRoot, consumerContext, sink, addFinding, seams) {
510
562
  const checkName = "agent-hooks-registration";
511
563
  if (!consumerContext) {
@@ -653,6 +705,11 @@ function runInstallIntegrityChecks(projectRoot, sink, addFinding, seams) {
653
705
  const name = String(entry.name ?? "install-integrity");
654
706
  const status = String(entry.status ?? "");
655
707
  const detail = String(entry.detail ?? "");
708
+ // Authoritative envelope-major emission is runXbriefEnvelopeVersionCheck
709
+ // (runs for framework repo + consumers). Skip install-integrity duplicate.
710
+ if (name === XBRIEF_ENVELOPE_MAJOR_CHECK) {
711
+ continue;
712
+ }
656
713
  if (status === "pass") {
657
714
  sink.success(`${name}: pass`);
658
715
  continue;
@@ -954,105 +1011,99 @@ function runTaskfileIncludeCheck(projectRoot, fixMode, jsonMode, sink, addFindin
954
1011
  });
955
1012
  }
956
1013
  /**
957
- * Fail closed when PROJECT-DEFINITION under an xbrief/ layout still declares
958
- * envelope 0.6 while the framework schema is 0.8 (#2971). Greenfield (no
959
- * project definition yet) and current 0.8 envelopes pass. Unreadable paths
960
- * (test seams / permission) skip rather than false-positive fail. Behind-major
961
- * records `deft migrate:xbrief` as the next action — layout rename alone is not
962
- * enough. Distinct from deposited-schema (`stale-xbrief-schema-deposit`) which
963
- * must NOT route to migrate:xbrief when only framework schema files are stale.
1014
+ * Fail closed when any scanned project xbrief lifecycle `*.xbrief.json` envelope
1015
+ * declares a schema major behind the framework target (#2971 / #3243). Greenfield
1016
+ * (no envelopes) skips. Behind-major records `deft migrate:xbrief` as the next
1017
+ * action (hybrid in-place rewrite on already-xbrief trees #3236). Distinct from
1018
+ * deposited-schema (`stale-xbrief-schema-deposit`) which must NOT route to
1019
+ * migrate:xbrief when only framework schema files are stale.
964
1020
  */
965
1021
  export function runXbriefEnvelopeVersionCheck(projectRoot, sink, addFinding, seams) {
966
- const checkName = "xbrief-envelope-version";
967
- const isFile = seams.isFile ?? ((p) => existsSync(p));
968
- const readText = seams.readText ?? readTextSafe;
969
- const targetVersion = VBRIEF_VERSION;
1022
+ const checkName = XBRIEF_ENVELOPE_MAJOR_CHECK;
970
1023
  if (seams.probeXbriefEnvelope) {
971
1024
  const probe = seams.probeXbriefEnvelope(projectRoot);
972
1025
  emitXbriefEnvelopeFinding(checkName, probe, sink, addFinding);
973
1026
  return;
974
1027
  }
975
- let definitionPath;
976
- try {
977
- definitionPath = resolveProjectDefinitionPath(projectRoot);
978
- }
979
- catch {
980
- // Pure vbrief/-only trees are already covered by layout migrate signposts.
981
- const skipMessage = `${checkName}: skip -- legacy-only layout (use layout migrate first)`;
982
- sink.info(skipMessage);
983
- addFinding({
984
- severity: "skip",
985
- message: skipMessage,
986
- check: checkName,
987
- status: "skip",
988
- reason: "legacy-only-layout",
989
- });
990
- return;
991
- }
992
- if (!isFile(definitionPath)) {
993
- const skipMessage = `${checkName}: skip -- no PROJECT-DEFINITION yet (greenfield)`;
994
- sink.info(skipMessage);
995
- addFinding({
996
- severity: "skip",
997
- message: skipMessage,
998
- check: checkName,
999
- status: "skip",
1000
- reason: "no-project-definition",
1001
- });
1002
- return;
1003
- }
1004
- const text = readText(definitionPath);
1005
- if (text === null) {
1006
- // Unreadable path (permissions or injectable seams) is not proof of 0.6.
1007
- const skipMessage = `${checkName}: skip -- PROJECT-DEFINITION unreadable`;
1028
+ const result = checkXbriefEnvelopeMajorVersion(projectRoot, {
1029
+ ...(seams.readText ? { readText: seams.readText } : {}),
1030
+ ...(seams.isFile ? { isFile: seams.isFile } : {}),
1031
+ ...(seams.isDir ? { isDir: seams.isDir } : {}),
1032
+ });
1033
+ const data = (result.data ?? {});
1034
+ const targetVersion = typeof data.target_version === "string"
1035
+ ? data.target_version
1036
+ : String(data.target_version ?? "");
1037
+ const declaredFromData = data.declared_versions;
1038
+ const declaredVersion = Array.isArray(declaredFromData)
1039
+ ? (declaredFromData.find((v) => typeof v === "string") ?? null)
1040
+ : null;
1041
+ if (result.status === "skip") {
1042
+ const skipMessage = `${checkName}: skip -- ${result.detail}`;
1008
1043
  sink.info(skipMessage);
1009
1044
  addFinding({
1010
1045
  severity: "skip",
1011
1046
  message: skipMessage,
1012
1047
  check: checkName,
1013
1048
  status: "skip",
1014
- reason: "unreadable",
1049
+ reason: typeof data.reason === "string" ? data.reason : "skip",
1050
+ target_version: targetVersion || undefined,
1015
1051
  });
1016
1052
  return;
1017
1053
  }
1018
- let declaredVersion = null;
1019
- try {
1020
- const parsed = JSON.parse(text);
1021
- if (typeof parsed === "object" && parsed !== null && !Array.isArray(parsed)) {
1022
- declaredVersion = readDeclaredArtifactVersion(parsed);
1023
- }
1024
- else {
1025
- const skipMessage = `${checkName}: skip -- PROJECT-DEFINITION is not a JSON object`;
1026
- sink.info(skipMessage);
1054
+ if (result.status === "pass") {
1055
+ const status = typeof data.status === "string" ? data.status : "current";
1056
+ if (status === "behind-minor") {
1057
+ // Major-only check (#3243) does not fail closed, but surface an advisory
1058
+ // when the shared scan observed behind-minor distance.
1059
+ const warnMessage = `${checkName}: behind-minor -- ${result.detail}. Prefer \`${XBRIEF_ENVELOPE_MIGRATE_COMMAND}\` when convenient.`;
1060
+ sink.warn(warnMessage);
1027
1061
  addFinding({
1028
- severity: "skip",
1029
- message: skipMessage,
1062
+ severity: "warning",
1063
+ message: warnMessage,
1030
1064
  check: checkName,
1031
- status: "skip",
1032
- reason: "invalid-json-shape",
1065
+ status: "behind-minor",
1066
+ suggestion: XBRIEF_ENVELOPE_MIGRATE_COMMAND,
1067
+ target_version: targetVersion || undefined,
1068
+ next_command: XBRIEF_ENVELOPE_MIGRATE_COMMAND,
1033
1069
  });
1034
1070
  return;
1035
1071
  }
1036
- }
1037
- catch {
1038
- const skipMessage = `${checkName}: skip -- PROJECT-DEFINITION JSON parse failed`;
1039
- sink.info(skipMessage);
1072
+ const okMessage = `${checkName}: ${result.detail}`;
1073
+ sink.success(okMessage);
1040
1074
  addFinding({
1041
1075
  severity: "skip",
1042
- message: skipMessage,
1076
+ message: okMessage,
1043
1077
  check: checkName,
1044
- status: "skip",
1045
- reason: "parse-error",
1078
+ status: "current",
1079
+ target_version: targetVersion || undefined,
1046
1080
  });
1047
1081
  return;
1048
1082
  }
1049
- const distance = classifyXbriefSchemaDistance(declaredVersion, targetVersion);
1050
- emitXbriefEnvelopeFinding(checkName, {
1051
- declaredVersion,
1052
- targetVersion,
1053
- distance,
1054
- stale: distance !== "current",
1055
- }, sink, addFinding);
1083
+ // fail / error → behind-major fail closed (migratable 0.6 or non-migratable)
1084
+ const status = typeof data.status === "string" ? data.status : "behind-major";
1085
+ const nextCommand = typeof data.next_command === "string"
1086
+ ? data.next_command
1087
+ : data.next_command === null
1088
+ ? null
1089
+ : XBRIEF_ENVELOPE_MIGRATE_COMMAND;
1090
+ const suggestion = typeof data.suggestion === "string"
1091
+ ? data.suggestion
1092
+ : (nextCommand ?? `set xBRIEFInfo.version to framework target`);
1093
+ const message = `${checkName}: ${result.detail}`;
1094
+ sink.error(message);
1095
+ addFinding({
1096
+ severity: "error",
1097
+ message,
1098
+ check: checkName,
1099
+ status,
1100
+ suggestion,
1101
+ declared_version: declaredVersion,
1102
+ target_version: targetVersion || undefined,
1103
+ next_command: nextCommand,
1104
+ sample_paths: data.sample_paths,
1105
+ behind_major_count: data.behind_major_count,
1106
+ });
1056
1107
  }
1057
1108
  function emitXbriefEnvelopeFinding(checkName, probe, sink, addFinding) {
1058
1109
  if (probe.distance === "current") {
@@ -0,0 +1,82 @@
1
+ /**
2
+ * Optional deterministic session coda for interactive doctor success (#2712).
3
+ *
4
+ * Human TTY affordance only: never JSON, never AGENTS/skills, never exit-code
5
+ * changes. Env three-state DEFT_SESSION_CODA (unset / 1 / 0). Copy lives in
6
+ * content/doctor/session-coda.json — not embedded as an engine literal.
7
+ */
8
+ /** Env var name (env-only v1; no USER.md key). */
9
+ export declare const SESSION_CODA_ENV = "DEFT_SESSION_CODA";
10
+ /**
11
+ * Fixed discoverability line when env is unset and gates pass (D3).
12
+ * Snapshot-stable; not selected from the pack; no ✦ prefix.
13
+ */
14
+ export declare const SESSION_CODA_OFF_HINT = "Session coda: off (set DEFT_SESSION_CODA=1 to enable)";
15
+ /** U+2726 FOUR RAYED STAR + space — stable; do not localize. */
16
+ export declare const SESSION_CODA_STAR_PREFIX = "\u2726 ";
17
+ /** Max body length; longer pack entries are skipped (fail-open). */
18
+ export declare const SESSION_CODA_MAX_CHARS = 100;
19
+ /** Relative path under contentRoot (source: content/doctor/…; deposit: doctor/…). */
20
+ export declare const SESSION_CODA_REL_PATH: string;
21
+ export type SessionCodaMode = "unset" | "on" | "off";
22
+ /**
23
+ * Map DEFT_SESSION_CODA to the three-state mode (D3).
24
+ * - undefined / "" → unset (discoverability off-hint)
25
+ * - "1" → on (✦ coda)
26
+ * - "0" → off (silent)
27
+ * - any other non-empty value → unset (show correct enable form)
28
+ */
29
+ export declare function sessionCodaMode(envValue: string | undefined): SessionCodaMode;
30
+ /**
31
+ * Whether any extra line (off-hint or coda) is allowed (D1 + surface gates).
32
+ * TTY = stdout.isTTY; CI truthy blocks; json blocks; exitOk = errorCount === 0.
33
+ */
34
+ export declare function shouldEmitSessionCodaLine(input: {
35
+ readonly tty: boolean;
36
+ readonly ci: boolean;
37
+ readonly json: boolean;
38
+ readonly exitOk: boolean;
39
+ }): boolean;
40
+ /**
41
+ * FNV-1a 32-bit (#2712). Documented choice for hash_u32(date + "\\0" + root).
42
+ * No Math.random; same inputs ⇒ same u32 on every platform.
43
+ */
44
+ export declare function hashU32(input: string): number;
45
+ /** UTC calendar date yyyy-mm-dd from a Date (stable for 24h across a team day). */
46
+ export declare function utcDateYmd(date: Date): string;
47
+ /** Best-effort realpath; falls back to the given path when realpath fails. */
48
+ export declare function projectRootRealpath(projectRoot: string): string;
49
+ /**
50
+ * Select one coda body from the pack (enabled path only).
51
+ * index = hash_u32(utc_date + "\\0" + project_root_realpath) % len(codas)
52
+ * Empty list → null (omit ✦; doctor still 0).
53
+ */
54
+ export declare function selectSessionCoda(input: {
55
+ readonly date: string;
56
+ readonly projectRoot: string;
57
+ readonly codas: readonly string[];
58
+ }): string | null;
59
+ /** Skip non-strings and bodies over SESSION_CODA_MAX_CHARS. */
60
+ export declare function filterCodaEntries(raw: unknown): string[];
61
+ /**
62
+ * Load + filter coda lines from content. Fail-open: missing/unreadable/invalid
63
+ * / empty after filter → [] (omit ✦; no error noise).
64
+ */
65
+ export declare function loadSessionCodas(frameworkRoot: string, readText?: (path: string) => string | null): string[];
66
+ /** Format enabled coda line: ✦ + space + body. */
67
+ export declare function formatSessionCodaEnabledLine(body: string): string;
68
+ export interface ResolveSessionCodaLineInput {
69
+ readonly mode: SessionCodaMode;
70
+ readonly shouldEmit: boolean;
71
+ readonly date: string;
72
+ readonly projectRoot: string;
73
+ readonly codas: readonly string[];
74
+ }
75
+ /**
76
+ * Resolve the single extra line (or null) after final success footer.
77
+ * Does not print; caller places after blank line post footer (D2).
78
+ */
79
+ export declare function resolveSessionCodaLine(input: ResolveSessionCodaLineInput): string | null;
80
+ /** Help / env-docs fragment for DEFT_SESSION_CODA three-state (#2712). */
81
+ export declare function sessionCodaHelpText(): string;
82
+ //# sourceMappingURL=session-coda.d.ts.map
@@ -0,0 +1,151 @@
1
+ /**
2
+ * Optional deterministic session coda for interactive doctor success (#2712).
3
+ *
4
+ * Human TTY affordance only: never JSON, never AGENTS/skills, never exit-code
5
+ * changes. Env three-state DEFT_SESSION_CODA (unset / 1 / 0). Copy lives in
6
+ * content/doctor/session-coda.json — not embedded as an engine literal.
7
+ */
8
+ import { readFileSync, realpathSync } from "node:fs";
9
+ import { join } from "node:path";
10
+ import { contentRoot } from "../content-root.js";
11
+ /** Env var name (env-only v1; no USER.md key). */
12
+ export const SESSION_CODA_ENV = "DEFT_SESSION_CODA";
13
+ /**
14
+ * Fixed discoverability line when env is unset and gates pass (D3).
15
+ * Snapshot-stable; not selected from the pack; no ✦ prefix.
16
+ */
17
+ export const SESSION_CODA_OFF_HINT = "Session coda: off (set DEFT_SESSION_CODA=1 to enable)";
18
+ /** U+2726 FOUR RAYED STAR + space — stable; do not localize. */
19
+ export const SESSION_CODA_STAR_PREFIX = "\u2726 ";
20
+ /** Max body length; longer pack entries are skipped (fail-open). */
21
+ export const SESSION_CODA_MAX_CHARS = 100;
22
+ /** Relative path under contentRoot (source: content/doctor/…; deposit: doctor/…). */
23
+ export const SESSION_CODA_REL_PATH = join("doctor", "session-coda.json");
24
+ /**
25
+ * Map DEFT_SESSION_CODA to the three-state mode (D3).
26
+ * - undefined / "" → unset (discoverability off-hint)
27
+ * - "1" → on (✦ coda)
28
+ * - "0" → off (silent)
29
+ * - any other non-empty value → unset (show correct enable form)
30
+ */
31
+ export function sessionCodaMode(envValue) {
32
+ if (envValue === undefined || envValue === "")
33
+ return "unset";
34
+ if (envValue === "1")
35
+ return "on";
36
+ if (envValue === "0")
37
+ return "off";
38
+ return "unset";
39
+ }
40
+ /**
41
+ * Whether any extra line (off-hint or coda) is allowed (D1 + surface gates).
42
+ * TTY = stdout.isTTY; CI truthy blocks; json blocks; exitOk = errorCount === 0.
43
+ */
44
+ export function shouldEmitSessionCodaLine(input) {
45
+ return input.exitOk === true && input.tty === true && input.ci !== true && input.json !== true;
46
+ }
47
+ /**
48
+ * FNV-1a 32-bit (#2712). Documented choice for hash_u32(date + "\\0" + root).
49
+ * No Math.random; same inputs ⇒ same u32 on every platform.
50
+ */
51
+ export function hashU32(input) {
52
+ let hash = 0x811c9dc5;
53
+ for (let i = 0; i < input.length; i += 1) {
54
+ hash ^= input.charCodeAt(i);
55
+ hash = Math.imul(hash, 0x01000193);
56
+ }
57
+ return hash >>> 0;
58
+ }
59
+ /** UTC calendar date yyyy-mm-dd from a Date (stable for 24h across a team day). */
60
+ export function utcDateYmd(date) {
61
+ return date.toISOString().slice(0, 10);
62
+ }
63
+ /** Best-effort realpath; falls back to the given path when realpath fails. */
64
+ export function projectRootRealpath(projectRoot) {
65
+ try {
66
+ return realpathSync(projectRoot);
67
+ }
68
+ catch {
69
+ return projectRoot;
70
+ }
71
+ }
72
+ /**
73
+ * Select one coda body from the pack (enabled path only).
74
+ * index = hash_u32(utc_date + "\\0" + project_root_realpath) % len(codas)
75
+ * Empty list → null (omit ✦; doctor still 0).
76
+ */
77
+ export function selectSessionCoda(input) {
78
+ const { date, projectRoot, codas } = input;
79
+ if (codas.length === 0)
80
+ return null;
81
+ const key = `${date}\0${projectRoot}`;
82
+ const index = hashU32(key) % codas.length;
83
+ return codas[index] ?? null;
84
+ }
85
+ /** Skip non-strings and bodies over SESSION_CODA_MAX_CHARS. */
86
+ export function filterCodaEntries(raw) {
87
+ if (!Array.isArray(raw))
88
+ return [];
89
+ const out = [];
90
+ for (const entry of raw) {
91
+ if (typeof entry !== "string")
92
+ continue;
93
+ if (entry.length === 0 || entry.length > SESSION_CODA_MAX_CHARS)
94
+ continue;
95
+ out.push(entry);
96
+ }
97
+ return out;
98
+ }
99
+ /**
100
+ * Load + filter coda lines from content. Fail-open: missing/unreadable/invalid
101
+ * / empty after filter → [] (omit ✦; no error noise).
102
+ */
103
+ export function loadSessionCodas(frameworkRoot, readText) {
104
+ const packPath = join(contentRoot(frameworkRoot), SESSION_CODA_REL_PATH);
105
+ try {
106
+ const text = readText !== undefined ? readText(packPath) : readFileSync(packPath, { encoding: "utf8" });
107
+ if (text === null || text === "")
108
+ return [];
109
+ return filterCodaEntries(JSON.parse(text));
110
+ }
111
+ catch {
112
+ return [];
113
+ }
114
+ }
115
+ /** Format enabled coda line: ✦ + space + body. */
116
+ export function formatSessionCodaEnabledLine(body) {
117
+ return `${SESSION_CODA_STAR_PREFIX}${body}`;
118
+ }
119
+ /**
120
+ * Resolve the single extra line (or null) after final success footer.
121
+ * Does not print; caller places after blank line post footer (D2).
122
+ */
123
+ export function resolveSessionCodaLine(input) {
124
+ if (!input.shouldEmit)
125
+ return null;
126
+ if (input.mode === "off")
127
+ return null;
128
+ if (input.mode === "unset")
129
+ return SESSION_CODA_OFF_HINT;
130
+ const body = selectSessionCoda({
131
+ date: input.date,
132
+ projectRoot: input.projectRoot,
133
+ codas: input.codas,
134
+ });
135
+ if (body === null)
136
+ return null;
137
+ return formatSessionCodaEnabledLine(body);
138
+ }
139
+ /** Help / env-docs fragment for DEFT_SESSION_CODA three-state (#2712). */
140
+ export function sessionCodaHelpText() {
141
+ return [
142
+ "Environment:",
143
+ ` ${SESSION_CODA_ENV} Session coda after interactive success (exit 0, TTY, not CI, not --json):`,
144
+ " unset Print discoverability line:",
145
+ ` ${SESSION_CODA_OFF_HINT}`,
146
+ " =1 Print one curated line, e.g.:",
147
+ ` ${formatSessionCodaEnabledLine("Doctor first. Then continue.")}`,
148
+ " =0 Silent (no hint, no coda)",
149
+ ].join("\n");
150
+ }
151
+ //# sourceMappingURL=session-coda.js.map
@@ -123,6 +123,22 @@ export interface DoctorSeams {
123
123
  now?: Date;
124
124
  }) => string | null;
125
125
  readonly isTty?: () => boolean;
126
+ /**
127
+ * stdout TTY probe for session coda (#2712). Defaults to
128
+ * `process.stdout.isTTY === true` (not stdin). Separate from `isTty` (stdin
129
+ * used by --fix prompts).
130
+ */
131
+ readonly stdoutIsTty?: () => boolean;
132
+ /**
133
+ * Override DEFT_SESSION_CODA for tests (#2712). `undefined` means read
134
+ * process.env; pass `""` or omit via explicit nullish env simulation with
135
+ * `undefined` from a seam that returns undefined.
136
+ */
137
+ readonly sessionCodaEnv?: string | undefined;
138
+ /** Override CI env string for session coda gates (#2712). */
139
+ readonly ciEnv?: string | undefined;
140
+ /** Override session-coda pack loader for fail-open tests (#2712). */
141
+ readonly loadSessionCodas?: (frameworkRoot: string) => string[];
126
142
  readonly readYn?: (prompt: string, defaultYes: boolean) => boolean;
127
143
  readonly writeText?: (path: string, content: string) => void;
128
144
  readonly now?: () => Date;
@@ -1,3 +1,22 @@
1
- /** Default PATH lookup mirroring Python `shutil.which`. */
1
+ export interface WhichAllOptions {
2
+ readonly env?: NodeJS.ProcessEnv;
3
+ readonly platform?: NodeJS.Platform;
4
+ readonly exists?: (path: string) => boolean;
5
+ /** Optional executable predicate (defaults to file + X_OK on posix). */
6
+ readonly isExecutable?: (path: string) => boolean;
7
+ }
8
+ /**
9
+ * Enumerate every PATH match for `name` (PATH order, first = highest precedence).
10
+ * Pure PATH scan — no shell locator — so tests and win32/posix stay hermetic and
11
+ * gated ritual never executes a PATH-substituted `which`/`where` (#3233).
12
+ * Only files (posix: executable bit) are accepted — not bare directories.
13
+ */
14
+ export declare function whichAllFromPath(name: string, options?: WhichAllOptions): string[];
15
+ /**
16
+ * All PATH matches for `name` without shelling out (#3233 security).
17
+ * Alias of {@link whichAllFromPath} — never runs bare `which` / `where`.
18
+ */
19
+ export declare function defaultWhichAll(name: string, options?: WhichAllOptions): string[];
20
+ /** Default PATH lookup mirroring Python `shutil.which` (first match only). */
2
21
  export declare function defaultWhich(name: string): string | null;
3
22
  //# sourceMappingURL=which.d.ts.map
@@ -1,5 +1,71 @@
1
1
  import { execFileSync } from "node:child_process";
2
- /** Default PATH lookup mirroring Python `shutil.which`. */
2
+ import { accessSync, constants, existsSync, statSync } from "node:fs";
3
+ import { posix, win32 } from "node:path";
4
+ function defaultIsExecutable(path, platform) {
5
+ try {
6
+ const st = statSync(path);
7
+ // Directories named `deft` must not shadow later executables (#3233 Greptile).
8
+ if (st.isDirectory())
9
+ return false;
10
+ if (!st.isFile() && !st.isSymbolicLink())
11
+ return false;
12
+ if (platform === "win32") {
13
+ return true;
14
+ }
15
+ accessSync(path, constants.X_OK);
16
+ return true;
17
+ }
18
+ catch {
19
+ return false;
20
+ }
21
+ }
22
+ /**
23
+ * Enumerate every PATH match for `name` (PATH order, first = highest precedence).
24
+ * Pure PATH scan — no shell locator — so tests and win32/posix stay hermetic and
25
+ * gated ritual never executes a PATH-substituted `which`/`where` (#3233).
26
+ * Only files (posix: executable bit) are accepted — not bare directories.
27
+ */
28
+ export function whichAllFromPath(name, options = {}) {
29
+ const env = options.env ?? process.env;
30
+ const platform = options.platform ?? process.platform;
31
+ const exists = options.exists ?? existsSync;
32
+ const isExecutable = options.isExecutable ?? ((p) => defaultIsExecutable(p, platform));
33
+ const pathValue = env.PATH ?? env.Path ?? "";
34
+ if (pathValue === "") {
35
+ return [];
36
+ }
37
+ const isWindows = platform === "win32";
38
+ const exts = isWindows ? (env.PATHEXT ?? ".COM;.EXE;.BAT;.CMD").split(";") : [""];
39
+ const sep = isWindows ? ";" : ":";
40
+ const joinPath = isWindows ? win32.join : posix.join;
41
+ const seen = new Set();
42
+ const out = [];
43
+ for (const dir of pathValue.split(sep)) {
44
+ if (dir === "")
45
+ continue;
46
+ for (const ext of exts) {
47
+ const candidate = joinPath(dir, `${name}${ext}`);
48
+ const key = isWindows ? candidate.toLowerCase() : candidate;
49
+ if (seen.has(key))
50
+ continue;
51
+ if (exists(candidate) && isExecutable(candidate)) {
52
+ seen.add(key);
53
+ out.push(candidate);
54
+ // One match per PATH directory (first PATHEXT hit wins on win32).
55
+ break;
56
+ }
57
+ }
58
+ }
59
+ return out;
60
+ }
61
+ /**
62
+ * All PATH matches for `name` without shelling out (#3233 security).
63
+ * Alias of {@link whichAllFromPath} — never runs bare `which` / `where`.
64
+ */
65
+ export function defaultWhichAll(name, options = {}) {
66
+ return whichAllFromPath(name, options);
67
+ }
68
+ /** Default PATH lookup mirroring Python `shutil.which` (first match only). */
3
69
  export function defaultWhich(name) {
4
70
  const locator = process.platform === "win32" ? "where" : "which";
5
71
  try {