@deftai/directive-core 0.102.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 (74) 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/init-deposit/hygiene.d.ts +7 -1
  13. package/dist/init-deposit/hygiene.js +11 -3
  14. package/dist/init-deposit/scaffold.d.ts +12 -0
  15. package/dist/init-deposit/scaffold.js +81 -8
  16. package/dist/intake/clause-derivation.d.ts +37 -0
  17. package/dist/intake/clause-derivation.js +207 -0
  18. package/dist/intake/index.d.ts +1 -0
  19. package/dist/intake/index.js +1 -0
  20. package/dist/intake/issue-ingest.js +2 -38
  21. package/dist/preflight/evaluate.d.ts +5 -0
  22. package/dist/preflight/evaluate.js +14 -0
  23. package/dist/preflight/index.d.ts +1 -1
  24. package/dist/product-first-done-gate/empty-resolution.d.ts +1 -1
  25. package/dist/product-first-done-gate/empty-resolution.js +1 -1
  26. package/dist/product-first-done-gate/evaluate.d.ts +29 -0
  27. package/dist/product-first-done-gate/evaluate.js +110 -23
  28. package/dist/product-first-done-gate/index.d.ts +1 -1
  29. package/dist/product-first-done-gate/index.js +1 -1
  30. package/dist/run-summary/emit.d.ts +42 -2
  31. package/dist/run-summary/emit.js +250 -25
  32. package/dist/run-summary/index.d.ts +1 -1
  33. package/dist/run-summary/index.js +1 -1
  34. package/dist/run-summary/share.d.ts +1 -1
  35. package/dist/run-summary/share.js +2 -2
  36. package/dist/run-summary/types.d.ts +10 -3
  37. package/dist/scope/acceptance-activate-gate.d.ts +1 -1
  38. package/dist/scope/acceptance-activate-gate.js +4 -4
  39. package/dist/scope/acceptance-evidence.d.ts +15 -0
  40. package/dist/scope/acceptance-evidence.js +39 -0
  41. package/dist/scope/capacity-stamp.d.ts +2 -0
  42. package/dist/scope/capacity-stamp.js +4 -0
  43. package/dist/scope/delivery-evidence.d.ts +6 -0
  44. package/dist/scope/delivery-evidence.js +17 -0
  45. package/dist/scope/transition.js +66 -4
  46. package/dist/session/ceremony-dial-evidence.d.ts +42 -0
  47. package/dist/session/ceremony-dial-evidence.js +194 -0
  48. package/dist/session/index.d.ts +1 -0
  49. package/dist/session/index.js +1 -0
  50. package/dist/session/session-start.js +21 -2
  51. package/dist/slice/existing.js +12 -1
  52. package/dist/slice/record.d.ts +8 -5
  53. package/dist/slice/record.js +21 -13
  54. package/dist/telemetry-coverage/evaluate.d.ts +42 -0
  55. package/dist/telemetry-coverage/evaluate.js +148 -0
  56. package/dist/telemetry-coverage/fake-trial.d.ts +45 -0
  57. package/dist/telemetry-coverage/fake-trial.js +194 -0
  58. package/dist/telemetry-coverage/index.d.ts +5 -0
  59. package/dist/telemetry-coverage/index.js +5 -0
  60. package/dist/telemetry-coverage/kinds.d.ts +19 -0
  61. package/dist/telemetry-coverage/kinds.js +52 -0
  62. package/dist/telemetry-coverage/scan-callers.d.ts +25 -0
  63. package/dist/telemetry-coverage/scan-callers.js +168 -0
  64. package/dist/vbrief-reconcile/index.d.ts +2 -0
  65. package/dist/vbrief-reconcile/index.js +1 -0
  66. package/dist/vbrief-reconcile/origin-freshness.d.ts +44 -0
  67. package/dist/vbrief-reconcile/origin-freshness.js +221 -0
  68. package/dist/verify-ac/evaluate.d.ts +18 -0
  69. package/dist/verify-ac/evaluate.js +24 -2
  70. package/dist/verify-ac/index.d.ts +1 -1
  71. package/dist/verify-ac/index.js +1 -1
  72. package/dist/verify-source/index.d.ts +1 -0
  73. package/dist/verify-source/index.js +1 -0
  74. package/package.json +3 -3
@@ -5,10 +5,11 @@
5
5
  * most one warning when an *explicit* DEFT_RUN_SUMMARY_PATH write fails.
6
6
  * Symlink destinations are refused (no-follow / containment) — fail-open.
7
7
  */
8
- import { mkdirSync, realpathSync } from "node:fs";
8
+ import { randomBytes } from "node:crypto";
9
+ import { existsSync, mkdirSync, readFileSync, realpathSync, unlinkSync } from "node:fs";
9
10
  import { basename, dirname, isAbsolute, relative, resolve, sep } from "node:path";
