@deftai/directive-core 0.100.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 (85) hide show
  1. package/dist/authz/classify.js +100 -23
  2. package/dist/authz/decompose-apply.d.ts +14 -0
  3. package/dist/authz/decompose-apply.js +93 -18
  4. package/dist/check/cached-orchestrator.d.ts +26 -5
  5. package/dist/check/cached-orchestrator.js +228 -6
  6. package/dist/check/gate-lists.d.ts +10 -4
  7. package/dist/check/gate-lists.js +19 -5
  8. package/dist/check/index.d.ts +3 -2
  9. package/dist/check/index.js +2 -1
  10. package/dist/check/named-cause.d.ts +45 -0
  11. package/dist/check/named-cause.js +121 -0
  12. package/dist/check/orchestrator.js +6 -1
  13. package/dist/doctor/help.d.ts +5 -0
  14. package/dist/doctor/help.js +31 -0
  15. package/dist/doctor/index.d.ts +2 -0
  16. package/dist/doctor/index.js +2 -0
  17. package/dist/doctor/main.js +58 -0
  18. package/dist/doctor/session-coda.d.ts +82 -0
  19. package/dist/doctor/session-coda.js +151 -0
  20. package/dist/doctor/types.d.ts +16 -0
  21. package/dist/index.d.ts +2 -0
  22. package/dist/index.js +2 -0
  23. package/dist/init-deposit/gitignore.js +2 -0
  24. package/dist/intake/issue-ingest.js +7 -0
  25. package/dist/policy/ac-pass-banking.d.ts +58 -0
  26. package/dist/policy/ac-pass-banking.js +150 -0
  27. package/dist/policy/ceremony-dial.d.ts +29 -0
  28. package/dist/policy/ceremony-dial.js +76 -0
  29. package/dist/policy/index.d.ts +1 -0
  30. package/dist/policy/index.js +15 -1
  31. package/dist/preflight-cache/evaluate.d.ts +3 -0
  32. package/dist/preflight-cache/evaluate.js +3 -0
  33. package/dist/product-first-done-gate/acceptance.d.ts +25 -0
  34. package/dist/product-first-done-gate/acceptance.js +247 -0
  35. package/dist/product-first-done-gate/check-mode.d.ts +45 -0
  36. package/dist/product-first-done-gate/check-mode.js +148 -0
  37. package/dist/product-first-done-gate/evaluate.d.ts +50 -0
  38. package/dist/product-first-done-gate/evaluate.js +255 -0
  39. package/dist/product-first-done-gate/index.d.ts +11 -0
  40. package/dist/product-first-done-gate/index.js +11 -0
  41. package/dist/product-first-done-gate/types.d.ts +64 -0
  42. package/dist/product-first-done-gate/types.js +61 -0
  43. package/dist/run-summary/emit.d.ts +56 -0
  44. package/dist/run-summary/emit.js +171 -0
  45. package/dist/run-summary/index.d.ts +4 -0
  46. package/dist/run-summary/index.js +4 -0
  47. package/dist/run-summary/path.d.ts +25 -0
  48. package/dist/run-summary/path.js +78 -0
  49. package/dist/run-summary/types.d.ts +76 -0
  50. package/dist/run-summary/types.js +21 -0
  51. package/dist/scope/acceptance-evidence.d.ts +34 -0
  52. package/dist/scope/acceptance-evidence.js +84 -10
  53. package/dist/scope/decompose.d.ts +5 -0
  54. package/dist/scope/decompose.js +11 -2
  55. package/dist/session/ac-pass-banking.d.ts +221 -0
  56. package/dist/session/ac-pass-banking.js +761 -0
  57. package/dist/session/deposit-sha.d.ts +49 -0
  58. package/dist/session/deposit-sha.js +121 -0
  59. package/dist/session/effort-budget.d.ts +6 -0
  60. package/dist/session/effort-budget.js +21 -3
  61. package/dist/session/index.d.ts +5 -0
  62. package/dist/session/index.js +5 -0
  63. package/dist/session/orientation-compression.d.ts +127 -0
  64. package/dist/session/orientation-compression.js +425 -0
  65. package/dist/session/orientation-state.d.ts +29 -0
  66. package/dist/session/orientation-state.js +85 -0
  67. package/dist/session/session-start.d.ts +25 -0
  68. package/dist/session/session-start.js +175 -0
  69. package/dist/session/toolchain-preflight.d.ts +64 -0
  70. package/dist/session/toolchain-preflight.js +151 -0
  71. package/dist/triage/bootstrap/gitignore.d.ts +1 -1
  72. package/dist/triage/bootstrap/gitignore.js +8 -2
  73. package/dist/triage/bootstrap/index.js +15 -3
  74. package/dist/triage/classify/index.d.ts +1 -0
  75. package/dist/triage/classify/index.js +2 -0
  76. package/dist/triage/classify/label-mirror.js +16 -0
  77. package/dist/triage/classify/mirror-discovery-tip.d.ts +95 -0
  78. package/dist/triage/classify/mirror-discovery-tip.js +234 -0
  79. package/dist/triage/reconcile/reconcile.js +28 -6
  80. package/dist/triage/summary/index.js +12 -4
  81. package/dist/triage/welcome/default-mode.js +16 -0
  82. package/dist/ts-check-lane/run-lane.js +21 -4
  83. package/dist/vbrief-validate/conformance.js +7 -0
  84. package/dist/vitest-runner/coverage-debt-teardown.js +16 -4
  85. package/package.json +11 -3
