@deftai/directive-core 0.103.0 → 0.104.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (66) hide show
  1. package/dist/authz/classify.js +167 -4
  2. package/dist/check/gate-lists.d.ts +3 -2
  3. package/dist/check/gate-lists.js +5 -2
  4. package/dist/check/index.d.ts +1 -0
  5. package/dist/check/index.js +1 -0
  6. package/dist/check/session-completed-ac.d.ts +39 -0
  7. package/dist/check/session-completed-ac.js +198 -0
  8. package/dist/doctor/main.js +12 -7
  9. package/dist/fs/contained-write.js +2 -1
  10. package/dist/index.d.ts +1 -0
  11. package/dist/index.js +1 -0
  12. package/dist/intake/clause-derivation.d.ts +37 -0
  13. package/dist/intake/clause-derivation.js +207 -0
  14. package/dist/intake/index.d.ts +1 -0
  15. package/dist/intake/index.js +1 -0
  16. package/dist/intake/issue-ingest.js +2 -38
  17. package/dist/preflight/evaluate.d.ts +5 -0
  18. package/dist/preflight/evaluate.js +14 -0
  19. package/dist/preflight/index.d.ts +1 -1
  20. package/dist/product-first-done-gate/empty-resolution.d.ts +1 -1
  21. package/dist/product-first-done-gate/empty-resolution.js +1 -1
  22. package/dist/product-first-done-gate/evaluate.d.ts +17 -0
  23. package/dist/product-first-done-gate/evaluate.js +61 -10
  24. package/dist/product-first-done-gate/index.d.ts +1 -1
  25. package/dist/product-first-done-gate/index.js +1 -1
  26. package/dist/run-summary/emit.d.ts +42 -2
  27. package/dist/run-summary/emit.js +250 -25
  28. package/dist/run-summary/index.d.ts +1 -1
  29. package/dist/run-summary/index.js +1 -1
  30. package/dist/run-summary/share.d.ts +1 -1
  31. package/dist/run-summary/share.js +2 -2
  32. package/dist/run-summary/types.d.ts +10 -3
  33. package/dist/scope/acceptance-activate-gate.d.ts +1 -1
  34. package/dist/scope/acceptance-activate-gate.js +4 -4
  35. package/dist/scope/acceptance-evidence.d.ts +15 -0
  36. package/dist/scope/acceptance-evidence.js +39 -0
  37. package/dist/scope/capacity-stamp.d.ts +2 -0
  38. package/dist/scope/capacity-stamp.js +4 -0
  39. package/dist/scope/delivery-evidence.d.ts +6 -0
  40. package/dist/scope/delivery-evidence.js +17 -0
  41. package/dist/scope/transition.js +66 -4
  42. package/dist/session/ceremony-dial-evidence.d.ts +42 -0
  43. package/dist/session/ceremony-dial-evidence.js +194 -0
  44. package/dist/session/index.d.ts +1 -0
  45. package/dist/session/index.js +1 -0
  46. package/dist/session/session-start.js +21 -2
  47. package/dist/slice/existing.js +12 -1
  48. package/dist/slice/record.d.ts +8 -5
  49. package/dist/slice/record.js +21 -13
  50. package/dist/telemetry-coverage/evaluate.d.ts +42 -0
  51. package/dist/telemetry-coverage/evaluate.js +148 -0
  52. package/dist/telemetry-coverage/fake-trial.d.ts +45 -0
  53. package/dist/telemetry-coverage/fake-trial.js +194 -0
  54. package/dist/telemetry-coverage/index.d.ts +5 -0
  55. package/dist/telemetry-coverage/index.js +5 -0
  56. package/dist/telemetry-coverage/kinds.d.ts +19 -0
  57. package/dist/telemetry-coverage/kinds.js +52 -0
  58. package/dist/telemetry-coverage/scan-callers.d.ts +25 -0
  59. package/dist/telemetry-coverage/scan-callers.js +168 -0
  60. package/dist/vbrief-reconcile/index.d.ts +2 -0
  61. package/dist/vbrief-reconcile/index.js +1 -0
  62. package/dist/vbrief-reconcile/origin-freshness.d.ts +44 -0
  63. package/dist/vbrief-reconcile/origin-freshness.js +221 -0
  64. package/dist/verify-source/index.d.ts +1 -0
  65. package/dist/verify-source/index.js +1 -0
  66. package/package.json +3 -3