10
11
  import { readCorePackageVersion } from "../engine-version.js";
11
- import { containedWrite } from "../fs/contained-write.js";
12
+ import { ContainedWriteError, ContainedWriteErrorCode, containedWrite, } from "../fs/contained-write.js";
12
13
  import { assertWriteTargetSafe } from "../fs/projection-containment.js";
13
14
  import { resolveRunSummaryDestination } from "./path.js";
14
15
  import { ENV_TOTAL_TOOL_TURNS, RUN_SUMMARY_SCHEMA_VERSION, RUN_SUMMARY_STDOUT_PREFIX, RUN_SUMMARY_WRITE_WARNING, } from "./types.js";
@@ -80,7 +81,169 @@ function writeRunSummaryLine(projectRoot, targetPath, textLine, mode) {
80
81
  assertRealpathStillContained(parentAbs, targetAbs);
81
82
  }
82
83
  /**
83
- * Stateful emitter: one instance per session tracks seq and write-warning once.
84
+ * Count non-empty lines in an existing append-only JSONL. Missing/unreadable
85
+ * files seed at 0 (fail-open). Used so a fresh CLI process continues seq
86
+ * from the destination file instead of resetting to 1 (#3350).
87
+ */
88
+ function countExistingJsonlLines(path) {
89
+ try {
90
+ if (!existsSync(path)) {
91
+ return 0;
92
+ }
93
+ const text = readFileSync(path, "utf8");
94
+ if (text.length === 0) {
95
+ return 0;
96
+ }
97
+ let count = 0;
98
+ for (const line of text.split(/\r?\n/)) {
99
+ if (line.trim().length > 0) {
100
+ count += 1;
101
+ }
102
+ }
103
+ return count;
104
+ }
105
+ catch {
106
+ return 0;
107
+ }
108
+ }
109
+ function seedSeqFromDestination(destination) {
110
+ if (destination.kind !== "file") {
111
+ return 0;
112
+ }
113
+ return countExistingJsonlLines(destination.path);
114
+ }
115
+ const SEQ_LOCK_WAIT_MS = 2_000;
116
+ const SEQ_LOCK_SPIN_MS = 15;
117
+ function isErrno(err, code) {
118
+ return Boolean(err && typeof err === "object" && "code" in err && err.code === code);
119
+ }
120
+ function isPidAlive(pid) {
121
+ try {
122
+ process.kill(pid, 0);
123
+ return true;
124
+ }
125
+ catch (err) {
126
+ // EPERM: process exists but we cannot signal it — treat as live.
127
+ return isErrno(err, "EPERM");
128
+ }
129
+ }
130
+ function parseSeqLockOwner(raw) {
131
+ try {
132
+ const parsed = JSON.parse(raw);
133
+ if (parsed === null || typeof parsed !== "object") {
134
+ return null;
135
+ }
136
+ const pid = parsed.pid;
137
+ const nonce = parsed.nonce;
138
+ if (typeof pid !== "number" || !Number.isInteger(pid) || pid <= 0) {
139
+ return null;
140
+ }
141
+ if (typeof nonce !== "string" || nonce.length === 0) {
142
+ return null;
143
+ }
144
+ return { pid, nonce };
145
+ }
146
+ catch {
147
+ return null;
148
+ }
149
+ }
150
+ function newSeqLockOwner() {
151
+ return { pid: process.pid, nonce: randomBytes(8).toString("hex") };
152
+ }
153
+ function serializeSeqLockOwner(owner) {
154
+ return `${JSON.stringify({ pid: owner.pid, nonce: owner.nonce })}\n`;
155
+ }
156
+ /**
157
+ * Reclaim `.seq.lock` only when the owner process is dead or the token is
158
+ * missing/unreadable/corrupt (unknown owner). Live holders are never stolen
159
+ * by mtime (#3361).
160
+ */
161
+ export function tryReclaimSeqLock(lockPath) {
162
+ try {
163
+ const owner = parseSeqLockOwner(readFileSync(lockPath, "utf8"));
164
+ if (owner !== null && isPidAlive(owner.pid)) {
165
+ return false;
166
+ }
167
+ unlinkSync(lockPath);
168
+ return true;
169
+ }
170
+ catch {
171
+ return false;
172
+ }
173
+ }
174
+ /**
175
+ * Unlink `.seq.lock` only when the on-disk token still matches this holder
176
+ * (ownership-blind release must not delete a successor's lock).
177
+ */
178
+ export function releaseSeqLockIfOwner(lockPath, owner) {
179
+ try {
180
+ const current = parseSeqLockOwner(readFileSync(lockPath, "utf8"));
181
+ if (current === null || current.pid !== owner.pid || current.nonce !== owner.nonce) {
182
+ return false;
183
+ }
184
+ unlinkSync(lockPath);
185
+ return true;
186
+ }
187
+ catch {
188
+ return false;
189
+ }
190
+ }
191
+ /**
192
+ * Cross-process exclusive lock so count-then-append is one critical section
193
+ * (#3350 / #3361). Fail-open: if the lock cannot be acquired within the wait
194
+ * window, emit still proceeds with a best-effort recount. Reclaim is allowed
195
+ * only when the owner process is dead or the token is unknown — never by
196
+ * mtime of a live holder.
197
+ */
198
+ function acquireSeqLock(targetPath) {
199
+ const noop = () => {
200
+ /* fail-open: no lock held */
201
+ };
202
+ try {
203
+ const targetAbs = resolve(targetPath);
204
+ const parent = dirname(targetAbs);
205
+ mkdirSync(parent, { recursive: true });
206
+ const lockPath = `${targetAbs}.seq.lock`;
207
+ const deadline = Date.now() + SEQ_LOCK_WAIT_MS;
208
+ while (true) {
209
+ const owner = newSeqLockOwner();
210
+ try {
211
+ containedWrite({
212
+ root: parent,
213
+ target: lockPath,
214
+ data: serializeSeqLockOwner(owner),
215
+ mode: "create",
216
+ });
217
+ return () => {
218
+ releaseSeqLockIfOwner(lockPath, owner);
219
+ };
220
+ }
221
+ catch (err) {
222
+ if (err instanceof ContainedWriteError && err.code === ContainedWriteErrorCode.EXISTS) {
223
+ if (tryReclaimSeqLock(lockPath)) {
224
+ continue;
225
+ }
226
+ if (Date.now() < deadline) {
227
+ const spinEnd = Date.now() + SEQ_LOCK_SPIN_MS;
228
+ while (Date.now() < spinEnd) {
229
+ /* brief spin */
230
+ }
231
+ continue;
232
+ }
233
+ }
234
+ return noop;
235
+ }
236
+ }
237
+ }
238
+ catch {
239
+ return noop;
240
+ }
241
+ }
242
+ /**
243
+ * Stateful emitter: one instance tracks seq and write-warning once.
244
+ * File destinations seed seq from the current JSONL line count so multiple
245
+ * CLI processes appending to one DEFT_RUN_SUMMARY_PATH share 1..N (#3350).
246
+ * Stdout destinations stay per-process (each constructor starts at 0).
84
247
  */