@@ -0,0 +1,95 @@
1
+ /**
2
+ * Operator discovery tip for SCM label mirror (#3124).
3
+ *
4
+ * Throttled existence + get-the-most-out-of-it disclosure for cold session /
5
+ * triage welcome / doctor. Hides after operator ack or first successful dry-run.
6
+ * Does not fire on session re-arm (welcome skipped).
7
+ *
8
+ * Never auto-apply; never auto-accept into proposed/.
9
+ */
10
+ /** State file basename under xbrief/.triage-cache/. */
11
+ export declare const MIRROR_DISCOVERY_STATE_FILE = "scm-label-mirror-discovery-state.json";
12
+ /** Display/back-compat relative path. */
13
+ export declare const MIRROR_DISCOVERY_STATE_RELATIVE_PATH: string;
14
+ export interface MirrorDiscoveryState {
15
+ /** ISO timestamp when the tip was last emitted. */
16
+ readonly shownAt?: string;
17
+ /** ISO timestamp when the operator acked (tip stays hidden). */
18
+ readonly ackedAt?: string;
19
+ /** ISO timestamp of first successful --mirror dry-run (tip stays hidden). */
20
+ readonly successfulDryRunAt?: string;
21
+ }
22
+ export interface MirrorDiscoveryTipOptions {
23
+ readonly now?: Date;
24
+ readonly readState?: (path: string) => string | null;
25
+ readonly writeState?: (path: string, content: string) => void;
26
+ /** When false, evaluate eligibility without persisting shownAt. Default true. */
27
+ readonly recordShown?: boolean;
28
+ }
29
+ export declare function parseMirrorDiscoveryState(text: string | null): MirrorDiscoveryState;
30
+ export declare function readMirrorDiscoveryState(projectRoot: string, options?: Pick<MirrorDiscoveryTipOptions, "readState">): MirrorDiscoveryState;
31
+ /** True when tip should still surface (not acked and no successful dry-run yet). */
32
+ export declare function isMirrorDiscoveryTipDue(projectRoot: string, options?: Pick<MirrorDiscoveryTipOptions, "readState">): boolean;
33
+ /**
34
+ * Recommended full five-chip machine map for consumers (#3124 / #2611).
35
+ * Names only — do not invent vocabulary; labels must exist on GitHub.
36
+ */
37
+ export declare const MIRROR_DISCOVERY_RECOMMENDED_ACTION_LABELS: {
38
+ readonly defer: readonly ["triage:deferred"];
39
+ readonly archive: readonly ["triage:archived"];
40
+ readonly accept: readonly ["triage:lifecycle-linked"];
41
+ readonly escalate: readonly ["triage:needs-human"];
42
+ };
43
+ /** Dry-run command shown in the tip (open-only default). */
44
+ export declare const MIRROR_DISCOVERY_DRY_RUN_COMMAND = "deft triage:classify -- --mirror";
45
+ /** Operator ack command that dismisses the tip without a dry-run (#3124 Greptile). */
46
+ export declare const MIRROR_DISCOVERY_ACK_COMMAND = "deft triage:classify -- --ack-discovery";
47
+ /** Policy inspect command. */
48
+ export declare const MIRROR_DISCOVERY_POLICY_SHOW_COMMAND = "deft policy:show --field=plan.policy.triageLabelMirror";
49
+ /**
50
+ * Agent-facing anti-swallow rule embedded in tip output (#3124).
51
+ * When this tip fires, agents MUST restate existence + get-the-most in the
52
+ * user-visible message (not only absorb ceremony output).
53
+ */
54
+ export declare const MIRROR_DISCOVERY_ANTI_SWALLOW_RULE: string;
55
+ /**
56
+ * Full operator-facing tip body (existence + get-the-most).
57
+ * Kept short enough for ceremony but complete enough for PROJECT-DEFINITION edits.
58
+ */
59
+ export declare function formatMirrorDiscoveryTipBody(): string;
60
+ export declare function formatMirrorDiscoveryTip(): string;
61
+ /**
62
+ * Emit tip when due; record shownAt. Returns empty string when throttled.
63
+ * Fail-open on write errors (ceremony must not hard-block).
64
+ */
65
+ export declare function maybeFormatMirrorDiscoveryTip(projectRoot: string, options?: MirrorDiscoveryTipOptions): string;
66
+ /** Persist operator ack so the tip no longer surfaces. */
67
+ export declare function recordMirrorDiscoveryAcked(projectRoot: string, options?: MirrorDiscoveryTipOptions): void;
68
+ /**
69
+ * Persist first successful --mirror dry-run so the tip no longer surfaces.
70
+ * Idempotent: keeps the earliest successfulDryRunAt.
71
+ */
72
+ export declare function recordMirrorDiscoverySuccessfulDryRun(projectRoot: string, options?: MirrorDiscoveryTipOptions): void;
73
+ /** State path helper for tests / docs. */
74
+ export declare function resolveMirrorDiscoveryStatePath(projectRoot: string): string;
75
+ /** Whether a state file currently exists (tests / diagnostics). */
76
+ export declare function mirrorDiscoveryStateExists(projectRoot: string): boolean;
77
+ export interface MirrorDiscoveryDigestCueInput {
78
+ readonly planned: number;
79
+ readonly applied: number;
80
+ readonly skipped_no_match: number;
81
+ readonly skipped_already_triaged: number;
82
+ readonly skipped_closed: number;
83
+ readonly actionLabelsEmpty: boolean;
84
+ readonly dry_run: boolean;
85
+ }
86
+ /** Footer when planned rows exist but actionLabels is empty. */
87
+ export declare const MIRROR_DISCOVERY_EMPTY_ACTION_LABELS_HINT: string;
88
+ /** Footer when open no_match dominates under universal rules. */
89
+ export declare const MIRROR_DISCOVERY_NO_MATCH_DOMINATION_HINT: string;
90
+ /**
91
+ * Build optional digest footer lines for consumer upgrade cues.
92
+ * Empty when neither condition holds.
93
+ */
94
+ export declare function formatMirrorDiscoveryDigestCues(input: MirrorDiscoveryDigestCueInput): readonly string[];
95
+ //# sourceMappingURL=mirror-discovery-tip.d.ts.map
@@ -0,0 +1,234 @@
1
+ /**
2
+ * Operator discovery tip for SCM label mirror (#3124).
3
+ *
4
+ * Throttled existence + get-the-most-out-of-it disclosure for cold session /
5
+ * triage welcome / doctor. Hides after operator ack or first successful dry-run.
6
+ * Does not fire on session re-arm (welcome skipped).
7
+ *
8
+ * Never auto-apply; never auto-accept into proposed/.
9
+ */
10
+ import { existsSync, mkdirSync, readFileSync } from "node:fs";
11
+ import { basename, dirname, join, resolve } from "node:path";
12
+ import { containedWrite } from "../../fs/contained-write.js";
13
+ import { resolveTriageCachePath } from "../cache-path.js";
14
+ /** State file basename under xbrief/.triage-cache/. */
15
+ export const MIRROR_DISCOVERY_STATE_FILE = "scm-label-mirror-discovery-state.json";
16
+ /** Display/back-compat relative path. */
17
+ export const MIRROR_DISCOVERY_STATE_RELATIVE_PATH = join("xbrief", ".triage-cache", MIRROR_DISCOVERY_STATE_FILE);
18
+ function resolveStatePath(projectRoot) {
19
+ return resolveTriageCachePath(projectRoot, MIRROR_DISCOVERY_STATE_FILE);
20
+ }
21
+ function defaultReadState(path) {
22
+ try {
23
+ return readFileSync(path, "utf8");
24
+ }
25
+ catch {
26
+ return null;
27
+ }
28
+ }
29
+ function defaultWriteState(path, content) {
30
+ const dir = dirname(path);
31
+ mkdirSync(dir, { recursive: true });
32
+ // Direct contained write (no tmp/rename) — tip state is advisory and small.
33
+ const base = basename(path);
34
+ containedWrite({
35
+ root: resolve(dir),
36
+ target: base,
37
+ data: content,
38
+ mode: "replace",
39
+ });
40
+ }
41
+ export function parseMirrorDiscoveryState(text) {
42
+ if (text === null)
43
+ return {};
44
+ try {
45
+ const parsed = JSON.parse(text);
46
+ if (parsed !== null && typeof parsed === "object" && !Array.isArray(parsed)) {
47
+ const obj = parsed;
48
+ return {
49
+ ...(typeof obj.shownAt === "string" ? { shownAt: obj.shownAt } : {}),
50
+ ...(typeof obj.ackedAt === "string" ? { ackedAt: obj.ackedAt } : {}),
51
+ ...(typeof obj.successfulDryRunAt === "string"
52
+ ? { successfulDryRunAt: obj.successfulDryRunAt }
53
+ : {}),
54
+ };
55
+ }
56
+ }
57
+ catch {
58
+ // Corrupt advisory state never blocks ceremony.
59
+ }
60
+ return {};
61
+ }
62
+ export function readMirrorDiscoveryState(projectRoot, options = {}) {
63
+ const path = resolveStatePath(projectRoot);
64
+ const read = options.readState ?? defaultReadState;
65
+ return parseMirrorDiscoveryState(read(path));
66
+ }
67
+ function writeMirrorDiscoveryState(projectRoot, state, options = {}) {
68
+ const path = resolveStatePath(projectRoot);
69
+ const write = options.writeState ?? defaultWriteState;
70
+ write(path, `${JSON.stringify(state, null, 2)}\n`);
71
+ }
72
+ /** True when tip should still surface (not acked and no successful dry-run yet). */
73
+ export function isMirrorDiscoveryTipDue(projectRoot, options = {}) {
74
+ const state = readMirrorDiscoveryState(projectRoot, options);
75
+ if (state.ackedAt !== undefined && state.ackedAt.length > 0)
76
+ return false;
77
+ if (state.successfulDryRunAt !== undefined && state.successfulDryRunAt.length > 0) {
78
+ return false;
79
+ }
80
+ return true;
81
+ }
82
+ /**
83
+ * Recommended full five-chip machine map for consumers (#3124 / #2611).
84
+ * Names only — do not invent vocabulary; labels must exist on GitHub.
85
+ */
86
+ export const MIRROR_DISCOVERY_RECOMMENDED_ACTION_LABELS = {
87
+ defer: ["triage:deferred"],
88
+ archive: ["triage:archived"],
89
+ accept: ["triage:lifecycle-linked"],
90
+ escalate: ["triage:needs-human"],
91
+ };
92
+ /** Dry-run command shown in the tip (open-only default). */
93
+ export const MIRROR_DISCOVERY_DRY_RUN_COMMAND = "deft triage:classify -- --mirror";
94
+ /** Operator ack command that dismisses the tip without a dry-run (#3124 Greptile). */
95
+ export const MIRROR_DISCOVERY_ACK_COMMAND = "deft triage:classify -- --ack-discovery";
96
+ /** Policy inspect command. */
97
+ export const MIRROR_DISCOVERY_POLICY_SHOW_COMMAND = "deft policy:show --field=plan.policy.triageLabelMirror";
98
+ /**
99
+ * Agent-facing anti-swallow rule embedded in tip output (#3124).
100
+ * When this tip fires, agents MUST restate existence + get-the-most in the
101
+ * user-visible message (not only absorb ceremony output).
102
+ */
103
+ export const MIRROR_DISCOVERY_ANTI_SWALLOW_RULE = "! Agent anti-swallow (#3124): when this tip fires, restate existence + get-the-most " +
104
+ "in the **user-visible** message (not ceremony dump alone): feature is default-on; " +
105
+ "dry-run first; defaults only stamp **triaged** on matches (control stamp, not disposition); " +
106
+ "board usability is greatly decreased without **actionLabels** (full five-chip map); " +
107
+ "more matches need **triageAutoClassify**; never auto-accept into proposed/; " +
108
+ "where policy lives + policy:show; labels must exist on GitHub; " +
109
+ "applying triaged before actionLabels skips re-enrichment on re-run; " +
110
+ "point at consumer kit #2611 — do not invent label vocabulary.";
111
+ /**
112
+ * Full operator-facing tip body (existence + get-the-most).
113
+ * Kept short enough for ceremony but complete enough for PROJECT-DEFINITION edits.
114
+ */
115
+ export function formatMirrorDiscoveryTipBody() {
116
+ const mapJson = JSON.stringify({
117
+ enabled: true,
118
+ idempotencyLabel: "triaged",
119
+ alwaysLabels: ["triaged"],
120
+ actionLabels: MIRROR_DISCOVERY_RECOMMENDED_ACTION_LABELS,
121
+ }, null, 2);
122
+ return [
123
+ "[deft triage] SCM label mirror discovery (#3124 / #1423):",
124
+ " Feature exists and is **default-on** (plan.policy.triageLabelMirror resolves even when absent).",
125
+ ` Dry-run first (open-only default; --include-closed opt-in): \`${MIRROR_DISCOVERY_DRY_RUN_COMMAND}\``,
126
+ " `--apply` writes labels in batches; **never** auto-accepts into proposed/ (triage:accept is separate).",
127
+ " Defaults only stamp **`triaged`** on **matches** — a machine control stamp (idempotency), not a disposition board.",
128
+ " **Board usability is greatly decreased without `actionLabels`** — every match looks like a single triaged chip.",
129
+ " Recommend full five-chip map (create these labels on GitHub first, or apply fails closed):",
130
+ " defer→triage:deferred, archive→triage:archived, accept→triage:lifecycle-linked,",
131
+ " escalate→triage:needs-human (+ always triaged).",
132
+ " Chip setup (all five) is separate from rule aggressiveness — start rules minimal; escalate carefully.",
133
+ " More matches → configure `plan.policy.triageAutoClassify` in PROJECT-DEFINITION",
134
+ ' (xbrief/PROJECT-DEFINITION.xbrief.json → plan["x-directive/policy"] / docs: plan.policy.*).',
135
+ ` Inspect: \`${MIRROR_DISCOVERY_POLICY_SHOW_COMMAND}\` (and triageAutoClassify).`,
136
+ " Labels must **exist on GitHub** (policy JSON alone is insufficient).",
137
+ " Warn: applying triaged **before** actionLabels stamps issues that re-run then skips",
138
+ " (use --mirror --re-enrich later for additive chips; #3197).",
139
+ " Recommended consumer labels + PD sketch: consumer kit **#2611**",
140
+ " (content/docs/consumer-issue-label-kit.md) — do not invent new triage:* vocabulary.",
141
+ " Depth: commands.md § triage:classify --mirror; tip hides after first successful dry-run",
142
+ ` or operator ack: \`${MIRROR_DISCOVERY_ACK_COMMAND}\` (not every session re-arm).`,
143
+ " Recommended PD sketch:",
144
+ ...mapJson.split("\n").map((line) => ` ${line}`),
145
+ MIRROR_DISCOVERY_ANTI_SWALLOW_RULE,
146
+ ].join("\n");
147
+ }
148
+ export function formatMirrorDiscoveryTip() {
149
+ return `${formatMirrorDiscoveryTipBody()}\n`;
150
+ }
151
+ /**
152
+ * Emit tip when due; record shownAt. Returns empty string when throttled.
153
+ * Fail-open on write errors (ceremony must not hard-block).
154
+ */
155
+ export function maybeFormatMirrorDiscoveryTip(projectRoot, options = {}) {
156
+ if (!isMirrorDiscoveryTipDue(projectRoot, options)) {
157
+ return "";
158
+ }
159
+ if (options.recordShown !== false) {
160
+ try {
161
+ const now = options.now ?? new Date();
162
+ const prior = readMirrorDiscoveryState(projectRoot, options);
163
+ writeMirrorDiscoveryState(projectRoot, {
164
+ ...prior,
165
+ shownAt: now.toISOString(),
166
+ }, options);
167
+ }
168
+ catch {
169
+ // Advisory tip state must never block welcome / session start.
170
+ }
171
+ }
172
+ return formatMirrorDiscoveryTip();
173
+ }
174
+ /** Persist operator ack so the tip no longer surfaces. */
175
+ export function recordMirrorDiscoveryAcked(projectRoot, options = {}) {
176
+ const now = options.now ?? new Date();
177
+ const prior = readMirrorDiscoveryState(projectRoot, options);
178
+ writeMirrorDiscoveryState(projectRoot, {
179
+ ...prior,
180
+ ackedAt: now.toISOString(),
181
+ }, options);
182
+ }
183
+ /**
184
+ * Persist first successful --mirror dry-run so the tip no longer surfaces.
185
+ * Idempotent: keeps the earliest successfulDryRunAt.
186
+ */
187
+ export function recordMirrorDiscoverySuccessfulDryRun(projectRoot, options = {}) {
188
+ const prior = readMirrorDiscoveryState(projectRoot, options);
189
+ if (prior.successfulDryRunAt !== undefined && prior.successfulDryRunAt.length > 0) {
190
+ return;
191
+ }
192
+ const now = options.now ?? new Date();
193
+ writeMirrorDiscoveryState(projectRoot, {
194
+ ...prior,
195
+ successfulDryRunAt: now.toISOString(),
196
+ }, options);
197
+ }
198
+ /** State path helper for tests / docs. */
199
+ export function resolveMirrorDiscoveryStatePath(projectRoot) {
200
+ return resolveStatePath(projectRoot);
201
+ }
202
+ /** Whether a state file currently exists (tests / diagnostics). */
203
+ export function mirrorDiscoveryStateExists(projectRoot) {
204
+ return existsSync(resolveStatePath(projectRoot));
205
+ }
206
+ /** Footer when planned rows exist but actionLabels is empty. */
207
+ export const MIRROR_DISCOVERY_EMPTY_ACTION_LABELS_HINT = "Hint (#3124): planned matches will only gain alwaysLabels (triaged) — " +
208
+ "set plan.policy.triageLabelMirror.actionLabels to map defer/archive/accept/escalate " +
209
+ "to extra SCM chips (full five recommended; see consumer kit #2611). " +
210
+ "Board usability is greatly decreased without actionLabels.";
211
+ /** Footer when open no_match dominates under universal rules. */
212
+ export const MIRROR_DISCOVERY_NO_MATCH_DOMINATION_HINT = "Hint (#3124): few open matches under current rules (no_match dominates) — " +
213
+ "add plan.policy.triageAutoClassify in PROJECT-DEFINITION to classify more of the backlog.";
214
+ /**
215
+ * Build optional digest footer lines for consumer upgrade cues.
216
+ * Empty when neither condition holds.
217
+ */
218
+ export function formatMirrorDiscoveryDigestCues(input) {
219
+ const lines = [];
220
+ const writeTotal = input.planned + input.applied;
221
+ if (writeTotal > 0 && input.actionLabelsEmpty) {
222
+ lines.push(MIRROR_DISCOVERY_EMPTY_ACTION_LABELS_HINT);
223
+ }
224
+ // Open-ish population: exclude closed_skipped from domination base.
225
+ const openish = writeTotal + input.skipped_no_match + Math.max(0, input.skipped_already_triaged);
226
+ if (input.skipped_no_match > 0 &&
227
+ openish > 0 &&
228
+ input.skipped_no_match >= writeTotal &&
229
+ input.skipped_no_match * 2 >= openish) {
230
+ lines.push(MIRROR_DISCOVERY_NO_MATCH_DOMINATION_HINT);
231
+ }
232
+ return lines;
233
+ }
234
+ //# sourceMappingURL=mirror-discovery-tip.js.map
@@ -1,7 +1,8 @@
1
1
  import { execFileSync } from "node:child_process";
