@deftai/directive-core 0.101.0 → 0.103.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 (75) hide show
  1. package/dist/authz/classify.js +97 -8
  2. package/dist/check/cached-orchestrator.d.ts +4 -0
  3. package/dist/check/cached-orchestrator.js +70 -5
  4. package/dist/check/cli-native-gates.d.ts +43 -0
  5. package/dist/check/cli-native-gates.js +84 -0
  6. package/dist/check/index.d.ts +1 -0
  7. package/dist/check/index.js +1 -0
  8. package/dist/check/named-cause.js +10 -0
  9. package/dist/doctor/checks.d.ts +5 -6
  10. package/dist/doctor/checks.js +13 -56
  11. package/dist/eval/later-graduation.d.ts +39 -0
  12. package/dist/eval/later-graduation.js +91 -0
  13. package/dist/eval/report.d.ts +6 -0
  14. package/dist/eval/report.js +27 -8
  15. package/dist/index.d.ts +1 -0
  16. package/dist/index.js +1 -0
  17. package/dist/init-deposit/hygiene.d.ts +7 -1
  18. package/dist/init-deposit/hygiene.js +11 -3
  19. package/dist/init-deposit/scaffold.d.ts +12 -0
  20. package/dist/init-deposit/scaffold.js +81 -8
  21. package/dist/intake/issue-ingest.js +42 -0
  22. package/dist/policy/ceremony-dial-escalation.d.ts +52 -0
  23. package/dist/policy/ceremony-dial-escalation.js +92 -0
  24. package/dist/policy/ceremony-dial.d.ts +3 -1
  25. package/dist/policy/ceremony-dial.js +52 -1
  26. package/dist/policy/check-resume.d.ts +9 -38
  27. package/dist/policy/check-resume.js +18 -156
  28. package/dist/policy/coverage-debt.d.ts +18 -37
  29. package/dist/policy/coverage-debt.js +38 -147
  30. package/dist/policy/index.d.ts +1 -1
  31. package/dist/policy/index.js +1 -1
  32. package/dist/product-first-done-gate/acceptance.js +22 -0
  33. package/dist/product-first-done-gate/empty-resolution.d.ts +26 -0
  34. package/dist/product-first-done-gate/empty-resolution.js +38 -0
  35. package/dist/product-first-done-gate/evaluate.d.ts +40 -2
  36. package/dist/product-first-done-gate/evaluate.js +253 -61
  37. package/dist/product-first-done-gate/index.d.ts +2 -1
  38. package/dist/product-first-done-gate/index.js +2 -1
  39. package/dist/product-first-done-gate/types.d.ts +3 -0
  40. package/dist/product-first-done-gate/types.js +0 -7
  41. package/dist/run-summary/emit.d.ts +11 -1
  42. package/dist/run-summary/emit.js +48 -2
  43. package/dist/run-summary/index.d.ts +2 -1
  44. package/dist/run-summary/index.js +2 -1
  45. package/dist/run-summary/path.d.ts +1 -1
  46. package/dist/run-summary/path.js +1 -1
  47. package/dist/run-summary/share.d.ts +25 -0
  48. package/dist/run-summary/share.js +106 -0
  49. package/dist/run-summary/types.d.ts +60 -2
  50. package/dist/run-summary/types.js +7 -0
  51. package/dist/scope/acceptance-activate-gate.d.ts +23 -0
  52. package/dist/scope/acceptance-activate-gate.js +70 -0
  53. package/dist/scope/index.d.ts +1 -0
  54. package/dist/scope/index.js +1 -0
  55. package/dist/scope/transition.js +5 -0
  56. package/dist/session/index.d.ts +0 -1
  57. package/dist/session/index.js +0 -1
  58. package/dist/session/orientation-compression.js +19 -9
  59. package/dist/session/session-start.d.ts +6 -0
  60. package/dist/session/session-start.js +44 -22
  61. package/dist/session/toolchain-preflight.d.ts +15 -0
  62. package/dist/session/toolchain-preflight.js +65 -5
  63. package/dist/verify-ac/clauses.d.ts +51 -0
  64. package/dist/verify-ac/clauses.js +490 -0
  65. package/dist/verify-ac/evaluate.d.ts +73 -0
  66. package/dist/verify-ac/evaluate.js +167 -0
  67. package/dist/verify-ac/flag.d.ts +35 -0
  68. package/dist/verify-ac/flag.js +104 -0
  69. package/dist/verify-ac/index.d.ts +10 -0
  70. package/dist/verify-ac/index.js +10 -0
  71. package/package.json +7 -3
  72. package/dist/policy/coverage-check-resume-presets.d.ts +0 -46
  73. package/dist/policy/coverage-check-resume-presets.js +0 -228
  74. package/dist/session/coverage-check-resume-nudge.d.ts +0 -34
  75. package/dist/session/coverage-check-resume-nudge.js +0 -66