85
248
  export class RunSummaryEmitter {
86
249
  seq = 0;
@@ -105,43 +268,51 @@ export class RunSummaryEmitter {
105
268
  env: this.env,
106
269
  gitignoreCovers: options.gitignoreCovers,
107
270
  });
271
+ this.seq = seedSeqFromDestination(this.destination);
108
272
  this.writeStdout = options.writeStdout ?? ((line) => process.stdout.write(`${line}\n`));
109
273
  this.writeStderr = options.writeStderr ?? ((line) => process.stderr.write(`${line}\n`));
110
274
  }
111
275
  getDestination() {
112
276
  return this.destination;
113
277
  }
278
+ buildLine(event, payload) {
279
+ const denominator = readPayloadToolTurnDenominator(payload) ?? readEnvToolTurnDenominator(this.env);
280
+ return {
281
+ schema_version: RUN_SUMMARY_SCHEMA_VERSION,
282
+ session_id: this.sessionId,
283
+ framework_version: this.frameworkVersion,
284
+ seq: this.seq,
285
+ ts: this.now().toISOString(),
286
+ event,
287
+ payload,
288
+ ...(denominator !== undefined ? { total_tool_turns: denominator } : {}),
289
+ };
290
+ }
114
291
  emit(event, payload) {
115
292
  try {
116
293
  if (this.destination.kind === "silent") {
117
294
  return { emitted: false, destination: this.destination, line: null, warning: false };
118
295
  }
119
- this.seq += 1;
120
- const denominator = readPayloadToolTurnDenominator(payload) ?? readEnvToolTurnDenominator(this.env);
121
- const line = {
122
- schema_version: RUN_SUMMARY_SCHEMA_VERSION,
123
- session_id: this.sessionId,
124
- framework_version: this.frameworkVersion,
125
- seq: this.seq,
126
- ts: this.now().toISOString(),
127
- event,
128
- payload,
129
- ...(denominator !== undefined ? { total_tool_turns: denominator } : {}),
130
- };
131
- const text = lineToJson(line);
132
296
  if (this.destination.kind === "stdout") {
133
- this.writeStdout(`${RUN_SUMMARY_STDOUT_PREFIX}${text}`);
297
+ this.seq += 1;
298
+ const line = this.buildLine(event, payload);
299
+ this.writeStdout(`${RUN_SUMMARY_STDOUT_PREFIX}${lineToJson(line)}`);
134
300
  return { emitted: true, destination: this.destination, line, warning: false };
135
301
  }
136
- // file destination — no-follow containment (#3282 Greptile P1 security)
302
+ // file destination — lock count-then-append so concurrent CLI processes
303
+ // cannot share the same next seq (#3350 Greptile P1).
137
304
  const { path, truncateOnSessionStart, explicit } = this.destination;
305
+ const release = acquireSeqLock(path);
138
306
  try {
139
- const mode = event === "session_start" && truncateOnSessionStart ? "replace" : "append";
140
- writeRunSummaryLine(this.projectRoot, path, text, mode);
307
+ const replace = event === "session_start" && truncateOnSessionStart;
308
+ this.seq = replace ? 1 : countExistingJsonlLines(path) + 1;
309
+ const line = this.buildLine(event, payload);
310
+ writeRunSummaryLine(this.projectRoot, path, lineToJson(line), replace ? "replace" : "append");
141
311
  return { emitted: true, destination: this.destination, line, warning: false };
142
312
  }
143
313
  catch {
144
314
  // Symlink / containment refusal and I/O both fail-open.
315
+ const line = this.buildLine(event, payload);
145
316
  if (explicit && !this.warned) {
146
317
  this.warned = true;
147
318
  this.writeStderr(RUN_SUMMARY_WRITE_WARNING);
@@ -149,6 +320,9 @@ export class RunSummaryEmitter {
149
320
  }
150
321
  return { emitted: false, destination: this.destination, line, warning: false };
151
322
  }
323
+ finally {
324
+ release();
325
+ }
152
326
  }
153
327
  catch {
154
328
  return { emitted: false, destination: this.destination, line: null, warning: false };
@@ -186,6 +360,16 @@ export class RunSummaryEmitter {
186
360
  }
187
361
  return this.emitToolTurnDenominator({ total_tool_turns: n });
188
362
  }
363
+ /**
364
+ * Always emit a session denominator when the destination is live (#3356).
365
+ * `emitKnownToolTurnDenominator` stays silent unless DEFT_TOTAL_TOOL_TURNS is
366
+ * set; this caller records host planned turns or the session:start CLI floor.
367
+ */
368
+ emitSessionToolTurnDenominator(hostMaxTurns) {
369
+ return this.emitToolTurnDenominator({
370
+ total_tool_turns: resolveSessionToolTurnDenominator(this.env, hostMaxTurns),
371
+ });
372
+ }
189
373
  }
190
374
  function readPayloadToolTurnDenominator(payload) {
191
375
  if (!("total_tool_turns" in payload)) {
@@ -196,19 +380,60 @@ function readPayloadToolTurnDenominator(payload) {
196
380
  }
197
381
  /** Parse harness-supplied DEFT_TOTAL_TOOL_TURNS (integer > 0). Invalid/unset → omit. */
198
382
  export function readEnvToolTurnDenominator(env) {
199
- const raw = env[ENV_TOTAL_TOOL_TURNS];
383
+ return readPositiveIntegerEnv(env, ENV_TOTAL_TOOL_TURNS);
384
+ }
385
+ /** Canonical host planned-turn budget recorded at session:start (#3356). */
386
+ export const ENV_MAX_TURNS_DENOMINATOR = "DEFT_MAX_TURNS";
387
+ /** session:start is one CLI invocation when no host/harness count is known (#3356). */
388
+ export const SESSION_START_CLI_INVOCATION_DENOMINATOR = 1;
389
+ function isPositiveIntegerDenominator(value) {
390
+ return (typeof value === "number" && Number.isInteger(value) && Number.isFinite(value) && value > 0);
391
+ }
392
+ /**
393
+ * Host descriptors (#3266) accept any finite number >= 0. Keep a positive
394
+ * planned-turn budget as-is so share uses the host value, not a floored proxy.
395
+ */
396
+ function coercePositiveHostDenominator(value) {
397
+ if (typeof value !== "number" || !Number.isFinite(value) || value <= 0) {
398
+ return undefined;
399
+ }
400
+ return value;
401
+ }
402
+ function readPositiveIntegerEnv(env, key) {
403
+ const raw = env[key];
200
404
  if (raw === undefined || raw.trim().length === 0) {
201
405
  return undefined;
202
406
  }
203
407
  const n = Number(raw.trim());
204
- if (!Number.isInteger(n) || !Number.isFinite(n) || n <= 0) {
205
- return undefined;
408
+ return isPositiveIntegerDenominator(n) ? n : undefined;
409
+ }
410
+ /**
411
+ * Resolve the session tool/turn denominator (#3356).
412
+ *
413
+ * Prefer harness actuals (`DEFT_TOTAL_TOOL_TURNS`), then a host planned-turn
414
+ * budget (`DEFT_MAX_TURNS` or `hostMaxTurns` from the session:start descriptor),
415
+ * else this CLI invocation (1). An emitted proxy beats a perfect unused kind.
416
+ */
417
+ export function resolveSessionToolTurnDenominator(env, hostMaxTurns) {
418
+ const known = readEnvToolTurnDenominator(env);
419
+ if (known !== undefined) {
420
+ return known;
421
+ }
422
+ const planned = readPositiveIntegerEnv(env, ENV_MAX_TURNS_DENOMINATOR);
423
+ if (planned !== undefined) {
424
+ return planned;
425
+ }
426
+ const host = coercePositiveHostDenominator(hostMaxTurns);
427
+ if (host !== undefined) {
428
+ return host;
206
429
  }
207
- return n;
430
+ return SESSION_START_CLI_INVOCATION_DENOMINATOR;
208
431
  }
209
432
  /**
210
433
  * One-shot helper for call sites that do not hold an emitter (e.g. dial escalate).
211
- * Still fail-open; creates a fresh seq=1 emitter unless `seqSeed` is passed via reuse.
434
+ * Still fail-open. File destinations seed seq from the existing JSONL line
435
+ * count so successive one-shot calls into the same path continue 1..N (#3350).
436
+ * Stdout destinations stay per-process (each call starts at seq=1).
212
437
  */
213
438
  export function emitRunSummaryEvent(options) {
214
439
  const emitter = new RunSummaryEmitter(options);
@@ -1,4 +1,4 @@
1
- export { type EmitRunSummaryResult, emitRunSummaryEvent, RunSummaryEmitter, type RunSummaryEmitterOptions, } from "./emit.js";
1
+ export { type EmitRunSummaryResult, ENV_MAX_TURNS_DENOMINATOR, emitRunSummaryEvent, RunSummaryEmitter, type RunSummaryEmitterOptions, resolveSessionToolTurnDenominator, SESSION_START_CLI_INVOCATION_DENOMINATOR, } from "./emit.js";
2
2
  export { gitignoreCoversRunSummary, type ResolveRunSummaryDestinationOptions, resolveRunSummaryDestination, } from "./path.js";
3
3
  export { computeRitualGateShare, parseRunSummaryJsonl, type RitualGateShare, readToolTurnDenominator, } from "./share.js";
4
4
  export { type AcceptanceClauseOutcomeRow, type AcceptanceRunSummaryOutcome, type AcceptanceRunSummaryPayload, type AcceptanceStampRunSummaryPayload, type CheckGateOutcome, type CheckInvocationRunSummaryPayload, DEFAULT_RUN_SUMMARY_BASENAME, type DialEscalationEvaluationOutcome, type DialEscalationEvaluationRunSummaryPayload, type DialTransitionRunSummaryPayload, ENV_RUN_SUMMARY_PATH, ENV_TOTAL_TOOL_TURNS, RUN_SUMMARY_EVENT_KINDS, RUN_SUMMARY_SCHEMA_VERSION, RUN_SUMMARY_STDOUT_PREFIX, RUN_SUMMARY_WRITE_WARNING, type RunSummaryBaseFields, type RunSummaryDestination, type RunSummaryEventKind, type RunSummaryLine, type RunSummaryPayload, type SessionStartRunSummaryPayload, type ToolTurnDenominatorRunSummaryPayload, type VerificationOutcome, type VerificationRunSummaryPayload, } from "./types.js";
@@ -1,4 +1,4 @@
1
- export { emitRunSummaryEvent, RunSummaryEmitter, } from "./emit.js";
1
+ export { ENV_MAX_TURNS_DENOMINATOR, emitRunSummaryEvent, RunSummaryEmitter, resolveSessionToolTurnDenominator, SESSION_START_CLI_INVOCATION_DENOMINATOR, } from "./emit.js";
2
2
  export { gitignoreCoversRunSummary, resolveRunSummaryDestination, } from "./path.js";
3
3
  export { computeRitualGateShare, parseRunSummaryJsonl, readToolTurnDenominator, } from "./share.js";
4
4
  export { DEFAULT_RUN_SUMMARY_BASENAME, ENV_RUN_SUMMARY_PATH, ENV_TOTAL_TOOL_TURNS, RUN_SUMMARY_EVENT_KINDS, RUN_SUMMARY_SCHEMA_VERSION, RUN_SUMMARY_STDOUT_PREFIX, RUN_SUMMARY_WRITE_WARNING, } from "./types.js";
@@ -11,7 +11,7 @@ export interface RitualGateShare {
11
11
  readonly totalToolTurns: number | null;
12
12
  readonly share: number | null;
13
13
  }
14
- /** True when n is a finite integer usable as a share denominator. */
14
+ /** True when n is a finite positive number usable as a share denominator. */
15
15
  export declare function isValidToolTurnDenominator(value: unknown): value is number;
16
16
  /** Read total_tool_turns from a line (top-level field or payload). */
17
17
  export declare function readToolTurnDenominator(line: unknown): number | undefined;
@@ -11,9 +11,9 @@ function asRecord(value) {
11
11
  }
12
12
  return value;
13
13
  }
14
- /** True when n is a finite integer usable as a share denominator. */
14
+ /** True when n is a finite positive number usable as a share denominator. */
15
15
  export function isValidToolTurnDenominator(value) {
16
- return (typeof value === "number" && Number.isInteger(value) && Number.isFinite(value) && value > 0);
16
+ return typeof value === "number" && Number.isFinite(value) && value > 0;
17
17
  }
18
18
  /** Read total_tool_turns from a line (top-level field or payload). */
19
19
  export function readToolTurnDenominator(line) {
@@ -21,7 +21,14 @@ export interface RunSummaryBaseFields {
21
21
  readonly schema_version: typeof RUN_SUMMARY_SCHEMA_VERSION;
22
22
  readonly session_id: string;
23
23
  readonly framework_version: string;
24
- /** Monotonic per-emitter sequence (1-based). */
24
+ /**
25
+ * Monotonic 1-based sequence for this destination (#3350).
26
+ * File destinations continue from the existing JSONL line count so multiple
27
+ * CLI processes appending to one DEFT_RUN_SUMMARY_PATH share 1..N. Concurrent
28
+ * file writers serialize count-then-append via a sibling lock; reclaim is
29
+ * allowed only when the lock owner process is dead or the token is unknown
30
+ * (#3361). Stdout destinations stay per-process (each process starts at 1).
31
+ */
25
32
  readonly seq: number;
26
33
  readonly ts: string;
27
34
  readonly event: RunSummaryEventKind;
@@ -87,8 +94,8 @@ export interface ToolTurnDenominatorRunSummaryPayload {
87
94
  }
88
95
  /** Product-oracle attempt (#3322). Same emitter as #3319 / #3320. */
89
96
  export type VerificationOutcome = "pass" | "fail";
90
- /** verify:ac resolution telemetry (#3334). Distinct from verification pass/fail. */
91
- export type AcceptanceRunSummaryOutcome = "verified-pass" | "empty-pass" | "soft_empty" | "fail";
97
+ /** verify:ac resolution telemetry (#3334 / #3355). Distinct from verification pass/fail. */
98
+ export type AcceptanceRunSummaryOutcome = "verified-pass" | "empty-pass" | "soft_empty" | "fail" | "config-error" | "soft-missing";
92
99
  export interface AcceptanceRunSummaryPayload {
93
100
  readonly resolved_command_count: number;
94
101
  readonly outcome: AcceptanceRunSummaryOutcome;
@@ -3,7 +3,7 @@
3
3
  *
4
4
  * Agents author Test / AcceptanceCriteria / Verification prose in narratives
5
5
  * and then skip the executable plan.acceptance block. Activation fails closed
6
- * with one remediation: stamp the acceptance block.
6
+ * with one remediation: stamp plan.acceptance via clause derivation.
7
7
  */
8
8
  export interface AcceptanceActivateHit {
9
9
  readonly key: string;
@@ -3,7 +3,7 @@
3
3
  *
4
4
  * Agents author Test / AcceptanceCriteria / Verification prose in narratives
5
5
  * and then skip the executable plan.acceptance block. Activation fails closed
6
- * with one remediation: stamp the acceptance block.
6
+ * with one remediation: stamp plan.acceptance via clause derivation.
7
7
  */
8
8
  import { validatePlanAcceptance } from "../product-first-done-gate/acceptance.js";
9
9
  function asRecord(value) {
@@ -51,7 +51,7 @@ export function evaluateAcceptanceActivateGate(plan) {
51
51
  ok: false,
52
52
  message: `Refusing activate: plan.acceptance is present but invalid (#3334): ` +
53
53
  `${schemaErrors.join("; ")}. Stamp a valid plan.acceptance block ` +
54
- `(author commands or none_stated via intake capture / task issue:ingest).`,
54
+ `via clause derivation (scope:activate / scope:promote runs #3323).`,
55
55
  hits,
56
56
  };
57
57
  }
@@ -61,8 +61,8 @@ export function evaluateAcceptanceActivateGate(plan) {
61
61
  return {
62
62
  ok: false,
63
63
  message: `Refusing activate: narratives contain acceptance-shaped keys (${keys}) ` +
64
- `but plan.acceptance is absent (#3334). Stamp plan.acceptance ` +
65
- `(author commands or none_stated via intake capture / task issue:ingest) — ` +
64
+ `but plan.acceptance is absent (#3334). Stamp plan.acceptance via clause derivation ` +
65
+ `(scope:activate / scope:promote runs #3323, or task issue:ingest) — ` +
66
66
  `the criteria are in the wrong, non-executable field.`,
67
67
  hits,
68
68
  };
@@ -13,6 +13,7 @@
13
13
  * ITEM_CORE is not expanded with bare keys; verify:vbrief-conformance rejects them.
14
14
  */
15
15
  import type { GrantOrigin } from "../authz/types.js";
16
+ import { type EvaluateVerifyAcOptions } from "../product-first-done-gate/evaluate.js";
16
17
  /** Canonical namespaced key for typed acceptance evidence (#3305 / #1620). */
17
18
  export declare const ACCEPTANCE_EVIDENCE_KEY: "x-directive/evidence";
18
19
  /** Canonical namespaced key for human-origin disposition (#3305 / #1620). */
@@ -98,6 +99,20 @@ export declare function stampNamespacedEvidence(item: Record<string, unknown>, r
98
99
  * cannot carry both acceptance fields after a replace (#3305 Greptile P2).
99
100
  */
100
101
  export declare function stampNamespacedDisposition(item: Record<string, unknown>, record: AcceptanceDispositionRecord): void;
102
+ /**
103
+ * One remediation when scope:complete cannot accept empty or failing product AC (#3357).
104
+ * Item-level disposition is not a substitute for the acceptance walk.
105
+ */
106
+ export declare const SCOPE_COMPLETE_ACCEPTANCE_REMEDIATION = "Run task verify:ac -- <completing-xbrief> and stamp executable plan.acceptance; scope:complete refuses empty or failing acceptance (disposition is not a substitute) (#3357)";
107
+ /**
108
+ * Hard precondition for scope:complete (#3357 / #3267 / #3284).
109
+ *
110
+ * Runs the same verify:ac walk check uses. Disposition on plan items does not
111
+ * skip it. Briefs with no stamped plan.acceptance and no executable commands
112
+ * keep the legacy #3267 "nothing to run" pass so existing evidence-only
113
+ * fixtures stay valid.
114
+ */
115
+ export declare function evaluateScopeCompleteAcceptanceWalk(plan: Record<string, unknown>, options?: EvaluateVerifyAcOptions): AcceptanceEvidenceGateResult;
101
116
  /**
102
117
  * Fail closed when any non-terminal plan item lacks suitable evidence or a human-origin disposition.
103
118
  */
@@ -13,6 +13,7 @@
13
13
  * ITEM_CORE is not expanded with bare keys; verify:vbrief-conformance rejects them.
14
14
  */
15
15
  import { isHumanOrigin } from "../authz/origin.js";
16
+ import { evaluateVerifyAcFromPlan, } from "../product-first-done-gate/evaluate.js";
16
17
  /** Canonical namespaced key for typed acceptance evidence (#3305 / #1620). */
17
18
  export const ACCEPTANCE_EVIDENCE_KEY = "x-directive/evidence";
18
19
  /** Canonical namespaced key for human-origin disposition (#3305 / #1620). */
@@ -369,6 +370,44 @@ function walkItems(items, pathPrefix, reports) {
369
370
  walkItems(obj.items, `${path}.items`, reports);
370
371
  });
371
372
  }
373
+ /**
374
+ * One remediation when scope:complete cannot accept empty or failing product AC (#3357).
375
+ * Item-level disposition is not a substitute for the acceptance walk.
376
+ */
377
+ export const SCOPE_COMPLETE_ACCEPTANCE_REMEDIATION = "Run task verify:ac -- <completing-xbrief> and stamp executable plan.acceptance; scope:complete refuses empty or failing acceptance (disposition is not a substitute) (#3357)";
378
+ /**
379
+ * Hard precondition for scope:complete (#3357 / #3267 / #3284).
380
+ *
381
+ * Runs the same verify:ac walk check uses. Disposition on plan items does not
382
+ * skip it. Briefs with no stamped plan.acceptance and no executable commands
383
+ * keep the legacy #3267 "nothing to run" pass so existing evidence-only
384
+ * fixtures stay valid.
385
+ */
386
+ export function evaluateScopeCompleteAcceptanceWalk(plan, options = {}) {
387
+ const stamped = plan.acceptance !== undefined;
388
+ const walk = evaluateVerifyAcFromPlan(plan, {
389
+ ...options,
390
+ checkIntegrated: false,
391
+ captureFromNarratives: options.captureFromNarratives ?? true,
392
+ });
393
+ const rejectedCount = walk.rejected?.length ?? 0;
394
+ const hadWork = walk.runs.length > 0 || walk.commands.length > 0 || rejectedCount > 0;
395
+ if (!stamped && !hadWork) {
396
+ return {
397
+ ok: true,
398
+ message: "Acceptance walk not required (no stamped plan.acceptance) (#3357)",
399
+ reports: [],
400
+ };
401
+ }
402
+ if (walk.ok) {
403
+ return { ok: true, message: walk.message, reports: [] };
404
+ }
405
+ return {
406
+ ok: false,
407
+ message: `${SCOPE_COMPLETE_ACCEPTANCE_REMEDIATION}\n${walk.message}`,
408
+ reports: [],
409
+ };
410
+ }
372
411
  /**
373
412
  * Fail closed when any non-terminal plan item lacks suitable evidence or a human-origin disposition.
374
413
  */
@@ -19,6 +19,8 @@ export interface StampCompletionOptions {
19
19
  * tests so the fallback path is exercised without a network call.
20
20
  */
21
21
  readonly liveLabelReader?: LabelReader;
22
+ /** Session that ran scope:complete; check uses this to target xbrief/completed (#3357). */
23
+ readonly completedSessionId?: string | null;
22
24
  }
23
25
  /** Stamp completedAt + label-matched capacityBucket onto a completing vBRIEF (#1419, #2237). */
24
26
  export declare function stampCompletionMetadata(plan: Record<string, unknown>, projectRoot: string, timestamp: string, options?: StampCompletionOptions): void;
@@ -86,6 +86,10 @@ export function stampCompletionMetadata(plan, projectRoot, timestamp, options =
86
86
  }
87
87
  const meta = metadata;
88
88
  meta.completedAt = timestamp;
89
+ const sessionId = options.completedSessionId?.trim() ?? "";
90
+ if (sessionId.length > 0) {
91
+ meta.completedSessionId = sessionId;
92
+ }
89
93
  const existing = meta.capacityBucket;
90
94
  if (typeof existing === "string" && existing.trim().length > 0) {
91
95
  return;
@@ -32,6 +32,8 @@ export interface CompletionProvenance {
32
32
  readonly deployed: boolean | null;
33
33
  /** Always null unless explicitly supplied — never inferred from Git (#3041). */
34
34
  readonly uatVerified: boolean | null;
35
+ /** Session that completed the brief; used by check to target xbrief/completed (#3357). */
36
+ readonly completedSessionId?: string | null;
35
37
  }
36
38
  export interface DeliveryEvidenceInput {
37
39
  readonly repository?: string | null;
@@ -67,6 +69,10 @@ export interface DeliveryGateResult {
67
69
  readonly provenance: CompletionProvenance | null;
68
70
  readonly codeBearing: boolean;
69
71
  }
72
+ /**
73
+ * Session id for a completing brief (#3357). Prefer DEFT_SESSION_ID, then ritual-state.
74
+ */
75
+ export declare function resolveCompletionSessionId(projectRoot: string, env?: NodeJS.ProcessEnv): string | null;
70
76
  export declare function isNonDeliveryDisposition(value: unknown): value is NonDeliveryDisposition;
71
77
  /** True when the brief is treated as code-bearing for the delivery gate (#3041). */
72
78
  export declare function isCodeBearingScope(plan: Record<string, unknown>): boolean;
@@ -9,6 +9,7 @@
9
9
  */
10
10
  import { resolveDeliveryBranch } from "../policy/delivery-branch.js";
11
11
  import { defaultGitRunner, gitIsAncestor } from "../session/git.js";
12
+ import { readRitualState } from "../session/ritual-sentinel.js";
12
13
  /** Handoff states that Git delivery can assert (deploy/UAT never inferred). */
13
14
  export const HANDOFF_STATES = [
14
15
  "implemented",
@@ -29,6 +30,18 @@ function asRecord(value) {
29
30
  }
30
31
  return null;
31
32
  }
33
+ /**
34
+ * Session id for a completing brief (#3357). Prefer DEFT_SESSION_ID, then ritual-state.
35
+ */
36
+ export function resolveCompletionSessionId(projectRoot, env = process.env) {
37
+ const fromEnv = typeof env.DEFT_SESSION_ID === "string" ? env.DEFT_SESSION_ID.trim() : "";
38
+ if (fromEnv.length > 0) {
39
+ return fromEnv;
40
+ }
41
+ const [state] = readRitualState(projectRoot);
42
+ const id = state?.sessionId.trim() ?? "";
43
+ return id.length > 0 ? id : null;
44
+ }
32
45
  export function isNonDeliveryDisposition(value) {
33
46
  return (typeof value === "string" && NON_DELIVERY_DISPOSITIONS.includes(value));
34
47
  }
@@ -380,6 +393,10 @@ export function stampDeliveryProvenance(plan, provenance) {
380
393
  // Deploy/UAT are explicit-only; default null so readers never infer from Git.
381
394
  deployed: provenance.deployed,
382
395
  uatVerified: provenance.uatVerified,
396
+ ...(typeof provenance.completedSessionId === "string" &&
397
+ provenance.completedSessionId.trim().length > 0
398
+ ? { completedSessionId: provenance.completedSessionId.trim() }
399
+ : {}),
383
400
  };
384
401
  meta.deliveryDisposition = provenance.disposition;
385
402
  meta.handoffState = provenance.handoffState;