@@ -0,0 +1,207 @@
1
+ /**
2
+ * Hand-authored briefs run #3323 clause derivation on activate/promote (#3360).
3
+ *
4
+ * issue:ingest already stamps clauses. The dominant consumer path authors the
5
+ * brief by hand; #3334 only required plan.acceptance to exist. This module is
6
+ * the missing derive step.
7
+ */
8
+ import { ENV_RUN_SUMMARY_PATH, RunSummaryEmitter } from "../run-summary/index.js";
9
+ import { deriveAcceptanceClauses, readAcceptanceClauses, serializeAcceptanceClauses, } from "../verify-ac/clauses.js";
10
+ const NARRATIVE_KEYS = [
11
+ "Overview",
12
+ "Description",
13
+ "Acceptance",
14
+ "AcceptanceCriteria",
15
+ "Acceptance sketch",
16
+ "AcceptanceSketch",
17
+ "Test",
18
+ "Verification",
19
+ "ImplementationPlan",
20
+ ];
21
+ function asRecord(value) {
22
+ if (typeof value === "object" && value !== null && !Array.isArray(value)) {
23
+ return value;
24
+ }
25
+ return null;
26
+ }
27
+ function isNonEmptyString(value) {
28
+ return typeof value === "string" && value.trim().length > 0;
29
+ }
30
+ /** True when activate/promote must run #3323 (absent, empty none_stated, or command-only). */
31
+ export function needsClauseDerivation(acceptance) {
32
+ if (acceptance === undefined || acceptance === null) {
33
+ return true;
34
+ }
35
+ const rec = asRecord(acceptance);
36
+ if (rec === null) {
37
+ return true;
38
+ }
39
+ return !(Array.isArray(rec.clauses) && rec.clauses.length > 0);
40
+ }
41
+ /** Task-statement text from title, narratives, and item Acceptance fields. */
42
+ export function collectTaskStatementFromPlan(plan) {
43
+ const parts = [];
44
+ if (isNonEmptyString(plan.title)) {
45
+ parts.push(plan.title.trim());
46
+ }
47
+ const narratives = asRecord(plan.narratives);
48
+ if (narratives !== null) {
49
+ const seen = new Set();
50
+ for (const key of NARRATIVE_KEYS) {
51
+ const value = narratives[key];
52
+ if (isNonEmptyString(value)) {
53
+ parts.push(value.trim());
54
+ seen.add(key.toLowerCase());
55
+ }
56
+ }
57
+ for (const [key, value] of Object.entries(narratives)) {
58
+ if (seen.has(key.toLowerCase()) || !isNonEmptyString(value)) {
59
+ continue;
60
+ }
61
+ parts.push(value.trim());
62
+ }
63
+ }
64
+ if (Array.isArray(plan.items)) {
65
+ for (const item of plan.items) {
66
+ const rec = asRecord(item);
67
+ if (rec === null) {
68
+ continue;
69
+ }
70
+ const narrative = asRecord(rec.narrative);
71
+ const text = narrative?.Acceptance;
72
+ if (isNonEmptyString(text)) {
73
+ parts.push(text.trim());
74
+ }
75
+ }
76
+ }
77
+ return parts.join("\n\n");
78
+ }
79
+ function formatAmbiguousClauseNotice(clauses) {
80
+ const flagged = clauses.filter((clause) => clause.ambiguous);
81
+ const lines = [
82
+ `#3323 clause derivation stamped ${clauses.length} clause(s)` +
83
+ (flagged.length > 0 ? `; flagged-ambiguous: ${flagged.length}` : ""),
84
+ ];
85
+ for (const clause of flagged) {
86
+ const chosen = clause.chosen_reading ?? 0;
87
+ const chosenPath = clause.readings?.[chosen]?.artifact_path ?? clause.artifact_path ?? "(none)";
88
+ lines.push(` clause ${clause.id}: two readings; chosen_reading=${chosen} (${chosenPath}) [headless #3323]`);
89
+ for (const [index, reading] of (clause.readings ?? []).entries()) {
90
+ lines.push(` reading ${index}: ${reading.artifact_path ?? "(none)"}`);
91
+ }
92
+ }
93
+ return lines.join("\n");
94
+ }
95
+ /**
96
+ * Stamp derived clauses onto plan.acceptance in place. Headless: records
97
+ * chosen_reading, never blocks on a question.
98
+ */
99
+ export function applyClauseDerivationToPlan(plan, options = {}) {
100
+ if (!needsClauseDerivation(plan.acceptance)) {
101
+ return {
102
+ applied: false,
103
+ clauses: readAcceptanceClauses(plan.acceptance),
104
+ notice: "",
105
+ };
106
+ }
107
+ const clauses = deriveAcceptanceClauses(collectTaskStatementFromPlan(plan));
108
+ if (clauses.length === 0) {
109
+ return { applied: false, clauses: [], notice: "" };
110
+ }
111
+ const existing = asRecord(plan.acceptance);
112
+ const commands = existing !== null && Array.isArray(existing.commands) ? existing.commands : [];
113
+ const hasCommands = commands.length > 0;
114
+ const noneStated = hasCommands ? existing?.none_stated === true : true;
115
+ const sourceRung = hasCommands && existing?.none_stated !== true
116
+ ? isNonEmptyString(existing?.source_rung)
117
+ ? existing.source_rung
118
+ : "stated"
119
+ : "derived";
120
+ plan.acceptance = {
121
+ ...(existing ?? {}),
122
+ commands: hasCommands ? commands : [],
123
+ none_stated: noneStated,
124
+ source_rung: sourceRung,
125
+ derived_reason: `derived ${clauses.length} independently testable clauses from the task statement before product edit (#3323)`,
126
+ clauses: serializeAcceptanceClauses(clauses),
127
+ };
128
+ if (options.emitStamp !== false && options.projectRoot !== undefined) {
129
+ emitAcceptanceStampFromPlan(options.projectRoot, plan);
130
+ }
131
+ return {
132
+ applied: true,
133
+ clauses,
134
+ notice: formatAmbiguousClauseNotice(clauses),
135
+ };
136
+ }
137
+ /** Stable fingerprint of plan.acceptance for first-write / material-change detection (#3355). */
138
+ export function acceptanceFingerprint(acceptance) {
139
+ const rec = asRecord(acceptance);
140
+ if (rec === null) {
141
+ return null;
142
+ }
143
+ const commands = Array.isArray(rec.commands) ? rec.commands.length : 0;
144
+ const clauses = Array.isArray(rec.clauses)
145
+ ? rec.clauses.map((entry, index) => {
146
+ const row = asRecord(entry);
147
+ if (row === null) {
148
+ return `${index}`;
149
+ }
150
+ return `${row.id ?? index}:${row.text ?? ""}:${row.artifact_path ?? ""}`;
151
+ })
152
+ : [];
153
+ const rung = typeof rec.source_rung === "string" ? rec.source_rung : "";
154
+ return `${rung}|${rec.none_stated === true}|${commands}|${clauses.join(";")}`;
155
+ }
156
+ /** True when next is a first write or a material change versus previous (#3355). */
157
+ export function isMaterialAcceptanceChange(previous, next) {
158
+ const nextFp = acceptanceFingerprint(next);
159
+ if (nextFp === null) {
160
+ return false;
161
+ }
162
+ return acceptanceFingerprint(previous) !== nextFp;
163
+ }
164
+ /** Fail-open acceptance_stamp emission (same contract as issue:ingest / #3355). */
165
+ export function emitAcceptanceStampFromPlan(projectRoot, plan, env = process.env) {
166
+ const rec = asRecord(plan);
167
+ if (rec === null) {
168
+ return;
169
+ }
170
+ const acceptance = asRecord(rec.acceptance);
171
+ if (acceptance === null) {
172
+ return;
173
+ }
174
+ const dest = env[ENV_RUN_SUMMARY_PATH];
175
+ if (dest === undefined || dest.trim().length === 0) {
176
+ return;
177
+ }
178
+ try {
179
+ const sessionId = typeof env.DEFT_SESSION_ID === "string" && env.DEFT_SESSION_ID.trim().length > 0
180
+ ? env.DEFT_SESSION_ID.trim()
181
+ : "clause-derivation";
182
+ const emitter = new RunSummaryEmitter({ projectRoot, sessionId, env });
183
+ const commands = Array.isArray(acceptance.commands) ? acceptance.commands.length : 0;
184
+ const clauses = Array.isArray(acceptance.clauses) ? acceptance.clauses.length : 0;
185
+ emitter.emitAcceptanceStamp({
186
+ rung: typeof acceptance.source_rung === "string" ? acceptance.source_rung : "project_floor",
187
+ none_stated: acceptance.none_stated === true,
188
+ command_count: commands,
189
+ clause_count: clauses,
190
+ });
191
+ }
192
+ catch {
193
+ // fail-open
194
+ }
195
+ }
196
+ /**
197
+ * Emit acceptance_stamp when plan.acceptance is first written or materially changed.
198
+ * State-observed: callers pass the before/after blocks from any lifecycle verb (#3355).
199
+ */
200
+ export function maybeEmitAcceptanceStampFromChange(projectRoot, previous, next, env = process.env) {
201
+ if (!isMaterialAcceptanceChange(previous, next)) {
202
+ return false;
203
+ }
204
+ emitAcceptanceStampFromPlan(projectRoot, { acceptance: next }, env);
205
+ return true;
206
+ }
207
+ //# sourceMappingURL=clause-derivation.js.map
@@ -1,4 +1,5 @@
1
1
  export * from "./candidates-log.js";