@@ -0,0 +1,91 @@
1
+ /**
2
+ * #3286 Later graduation trigger from a #3282 run summary (#3320).
3
+ *
4
+ * Named surface: eval:report prints graduate / do-not-graduate / unevaluable.
5
+ * A graduation or decline MUST cite an emitted share. Counts without a
6
+ * denominator are unevaluable — never invent a ratio.
7
+ */
8
+ import { existsSync, readFileSync } from "node:fs";
9
+ import { isAbsolute, resolve } from "node:path";
10
+ import { computeRitualGateShare, parseRunSummaryJsonl, } from "../run-summary/share.js";
11
+ import { DEFAULT_RUN_SUMMARY_BASENAME, ENV_RUN_SUMMARY_PATH } from "../run-summary/types.js";
12
+ /** #3286 Later trigger: graduate when ritual+gate share is at least this fraction. */
13
+ export const LATER_GRADUATION_SHARE_THRESHOLD = 0.25;
14
+ function formatPercent(value) {
15
+ return `${(value * 100).toFixed(1)}%`;
16
+ }
17
+ export function laterGraduationFromShare(share, threshold = LATER_GRADUATION_SHARE_THRESHOLD) {
18
+ if (!share.evaluable || share.share === null || share.totalToolTurns === null) {
19
+ return {
20
+ evaluable: false,
21
+ verdict: "unevaluable",
22
+ summary: "trigger unevaluable (no tool/turn denominator on run summary)",
23
+ ritualGateCount: share.ritualGateCount,
24
+ totalToolTurns: share.totalToolTurns,
25
+ share: null,
26
+ threshold,
27
+ };
28
+ }
29
+ const graduate = share.share >= threshold;
30
+ const verdict = graduate ? "graduate" : "do-not-graduate";
31
+ return {
32
+ evaluable: true,
33
+ verdict,
34
+ summary: `ritual+gate share ${formatPercent(share.share)} (${share.ritualGateCount}/${share.totalToolTurns}) — ${verdict} (threshold ${formatPercent(threshold)})`,
35
+ ritualGateCount: share.ritualGateCount,
36
+ totalToolTurns: share.totalToolTurns,
37
+ share: share.share,
38
+ threshold,
39
+ };
40
+ }
41
+ /**
42
+ * Resolve a readable run-summary path. Unset env + missing default file → null
43
+ * (fail-open / unevaluable). stdout dest (`-`) is not a file.
44
+ */
45
+ export function resolveRunSummaryReadPath(projectRoot, options = {}) {
46
+ if (options.runSummaryPath !== undefined && options.runSummaryPath.trim().length > 0) {
47
+ return resolve(options.runSummaryPath.trim());
48
+ }
49
+ const env = options.env ?? process.env;
50
+ const raw = env[ENV_RUN_SUMMARY_PATH];
51
+ if (raw !== undefined && raw.trim() === "-") {
52
+ return null;
53
+ }
54
+ if (raw !== undefined && raw.trim().length > 0) {
55
+ const trimmed = raw.trim();
56
+ return isAbsolute(trimmed) ? resolve(trimmed) : resolve(projectRoot, trimmed);
57
+ }
58
+ const fallback = resolve(projectRoot, DEFAULT_RUN_SUMMARY_BASENAME);
59
+ return existsSync(fallback) ? fallback : null;
60
+ }
61
+ function readSummaryText(path) {
62
+ try {
63
+ if (!existsSync(path)) {
64
+ return null;
65
+ }
66
+ return readFileSync(path, "utf8");
67
+ }
68
+ catch {
69
+ return null;
70
+ }
71
+ }
72
+ /** Evaluate #3286 Later from the run-summary file (or unevaluable). */
73
+ export function evaluateLaterGraduationTrigger(options) {
74
+ const threshold = options.threshold ?? LATER_GRADUATION_SHARE_THRESHOLD;
75
+ const path = resolveRunSummaryReadPath(options.projectRoot, {
76
+ runSummaryPath: options.runSummaryPath,
77
+ env: options.env,
78
+ });
79
+ if (path === null) {
80
+ return laterGraduationFromShare({ evaluable: false, ritualGateCount: 0, totalToolTurns: null, share: null }, threshold);
81
+ }
82
+ const text = readSummaryText(path);
83
+ if (text === null) {
84
+ return laterGraduationFromShare({ evaluable: false, ritualGateCount: 0, totalToolTurns: null, share: null }, threshold);
85
+ }
86
+ return laterGraduationFromShare(computeRitualGateShare(parseRunSummaryJsonl(text)), threshold);
87
+ }
88
+ export function formatLaterGraduationLine(trigger) {
89
+ return `#3286 Later: ${trigger.summary}`;
90
+ }
91
+ //# sourceMappingURL=later-graduation.js.map
@@ -1,3 +1,4 @@
1
+ import { type LaterGraduationTrigger } from "./later-graduation.js";
1
2
  import { type GoldenRunRecord } from "./run.js";
2
3
  import { type CellVersionPurity, type MixedVersionPolicy } from "./version-pin.js";
3
4
  export declare const REPORT_SCHEMA_VERSION: 1;
@@ -39,6 +40,8 @@ export interface GoldenEvalReport {
39
40
  readonly holdoutTripwire: HoldoutTripwire;
40
41
  /** Cell-level framework version purity for the reported model (#3215). */
41
42
  readonly versionPurity: VersionPurityEvidence;
43
+ /** #3286 Later graduation trigger from the run-summary denominator (#3320). */
44
+ readonly laterGraduation: LaterGraduationTrigger;
42
45
  }
