@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/scope/transition.js
CHANGED
|
@@ -1,18 +1,20 @@
|
|
|
1
1
|
import { existsSync, mkdirSync, unlinkSync } from "node:fs";
|
|
2
2
|
import { dirname, join, resolve } from "node:path";
|
|
3
|
+
import { SESSION_COMPLETED_AC_REMEDIATION, writeSessionCompletedMarker, } from "../check/session-completed-ac.js";
|
|
3
4
|
import { InstrumentedVbriefCrud, persistCrudMetrics } from "../eval/crud-telemetry.js";
|
|
4
5
|
import { assertProjectionContained, ProjectionContainmentError, } from "../fs/projection-containment.js";
|
|
6
|
+
import { applyClauseDerivationToPlan, maybeEmitAcceptanceStampFromChange, } from "../intake/clause-derivation.js";
|
|
5
7
|
import { hasArtifactSuffix } from "../layout/resolve.js";
|
|
6
8
|
import { evaluateCompletedPlanConsistency } from "../lifecycle/completed-consistency.js";
|
|
7
9
|
import { evaluateLiteralAcceptanceFromPlan } from "../literal-acceptance/index.js";
|
|
8
10
|
import { evaluateAcceptanceActivateGate } from "./acceptance-activate-gate.js";
|
|
9
|
-
import { evaluateAcceptanceEvidenceGate, formatAcceptanceCompletionListing, } from "./acceptance-evidence.js";
|
|
11
|
+
import { evaluateAcceptanceEvidenceGate, evaluateScopeCompleteAcceptanceWalk, formatAcceptanceCompletionListing, } from "./acceptance-evidence.js";
|
|
10
12
|
import { append, canonicalLogPath, newDecisionId } from "./audit-log.js";
|
|
11
13
|
import { atomicWriteBrief, formatBriefJson, readBriefForMutation } from "./brief-io.js";
|
|
12
14
|
import { stampCompletionMetadata } from "./capacity-stamp.js";
|
|
13
15
|
import { LIFECYCLE_FOLDERS, MOVE_LABELS, STATUS_PRECONDITIONS, STAY_LABELS, TRANSITIONS, } from "./constants.js";
|
|
14
16
|
import { detectLifecycleFolder, updateDecomposedChildBackReferences, updateDecomposedParentBackReferences, } from "./decomposed-refs.js";
|
|
15
|
-
import { classifyStoredDeliveryDisposition, evaluateDeliveryGate, stampDeliveryProvenance, } from "./delivery-evidence.js";
|
|
17
|
+
import { classifyStoredDeliveryDisposition, evaluateDeliveryGate, resolveCompletionSessionId, stampDeliveryProvenance, } from "./delivery-evidence.js";
|
|
16
18
|
import { evaluateEffortActivateGate } from "./effort-activate-gate.js";
|
|
17
19
|
import { syncProjectDefinitionAfterScopeMove } from "./project-definition-sync.js";
|
|
18
20
|
import { syncSpecificationAfterScopeMove } from "./specification-sync.js";
|
|
@@ -143,6 +145,19 @@ export function runTransition(action, filePath, now = new Date(), options = {})
|
|
|
143
145
|
}
|
|
144
146
|
}
|
|
145
147
|
const nowIso = utcNowIso(now);
|
|
148
|
+
// #3360: hand-authored briefs run #3323 clause derivation on activate/promote.
|
|
149
|
+
// #3355: stamp is state-observed (before vs after), not derivation-notice-bound.
|
|
150
|
+
const previousAcceptance = planObj.acceptance;
|
|
151
|
+
let derivationNotice = "";
|
|
152
|
+
if (act === "activate" || act === "promote") {
|
|
153
|
+
const derivation = applyClauseDerivationToPlan(planObj, {
|
|
154
|
+
projectRoot,
|
|
155
|
+
emitStamp: false,
|
|
156
|
+
});
|
|
157
|
+
if (derivation.applied) {
|
|
158
|
+
derivationNotice = derivation.notice;
|
|
159
|
+
}
|
|
160
|
+
}
|
|
146
161
|
// #1581: fail closed before activating a scope that still has XL plan items.
|
|
147
162
|
if (act === "activate") {
|
|
148
163
|
const effortGate = evaluateEffortActivateGate(planObj);
|
|
@@ -170,7 +185,10 @@ export function runTransition(action, filePath, now = new Date(), options = {})
|
|
|
170
185
|
return { ok: false, message: gate.message };
|
|
171
186
|
}
|
|
172
187
|
if (gate.provenance !== null) {
|
|
173
|
-
|
|
188
|
+
const sessionId = resolveCompletionSessionId(projectRoot);
|
|
189
|
+
stampDeliveryProvenance(planObj, sessionId !== null
|
|
190
|
+
? { ...gate.provenance, completedSessionId: sessionId }
|
|
191
|
+
: gate.provenance);
|
|
174
192
|
}
|
|
175
193
|
}
|
|
176
194
|
// #3240: per-criterion typed evidence or human-origin disposition before auto-advance.
|
|
@@ -207,6 +225,19 @@ export function runTransition(action, filePath, now = new Date(), options = {})
|
|
|
207
225
|
? `${acceptanceListing}\n${literalGate.message}`
|
|
208
226
|
: literalGate.message;
|
|
209
227
|
}
|
|
228
|
+
// #3357: verify:ac walk is a hard precondition. Disposition does not skip it.
|
|
229
|
+
const acWalk = evaluateScopeCompleteAcceptanceWalk(planObj, { projectRoot });
|
|
230
|
+
if (!acWalk.ok) {
|
|
231
|
+
return {
|
|
232
|
+
ok: false,
|
|
233
|
+
message: acWalk.message,
|
|
234
|
+
acceptanceReports: acceptanceGate.reports,
|
|
235
|
+
};
|
|
236
|
+
}
|
|
237
|
+
if (acWalk.message.length > 0) {
|
|
238
|
+
acceptanceListing =
|
|
239
|
+
acceptanceListing.length > 0 ? `${acceptanceListing}\n${acWalk.message}` : acWalk.message;
|
|
240
|
+
}
|
|
210
241
|
}
|
|
211
242
|
planObj.status = targetStatus;
|
|
212
243
|
planObj.updated = nowIso;
|
|
@@ -218,7 +249,9 @@ export function runTransition(action, filePath, now = new Date(), options = {})
|
|
|
218
249
|
advanceNonTerminalOwnItems(planObj.items, targetStatus);
|
|
219
250
|
}
|
|
220
251
|
if (act === "complete") {
|
|
221
|
-
stampCompletionMetadata(planObj, projectRoot, nowIso
|
|
252
|
+
stampCompletionMetadata(planObj, projectRoot, nowIso, {
|
|
253
|
+
completedSessionId: resolveCompletionSessionId(projectRoot),
|
|
254
|
+
});
|
|
222
255
|
// #3242 / epic #3237 Q4: after reconcile, completed lifecycle must match
|
|
223
256
|
// plan.status=completed and terminal plan.items (compose with #3240).
|
|
224
257
|
const consistency = evaluateCompletedPlanConsistency(planObj, {
|
|
@@ -249,6 +282,27 @@ export function runTransition(action, filePath, now = new Date(), options = {})
|
|
|
249
282
|
return { ok: false, message: writeResult.message };
|
|
250
283
|
}
|
|
251
284
|
crud.recordTrustedUpdate(destPath, formatted);
|
|
285
|
+
if (act === "complete") {
|
|
286
|
+
const sessionId = resolveCompletionSessionId(projectRoot);
|
|
287
|
+
if (sessionId !== null) {
|
|
288
|
+
try {
|
|
289
|
+
writeSessionCompletedMarker(projectRoot, {
|
|
290
|
+
path: destPath,
|
|
291
|
+
sessionId,
|
|
292
|
+
completedAt: nowIso,
|
|
293
|
+
});
|
|
294
|
+
}
|
|
295
|
+
catch {
|
|
296
|
+
try {
|
|
297
|
+
unlinkSync(destPath);
|
|
298
|
+
}
|
|
299
|
+
catch {
|
|
300
|
+
/* dest rollback after marker failure */
|
|
301
|
+
}
|
|
302
|
+
return { ok: false, message: SESSION_COMPLETED_AC_REMEDIATION };
|
|
303
|
+
}
|
|
304
|
+
}
|
|
305
|
+
}
|
|
252
306
|
try {
|
|
253
307
|
unlinkSync(resolvedPath);
|
|
254
308
|
}
|
|
@@ -280,7 +334,11 @@ export function runTransition(action, filePath, now = new Date(), options = {})
|
|
|
280
334
|
};
|
|
281
335
|
}
|
|
282
336
|
syncSpecificationAfterScopeMove(data, resolvedPath, destPath, vbriefRoot, targetStatus);
|
|
337
|
+
if (act === "activate" || act === "promote") {
|
|
338
|
+
maybeEmitAcceptanceStampFromChange(projectRoot, previousAcceptance, planObj.acceptance);
|
|
339
|
+
}
|
|
283
340
|
const moveMsg = `${actionLabel} ${basename}: ${currentFolder}/ -> ${targetFolder}/ (status: ${targetStatus})` +
|
|
341
|
+
(derivationNotice.length > 0 ? `\n${derivationNotice}` : "") +
|
|
284
342
|
(acceptanceListing.length > 0 ? `\n${acceptanceListing}` : "");
|
|
285
343
|
return {
|
|
286
344
|
ok: true,
|
|
@@ -300,7 +358,11 @@ export function runTransition(action, filePath, now = new Date(), options = {})
|
|
|
300
358
|
/* best-effort telemetry persistence */
|
|
301
359
|
}
|
|
302
360
|
const actionLabel = STAY_LABELS[act] ?? act.charAt(0).toUpperCase() + act.slice(1);
|
|
361
|
+
if (act === "activate" || act === "promote") {
|
|
362
|
+
maybeEmitAcceptanceStampFromChange(projectRoot, previousAcceptance, planObj.acceptance);
|
|
363
|
+
}
|
|
303
364
|
const stayMsg = `${actionLabel} ${basename}: stays in ${currentFolder}/ (status: ${targetStatus})` +
|
|
365
|
+
(derivationNotice.length > 0 ? `\n${derivationNotice}` : "") +
|
|
304
366
|
(acceptanceListing.length > 0 ? `\n${acceptanceListing}` : "");
|
|
305
367
|
return {
|
|
306
368
|
ok: true,
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Consumer-environment evidence for ceremony-dial escalation (#3358).
|
|
3
|
+
*
|
|
4
|
+
* Session:start left `taskSize` and `modelTier` null unless CLI flags or
|
|
5
|
+
* provisional env/verb/file hints were set. Consumer runs then always
|
|
6
|
+
* declined with "insufficient evidence (size=- modelTier=-)".
|
|
7
|
+
*
|
|
8
|
+
* This module fills at least one input the consumer environment can supply:
|
|
9
|
+
* 1. stamped #3323 clause count on active/pending briefs (size proxy)
|
|
10
|
+
* 2. host-supplied model-tier env (`DEFT_HOST_MODEL_TIER` and aliases)
|
|
11
|
+
* 3. failing-gate count env (mid-session size proxy)
|
|
12
|
+
*
|
|
13
|
+
* ⊗ Change the rapid default or decline threshold when no evidence exists.
|
|
14
|
+
* Explicit CLI / session inputs still win at the merge site.
|
|
15
|
+
*/
|
|
16
|
+
import { type CeremonyDialInputs, type CeremonyModelTier, type CeremonyTaskSize } from "../policy/ceremony-dial.js";
|
|
17
|
+
/** Host-supplied model-tier hint when `DEFT_CEREMONY_MODEL_TIER` is unset. */
|
|
18
|
+
export declare const ENV_HOST_MODEL_TIER = "DEFT_HOST_MODEL_TIER";
|
|
19
|
+
/** Mid-session failing-gate count the consumer/harness can supply. */
|
|
20
|
+
export declare const ENV_FAILING_GATE_COUNT = "DEFT_FAILING_GATE_COUNT";
|
|
21
|
+
export interface CeremonyDialConsumerEvidence {
|
|
22
|
+
readonly taskSize: CeremonyTaskSize | null;
|
|
23
|
+
readonly modelTier: CeremonyModelTier | null;
|
|
24
|
+
readonly clauseCount: number | null;
|
|
25
|
+
readonly failingGateCount: number | null;
|
|
26
|
+
readonly reasons: readonly string[];
|
|
27
|
+
}
|
|
28
|
+
export declare function taskSizeFromClauseCount(count: number): CeremonyTaskSize | null;
|
|
29
|
+
export declare function taskSizeFromFailingGateCount(count: number): CeremonyTaskSize | null;
|
|
30
|
+
/** Max stamped clause count across `xbrief/{active,pending}` (#3323). */
|
|
31
|
+
export declare function readStampedClauseCount(projectRoot: string): {
|
|
32
|
+
readonly count: number;
|
|
33
|
+
readonly source: string;
|
|
34
|
+
} | null;
|
|
35
|
+
export declare function collectCeremonyDialConsumerEvidence(projectRoot: string, options?: {
|
|
36
|
+
readonly env?: NodeJS.ProcessEnv;
|
|
37
|
+
}): CeremonyDialConsumerEvidence;
|
|
38
|
+
/** Explicit non-null wins; consumer evidence fills only missing fields. */
|
|
39
|
+
export declare function mergeCeremonyDialInputsWithConsumerEvidence(explicit: CeremonyDialInputs | undefined, evidence: CeremonyDialConsumerEvidence): CeremonyDialInputs;
|
|
40
|
+
export declare function formatCeremonyDialEvidenceLine(evidence: CeremonyDialConsumerEvidence): string | null;
|
|
41
|
+
export declare function ceremonyDialEvidenceToDict(evidence: CeremonyDialConsumerEvidence): Record<string, unknown>;
|
|
42
|
+
//# sourceMappingURL=ceremony-dial-evidence.d.ts.map
|
|
@@ -0,0 +1,194 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Consumer-environment evidence for ceremony-dial escalation (#3358).
|
|
3
|
+
*
|
|
4
|
+
* Session:start left `taskSize` and `modelTier` null unless CLI flags or
|
|
5
|
+
* provisional env/verb/file hints were set. Consumer runs then always
|
|
6
|
+
* declined with "insufficient evidence (size=- modelTier=-)".
|
|
7
|
+
*
|
|
8
|
+
* This module fills at least one input the consumer environment can supply:
|
|
9
|
+
* 1. stamped #3323 clause count on active/pending briefs (size proxy)
|
|
10
|
+
* 2. host-supplied model-tier env (`DEFT_HOST_MODEL_TIER` and aliases)
|
|
11
|
+
* 3. failing-gate count env (mid-session size proxy)
|
|
12
|
+
*
|
|
13
|
+
* ⊗ Change the rapid default or decline threshold when no evidence exists.
|
|
14
|
+
* Explicit CLI / session inputs still win at the merge site.
|
|
15
|
+
*/
|
|
16
|
+
import { existsSync, readdirSync, readFileSync } from "node:fs";
|
|
17
|
+
import { join } from "node:path";
|
|
18
|
+
import { ENV_CEREMONY_MODEL_HINT, ENV_CEREMONY_MODEL_TIER, normalizeCeremonyModelTier, } from "../policy/ceremony-dial.js";
|
|
19
|
+
/** Host-supplied model-tier hint when `DEFT_CEREMONY_MODEL_TIER` is unset. */
|
|
20
|
+
export const ENV_HOST_MODEL_TIER = "DEFT_HOST_MODEL_TIER";
|
|
21
|
+
/** Mid-session failing-gate count the consumer/harness can supply. */
|
|
22
|
+
export const ENV_FAILING_GATE_COUNT = "DEFT_FAILING_GATE_COUNT";
|
|
23
|
+
const LIFECYCLE_DIRS = ["active", "pending"];
|
|
24
|
+
export function taskSizeFromClauseCount(count) {
|
|
25
|
+
if (!Number.isFinite(count) || count <= 0)
|
|
26
|
+
return null;
|
|
27
|
+
if (count === 1)
|
|
28
|
+
return "S";
|
|
29
|
+
if (count <= 3)
|
|
30
|
+
return "M";
|
|
31
|
+
if (count <= 6)
|
|
32
|
+
return "L";
|
|
33
|
+
return "XL";
|
|
34
|
+
}
|
|
35
|
+
export function taskSizeFromFailingGateCount(count) {
|
|
36
|
+
if (!Number.isFinite(count) || count <= 0)
|
|
37
|
+
return null;
|
|
38
|
+
if (count <= 2)
|
|
39
|
+
return "M";
|
|
40
|
+
return "L";
|
|
41
|
+
}
|
|
42
|
+
function rankSize(size) {
|
|
43
|
+
switch (size) {
|
|
44
|
+
case "S":
|
|
45
|
+
return 0;
|
|
46
|
+
case "M":
|
|
47
|
+
return 1;
|
|
48
|
+
case "L":
|
|
49
|
+
return 2;
|
|
50
|
+
case "XL":
|
|
51
|
+
return 3;
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
function maxSize(a, b) {
|
|
55
|
+
if (a === null)
|
|
56
|
+
return b;
|
|
57
|
+
if (b === null)
|
|
58
|
+
return a;
|
|
59
|
+
return rankSize(a) >= rankSize(b) ? a : b;
|
|
60
|
+
}
|
|
61
|
+
function asRecord(value) {
|
|
62
|
+
if (typeof value === "object" && value !== null && !Array.isArray(value)) {
|
|
63
|
+
return value;
|
|
64
|
+
}
|
|
65
|
+
return null;
|
|
66
|
+
}
|
|
67
|
+
function clauseCountFromBrief(raw) {
|
|
68
|
+
const root = asRecord(raw);
|
|
69
|
+
const plan = asRecord(root?.plan);
|
|
70
|
+
const acceptance = asRecord(plan?.acceptance);
|
|
71
|
+
if (acceptance === null)
|
|
72
|
+
return null;
|
|
73
|
+
if (Array.isArray(acceptance.clauses) && acceptance.clauses.length > 0) {
|
|
74
|
+
return acceptance.clauses.length;
|
|
75
|
+
}
|
|
76
|
+
const stamped = acceptance.clause_count;
|
|
77
|
+
if (typeof stamped === "number" && Number.isFinite(stamped) && stamped > 0) {
|
|
78
|
+
return stamped;
|
|
79
|
+
}
|
|
80
|
+
return null;
|
|
81
|
+
}
|
|
82
|
+
function parseNonNegativeInt(raw) {
|
|
83
|
+
if (typeof raw === "number" && Number.isFinite(raw) && raw >= 0) {
|
|
84
|
+
return Math.trunc(raw);
|
|
85
|
+
}
|
|
86
|
+
if (typeof raw !== "string")
|
|
87
|
+
return null;
|
|
88
|
+
const trimmed = raw.trim();
|
|
89
|
+
if (trimmed.length === 0)
|
|
90
|
+
return null;
|
|
91
|
+
const n = Number(trimmed);
|
|
92
|
+
if (!Number.isFinite(n) || n < 0)
|
|
93
|
+
return null;
|
|
94
|
+
return Math.trunc(n);
|
|
95
|
+
}
|
|
96
|
+
/** Max stamped clause count across `xbrief/{active,pending}` (#3323). */
|
|
97
|
+
export function readStampedClauseCount(projectRoot) {
|
|
98
|
+
let best = null;
|
|
99
|
+
for (const dir of LIFECYCLE_DIRS) {
|
|
100
|
+
const folder = join(projectRoot, "xbrief", dir);
|
|
101
|
+
if (!existsSync(folder))
|
|
102
|
+
continue;
|
|
103
|
+
let names = [];
|
|
104
|
+
try {
|
|
105
|
+
names = readdirSync(folder);
|
|
106
|
+
}
|
|
107
|
+
catch {
|
|
108
|
+
continue;
|
|
109
|
+
}
|
|
110
|
+
for (const name of names) {
|
|
111
|
+
if (!name.endsWith(".xbrief.json") || name === "PROJECT-DEFINITION.xbrief.json") {
|
|
112
|
+
continue;
|
|
113
|
+
}
|
|
114
|
+
const path = join(folder, name);
|
|
115
|
+
try {
|
|
116
|
+
const parsed = JSON.parse(readFileSync(path, "utf8"));
|
|
117
|
+
const count = clauseCountFromBrief(parsed);
|
|
118
|
+
if (count === null)
|
|
119
|
+
continue;
|
|
120
|
+
if (best === null || count > best.count) {
|
|
121
|
+
best = { count, source: `xbrief/${dir}/${name}` };
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
catch {
|
|
125
|
+
// fail-open — a malformed brief is not an evidence source
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
return best;
|
|
130
|
+
}
|
|
131
|
+
export function collectCeremonyDialConsumerEvidence(projectRoot, options = {}) {
|
|
132
|
+
const env = options.env ?? process.env;
|
|
133
|
+
const reasons = [];
|
|
134
|
+
const stamped = readStampedClauseCount(projectRoot);
|
|
135
|
+
const clauseCount = stamped?.count ?? null;
|
|
136
|
+
let taskSize = null;
|
|
137
|
+
if (stamped !== null) {
|
|
138
|
+
const fromClauses = taskSizeFromClauseCount(stamped.count);
|
|
139
|
+
if (fromClauses !== null) {
|
|
140
|
+
taskSize = fromClauses;
|
|
141
|
+
reasons.push(`taskSize=${fromClauses} from stamped clauseCount=${stamped.count} (${stamped.source})`);
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
const failingGateCount = parseNonNegativeInt(env[ENV_FAILING_GATE_COUNT]);
|
|
145
|
+
if (failingGateCount !== null) {
|
|
146
|
+
const fromGates = taskSizeFromFailingGateCount(failingGateCount);
|
|
147
|
+
if (fromGates !== null) {
|
|
148
|
+
const next = maxSize(taskSize, fromGates);
|
|
149
|
+
if (next !== taskSize) {
|
|
150
|
+
taskSize = next;
|
|
151
|
+
reasons.push(`taskSize=${next} from failingGateCount=${failingGateCount}`);
|
|
152
|
+
}
|
|
153
|
+
else if (taskSize === fromGates) {
|
|
154
|
+
reasons.push(`failingGateCount=${failingGateCount} (size already ${taskSize})`);
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
const modelTier = normalizeCeremonyModelTier(env[ENV_CEREMONY_MODEL_TIER]) ??
|
|
159
|
+
normalizeCeremonyModelTier(env[ENV_CEREMONY_MODEL_HINT]) ??
|
|
160
|
+
normalizeCeremonyModelTier(env[ENV_HOST_MODEL_TIER]);
|
|
161
|
+
if (modelTier !== null) {
|
|
162
|
+
reasons.push(`modelTier=${modelTier} from host env`);
|
|
163
|
+
}
|
|
164
|
+
return {
|
|
165
|
+
taskSize,
|
|
166
|
+
modelTier,
|
|
167
|
+
clauseCount,
|
|
168
|
+
failingGateCount,
|
|
169
|
+
reasons,
|
|
170
|
+
};
|
|
171
|
+
}
|
|
172
|
+
/** Explicit non-null wins; consumer evidence fills only missing fields. */
|
|
173
|
+
export function mergeCeremonyDialInputsWithConsumerEvidence(explicit, evidence) {
|
|
174
|
+
return {
|
|
175
|
+
taskSize: explicit?.taskSize ?? evidence.taskSize ?? null,
|
|
176
|
+
modelTier: explicit?.modelTier ?? evidence.modelTier ?? null,
|
|
177
|
+
projectShape: explicit?.projectShape ?? null,
|
|
178
|
+
};
|
|
179
|
+
}
|
|
180
|
+
export function formatCeremonyDialEvidenceLine(evidence) {
|
|
181
|
+
if (evidence.reasons.length === 0)
|
|
182
|
+
return null;
|
|
183
|
+
return `[deft ceremony-dial] evidence: ${evidence.reasons.join("; ")}`;
|
|
184
|
+
}
|
|
185
|
+
export function ceremonyDialEvidenceToDict(evidence) {
|
|
186
|
+
return {
|
|
187
|
+
taskSize: evidence.taskSize,
|
|
188
|
+
modelTier: evidence.modelTier,
|
|
189
|
+
clauseCount: evidence.clauseCount,
|
|
190
|
+
failingGateCount: evidence.failingGateCount,
|
|
191
|
+
reasons: [...evidence.reasons],
|
|
192
|
+
};
|
|
193
|
+
}
|
|
194
|
+
//# sourceMappingURL=ceremony-dial-evidence.js.map
|
package/dist/session/index.d.ts
CHANGED
package/dist/session/index.js
CHANGED
|
@@ -23,6 +23,7 @@ import { runDefaultMode } from "../triage/welcome/default-mode.js";
|
|
|
23
23
|
import { resolveUserMdPath } from "../user-config/resolve-user-md.js";
|
|
24
24
|
import { emitSessionValueReadback } from "../value/readback.js";
|
|
25
25
|
import { verifyRequiredTools } from "../verify-env/verify-tools.js";
|
|
26
|
+
import { ceremonyDialEvidenceToDict, collectCeremonyDialConsumerEvidence, formatCeremonyDialEvidenceLine, mergeCeremonyDialInputsWithConsumerEvidence, } from "./ceremony-dial-evidence.js";
|
|
26
27
|
import { effortBudgetToDict, maybeFormatEffortBudgetLines, resolveProductionHostEffortDescriptor, } from "./effort-budget.js";
|
|
27
28
|
import { defaultGitRunner, gitHead, gitIsAncestor, worktreePath } from "./git.js";
|
|
28
29
|
import { runOrientationCompression, } from "./orientation-compression.js";
|
|
@@ -801,7 +802,16 @@ export function runSessionStart(projectRoot, options = {}) {
|
|
|
801
802
|
// missing size/tier/shape from env/verb/files/deposit BEFORE resolve — never
|
|
802
803
|
// block on plan-item effort (post-planning only). Cold incomplete size is
|
|
803
804
|
// tier-conditional (#3263): mid/low → standard; frontier/unknown → rapid.
|
|
804
|
-
|
|
805
|
+
// #3358: fill at least one consumer-supplied input (stamped clause count,
|
|
806
|
+
// host-tier env, failing-gate count) so evaluation is not permanently
|
|
807
|
+
// size=- / modelTier=-. ⊗ Change rapid default when no evidence exists.
|
|
808
|
+
const consumerDialEvidence = collectCeremonyDialConsumerEvidence(projectRoot, {
|
|
809
|
+
env: options.env,
|
|
810
|
+
});
|
|
811
|
+
const dialInputsWithEvidence = options.ceremonyDial === undefined
|
|
812
|
+
? mergeCeremonyDialInputsWithConsumerEvidence(options.ceremonyDialInputs, consumerDialEvidence)
|
|
813
|
+
: (options.ceremonyDialInputs ?? {});
|
|
814
|
+
const { inputs: resolvedDialInputs, provisional: provisionalDial } = resolveSessionCeremonyDialInputs(projectRoot, dialInputsWithEvidence, {
|
|
805
815
|
...options.ceremonyDialHints,
|
|
806
816
|
env: options.env,
|
|
807
817
|
});
|
|
@@ -850,6 +860,10 @@ export function runSessionStart(projectRoot, options = {}) {
|
|
|
850
860
|
if (provisionalDial.reasons.length > 0 && options.ceremonyDial === undefined) {
|
|
851
861
|
lines.push(`[deft ceremony-dial] provisional: ${provisionalDial.reasons.join("; ")}`);
|
|
852
862
|
}
|
|
863
|
+
const evidenceLine = formatCeremonyDialEvidenceLine(consumerDialEvidence);
|
|
864
|
+
if (evidenceLine !== null && options.ceremonyDial === undefined) {
|
|
865
|
+
lines.push(evidenceLine);
|
|
866
|
+
}
|
|
853
867
|
// Resolve USER.md via the shared first-hit-wins resolver so the alignment
|
|
854
868
|
// step finds preferences automatically in mismatched / headless sandboxes
|
|
855
869
|
// with zero manual DEFT_USER_PATH (#2271 / #2124). Never throws: an absent
|
|
@@ -1211,6 +1225,7 @@ export function runSessionStart(projectRoot, options = {}) {
|
|
|
1211
1225
|
projectShape: provisionalDial.projectShape,
|
|
1212
1226
|
reasons: [...provisionalDial.reasons],
|
|
1213
1227
|
},
|
|
1228
|
+
consumer_evidence: ceremonyDialEvidenceToDict(consumerDialEvidence),
|
|
1214
1229
|
};
|
|
1215
1230
|
const preflightDict = toolchainPreflightResult !== null ? toolchainPreflightToDict(toolchainPreflightResult) : null;
|
|
1216
1231
|
const payload = {
|
|
@@ -1283,7 +1298,11 @@ export function runSessionStart(projectRoot, options = {}) {
|
|
|
1283
1298
|
}
|
|
1284
1299
|
: {}),
|
|
1285
1300
|
});
|
|
1286
|
-
|
|
1301
|
+
// #3356: always emit a denominator when the destination is live so
|
|
1302
|
+
// share = ritual_events / denominator is computable from the stream.
|
|
1303
|
+
// Prefer DEFT_TOTAL_TOOL_TURNS, then DEFT_MAX_TURNS / host maxTurns,
|
|
1304
|
+
// else this session:start CLI invocation (1).
|
|
1305
|
+
emitter.emitSessionToolTurnDenominator(effortBudget.budget.maxTurns);
|
|
1287
1306
|
// #3319: evaluate escalate-on-evidence only when #3274 is live. A pin
|
|
1288
1307
|
// emits nothing so never-evaluated stays distinguishable from declined.
|
|
1289
1308
|
if (!isCeremonyStartTierPinned(startTierProvenance)) {
|
package/dist/slice/existing.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { ContainedWriteError } from "../fs/contained-write.js";
|
|
1
2
|
import { call as scmCall } from "../scm/call.js";
|
|
2
3
|
import { pyRepr } from "../scm/py-format.js";
|
|
3
4
|
import { DEFAULT_ACTOR, DEFAULT_EXPECTED_CLOSE_SIGNAL, DEFAULT_ROLE, WAVE_FLAG_RE, } from "./constants.js";
|
|
@@ -278,7 +279,10 @@ export function runRecordExisting(args, waveMap, deps = {}) {
|
|
|
278
279
|
if (authoritativeDup !== null && !args.force) {
|
|
279
280
|
return { kind: "duplicate", duplicate: authoritativeDup };
|
|
280
281
|
}
|
|
281
|
-
const id = writeSliceUnlocked(record, {
|
|
282
|
+
const id = writeSliceUnlocked(record, {
|
|
283
|
+
path: logPath,
|
|
284
|
+
projectRoot: resolved.projectRoot,
|
|
285
|
+
});
|
|
282
286
|
return { kind: "written", sliceId: id };
|
|
283
287
|
});
|
|
284
288
|
if (outcome.kind === "duplicate") {
|
|
@@ -301,6 +305,13 @@ export function runRecordExisting(args, waveMap, deps = {}) {
|
|
|
301
305
|
if (err instanceof SliceRecordError) {
|
|
302
306
|
return { exitCode: 1, stdout: "", stderr: `error: invalid record -- ${err.message}\n` };
|
|
303
307
|
}
|
|
308
|
+
if (err instanceof ContainedWriteError) {
|
|
309
|
+
return {
|
|
310
|
+
exitCode: 1,
|
|
311
|
+
stdout: "",
|
|
312
|
+
stderr: `error: refused slices.jsonl write -- ${err.message}\n`,
|
|
313
|
+
};
|
|
314
|
+
}
|
|
304
315
|
throw err;
|
|
305
316
|
}
|
|
306
317
|
}
|
package/dist/slice/record.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { existsSync, mkdirSync, readFileSync } from "node:fs";
|
|
2
2
|
import { withAppendLock } from "./lock.js";
|
|
3
3
|
export interface WriteSliceOptions {
|
|
4
4
|
readonly umbrellaUrl: string;
|
|
@@ -14,13 +14,15 @@ export interface WriteSliceOptions {
|
|
|
14
14
|
export interface RecordModuleDeps {
|
|
15
15
|
readonly readFile?: typeof readFileSync;
|
|
16
16
|
readonly exists?: typeof existsSync;
|
|
17
|
-
readonly append?: typeof appendFileSync;
|
|
18
|
-
readonly fsync?: typeof fsyncSync;
|
|
19
|
-
readonly open?: typeof openSync;
|
|
20
|
-
readonly close?: typeof closeSync;
|
|
21
17
|
readonly mkdir?: typeof mkdirSync;
|
|
22
18
|
readonly withLock?: typeof withAppendLock;
|
|
23
19
|
}
|
|
20
|
+
/**
|
|
21
|
+
* Containment root for slices.jsonl append (#3354 / #3288).
|
|
22
|
+
* Prefer `projectRoot` when the log is nested under it so out-of-tree dests refuse.
|
|
23
|
+
* Otherwise use the log parent (still refuses leaf symlink follow).
|
|
24
|
+
*/
|
|
25
|
+
export declare function containmentRootForSliceLog(logPath: string, projectRoot?: string): string;
|
|
24
26
|
export declare function newSliceId(): string;
|
|
25
27
|
/** Return the current UTC time in canonical ISO-8601 form with Z suffix (no fractional seconds). */
|
|
26
28
|
export declare function nowIso(): string;
|
|
@@ -29,6 +31,7 @@ export declare function writeSliceUnlocked(record: Record<string, unknown>, opti
|
|
|
29
31
|
path?: string;
|
|
30
32
|
deps?: RecordModuleDeps;
|
|
31
33
|
warn?: (message: string) => void;
|
|
34
|
+
projectRoot?: string;
|
|
32
35
|
}): string;
|
|
33
36
|
/** Validate and atomically append a cohort record to slices.jsonl. */
|
|
34
37
|
export declare function writeSlice(umbrella: number, children: Iterable<Record<string, unknown>>, options: WriteSliceOptions, deps?: RecordModuleDeps): string;
|
package/dist/slice/record.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { randomUUID } from "node:crypto";
|
|
2
|
-
import {
|
|
2
|
+
import { existsSync, mkdirSync, readFileSync } from "node:fs";
|
|
3
3
|
import { dirname, resolve } from "node:path";
|
|
4
|
+
import { containedWrite } from "../fs/contained-write.js";
|
|
4
5
|
import { resolveTriageCachePath } from "../triage/cache-path.js";
|
|
5
6
|
import { pythonJsonStringify } from "./json.js";
|
|
6
7
|
import { withAppendLock } from "./lock.js";
|
|
@@ -8,13 +9,20 @@ import { validateRecord } from "./validate.js";
|
|
|
8
9
|
const defaultDeps = {
|
|
9
10
|
readFile: readFileSync,
|
|
10
11
|
exists: existsSync,
|
|
11
|
-
append: appendFileSync,
|
|
12
|
-
fsync: fsyncSync,
|
|
13
|
-
open: openSync,
|
|
14
|
-
close: closeSync,
|
|
15
12
|
mkdir: mkdirSync,
|
|
16
13
|
withLock: withAppendLock,
|
|
17
14
|
};
|
|
15
|
+
/**
|
|
16
|
+
* Containment root for slices.jsonl append (#3354 / #3288).
|
|
17
|
+
* Prefer `projectRoot` when the log is nested under it so out-of-tree dests refuse.
|
|
18
|
+
* Otherwise use the log parent (still refuses leaf symlink follow).
|
|
19
|
+
*/
|
|
20
|
+
export function containmentRootForSliceLog(logPath, projectRoot) {
|
|
21
|
+
if (projectRoot !== undefined) {
|
|
22
|
+
return resolve(projectRoot);
|
|
23
|
+
}
|
|
24
|
+
return resolve(dirname(logPath));
|
|
25
|
+
}
|
|
18
26
|
function resolvePath(path) {
|
|
19
27
|
return path !== undefined ? resolve(path) : resolveTriageCachePath(process.cwd(), "slices.jsonl");
|
|
20
28
|
}
|
|
@@ -64,14 +72,14 @@ export function writeSliceUnlocked(record, options = {}) {
|
|
|
64
72
|
return resolvedId;
|
|
65
73
|
}
|
|
66
74
|
const line = `${pythonJsonStringify(record)}\n`;
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
}
|
|
75
|
+
const root = containmentRootForSliceLog(logPath, options.projectRoot);
|
|
76
|
+
deps.mkdir(root, { recursive: true });
|
|
77
|
+
containedWrite({
|
|
78
|
+
root,
|
|
79
|
+
target: logPath,
|
|
80
|
+
data: line,
|
|
81
|
+
mode: "append",
|
|
82
|
+
});
|
|
75
83
|
return resolvedId;
|
|
76
84
|
}
|
|
77
85
|
/** Validate and atomically append a cohort record to slices.jsonl. */
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* verify:telemetry-coverage — dead-surface detector (#3362).
|
|
3
|
+
*
|
|
4
|
+
* Two checks, one remediation that names the missing half:
|
|
5
|
+
* 1. Every RUN_SUMMARY_EVENT_KINDS member (and exported emitter method)
|
|
6
|
+
* has a production caller outside the emitter module.
|
|
7
|
+
* 2. Every kind is enrolled, has a trial step, and appears in a run of
|
|
8
|
+
* the shared fake-trial harness JSONL.
|
|
9
|
+
*
|
|
10
|
+
* Default is warn-only (exit 0). Pass --enforce to fail closed.
|
|
11
|
+
*/
|
|
12
|
+
import { type FakeTrialResult } from "./fake-trial.js";
|
|
13
|
+
export interface TelemetryCoverageFinding {
|
|
14
|
+
readonly subject: string;
|
|
15
|
+
readonly missingCaller: boolean;
|
|
16
|
+
readonly missingFixture: boolean;
|
|
17
|
+
readonly remediation: string;
|
|
18
|
+
}
|
|
19
|
+
export interface TelemetryCoverageOptions {
|
|
20
|
+
readonly projectRoot: string;
|
|
21
|
+
readonly enforce?: boolean;
|
|
22
|
+
readonly scanRoots?: readonly string[];
|
|
23
|
+
readonly kinds?: readonly string[];
|
|
24
|
+
readonly enrolledKinds?: readonly string[];
|
|
25
|
+
/** Override trial-step kinds (tests). Default: DEFAULT_TRIAL_STEPS. */
|
|
26
|
+
readonly trialKinds?: readonly string[];
|
|
27
|
+
/** Skip running the fake trial (unit tests). Production default is to run it. */
|
|
28
|
+
readonly skipTrial?: boolean;
|
|
29
|
+
/** Injected trial result (tests). */
|
|
30
|
+
readonly trialResult?: Pick<FakeTrialResult, "presentKinds" | "stepOutcomes">;
|
|
31
|
+
}
|
|
32
|
+
export interface TelemetryCoverageResult {
|
|
33
|
+
readonly code: number;
|
|
34
|
+
readonly message: string;
|
|
35
|
+
readonly stream: "stdout" | "stderr";
|
|
36
|
+
readonly findings: readonly TelemetryCoverageFinding[];
|
|
37
|
+
readonly enforce: boolean;
|
|
38
|
+
readonly failOpen: boolean;
|
|
39
|
+
}
|
|
40
|
+
export declare function remediationFor(subject: string, missingCaller: boolean, missingFixture: boolean): string;
|
|
41
|
+
export declare function evaluateTelemetryCoverage(options: TelemetryCoverageOptions): TelemetryCoverageResult;
|
|
42
|
+
//# sourceMappingURL=evaluate.d.ts.map
|