2
+ export * from "./clause-derivation.js";
2
3
  export * from "./github-auth-modes.js";
3
4
  export { resultToDict } from "./github-auth-modes.js";
4
5
  export * from "./github-body.js";
@@ -1,4 +1,5 @@
1
1
  export * from "./candidates-log.js";
2
+ export * from "./clause-derivation.js";
2
3
  export * from "./github-auth-modes.js";
3
4
  export { resultToDict } from "./github-auth-modes.js";
4
5
  export * from "./github-body.js";
@@ -6,7 +6,6 @@ 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";
10
9
  import { call } from "../scm/call.js";
11
10
  import { resolveProjectRoot } from "../scope/project-context.js";
12
11
  import { resolveProjectRepo } from "../slice/project-context.js";
@@ -15,6 +14,7 @@ import { slugify, TODAY } from "../vbrief-build/build.js";
15
14
  import { EMITTED_VBRIEF_VERSION } from "../vbrief-build/constants.js";
16
15
  import { stampDerivedClausesOnAcceptance } from "../verify-ac/clauses.js";
17
16
  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";
17
+ import { emitAcceptanceStampFromPlan } from "./clause-derivation.js";
18
18
  import { findAcHeading, parseCheckboxItems, parseListItems, sliceAcSection, stripCodeBlocks, stripFencedCodeBlocks, } from "./markdown-scanners.js";