43
46
  export interface ReportGoldenEvalOptions {
44
47
  readonly projectRoot?: string;
@@ -50,6 +53,9 @@ export interface ReportGoldenEvalOptions {
50
53
  * ledger runs for this model disagree on framework version within a treatment.
51
54
  */
52
55
  readonly mixedVersionPolicy?: MixedVersionPolicy;
56
+ /** Override run-summary JSONL path (#3320). Default: DEFT_RUN_SUMMARY_PATH or repo default. */
57
+ readonly runSummaryPath?: string;
58
+ readonly env?: NodeJS.ProcessEnv;
53
59
  }
54
60
  export interface ReportGoldenEvalResult {
55
61
  readonly code: 0 | 1 | 2;
@@ -1,5 +1,6 @@
1
1
  import { existsSync, readFileSync } from "node:fs";
2
2
  import { resolveEvalPath } from "../layout/resolve.js";
3
+ import { evaluateLaterGraduationTrigger, formatLaterGraduationLine, } from "./later-graduation.js";
3
4
  import { GOLDEN_RUNS_HISTORY_REL } from "./run.js";
4
5
  import { aggregateCellWithVersionPurity, evaluateLedgerVersionPurity, } from "./version-pin.js";
5
6
  export const REPORT_SCHEMA_VERSION = 1;
@@ -107,17 +108,33 @@ export function goldenRunToVersionedRun(record) {
107
108
  }
108
109
  /** Diff two directive versions with metric deltas and significance (#1703 Tier 2). */
109
110
  export function reportGoldenEval(options) {
110
- if (!options.championVersion.trim() || !options.challengerVersion.trim()) {
111
+ const projectRoot = options.projectRoot ?? process.cwd();
112
+ const laterGraduation = evaluateLaterGraduationTrigger({
113
+ projectRoot,
114
+ runSummaryPath: options.runSummaryPath,
115
+ env: options.env,
116
+ });
117
+ const laterLine = formatLaterGraduationLine(laterGraduation);
118
+ const championMissing = !options.championVersion.trim();
119
+ const challengerMissing = !options.challengerVersion.trim();
120
+ const modelMissing = !options.model.trim();
121
+ if (championMissing && challengerMissing && modelMissing) {
122
+ return {
123
+ code: 0,
124
+ report: null,
125
+ message: `eval:report ${laterLine}`,
126
+ };
127
+ }
128
+ if (championMissing || challengerMissing) {
111
129
  return {
112
130
  code: 2,
113
131
  report: null,
114
- message: "eval:report: --champion and --challenger are required",
132
+ message: `eval:report: --champion and --challenger are required\n ${laterLine}`,
115
133
  };
116
134
  }
117
- if (!options.model.trim()) {
118
- return { code: 2, report: null, message: "eval:report: --model is required" };
135
+ if (modelMissing) {
136
+ return { code: 2, report: null, message: `eval:report: --model is required\n ${laterLine}` };
119
137
  }
120
- const projectRoot = options.projectRoot ?? process.cwd();
121
138
  const policy = options.mixedVersionPolicy ?? "refuse";
122
139
  const records = readGoldenRuns(projectRoot);
123
140
  const modelRecords = records.filter((r) => r.model === options.model);
@@ -157,7 +174,7 @@ export function reportGoldenEval(options) {
157
174
  return {
158
175
  code: 1,
159
176
  report: null,
160
- message: `eval:report: mixed framework versions in treatment cell(s) — aggregation refused (#3215)\n ${versionPurity.summary}`,
177
+ message: `eval:report: mixed framework versions in treatment cell(s) — aggregation refused (#3215)\n ${versionPurity.summary}\n ${laterLine}`,
161
178
  };
162
179
  }
163
180
  const champion = findLatestGoldenRun(records, options.championVersion, options.model);
@@ -166,14 +183,14 @@ export function reportGoldenEval(options) {
166
183
  return {
167
184
  code: 1,
168
185
  report: null,
169
- message: `eval:report: no golden run found for champion v${options.championVersion} model=${options.model}`,
186
+ message: `eval:report: no golden run found for champion v${options.championVersion} model=${options.model}\n ${laterLine}`,
170
187
  };
171
188
  }
172
189
  if (challenger === null) {
173
190
  return {
174
191
  code: 1,
175
192
  report: null,
176
- message: `eval:report: no golden run found for challenger v${options.challengerVersion} model=${options.model}`,
193
+ message: `eval:report: no golden run found for challenger v${options.challengerVersion} model=${options.model}\n ${laterLine}`,
177
194
  };
178
195
  }
179
196
  const championPrimary = champion.results.filter((r) => !r.holdout);
@@ -208,6 +225,7 @@ export function reportGoldenEval(options) {
208
225
  deltas,
209
226
  holdoutTripwire,
210
227
  versionPurity,
228
+ laterGraduation,
211
229
  };
212
230
  const lines = [
213
231
  `eval:report champion=v${options.championVersion} challenger=v${options.challengerVersion} model=${options.model}`,
@@ -223,6 +241,7 @@ export function reportGoldenEval(options) {
223
241
  ` version purity: ${versionPurity.pure ? "ok" : "mixed"} — ${versionPurity.summary}`,
224
242
  ` champion cell pin: v${champion.frameworkVersionPin?.frameworkVersion ?? champion.directiveVersion}`,
225
243
  ` challenger cell pin: v${challenger.frameworkVersionPin?.frameworkVersion ?? challenger.directiveVersion}`,
244
+ ` ${laterLine}`,
226
245
  ];
227
246
  const code = holdoutTripwire.triggered ? 1 : 0;
228
247
  return { code, report, message: lines.join("\n") };
package/dist/index.d.ts CHANGED
@@ -74,6 +74,7 @@ export * as vbriefBuild from "./vbrief-build/index.js";
74
74
  export * as vbriefReconcile from "./vbrief-reconcile/index.js";
75
75
  export * as vbriefValidate from "./vbrief-validate/index.js";
76
76
  export * as vbriefValidation from "./vbrief-validation/index.js";
77
+ export * as verifyAc from "./verify-ac/index.js";
77
78
  export * as verifyEnv from "./verify-env/index.js";
78
79
  export * as verifySource from "./verify-source/index.js";
79
80
  export * as wipCap from "./wip-cap/index.js";
package/dist/index.js CHANGED
@@ -75,6 +75,7 @@ export * as vbriefBuild from "./vbrief-build/index.js";
75
75
  export * as vbriefReconcile from "./vbrief-reconcile/index.js";
76
76
  export * as vbriefValidate from "./vbrief-validate/index.js";
77
77
  export * as vbriefValidation from "./vbrief-validation/index.js";
78
+ export * as verifyAc from "./verify-ac/index.js";
78
79
  export * as verifyEnv from "./verify-env/index.js";
79
80
  export * as verifySource from "./verify-source/index.js";
80
81
  export * as wipCap from "./wip-cap/index.js";
@@ -42,7 +42,13 @@ export declare function installerManagedMatchers(): InstallerManagedMatcher[];
42
42
  * so an unlisted `xbrief/active/another-scope.xbrief.json` matcher still fails.
43
43
  */
44
44
  export declare function assertInstallerAllowlistHonors1430(matchers?: readonly InstallerManagedMatcher[]): void;
45
- /** POSIX ERE alternation embedded in the deposited deft-core-guard workflow. */
45
+ /**
46
+ * Individual POSIX ERE patterns for the deposited deft-core-guard allowlist.
47
+ * One pattern per line in the workflow heredoc (#3345) — joined form is still
48
+ * available via {@link installerManagedGuardEre} for tests and classifiers.
49
+ */
50
+ export declare function installerManagedGuardErePatterns(): string[];
51
+ /** POSIX ERE alternation (joined) for tests / tooling that want a single pattern. */
46
52
  export declare function installerManagedGuardEre(): string;
47
53
  export declare function isInstallerManagedPath(path: string): boolean;
48
54
  export interface MixedCoreAndAppClassification {
@@ -174,12 +174,20 @@ export function assertInstallerAllowlistHonors1430(matchers = installerManagedMa
174
174
  }
175
175
  }
176
176
  }
177
- /** POSIX ERE alternation embedded in the deposited deft-core-guard workflow. */
178
- export function installerManagedGuardEre() {
177
+ /**
178
+ * Individual POSIX ERE patterns for the deposited deft-core-guard allowlist.
179
+ * One pattern per line in the workflow heredoc (#3345) — joined form is still
180
+ * available via {@link installerManagedGuardEre} for tests and classifiers.
181
+ */
182
+ export function installerManagedGuardErePatterns() {
179
183
  const matchers = installerManagedMatchers();
180
184
  // Refuse to emit a guard workflow that would exempt consumer denylist paths.
181
185
  assertInstallerAllowlistHonors1430(matchers);
182
- return matchers.map((matcher) => matcherToEre(matcher)).join("|");
186
+ return matchers.map((matcher) => matcherToEre(matcher));
187
+ }
188
+ /** POSIX ERE alternation (joined) for tests / tooling that want a single pattern. */
189
+ export function installerManagedGuardEre() {
190
+ return installerManagedGuardErePatterns().join("|");
183
191
  }
184
192
  export function isInstallerManagedPath(path) {
185
193
  return matchesInstallerManaged(path, installerManagedMatchers());
@@ -82,6 +82,18 @@ export declare function coreGuardCheckoutUsesLine(sha?: string, tag?: string): s
82
82
  * the managed guard script / allowlist body (#1672 option 1).
83
83
  */
84
84
  export declare function mergeCoreGuardWorkflowRefresh(existing: string, desired: string): string;
85
+ /**
86
+ * Soft max line length for deposited core-guard workflow YAML (#3345).
87
+ * Mega-line allowlist EREs previously sat near ~5k chars; keep well below.
88
+ * Enforced by {@link assertCoreGuardWorkflowLoadable} before deposit write.
89
+ */
90
+ export declare const CORE_GUARD_WORKFLOW_MAX_LINE = 500;
91
+ /**
92
+ * Fail closed if a rendered deft-core-guard workflow would not load in GHA
93
+ * (#3345): any `run: |` content line less indented than the block base, or a
94
+ * line longer than {@link CORE_GUARD_WORKFLOW_MAX_LINE}.
95
+ */
96
+ export declare function assertCoreGuardWorkflowLoadable(content: string): void;
85
97
  export declare function ensureGitattributes(projectDir: string, io: InitDepositIo): boolean;
86
98
  export declare function ensureGreptileIgnore(projectDir: string, io: InitDepositIo): boolean;
87
99
  export declare function ensureCodeqlPathsIgnore(projectDir: string, io: InitDepositIo): boolean;
@@ -14,7 +14,7 @@ import { assertDestinationNotSymlink, ProjectionContainmentError, } from "../fs/
14
14
  import { agentsRefreshPlan } from "../platform/agents-md.js";
15
15
  import { MIGRATED_ARTIFACT_DIR } from "../xbrief-migrate/constants.js";
16
16
  import { CANONICAL_INSTALL_ROOT } from "./constants.js";
17
- import { assertInstallerAllowlistHonors1430, installerManagedGuardEre } from "./hygiene.js";
17
+ import { assertInstallerAllowlistHonors1430, installerManagedGuardErePatterns } from "./hygiene.js";
18
18
  import { writeAgentsSkillsFromInventory } from "./skill-discovery-deposit.js";
19
19
  import { syncConsumerXbriefSchemas } from "./xbrief-projections.js";
20
20
  export { CANONICAL_INSTALL_ROOT };
@@ -501,16 +501,66 @@ export function mergeCoreGuardWorkflowRefresh(existing, desired) {
501
501
  return desired;
502
502
  return desired.slice(0, idx) + existingCheckout + desired.slice(idx + desiredCheckout.length);
503
503
  }
504
+ /**
505
+ * Indent for shell lines inside the deposited workflow `run: |` block.
506
+ * YAML literal blocks end at any line less indented than this; the #3193
507
+ * Python body must stay at this column so GHA can load the workflow (#3345).
508
+ */
509
+ const CORE_GUARD_RUN_INDENT = " ";
510
+ /**
511
+ * Soft max line length for deposited core-guard workflow YAML (#3345).
512
+ * Mega-line allowlist EREs previously sat near ~5k chars; keep well below.
513
+ * Enforced by {@link assertCoreGuardWorkflowLoadable} before deposit write.
514
+ */
515
+ export const CORE_GUARD_WORKFLOW_MAX_LINE = 500;
516
+ /**
517
+ * Fail closed if a rendered deft-core-guard workflow would not load in GHA
518
+ * (#3345): any `run: |` content line less indented than the block base, or a
519
+ * line longer than {@link CORE_GUARD_WORKFLOW_MAX_LINE}.
520
+ */
521
+ export function assertCoreGuardWorkflowLoadable(content) {
522
+ if (!content.startsWith("name: deft-core-guard\n") &&
523
+ !content.startsWith("name: deft-core-guard\r\n")) {
524
+ throw new Error("deft-core-guard workflow must start with name: deft-core-guard (#3345)");
525
+ }
526
+ let inRun = false;
527
+ let baseIndent = null;
528
+ for (const [index, line] of content.split("\n").entries()) {
529
+ if (line.length > CORE_GUARD_WORKFLOW_MAX_LINE) {
530
+ throw new Error(`deft-core-guard workflow line ${index + 1} is ${line.length} chars ` +
531
+ `(max ${CORE_GUARD_WORKFLOW_MAX_LINE}; #3345 loadable YAML)`);
532
+ }
533
+ if (line.trim() === "run: |") {
534
+ inRun = true;
535
+ baseIndent = null;
536
+ continue;
537
+ }
538
+ if (!inRun || line.trim() === "")
539
+ continue;
540
+ const lead = line.match(/^( *)/)?.[1]?.length ?? 0;
541
+ if (baseIndent === null) {
542
+ baseIndent = lead;
543
+ continue;
544
+ }
545
+ if (lead < baseIndent) {
546
+ throw new Error(`deft-core-guard workflow line ${index + 1} dedents below run-block base ` +
547
+ `(indent ${lead} < ${baseIndent}; #3345 YAML load failure class)`);
548
+ }
549
+ }
550
+ }
504
551
  /**
505
552
  * Embedded python3 content check for package.json / lockfiles when co-travelling
506
553
  * with .deft/core/** (#3193). Mirrors TS `isUpgradePinPathContentAllowed`.
507
554
  * Uses a single-quoted heredoc so GHA does not interpolate shell variables
508
555
  * inside the Python source (BASE/HEAD are argv).
556
+ *
557
+ * Every emitted line is indented to {@link CORE_GUARD_RUN_INDENT} so the YAML
558
+ * `run: |` block stays intact (#3345). After GHA strips the common indent, the
559
+ * shell heredoc body reaches python3 with correct relative indentation.
509
560
  */
510
561
  function coreGuardPinContentPython() {
511
562
  // Keep this compact: deposited into every consumer workflow on init/update.
512
- return [
513
- ' python3 - "$BASE_SHA" "$HEAD_SHA" <<\'PY\'',
563
+ const pyBody = [
514
564
  "import json, re, subprocess, sys",
515
565
  "base_sha, head_sha = sys.argv[1], sys.argv[2]",
516
566
  "def git_show(sha, path):",
@@ -766,7 +816,31 @@ function coreGuardPinContentPython() {
766
816
  " print('\\n'.join(bad))",
767
817
  " sys.exit(1)",
768
818
  "print('OK: package/lock content is Directive pin unit (#3193).')",
769
- "PY",
819
+ ];
820
+ const run = CORE_GUARD_RUN_INDENT;
821
+ return [
822
+ `${run}python3 - "$BASE_SHA" "$HEAD_SHA" <<'PY'`,
823
+ ...pyBody.map((line) => `${run}${line}`),
824
+ `${run}PY`,
825
+ ].join("\n");
826
+ }
827
+ /**
828
+ * Shell that materializes the installer-managed allowlist as one ERE per line
829
+ * and filters "app" paths with `grep -vE -f` (#3345 — avoids a ~5k-char line
830
+ * that contributed to invalid workflow load). Semantics match the prior
831
+ * single-alternation ERE (OR of all matchers).
832
+ */
833
+ function coreGuardAllowlistShell() {
834
+ const run = CORE_GUARD_RUN_INDENT;
835
+ const patterns = installerManagedGuardErePatterns();
836
+ return [
837
+ `${run}# Installer-managed allowlist — one ERE per line (#3345 loadable YAML)`,
838
+ `${run}allowlist=$(mktemp)`,
839
+ `${run}trap 'rm -f "$allowlist"' EXIT`,
840
+ `${run}cat > "$allowlist" <<'ALLOW'`,
841
+ ...patterns.map((p) => `${run}${p}`),
842
+ `${run}ALLOW`,
843
+ `${run}app=$(printf '%s\\n' "$changed" | grep -vE '^\\.deft/core/' | grep -vE -f "$allowlist" | grep -v '^$' || true)`,
770
844
  ].join("\n");
771
845
  }
772
846
  function coreGuardWorkflowContent() {
@@ -775,7 +849,7 @@ function coreGuardWorkflowContent() {
775
849
  const baseSha = githubActionsExpr("github.event.pull_request.base.sha");
776
850
  const headSha = githubActionsExpr("github.event.pull_request.head.sha");
777
851
  return ("name: deft-core-guard\n\n" +
778
- "# Deft framework guard (#1430 / #3127 / #3193): a single PR should not mix changes to the\n" +
852
+ "# Deft framework guard (#1430 / #3127 / #3193 / #3345): a single PR should not mix changes to the\n" +
779
853
  "# vendored framework payload (.deft/core/**) with true application/product files.\n" +
780
854
  "# One upgrade PR MAY include deposit + installer-managed paths + package.json pin/lock\n" +
781
855
  "# + .deft/GENERATION.json when package.json is @deftai/directive* dependency-key pin-only\n" +
@@ -803,9 +877,7 @@ function coreGuardWorkflowContent() {
803
877
  ' echo "Changed files:"\n' +
804
878
  ' echo "$changed"\n' +
805
879
  " core=$(printf '%s\\n' \"$changed\" | grep -E '^\\.deft/core/' || true)\n" +
806
- " app=$(printf '%s\\n' \"$changed\" | grep -vE '^\\.deft/core/' | grep -vE '" +
807
- installerManagedGuardEre() +
808
- "' | grep -v '^$' || true)\n" +
880
+ `${coreGuardAllowlistShell()}\n` +
809
881
  ' if [ -n "$core" ] && [ -n "$app" ]; then\n' +
810
882
  ' echo "::error title=deft-core guard (#1430)::This PR changes the vendored framework payload (.deft/core/**) AND non-framework files. Split the framework update into its own PR."\n' +
811
883
  ' echo "--- framework (.deft/core/**) changes ---"; printf \'%s\\n\' "$core"\n' +
@@ -951,6 +1023,7 @@ export function ensureCodeqlPathsIgnore(projectDir, io) {
951
1023
  export function ensureCoreGuardWorkflow(projectDir, io) {
952
1024
  const path = projectionTarget(projectDir, CORE_GUARD_WORKFLOW_REL);
953
1025
  const desired = coreGuardWorkflowContent();
1026
+ assertCoreGuardWorkflowLoadable(desired);
954
1027
  if (existsSync(path)) {
955
1028
  const existing = readFileSync(path, "utf8");
956
1029
  if (!existing.includes("name: deft-core-guard")) {
@@ -6,12 +6,14 @@ import { assertWriteTargetSafe, ProjectionContainmentError } from "../fs/project
6
6
  import { hasArtifactSuffix, resolveLifecycleRoot } from "../layout/resolve.js";
7
7
  import { captureAndAttachLiteralAcceptance } from "../literal-acceptance/index.js";
8
8
  import { stampAcceptanceFromLiteralCapture } from "../product-first-done-gate/index.js";
9
+ import { ENV_RUN_SUMMARY_PATH, RunSummaryEmitter } from "../run-summary/index.js";
9
10
  import { call } from "../scm/call.js";
10
11
  import { resolveProjectRoot } from "../scope/project-context.js";
11
12
  import { resolveProjectRepo } from "../slice/project-context.js";
12
13
  import { countMaintainerCurrentShapeComments, mapIssueCommentEntry, RAW_ISSUE_COMMENTS_KEY, selectCurrentShapeComment, } from "../umbrella-current-shape/index.js";
13
14
  import { slugify, TODAY } from "../vbrief-build/build.js";
14
15
  import { EMITTED_VBRIEF_VERSION } from "../vbrief-build/constants.js";
16
+ import { stampDerivedClausesOnAcceptance } from "../verify-ac/clauses.js";
15
17
  import { LEGACY_ARTIFACT_SUFFIX, LEGACY_INFO_ROOT_KEY, MIGRATED_ARTIFACT_DIR, MIGRATED_ARTIFACT_SUFFIX, MIGRATED_INFO_ROOT_KEY, VBRIEF_VERSION, } from "../xbrief-migrate/constants.js";
16
18
  import { findAcHeading, parseCheckboxItems, parseListItems, sliceAcSection, stripCodeBlocks, stripFencedCodeBlocks, } from "./markdown-scanners.js";
17
19
  import { detectRepo, extractReferencesFromVbrief, fetchOpenIssues, GITHUB_ISSUE_REF_TYPES, LIFECYCLE_FOLDERS, parseIssueNumber, } from "./reconcile-issues.js";
@@ -475,6 +477,9 @@ export function buildIssueVbrief(issue, status, repoUrl, options = {}) {
475
477
  Object.assign(plan, attached.plan);
476
478
  // #3284: stamp plan.acceptance from captured literals (stated or none_stated floor).
477
479
  Object.assign(plan, stampAcceptanceFromLiteralCapture(plan));
480
+ // #3323: when no commands were stated, derive numbered clauses before product edit.
481
+ const derived = stampDerivedClausesOnAcceptance(plan, [title, overviewSource].filter((s) => s.length > 0).join("\n\n"));
482
+ Object.assign(plan, derived.plan);
478
483
  }
479
484
  else {
480
485
  // No body: still record none_stated acceptance so absence is a decision.
@@ -718,8 +723,45 @@ export function ingestOne(issue, options) {
718
723
  assertWriteTargetSafe(projectRoot, target);
719
724
  mkdirSync(folderPath, { recursive: true });
720
725
  writeFileSync(target, `${JSON.stringify(vbrief, null, 2)}\n`, "utf8");
726
+ emitIntakeAcceptanceStamp(projectRoot, vbrief.plan);
721
727
  return ["created", target, `CREATED ${folder}/${filename}`];
722
728
  }
729
+ function emitIntakeAcceptanceStamp(projectRoot, plan) {
730
+ const rec = plan !== null && typeof plan === "object" && !Array.isArray(plan)
731
+ ? plan
732
+ : null;
733
+ if (rec === null) {
734
+ return;
735
+ }
736
+ const acceptance = rec.acceptance !== null && typeof rec.acceptance === "object" && !Array.isArray(rec.acceptance)
737
+ ? rec.acceptance
738
+ : null;
739
+ if (acceptance === null) {
740
+ return;
741
+ }
742
+ const env = process.env;
743
+ const dest = env[ENV_RUN_SUMMARY_PATH];
744
+ if (dest === undefined || dest.trim().length === 0) {
745
+ return;
746
+ }
747
+ try {
748
+ const sessionId = typeof env.DEFT_SESSION_ID === "string" && env.DEFT_SESSION_ID.trim().length > 0
749
+ ? env.DEFT_SESSION_ID.trim()
750
+ : "issue-ingest";
751
+ const emitter = new RunSummaryEmitter({ projectRoot, sessionId, env });
752
+ const commands = Array.isArray(acceptance.commands) ? acceptance.commands.length : 0;
753
+ const clauses = Array.isArray(acceptance.clauses) ? acceptance.clauses.length : 0;
754
+ emitter.emitAcceptanceStamp({
755
+ rung: typeof acceptance.source_rung === "string" ? acceptance.source_rung : "project_floor",
756
+ none_stated: acceptance.none_stated === true,
757
+ command_count: commands,
758
+ clause_count: clauses,
759
+ });
760
+ }
761
+ catch {
762
+ // fail-open
763
+ }
764
+ }
723
765
  export function ingestBulk(issues, options) {
724
766
  let filtered = issues;
725
767
  if (options.label !== undefined && options.label !== null) {
@@ -0,0 +1,52 @@
1
+ /**
2
+ * Ceremony-dial escalation evaluation + start-tier provenance (#3319).
3
+ *
4
+ * Observability only: does not change escalate-on-evidence thresholds or the
5
+ * default start tier. A declined evaluation is distinct from never-evaluated.
6
+ */
7
+ import type { DialEscalationEvaluationOutcome } from "../run-summary/types.js";
8
+ import { type CeremonyDepth, type CeremonyDialInputs, type CeremonyDialSelection, type CeremonyModelTier } from "./ceremony-dial.js";
9
+ export declare const CEREMONY_START_TIER_PROVENANCES: readonly ["cold-start", "external-pin", "operator"];
10
+ export type CeremonyStartTierProvenance = (typeof CEREMONY_START_TIER_PROVENANCES)[number];
11
+ export declare const CEREMONY_DEPTH_RANK: Readonly<Record<CeremonyDepth, number>>;
12
+ export interface ResolveCeremonyStartTierProvenanceInput {
13
+ readonly selection: CeremonyDialSelection;
14
+ /** True when session:start received a pre-resolved selection (CLI/harness). */
15
+ readonly injectedSelection: boolean;
16
+ readonly hint?: CeremonyStartTierProvenance;
17
+ }
18
+ export declare function resolveCeremonyStartTierProvenance(input: ResolveCeremonyStartTierProvenanceInput): CeremonyStartTierProvenance;
19
+ export declare function isCeremonyStartTierPinned(provenance: CeremonyStartTierProvenance): boolean;
20
+ /** Session-start fail-closed line when a pin bypasses #3274 cold-start selection. */
21
+ export declare function formatCeremonyDialPinBypassLine(provenance: CeremonyStartTierProvenance): string | null;
22
+ export interface CeremonyDialEscalationEvaluation {
23
+ readonly tier: CeremonyDepth;
24
+ readonly outcome: DialEscalationEvaluationOutcome;
25
+ readonly reason: string;
26
+ readonly from: CeremonyDepth;
27
+ readonly to: CeremonyDepth;
28
+ }
29
+ export interface EvaluateCeremonyDialEscalationInput {
30
+ readonly from: CeremonyDepth;
31
+ readonly to: CeremonyDepth;
32
+ readonly inputs?: CeremonyDialInputs;
33
+ }
34
+ /**
35
+ * Pure escalate-on-evidence evaluation. `to > from` → escalated; else declined.
36
+ * Does not persist a transition.
37
+ */
38
+ export declare function evaluateCeremonyDialEscalation(input: EvaluateCeremonyDialEscalationInput): CeremonyDialEscalationEvaluation;
39
+ /** Cold-start vs selected depth at session:start (unpinned path only). */
40
+ export declare function evaluateSessionStartCeremonyDialEscalation(input: {
41
+ readonly selection: CeremonyDialSelection;
42
+ readonly modelTier?: CeremonyModelTier | null;
43
+ }): CeremonyDialEscalationEvaluation;
44
+ export interface EmitCeremonyDialEscalationEvaluationOptions {
45
+ readonly projectRoot: string;
46
+ readonly sessionId: string;
47
+ readonly env?: NodeJS.ProcessEnv;
48
+ readonly evaluation: CeremonyDialEscalationEvaluation;
49
+ }
50
+ /** Fail-open emit of a #3319 evaluation event. */
51
+ export declare function emitCeremonyDialEscalationEvaluation(options: EmitCeremonyDialEscalationEvaluationOptions): void;
52
+ //# sourceMappingURL=ceremony-dial-escalation.d.ts.map
@@ -0,0 +1,92 @@
1
+ /**
2
+ * Ceremony-dial escalation evaluation + start-tier provenance (#3319).
3
+ *
4
+ * Observability only: does not change escalate-on-evidence thresholds or the
5
+ * default start tier. A declined evaluation is distinct from never-evaluated.
6
+ */
7
+ import { RunSummaryEmitter } from "../run-summary/emit.js";
8
+ import { selectCeremonyColdStartDepth, } from "./ceremony-dial.js";
9
+ export const CEREMONY_START_TIER_PROVENANCES = ["cold-start", "external-pin", "operator"];
10
+ export const CEREMONY_DEPTH_RANK = {
11
+ minimal: 0,
12
+ rapid: 1,
13
+ standard: 2,
14
+ elevated: 3,
15
+ };
16
+ export function resolveCeremonyStartTierProvenance(input) {
17
+ if (input.hint !== undefined) {
18
+ return input.hint;
19
+ }
20
+ if (input.selection.source === "override") {
21
+ return input.injectedSelection ? "external-pin" : "operator";
22
+ }
23
+ return "cold-start";
24
+ }
25
+ export function isCeremonyStartTierPinned(provenance) {
26
+ return provenance === "external-pin" || provenance === "operator";
27
+ }
28
+ /** Session-start fail-closed line when a pin bypasses #3274 cold-start selection. */
29
+ export function formatCeremonyDialPinBypassLine(provenance) {
30
+ if (!isCeremonyStartTierPinned(provenance)) {
31
+ return null;
32
+ }
33
+ const pinLabel = provenance === "external-pin" ? "external-pin" : "operator pin";
34
+ return (`[deft ceremony-dial] #3274 cold-start selection is bypassed (${pinLabel}). ` +
35
+ "Unset the pin (--ceremony-depth or plan.policy.ceremonyDial.override) to exercise #3274.");
36
+ }
37
+ /**
38
+ * Pure escalate-on-evidence evaluation. `to > from` → escalated; else declined.
39
+ * Does not persist a transition.
40
+ */
41
+ export function evaluateCeremonyDialEscalation(input) {
42
+ const size = input.inputs?.taskSize ?? null;
43
+ const modelTier = input.inputs?.modelTier ?? null;
44
+ const evidence = `size=${size ?? "-"} modelTier=${modelTier ?? "-"}`;
45
+ const raised = CEREMONY_DEPTH_RANK[input.to] > CEREMONY_DEPTH_RANK[input.from];
46
+ if (raised) {
47
+ return {
48
+ tier: input.to,
49
+ outcome: "escalated",
50
+ reason: `evidence raised ${input.from} -> ${input.to} (${evidence})`,
51
+ from: input.from,
52
+ to: input.to,
53
+ };
54
+ }
55
+ return {
56
+ tier: input.from,
57
+ outcome: "declined",
58
+ reason: `insufficient evidence to raise above ${input.from} (${evidence})`,
59
+ from: input.from,
60
+ to: input.to,
61
+ };
62
+ }
63
+ /** Cold-start vs selected depth at session:start (unpinned path only). */
64
+ export function evaluateSessionStartCeremonyDialEscalation(input) {
65
+ const modelTier = input.modelTier ?? input.selection.inputs.modelTier;
66
+ const from = selectCeremonyColdStartDepth(modelTier);
67
+ return evaluateCeremonyDialEscalation({
68
+ from,
69
+ to: input.selection.depth,
70
+ inputs: input.selection.inputs,
71
+ });
72
+ }
73
+ /** Fail-open emit of a #3319 evaluation event. */
74
+ export function emitCeremonyDialEscalationEvaluation(options) {
75
+ try {
76
+ const emitter = new RunSummaryEmitter({
77
+ projectRoot: options.projectRoot,
78
+ sessionId: options.sessionId,
79
+ env: options.env,
80
+ });
81
+ emitter.emitDialEscalationEvaluation({
82
+ tier: options.evaluation.tier,
83
+ outcome: options.evaluation.outcome,
84
+ reason: options.evaluation.reason,
85
+ });
86
+ emitter.emitKnownToolTurnDenominator();
87
+ }
88
+ catch {
89
+ // fail-open
90
+ }
91
+ }
92
+ //# sourceMappingURL=ceremony-dial-escalation.js.map
@@ -161,7 +161,9 @@ export interface ResolveCeremonyDialOptions {
161
161
  */
162
162
  export declare function resolveCeremonyDial(projectRoot: string, options?: ResolveCeremonyDialOptions): CeremonyDialSelection;
163
163
  /** Human-readable status line for session:start / policy:show. */
164
- export declare function formatCeremonyDialStatusLine(selection: CeremonyDialSelection): string;
164
+ export declare function formatCeremonyDialStatusLine(selection: CeremonyDialSelection, extras?: {
165
+ readonly startTierProvenance?: string;
166
+ }): string;
165
167
  /** JSON-serializable dial snapshot for ritual-state / session payload. */
166
168
  export declare function ceremonyDialToDict(selection: CeremonyDialSelection): Record<string, unknown>;
167
169
  export interface CeremonyDialPolicyField {