@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.
- package/dist/authz/classify.js +167 -4
- package/dist/check/gate-lists.d.ts +3 -2
- package/dist/check/gate-lists.js +5 -2
- package/dist/check/index.d.ts +1 -0
- package/dist/check/index.js +1 -0
- package/dist/check/session-completed-ac.d.ts +39 -0
- package/dist/check/session-completed-ac.js +198 -0
- package/dist/doctor/main.js +12 -7
- package/dist/fs/contained-write.js +2 -1
- package/dist/index.d.ts +1 -0
- package/dist/index.js +1 -0
- package/dist/init-deposit/hygiene.d.ts +7 -1
- package/dist/init-deposit/hygiene.js +11 -3
- package/dist/init-deposit/scaffold.d.ts +12 -0
- package/dist/init-deposit/scaffold.js +81 -8
- package/dist/intake/clause-derivation.d.ts +37 -0
- package/dist/intake/clause-derivation.js +207 -0
- package/dist/intake/index.d.ts +1 -0
- package/dist/intake/index.js +1 -0
- package/dist/intake/issue-ingest.js +2 -38
- package/dist/preflight/evaluate.d.ts +5 -0
- package/dist/preflight/evaluate.js +14 -0
- package/dist/preflight/index.d.ts +1 -1
- package/dist/product-first-done-gate/empty-resolution.d.ts +1 -1
- package/dist/product-first-done-gate/empty-resolution.js +1 -1
- package/dist/product-first-done-gate/evaluate.d.ts +29 -0
- package/dist/product-first-done-gate/evaluate.js +110 -23
- package/dist/product-first-done-gate/index.d.ts +1 -1
- package/dist/product-first-done-gate/index.js +1 -1
- package/dist/run-summary/emit.d.ts +42 -2
- package/dist/run-summary/emit.js +250 -25
- package/dist/run-summary/index.d.ts +1 -1
- package/dist/run-summary/index.js +1 -1
- package/dist/run-summary/share.d.ts +1 -1
- package/dist/run-summary/share.js +2 -2
- package/dist/run-summary/types.d.ts +10 -3
- package/dist/scope/acceptance-activate-gate.d.ts +1 -1
- package/dist/scope/acceptance-activate-gate.js +4 -4
- package/dist/scope/acceptance-evidence.d.ts +15 -0
- package/dist/scope/acceptance-evidence.js +39 -0
- package/dist/scope/capacity-stamp.d.ts +2 -0
- package/dist/scope/capacity-stamp.js +4 -0
- package/dist/scope/delivery-evidence.d.ts +6 -0
- package/dist/scope/delivery-evidence.js +17 -0
- package/dist/scope/transition.js +66 -4
- package/dist/session/ceremony-dial-evidence.d.ts +42 -0
- package/dist/session/ceremony-dial-evidence.js +194 -0
- package/dist/session/index.d.ts +1 -0
- package/dist/session/index.js +1 -0
- package/dist/session/session-start.js +21 -2
- package/dist/slice/existing.js +12 -1
- package/dist/slice/record.d.ts +8 -5
- package/dist/slice/record.js +21 -13
- package/dist/telemetry-coverage/evaluate.d.ts +42 -0
- package/dist/telemetry-coverage/evaluate.js +148 -0
- package/dist/telemetry-coverage/fake-trial.d.ts +45 -0
- package/dist/telemetry-coverage/fake-trial.js +194 -0
- package/dist/telemetry-coverage/index.d.ts +5 -0
- package/dist/telemetry-coverage/index.js +5 -0
- package/dist/telemetry-coverage/kinds.d.ts +19 -0
- package/dist/telemetry-coverage/kinds.js +52 -0
- package/dist/telemetry-coverage/scan-callers.d.ts +25 -0
- package/dist/telemetry-coverage/scan-callers.js +168 -0
- package/dist/vbrief-reconcile/index.d.ts +2 -0
- package/dist/vbrief-reconcile/index.js +1 -0
- package/dist/vbrief-reconcile/origin-freshness.d.ts +44 -0
- package/dist/vbrief-reconcile/origin-freshness.js +221 -0
- package/dist/verify-ac/evaluate.d.ts +18 -0
- package/dist/verify-ac/evaluate.js +24 -2
- package/dist/verify-ac/index.d.ts +1 -1
- package/dist/verify-ac/index.js +1 -1
- package/dist/verify-source/index.d.ts +1 -0
- package/dist/verify-source/index.js +1 -0
- package/package.json +3 -3
package/dist/run-summary/emit.js
CHANGED
|
@@ -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 {
|
|
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
|
-
*
|
|
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.
|
|
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 —
|
|
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
|
|
140
|
-
|
|
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
|
-
|
|
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
|
-
|
|
205
|
-
|
|
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
|
|
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
|
|
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
|
|
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
|
|
14
|
+
/** True when n is a finite positive number usable as a share denominator. */
|
|
15
15
|
export function isValidToolTurnDenominator(value) {
|
|
16
|
-
return
|
|
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
|
-
/**
|
|
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
|
|
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
|
|
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
|
-
`
|
|
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
|
-
`(
|
|
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;
|