19
19
  import { detectRepo, extractReferencesFromVbrief, fetchOpenIssues, GITHUB_ISSUE_REF_TYPES, LIFECYCLE_FOLDERS, parseIssueNumber, } from "./reconcile-issues.js";
20
20
  /** Reference type pointing at the canonical current-shape comment permalink (#1870). */
@@ -723,45 +723,9 @@ export function ingestOne(issue, options) {
723
723
  assertWriteTargetSafe(projectRoot, target);
724
724
  mkdirSync(folderPath, { recursive: true });
725
725
  writeFileSync(target, `${JSON.stringify(vbrief, null, 2)}\n`, "utf8");
726
- emitIntakeAcceptanceStamp(projectRoot, vbrief.plan);
726
+ emitAcceptanceStampFromPlan(projectRoot, vbrief.plan);
727
727
  return ["created", target, `CREATED ${folder}/${filename}`];
728
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
- }
765
729
  export function ingestBulk(issues, options) {
766
730
  let filtered = issues;
767
731
  if (options.label !== undefined && options.label !== null) {
@@ -1,4 +1,5 @@
1
1
  import { type ParentLineageResult } from "../scope/parent-lineage.js";
2
+ import { type FetchOriginUpdatedAt } from "../vbrief-reconcile/origin-freshness.js";
2
3
  /** Canonical eligibility folder — only vbrief/active/ may spawn implementation. */
3
4
  export declare const ACTIVE_FOLDER = "active";
4
5
  /** Canonical eligibility status — only `running` signals an active handoff. */
@@ -20,6 +21,10 @@ export interface EvaluateOptions {
20
21
  readonly projectRoot?: string;
21
22
  /** Skip parent-lineage check (tests / opt-out). Default false. */
22
23
  readonly skipParentLineage?: boolean;
24
+ /** Skip origin timestamp freshness (#3363). Default false. */
25
+ readonly skipOriginFreshness?: boolean;
26
+ /** Injected origin fetch for tests. Default: live `gh api` REST. */
27
+ readonly fetchOriginUpdatedAt?: FetchOriginUpdatedAt;
23
28
  }
24
29
  /** Substitute `{path}` without `$`-pattern expansion in user paths (#1721). */
25
30
  export declare function formatActivateHint(path: string): string;
@@ -2,6 +2,7 @@ import { readFileSync, statSync } from "node:fs";
2
2
  import { basename, dirname } from "node:path";
3
3
  import { evaluateIntentCeilingFromEnv } from "../policy/intent-ceiling.js";
4
4
  import { evaluateParentLineage, formatParentLineageLine, } from "../scope/parent-lineage.js";
5
+ import { evaluateOriginFreshness, } from "../vbrief-reconcile/origin-freshness.js";
5
6
  /** Canonical eligibility folder — only vbrief/active/ may spawn implementation. */
6
7
  export const ACTIVE_FOLDER = "active";
7
8
  /** Canonical eligibility status — only `running` signals an active handoff. */
@@ -154,6 +155,19 @@ export function evaluate(vbriefPath, options = {}) {
154
155
  message: buildReject(path, `${lineage.message}${defect}\n ${formatParentLineageLine(lineage)}`),
155
156
  };
156
157
  }
158
+ // #3363: fail closed when the live GitHub origin is newer than the brief.
159
+ const originFreshness = evaluateOriginFreshness(record, {
160
+ skip: options.skipOriginFreshness === true,
161
+ fetchOriginUpdatedAt: options.fetchOriginUpdatedAt,
162
+ cwd: options.projectRoot,
163
+ });
164
+ if (!originFreshness.ok) {
165
+ return {
166
+ exitCode: 1,
167
+ parentLineage: lineage,
168
+ message: buildReject(path, originFreshness.message),
169
+ };
170
+ }
157
171
  // Keep the historical OK line when lineage is N/A (backward-compatible tests / agents).
158
172
  const message = lineage.applicable
159
173
  ? `OK ${path} -- ready for implementation. parent lineage OK ` +
@@ -1,3 +1,3 @@
1
- export type { EvaluateResult } from "./evaluate.js";
1
+ export type { EvaluateOptions, EvaluateResult } from "./evaluate.js";
2
2
  export { ACTIVATE_HINT, ACTIVE_FOLDER, ELIGIBLE_LIFECYCLE_DIRS, ELIGIBLE_STATUS, emitJson, evaluate, formatActivateHint, PREFLIGHT_USAGE_HINT, } from "./evaluate.js";
3
3
  //# sourceMappingURL=index.d.ts.map
@@ -7,7 +7,7 @@
7
7
  import type { PlanAcceptance } from "./types.js";
8
8
  export declare const EMPTY_AC_OUTCOME: "soft_empty";
9
9
  export declare const EMPTY_AC_CAUSE = "no acceptance stamped \u2014 floor is empty in this project";
10
- export declare const EMPTY_AC_REMEDY = "stamp plan.acceptance (author commands or run intake capture / task issue:ingest)";
10
+ export declare const EMPTY_AC_REMEDY = "stamp plan.acceptance via clause derivation (scope:activate / scope:promote runs #3323, or task issue:ingest)";
11
11
  export type VerifyAcResolution = "verified-pass" | "empty-pass" | "soft_empty" | "fail" | "config" | "skipped";
12
12
  /** True when this project check composition includes a suite gate (#3188). */
13
13
  export declare function projectHasSuiteFloor(projectRoot: string): boolean;
@@ -7,7 +7,7 @@
7
7
  import { isFrameworkRepoRoot } from "../check/context.js";
8
8
  export const EMPTY_AC_OUTCOME = "soft_empty";
9
9
  export const EMPTY_AC_CAUSE = "no acceptance stamped — floor is empty in this project";
10
- export const EMPTY_AC_REMEDY = "stamp plan.acceptance (author commands or run intake capture / task issue:ingest)";
10
+ export const EMPTY_AC_REMEDY = "stamp plan.acceptance via clause derivation (scope:activate / scope:promote runs #3323, or task issue:ingest)";
11
11
  /** True when this project check composition includes a suite gate (#3188). */
12
12
  export function projectHasSuiteFloor(projectRoot) {
13
13
  return isFrameworkRepoRoot(projectRoot);
@@ -8,6 +8,7 @@
8
8
  * floor, empty resolution is soft_empty — not a green run (#3334).
9
9
  */
10
10
  import { type EvaluateLiteralAcceptanceOptions, type LiteralAcceptanceGateResult } from "../literal-acceptance/index.js";
11
+ import { type AcceptanceRunSummaryOutcome } from "../run-summary/index.js";
11
12
  import { type ClauseWalkResult } from "../verify-ac/clauses.js";
12
13
  import { type VerifyAcResolution } from "./empty-resolution.js";
13
14
  import type { AcSourceRung, PlanAcceptance } from "./types.js";
@@ -62,6 +63,11 @@ export interface EvaluateVerifyAcOptions extends EvaluateLiteralAcceptanceOption
62
63
  * emit after the #3285 bank checkpoint.
63
64
  */
64
65
  readonly skipAcceptanceEmit?: boolean;
66
+ /**
67
+ * Raw plan.acceptance as observed on the brief (#3355). Distinct from the
68
+ * synthesized floor: stamp only when the plan actually carries a block.
69
+ */
70
+ readonly observedAcceptance?: unknown;
65
71
  /**
66
72
  * Active scope key for product-oracle check_id namespacing (#3337).
67
73
  * Prefer plan.id; path stem when id is missing. Multi-active verify:ac
@@ -73,6 +79,17 @@ export interface EvaluateVerifyAcOptions extends EvaluateLiteralAcceptanceOption
73
79
  * Evaluate product AC from an in-memory plan.
74
80
  */
75
81
  export declare function evaluateVerifyAcFromPlan(plan: Record<string, unknown>, options?: EvaluateVerifyAcOptions): VerifyAcResult;
82
+ /**
83
+ * CLI-only early returns that never reach evaluate still emit an acceptance
84
+ * outcome so field streams see config-error / soft-missing (#3355).
85
+ */
86
+ export declare function emitVerifyAcTerminalOutcome(input: {
87
+ readonly projectRoot: string;
88
+ readonly env?: NodeJS.ProcessEnv;
89
+ readonly outcome: AcceptanceRunSummaryOutcome;
90
+ readonly sourceRung?: AcSourceRung;
91
+ readonly noneStated?: boolean;
92
+ }): void;
76
93
  /**
77
94
  * Evaluate from xBRIEF path.
78
95
  */
@@ -9,6 +9,7 @@
9
9
  */
10
10
  import { existsSync, readFileSync } from "node:fs";
11
11
  import { basename, isAbsolute, relative, resolve } from "node:path";
12
+ import { emitAcceptanceStampFromPlan } from "../intake/clause-derivation.js";
12
13
  import { evaluateLiteralAcceptanceFromPlan, runLiteralAcceptanceCommands, } from "../literal-acceptance/index.js";
13
14
  import { ENV_RUN_SUMMARY_PATH, RunSummaryEmitter, } from "../run-summary/index.js";
14
15
  import { maybeBankOnAcPass } from "../session/ac-pass-banking.js";
@@ -30,6 +31,7 @@ export function evaluateVerifyAcFromPlan(plan, options = {}) {
30
31
  const optionsWithScope = {
31
32
  ...options,
32
33
  oracleScopeKey: options.oracleScopeKey?.trim() || planId || null,
34
+ observedAcceptance: options.observedAcceptance !== undefined ? options.observedAcceptance : plan.acceptance,
33
35
  };
34
36
  const acceptance = readPlanAcceptance(plan);
35
37
  const schemaErrors = validatePlanAcceptance(plan.acceptance ?? acceptance);
@@ -181,9 +183,11 @@ function acceptanceOutcomeOf(result) {
181
183
  return "soft_empty";
182
184
  if (result.resolution === "fail")
183
185
  return "fail";
184
- return null;
186
+ if (result.resolution === "config")
187
+ return "config-error";
188
+ return "soft-missing";
185
189
  }
186
- function emitAcceptanceOutcome(result, options, projectRoot) {
190
+ function emitAcceptanceObservedStamp(options, projectRoot) {
187
191
  if (options.env === undefined) {
188
192
  return;
189
193
  }
@@ -191,8 +195,14 @@ function emitAcceptanceOutcome(result, options, projectRoot) {
191
195
  if (dest === undefined || dest.trim().length === 0) {
192
196
  return;
193
197
  }
194
- const outcome = acceptanceOutcomeOf(result);
195
- if (outcome === null) {
198
+ emitAcceptanceStampFromPlan(projectRoot, { acceptance: options.observedAcceptance }, options.env);
199
+ }
200
+ function emitAcceptanceOutcome(result, options, projectRoot) {
201
+ if (options.env === undefined) {
202
+ return;
203
+ }
204
+ const dest = options.env[ENV_RUN_SUMMARY_PATH];
205
+ if (dest === undefined || dest.trim().length === 0) {
196
206
  return;
197
207
  }
198
208
  try {
@@ -204,7 +214,7 @@ function emitAcceptanceOutcome(result, options, projectRoot) {
204
214
  });
205
215
  emitter.emitAcceptance({
206
216
  resolved_command_count: result.resolvedCommandCount,
207
- outcome,
217
+ outcome: acceptanceOutcomeOf(result),
208
218
  source_rung: result.sourceRung,
209
219
  none_stated: result.noneStated,
210
220
  clause_count: result.clauseOutcomes?.length,
@@ -218,6 +228,43 @@ function emitAcceptanceOutcome(result, options, projectRoot) {
218
228
  // fail-open
219
229
  }
220
230
  }
231
+ /** Outcome on every terminal path; stamp from observed plan.acceptance (#3355). */
232
+ function emitAcceptanceTelemetry(result, options, projectRoot) {
233
+ emitAcceptanceObservedStamp(options, projectRoot);
234
+ emitAcceptanceOutcome(result, options, projectRoot);
235
+ }
236
+ /**
237
+ * CLI-only early returns that never reach evaluate still emit an acceptance
238
+ * outcome so field streams see config-error / soft-missing (#3355).
239
+ */
240
+ export function emitVerifyAcTerminalOutcome(input) {
241
+ emitAcceptanceOutcome({
242
+ ok: input.outcome !== "config-error" && input.outcome !== "fail",
243
+ code: input.outcome === "config-error" ? 2 : input.outcome === "fail" ? 1 : 0,
244
+ message: "",
245
+ commands: [],
246
+ runs: [],
247
+ sourceRung: input.sourceRung ?? "project_floor",
248
+ noneStated: input.noneStated ?? true,
249
+ acceptance: {
250
+ commands: [],
251
+ none_stated: input.noneStated ?? true,
252
+ source_rung: input.sourceRung ?? "project_floor",
253
+ },
254
+ resolution: input.outcome === "config-error"
255
+ ? "config"
256
+ : input.outcome === "soft-missing"
257
+ ? "skipped"
258
+ : input.outcome === "fail"
259
+ ? "fail"
260
+ : input.outcome === "soft_empty"
261
+ ? "soft_empty"
262
+ : input.outcome === "verified-pass"
263
+ ? "verified-pass"
264
+ : "empty-pass",
265
+ resolvedCommandCount: 0,
266
+ }, { projectRoot: input.projectRoot, env: input.env }, resolve(input.projectRoot));
267
+ }
221
268
  function applyClauseWalk(result, options) {
222
269
  const clauses = result.acceptance.clauses ?? [];
223
270
  if (clauses.length === 0 || result.resolution === "config" || result.resolution === "skipped") {
@@ -268,7 +315,7 @@ function applyOracle(result, options) {
268
315
  }
269
316
  }
270
317
  if (options.skipAcceptanceEmit !== true) {
271
- emitAcceptanceOutcome(next, options, projectRoot);
318
+ emitAcceptanceTelemetry(next, options, projectRoot);
272
319
  }
273
320
  return next;
274
321
  }
@@ -338,13 +385,17 @@ export function evaluateVerifyAcFromPath(xbriefPath, options = {}) {
338
385
  const projectRoot = resolve(options.projectRoot ?? process.cwd());
339
386
  // Path-relative keys stay unique across xbrief/ vs vbrief/ and duplicate plan.id (#3337 Greptile).
340
387
  const oracleScopeKey = options.oracleScopeKey?.trim() || resolveOracleScopeKey(plan, abs, projectRoot);
341
- const result = evaluateVerifyAcFromPlan(plan, {
388
+ const emitOptions = {
342
389
  ...options,
343
- skipAcceptanceEmit: true,
344
390
  oracleScopeKey,
391
+ observedAcceptance: plan.acceptance,
392
+ };
393
+ const result = evaluateVerifyAcFromPlan(plan, {
394
+ ...emitOptions,
395
+ skipAcceptanceEmit: true,
345
396
  });
346
- const banked = maybeAttachAcPassBank(result, plan, abs, options);
347
- emitAcceptanceOutcome(banked, options, projectRoot);
397
+ const banked = maybeAttachAcPassBank(result, plan, abs, emitOptions);
398
+ emitAcceptanceTelemetry(banked, emitOptions, projectRoot);
348
399
  return banked;
349
400
  }
350
401
  /**
@@ -7,6 +7,6 @@
7
7
  export { attachPlanAcceptance, buildAcceptanceFromIntakeCapture, readPlanAcceptance, stampAcceptanceFromLiteralCapture, validatePlanAcceptance, } from "./acceptance.js";
8
8
  export { applyProductFirstGateMode, isHygieneGate, isProductAcGate, type ProductFirstCheckModeResolution, type ResolveProductFirstCheckModeInput, resolveProductFirstCheckMode, } from "./check-mode.js";
9
9
  export { EMPTY_AC_CAUSE, EMPTY_AC_OUTCOME, EMPTY_AC_REMEDY, formatSoftEmptyMessage, isEmptyAcResolution, isSoftEmptyAcText, projectHasSuiteFloor, type VerifyAcResolution, } from "./empty-resolution.js";
10
- export { type EvaluateVerifyAcOptions, evaluateVerifyAcFromPath, evaluateVerifyAcFromPlan, isVerifyAcRequiredAtCeremonyDepth, resolveOracleScopeKey, type VerifyAcResult, } from "./evaluate.js";
10
+ export { type EvaluateVerifyAcOptions, emitVerifyAcTerminalOutcome, evaluateVerifyAcFromPath, evaluateVerifyAcFromPlan, isVerifyAcRequiredAtCeremonyDepth, resolveOracleScopeKey, type VerifyAcResult, } from "./evaluate.js";
11
11
  export { type AcceptanceCommand, type AcSourceRung, ENV_CHECK_AC_ONLY, ENV_CHECK_MODE, ENV_HYGIENE_ADVISORY, HYGIENE_GATE_ID_PREFIXES, PLAN_ACCEPTANCE_KEY, type PlanAcceptance, PRODUCT_AC_GATE_ID, type ProductFirstCheckMode, } from "./types.js";
12
12
  //# sourceMappingURL=index.d.ts.map
@@ -7,6 +7,6 @@
7
7
  export { attachPlanAcceptance, buildAcceptanceFromIntakeCapture, readPlanAcceptance, stampAcceptanceFromLiteralCapture, validatePlanAcceptance, } from "./acceptance.js";
8
8
  export { applyProductFirstGateMode, isHygieneGate, isProductAcGate, resolveProductFirstCheckMode, } from "./check-mode.js";
9
9
  export { EMPTY_AC_CAUSE, EMPTY_AC_OUTCOME, EMPTY_AC_REMEDY, formatSoftEmptyMessage, isEmptyAcResolution, isSoftEmptyAcText, projectHasSuiteFloor, } from "./empty-resolution.js";
10
- export { evaluateVerifyAcFromPath, evaluateVerifyAcFromPlan, isVerifyAcRequiredAtCeremonyDepth, resolveOracleScopeKey, } from "./evaluate.js";
10
+ export { emitVerifyAcTerminalOutcome, evaluateVerifyAcFromPath, evaluateVerifyAcFromPlan, isVerifyAcRequiredAtCeremonyDepth, resolveOracleScopeKey, } from "./evaluate.js";
11
11
  export { ENV_CHECK_AC_ONLY, ENV_CHECK_MODE, ENV_HYGIENE_ADVISORY, HYGIENE_GATE_ID_PREFIXES, PLAN_ACCEPTANCE_KEY, PRODUCT_AC_GATE_ID, } from "./types.js";
12
12
  //# sourceMappingURL=index.js.map
@@ -25,8 +25,27 @@ export interface EmitRunSummaryResult {
25
25
  readonly line: RunSummaryLine | null;
26
26
  readonly warning: boolean;
27
27
  }
28
+ /** Owner token stored in `.seq.lock` so reclaim is not age-based (#3361). */
29
+ export interface SeqLockOwner {
30
+ readonly pid: number;
31
+ readonly nonce: string;
32
+ }
33
+ /**
34
+ * Reclaim `.seq.lock` only when the owner process is dead or the token is
35
+ * missing/unreadable/corrupt (unknown owner). Live holders are never stolen
36
+ * by mtime (#3361).
37
+ */
38
+ export declare function tryReclaimSeqLock(lockPath: string): boolean;
39
+ /**
40
+ * Unlink `.seq.lock` only when the on-disk token still matches this holder
41
+ * (ownership-blind release must not delete a successor's lock).
42
+ */
43
+ export declare function releaseSeqLockIfOwner(lockPath: string, owner: SeqLockOwner): boolean;
28
44
  /**
29
- * Stateful emitter: one instance per session tracks seq and write-warning once.
45
+ * Stateful emitter: one instance tracks seq and write-warning once.
46
+ * File destinations seed seq from the current JSONL line count so multiple
47
+ * CLI processes appending to one DEFT_RUN_SUMMARY_PATH share 1..N (#3350).
48
+ * Stdout destinations stay per-process (each constructor starts at 0).
30
49
  */
31
50
  export declare class RunSummaryEmitter {
32
51
  private seq;
@@ -41,6 +60,7 @@ export declare class RunSummaryEmitter {
41
60
  private readonly writeStderr;
42
61
  constructor(options: RunSummaryEmitterOptions);
43
62
  getDestination(): RunSummaryDestination;
63
+ private buildLine;
44
64
  emit(event: RunSummaryEventKind, payload: RunSummaryPayload): EmitRunSummaryResult;
45
65
  emitSessionStart(payload: SessionStartRunSummaryPayload): EmitRunSummaryResult;
46
66
  emitDialTransition(payload: DialTransitionRunSummaryPayload): EmitRunSummaryResult;
@@ -52,12 +72,32 @@ export declare class RunSummaryEmitter {
52
72
  emitAcceptanceStamp(payload: AcceptanceStampRunSummaryPayload): EmitRunSummaryResult;
53
73
  /** Emit the harness-supplied denominator when DEFT_TOTAL_TOOL_TURNS is set. */
54
74
  emitKnownToolTurnDenominator(): EmitRunSummaryResult;
75
+ /**
76
+ * Always emit a session denominator when the destination is live (#3356).
77
+ * `emitKnownToolTurnDenominator` stays silent unless DEFT_TOTAL_TOOL_TURNS is
78
+ * set; this caller records host planned turns or the session:start CLI floor.
79
+ */
80
+ emitSessionToolTurnDenominator(hostMaxTurns?: number | null): EmitRunSummaryResult;
55
81
  }
56
82
  /** Parse harness-supplied DEFT_TOTAL_TOOL_TURNS (integer > 0). Invalid/unset → omit. */
57
83
  export declare function readEnvToolTurnDenominator(env: NodeJS.ProcessEnv): number | undefined;
84
+ /** Canonical host planned-turn budget recorded at session:start (#3356). */
85
+ export declare const ENV_MAX_TURNS_DENOMINATOR = "DEFT_MAX_TURNS";
86
+ /** session:start is one CLI invocation when no host/harness count is known (#3356). */
87
+ export declare const SESSION_START_CLI_INVOCATION_DENOMINATOR: 1;
88
+ /**
89
+ * Resolve the session tool/turn denominator (#3356).
90
+ *
91
+ * Prefer harness actuals (`DEFT_TOTAL_TOOL_TURNS`), then a host planned-turn
92
+ * budget (`DEFT_MAX_TURNS` or `hostMaxTurns` from the session:start descriptor),
93
+ * else this CLI invocation (1). An emitted proxy beats a perfect unused kind.
94
+ */
95
+ export declare function resolveSessionToolTurnDenominator(env: NodeJS.ProcessEnv, hostMaxTurns?: number | null): number;
58
96
  /**
59
97
  * One-shot helper for call sites that do not hold an emitter (e.g. dial escalate).
60
- * Still fail-open; creates a fresh seq=1 emitter unless `seqSeed` is passed via reuse.
98
+ * Still fail-open. File destinations seed seq from the existing JSONL line
99
+ * count so successive one-shot calls into the same path continue 1..N (#3350).
100
+ * Stdout destinations stay per-process (each call starts at seq=1).
61
101
  */
62
102
  export declare function emitRunSummaryEvent(options: RunSummaryEmitterOptions & {
63
103
  readonly event: RunSummaryEventKind;