2
2
  import { randomUUID } from "node:crypto";
3
- import { appendFileSync, mkdirSync } from "node:fs";
4
- import { dirname, resolve } from "node:path";
3
+ import { mkdirSync } from "node:fs";
4
+ import { dirname, isAbsolute, relative, resolve } from "node:path";
5
+ import { containedWrite } from "../../fs/contained-write.js";
5
6
  import { resolveLifecycleFolder } from "../../layout/resolve.js";
6
7
  import { resolveCandidatesLogPath } from "../cache-path.js";
7
8
  import { auditKey, existingAuditRefs, scanLifecycleRefs } from "./audit.js";
@@ -127,9 +128,30 @@ function buildReconcileEntry(repo, issueNumber, sourceFolder) {
127
128
  "with a github-issue reference but no prior decision in the audit log",
128
129
  };
129
130
  }
130
- function appendAuditEntry(auditPath, entry) {
131
- mkdirSync(dirname(auditPath), { recursive: true });
132
- appendFileSync(auditPath, `${JSON.stringify(entry)}\n`, "utf8");
131
+ /**
132
+ * Containment root for reconcile audit appends (#3288 / #3245).
133
+ * Prefer project root when the log is nested under it; otherwise parent dir.
134
+ */
135
+ function containmentRootForAudit(projectRoot, auditPath) {
136
+ const rootAbs = resolve(projectRoot);
137
+ const logAbs = resolve(auditPath);
138
+ const parent = dirname(logAbs);
139
+ mkdirSync(parent, { recursive: true });
140
+ const rel = relative(rootAbs, logAbs);
141
+ if (rel.length > 0 && !rel.startsWith("..") && !isAbsolute(rel)) {
142
+ return rootAbs;
143
+ }
144
+ return resolve(parent);
145
+ }
146
+ /** #3288 / #3245 / #2980: product append refuses leaf symlink follow. */
147
+ function appendAuditEntry(projectRoot, auditPath, entry) {
148
+ const root = containmentRootForAudit(projectRoot, auditPath);
149
+ containedWrite({
150
+ root,
151
+ target: auditPath,
152
+ data: `${JSON.stringify(entry)}\n`,
153
+ mode: "append",
154
+ });
133
155
  }
134
156
  export function reconcile(projectRoot, options = {}) {
135
157
  const root = resolve(projectRoot);
@@ -158,7 +180,7 @@ export function reconcile(projectRoot, options = {}) {
158
180
  for (const item of items) {
159
181
  const entry = buildReconcileEntry(item.repo, item.issueNumber, item.folder);
160
182
  try {
161
- appendAuditEntry(auditPath, entry);
183
+ appendAuditEntry(root, auditPath, entry);
162
184
  }
163
185
  catch (err) {
164
186
  return {
@@ -1,6 +1,7 @@
1
1
  import { createHash } from "node:crypto";
2
- import { appendFileSync, existsSync, mkdirSync, readdirSync, readFileSync } from "node:fs";
3
- import { join, resolve as pathResolve } from "node:path";
2
+ import { existsSync, mkdirSync, readdirSync, readFileSync } from "node:fs";
3
+ import { dirname, join, resolve as pathResolve } from "node:path";
4
+ import { containedWrite } from "../../fs/contained-write.js";
4
5
  import { hasArtifactSuffix, resolveLifecycleFolder, resolveProjectDefinitionPath, } from "../../layout/resolve.js";
5
6
  import { readPlanPolicy } from "../../policy/plan-extensions.js";
6
7
  import { loadProjectDefinition, PROJECT_DEFINITION_REL_PATH } from "../../policy/resolve.js";
@@ -542,8 +543,15 @@ export function appendHistory(historyPath, result, line, options = {}) {
542
543
  });
543
544
  const payload = pythonStyleStringify(record);
544
545
  try {
545
- mkdirSync(join(historyPath, ".."), { recursive: true });
546
- appendFileSync(historyPath, `${payload}\n`, { encoding: "utf8" });
546
+ // #3288 / #3245 / #2980: refuse leaf symlink follow (in-tree diversion).
547
+ const parent = dirname(historyPath);
548
+ mkdirSync(parent, { recursive: true });
549
+ containedWrite({
550
+ root: pathResolve(parent),
551
+ target: historyPath,
552
+ data: `${payload}\n`,
553
+ mode: "append",
554
+ });
547
555
  }
548
556
  catch {
549
557
  // observability only — never crash the ritual
@@ -1,6 +1,7 @@
1
1
  import { resolve } from "node:path";
2
2
  import { ensureTriageCacheHydrated } from "../../cache/empty-populate.js";
3
3
  import { maybeSelfHealCache } from "../../cache/fetch.js";
4
+ import { maybeFormatMirrorDiscoveryTip } from "../classify/mirror-discovery-tip.js";
4
5
  import { FIRST_TIME_NUDGE, INCOMPLETE_NUDGE_TEMPLATE } from "./constants.js";
5
6
  import { classifyOnboarding, detectPriorState } from "./prior-state.js";
6
7
  import { emitOneliner } from "./summary.js";
@@ -52,6 +53,21 @@ export function runDefaultMode(projectRoot, options = {}) {
52
53
  else if (label === "incomplete") {
53
54
  out(INCOMPLETE_NUDGE_TEMPLATE.replace("{missing}", missing.join(" + ")).replace(canonicalOnboard, onboardCommand));
54
55
  }
56
+ // #3124: throttled SCM label-mirror discovery tip (existence + get-the-most).
57
+ // Cold session:start runs this welcome path; re-arm skips welcome so tip is not spammed.
58
+ try {
59
+ const tip = maybeFormatMirrorDiscoveryTip(projectRoot, {
60
+ ...(options.now !== undefined ? { now: options.now } : {}),
61
+ });
62
+ if (tip.length > 0) {
63
+ for (const line of tip.trimEnd().split(/\r?\n/)) {
64
+ out(line);
65
+ }
66
+ }
67
+ }
68
+ catch {
69
+ // Discovery tip is advisory — never fail welcome.
70
+ }
55
71
  return outcome;
56
72
  }
57
73
  //# sourceMappingURL=default-mode.js.map
@@ -51,12 +51,12 @@ export function shouldUseShellForCommand(command, platform = process.platform) {
51
51
  return platform === "win32" && /\.(?:cmd|bat)$/i.test(command);
52
52
  }
53
53
  /** Default runner: an inherited-stdio pnpm invocation. */
54
- function defaultRunner(argv, cwd) {
54
+ function defaultRunner(argv, cwd, envOverride) {
55
55
  const [command, ...rest] = argv;
56
56
  const commandPath = command ?? "";
57
57
  const result = spawnSync(commandPath, rest, {
58
58
  cwd,
59
- env: sanitizeTsLaneEnv(process.env),
59
+ env: envOverride ?? sanitizeTsLaneEnv(process.env),
60
60
  stdio: "inherit",
61
61
  shell: shouldUseShellForCommand(commandPath),
62
62
  });
@@ -108,10 +108,27 @@ export function runTsLane(projectRoot, options) {
108
108
  }
109
109
  for (const command of LANE_COMMANDS) {
110
110
  const argv = [pnpm, ...command];
111
+ // Soft-pass via lane-private env (vitest 3 CAC rejects unknown CLI debt tokens).
112
+ // vitest.config reads DEFT_TS_LANE_COVERAGE_DEBT without DEFT_RELEASE_PREFLIGHT
113
+ // (sanitizeTsLaneEnv strips preflight). Refs #2573 / #2618.
114
+ const prevDebt = process.env.DEFT_TS_LANE_COVERAGE_DEBT;
111
115
  if (debtIssue !== null && command[1] === "test") {
112
- argv.push("--", `--allow-coverage-debt=${debtIssue}`);
116
+ process.env.DEFT_TS_LANE_COVERAGE_DEBT = String(debtIssue);
117
+ }
118
+ let result;
119
+ try {
120
+ result = runner(argv, projectRoot);
121
+ }
122
+ finally {
123
+ if (debtIssue !== null && command[1] === "test") {
124
+ if (prevDebt === undefined) {
125
+ delete process.env.DEFT_TS_LANE_COVERAGE_DEBT;
126
+ }
127
+ else {
128
+ process.env.DEFT_TS_LANE_COVERAGE_DEBT = prevDebt;
129
+ }
130
+ }
113
131
  }
114
- const result = runner(argv, projectRoot);
115
132
  const code = result.status;
116
133
  // A null status means the child was terminated by a signal (SIGKILL / OOM /
117
134
  // SIGTERM) before it could exit. Mapping that to 0 would silently pass a
@@ -28,6 +28,8 @@ export const PLAN_CORE = new Set([
28
28
  "changeLog",
29
29
  "sequence",
30
30
  "fork",
31
+ // Product-first done-gate (#3284): plan.acceptance.commands / none_stated / source_rung.
32
+ "acceptance",
31
33
  ]);
32
34
  export const ITEM_CORE = new Set([
33
35
  "id",
@@ -62,6 +64,11 @@ export const ITEM_CORE = new Set([
62
64
  "lastModifiedBy",
63
65
  "lockedBy",
64
66
  "items",
67
+ // Optional PlanItem.effort enum S/M/L/XL (#1581) — schema-validated, core not extension.
68
+ "effort",
69
+ // #3305 Option B: acceptance evidence/disposition are NOT ITEM_CORE.
70
+ // Canonical keys: plan.items[].x-directive/evidence and x-directive/disposition
71
+ // (accepted via EXTENSION_PREFIXES). Bare evidence/disposition fail #1620.
65
72
  ]);
66
73
  export const EXTENSION_PREFIXES = ["x-directive/", "x-vbrief/", "x-xbrief/"];
67
74
  export function renderFinding(finding) {
@@ -1,14 +1,26 @@
1
1
  import { readFileSync } from "node:fs";
2
2
  import { join } from "node:path";
3
3
  import { COVERAGE_GOAL, countRecentCoverageDebtMentions, formatCoverageAttribution, formatOveruseWarning, metricsBelowGoal, readCoverageTotalsFromReport, resolveCoverageDebtIssue, } from "./coverage-debt.js";
4
- /** Vitest globalTeardown: enforce coverage goal or debt soft-pass (#2573). */
5
- export default async function coverageDebtTeardown() {
4
+ function resolveDebtIssueForTeardown() {
6
5
  const resolution = resolveCoverageDebtIssue(process.argv, process.env);
7
- if (resolution.kind === "none")
8
- return;
6
+ if (resolution.kind === "valid")
7
+ return resolution;
9
8
  if (resolution.kind === "invalid") {
10
9
  throw new Error(`coverage-debt: ${resolution.reason}`);
11
10
  }
11
+ const laneRaw = process.env.DEFT_TS_LANE_COVERAGE_DEBT;
12
+ if (laneRaw !== undefined && /^\d+$/.test(laneRaw.trim())) {
13
+ const issue = Number.parseInt(laneRaw.trim(), 10);
14
+ if (issue > 0)
15
+ return { kind: "valid", issue };
16
+ }
17
+ return { kind: "none" };
18
+ }
19
+ /** Vitest globalTeardown: enforce coverage goal or debt soft-pass (#2573). */
20
+ export default async function coverageDebtTeardown() {
21
+ const resolution = resolveDebtIssueForTeardown();
22
+ if (resolution.kind === "none")
23
+ return;
12
24
  const repoRoot = join(import.meta.dirname, "..", "..", "..", "..");
13
25
  const totals = readCoverageTotalsFromReport(join(repoRoot, "coverage"));
14
26
  if (totals === null) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@deftai/directive-core",
3
- "version": "0.100.0",
3
+ "version": "0.101.0",
4
4
  "description": "TypeScript engine core for the Directive framework.",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -287,6 +287,10 @@
287
287
  "types": "./dist/literal-acceptance/index.d.ts",
288
288
  "default": "./dist/literal-acceptance/index.js"
289
289
  },
290
+ "./product-first-done-gate": {
291
+ "types": "./dist/product-first-done-gate/index.d.ts",
292
+ "default": "./dist/product-first-done-gate/index.js"
293
+ },
290
294
  "./packs": {
291
295
  "types": "./dist/packs/index.d.ts",
292
296
  "default": "./dist/packs/index.js"
@@ -315,6 +319,10 @@
315
319
  "types": "./dist/check/index.d.ts",
316
320
  "default": "./dist/check/index.js"
317
321
  },
322
+ "./run-summary": {
323
+ "types": "./dist/run-summary/index.d.ts",
324
+ "default": "./dist/run-summary/index.js"
325
+ },
318
326
  "./preflight-gh": {
319
327
  "types": "./dist/preflight-gh/index.d.ts",
320
328
  "default": "./dist/preflight-gh/index.js"
@@ -370,8 +378,8 @@
370
378
  "provenance": true
371
379
  },
372
380
  "dependencies": {
373
- "@deftai/directive-content": "^0.100.0",
374
- "@deftai/directive-types": "^0.100.0",
381
+ "@deftai/directive-content": "^0.101.0",
382
+ "@deftai/directive-types": "^0.101.0",
375
383
  "archiver": "^8.0.0"
376
384
  },
377
385
  "scripts": {