@deftai/directive-core 0.96.0 → 0.97.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 (73) hide show
  1. package/dist/cache/archive.js +10 -4
  2. package/dist/check/gate-lists.js +8 -0
  3. package/dist/consumer-check-contract/evaluate.d.ts +124 -0
  4. package/dist/consumer-check-contract/evaluate.js +699 -0
  5. package/dist/consumer-check-contract/index.d.ts +5 -0
  6. package/dist/consumer-check-contract/index.js +5 -0
  7. package/dist/delivery-attempt/disk-begin.d.ts +51 -0
  8. package/dist/delivery-attempt/disk-begin.js +68 -0
  9. package/dist/delivery-attempt/evaluate.d.ts +26 -0
  10. package/dist/delivery-attempt/evaluate.js +443 -0
  11. package/dist/delivery-attempt/fingerprint.d.ts +32 -0
  12. package/dist/delivery-attempt/fingerprint.js +100 -0
  13. package/dist/delivery-attempt/handoff.d.ts +25 -0
  14. package/dist/delivery-attempt/handoff.js +102 -0
  15. package/dist/delivery-attempt/index.d.ts +17 -0
  16. package/dist/delivery-attempt/index.js +17 -0
  17. package/dist/delivery-attempt/ledger.d.ts +169 -0
  18. package/dist/delivery-attempt/ledger.js +758 -0
  19. package/dist/delivery-attempt/material-delta.d.ts +38 -0
  20. package/dist/delivery-attempt/material-delta.js +126 -0
  21. package/dist/delivery-attempt/types.d.ts +210 -0
  22. package/dist/delivery-attempt/types.js +77 -0
  23. package/dist/doctor/index.d.ts +1 -0
  24. package/dist/doctor/index.js +1 -0
  25. package/dist/doctor/main.js +12 -0
  26. package/dist/doctor/openclaw-soft-rebind.d.ts +26 -0
  27. package/dist/doctor/openclaw-soft-rebind.js +164 -0
  28. package/dist/hooks/dispatcher.d.ts +2 -1
  29. package/dist/hooks/dispatcher.js +63 -9
  30. package/dist/index.d.ts +4 -0
  31. package/dist/index.js +4 -0
  32. package/dist/init-deposit/gitignore.js +7 -0
  33. package/dist/init-deposit/init-deposit.js +5 -0
  34. package/dist/init-deposit/refresh.js +3 -0
  35. package/dist/pr-merge-readiness/ci-gate.d.ts +29 -1
  36. package/dist/pr-merge-readiness/ci-gate.js +191 -24
  37. package/dist/pr-merge-readiness/compute.js +10 -1
  38. package/dist/pr-merge-readiness/index.d.ts +2 -1
  39. package/dist/pr-merge-readiness/index.js +2 -1
  40. package/dist/pr-merge-readiness/output.js +14 -0
  41. package/dist/pr-merge-readiness/platform-status.d.ts +29 -0
  42. package/dist/pr-merge-readiness/platform-status.js +49 -0
  43. package/dist/pr-watch/constants.d.ts +10 -0
  44. package/dist/pr-watch/constants.js +12 -1
  45. package/dist/pr-watch/main.js +16 -1
  46. package/dist/pr-watch/probe.js +13 -1
  47. package/dist/pr-watch/types.d.ts +3 -2
  48. package/dist/pr-watch/watch.js +14 -7
  49. package/dist/scope-provenance/digest.d.ts +67 -0
  50. package/dist/scope-provenance/digest.js +188 -0
  51. package/dist/scope-provenance/evaluate.d.ts +82 -0
  52. package/dist/scope-provenance/evaluate.js +528 -0
  53. package/dist/scope-provenance/index.d.ts +6 -0
  54. package/dist/scope-provenance/index.js +6 -0
  55. package/dist/session/compact-ritual.d.ts +96 -0
  56. package/dist/session/compact-ritual.js +237 -0
  57. package/dist/session/compact-ritual.spec.d.ts +2 -0
  58. package/dist/session/compact-ritual.spec.js +21 -0
  59. package/dist/session/index.d.ts +2 -0
  60. package/dist/session/index.js +2 -0
  61. package/dist/session/openclaw-soft-rebind-deposit.d.ts +46 -0
  62. package/dist/session/openclaw-soft-rebind-deposit.js +165 -0
  63. package/dist/test-boundary/evaluate.d.ts +54 -0
  64. package/dist/test-boundary/evaluate.js +368 -0
  65. package/dist/test-boundary/index.d.ts +6 -0
  66. package/dist/test-boundary/index.js +6 -0
  67. package/dist/test-boundary/policy.d.ts +52 -0
  68. package/dist/test-boundary/policy.js +182 -0
  69. package/dist/triage/bootstrap/gitignore.d.ts +1 -1
  70. package/dist/triage/bootstrap/gitignore.js +15 -1
  71. package/dist/vbrief-activate/activate.js +22 -6
  72. package/dist/xbrief/styles.js +33 -17
  73. package/package.json +7 -3
@@ -0,0 +1,5 @@
1
+ /**
2
+ * consumer-check-contract package surface (#3145).
3
+ */
4
+ export { type ConsumerCheckContractFinding, type ConsumerCheckContractOptions, type ConsumerCheckContractResult, evaluateConsumerCheckContract, extractCheckDeps, REQUIRED_CONSUMER_ENFORCEMENT_GATES, requiredGatesFromConsumerList, taskfileInvokesCheckOrchestrator, textReferencesGate, workflowExecutesCheck, } from "./evaluate.js";
5
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1,5 @@
1
+ /**
2
+ * consumer-check-contract package surface (#3145).
3
+ */
4
+ export { evaluateConsumerCheckContract, extractCheckDeps, REQUIRED_CONSUMER_ENFORCEMENT_GATES, requiredGatesFromConsumerList, taskfileInvokesCheckOrchestrator, textReferencesGate, workflowExecutesCheck, } from "./evaluate.js";
5
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1,51 @@
1
+ /**
2
+ * Disk-safe begin under exclusive unit lock (#3143).
3
+ *
4
+ * Lives outside ledger.ts so it can import evaluatePreDispatch without a cycle
5
+ * (evaluate → ledger helpers; disk-begin → evaluate + ledger).
6
+ */
7
+ import type { AttemptTrigger, DeliveryAttemptRecord, DeliveryBudgetPolicy, DeliveryUnitLedger, FailureInfo, MaterialDeltaClaim } from "./types.js";
8
+ export interface BeginAttemptOnDiskInput {
9
+ readonly scopeId: string;
10
+ readonly targetId: string;
11
+ readonly workflowId: string;
12
+ readonly phaseId?: string;
13
+ readonly attemptId?: string;
14
+ readonly sourceRevision: string;
15
+ readonly trigger: AttemptTrigger;
16
+ readonly status?: "queued" | "running";
17
+ readonly workerId?: string | null;
18
+ readonly externalRunId?: string | null;
19
+ readonly materialDelta?: readonly MaterialDeltaClaim[];
20
+ readonly anticipatedFailure?: FailureInfo | null;
21
+ readonly now?: string;
22
+ readonly policy?: Partial<DeliveryBudgetPolicy>;
23
+ }
24
+ /**
25
+ * Exclusive lock + reload + full pre-dispatch re-evaluation + begin+save.
26
+ * Prevents concurrent workers from racing past empty/active or blocked state.
27
+ */
28
+ export declare function beginAttemptOnDisk(projectRoot: string, input: BeginAttemptOnDiskInput): {
29
+ ledger: DeliveryUnitLedger;
30
+ attempt: DeliveryAttemptRecord;
31
+ };
32
+ export interface CompleteAttemptOnDiskInput {
33
+ readonly scopeId: string;
34
+ readonly targetId: string;
35
+ readonly workflowId: string;
36
+ readonly attemptId?: string;
37
+ readonly externalRunId?: string | null;
38
+ readonly status: "succeeded" | "failed" | "cancelled" | "blocked";
39
+ readonly failure?: FailureInfo | null;
40
+ readonly materialDelta?: readonly MaterialDeltaClaim[];
41
+ readonly elapsedSeconds?: number;
42
+ readonly toolCallCount?: number;
43
+ readonly hostTokenCount?: number | null;
44
+ readonly now?: string;
45
+ }
46
+ /**
47
+ * Exclusive lock + reload + complete + save so completion cannot clobber a
48
+ * concurrent begin write (#3143 Greptile race diagram).
49
+ */
50
+ export declare function completeAttemptOnDisk(projectRoot: string, input: CompleteAttemptOnDiskInput): DeliveryUnitLedger;
51
+ //# sourceMappingURL=disk-begin.d.ts.map
@@ -0,0 +1,68 @@
1
+ /**
2
+ * Disk-safe begin under exclusive unit lock (#3143).
3
+ *
4
+ * Lives outside ledger.ts so it can import evaluatePreDispatch without a cycle
5
+ * (evaluate → ledger helpers; disk-begin → evaluate + ledger).
6
+ */
7
+ import { evaluatePreDispatch } from "./evaluate.js";
8
+ import { beginAttempt, completeAttempt, loadOrCreateUnitLedger, loadUnitLedger, markBlocked, saveUnitLedger, withUnitLock, } from "./ledger.js";
9
+ /**
10
+ * Exclusive lock + reload + full pre-dispatch re-evaluation + begin+save.
11
+ * Prevents concurrent workers from racing past empty/active or blocked state.
12
+ */
13
+ export function beginAttemptOnDisk(projectRoot, input) {
14
+ return withUnitLock(projectRoot, input.scopeId, input.targetId, input.workflowId, () => {
15
+ const current = loadOrCreateUnitLedger(projectRoot, {
16
+ scopeId: input.scopeId,
17
+ targetId: input.targetId,
18
+ workflowId: input.workflowId,
19
+ phaseId: input.phaseId,
20
+ now: input.now,
21
+ });
22
+ const decision = evaluatePreDispatch(current, {
23
+ scopeId: input.scopeId,
24
+ targetId: input.targetId,
25
+ workflowId: input.workflowId,
26
+ phaseId: input.phaseId,
27
+ sourceRevision: input.sourceRevision,
28
+ trigger: input.trigger,
29
+ materialDelta: input.materialDelta,
30
+ anticipatedFailure: input.anticipatedFailure,
31
+ now: input.now,
32
+ policy: input.policy,
33
+ });
34
+ if (!decision.allowed) {
35
+ if (decision.handoff !== null) {
36
+ const blocked = markBlocked(current, decision.decision, decision.handoff.resumeCondition, input.now);
37
+ saveUnitLedger(projectRoot, blocked);
38
+ }
39
+ throw new Error(`delivery-attempt ${decision.decision}: ${decision.reason}`);
40
+ }
41
+ const { ledger, attempt } = beginAttempt(current, {
42
+ ...input,
43
+ // Only burn override quota when the gate decision required it.
44
+ consumeOverride: decision.decision === "ALLOW_OVERRIDE",
45
+ });
46
+ saveUnitLedger(projectRoot, ledger);
47
+ return { ledger, attempt };
48
+ });
49
+ }
50
+ /**
51
+ * Exclusive lock + reload + complete + save so completion cannot clobber a
52
+ * concurrent begin write (#3143 Greptile race diagram).
53
+ */
54
+ export function completeAttemptOnDisk(projectRoot, input) {
55
+ return withUnitLock(projectRoot, input.scopeId, input.targetId, input.workflowId, () => {
56
+ const current = loadUnitLedger(projectRoot, input.scopeId, input.targetId, input.workflowId) ??
57
+ loadOrCreateUnitLedger(projectRoot, {
58
+ scopeId: input.scopeId,
59
+ targetId: input.targetId,
60
+ workflowId: input.workflowId,
61
+ now: input.now,
62
+ });
63
+ const next = completeAttempt(current, input);
64
+ saveUnitLedger(projectRoot, next);
65
+ return next;
66
+ });
67
+ }
68
+ //# sourceMappingURL=disk-begin.js.map
@@ -0,0 +1,26 @@
1
+ /**
2
+ * Pre-dispatch gate for delivery-attempt circuit breaker (#3143).
3
+ *
4
+ * Loads durable unit state and returns a single deterministic decision
5
+ * before every delivery dispatch or retry.
6
+ */
7
+ import type { DeliveryUnitLedger, PreDispatchInput, PreDispatchResult } from "./types.js";
8
+ /**
9
+ * Evaluate whether a delivery dispatch may proceed.
10
+ *
11
+ * Pure w.r.t. the provided ledger snapshot — callers load/save durability.
12
+ *
13
+ * Operator override is applied **only** when the natural gate would `BLOCK_*`
14
+ * (not before ordinary ALLOW paths, and not for DENY_* such as duplicate active).
15
+ * That keeps bounded override quota for dispatches that truly need it.
16
+ */
17
+ export declare function evaluatePreDispatch(ledger: DeliveryUnitLedger, input: PreDispatchInput): PreDispatchResult;
18
+ /**
19
+ * Convenience: evaluate against ledger, and if blocked, return handoff suitable
20
+ * for persistence via markBlocked + saveUnitLedger.
21
+ */
22
+ export declare function evaluateAndPrepareBlock(ledger: DeliveryUnitLedger, input: PreDispatchInput): {
23
+ readonly evaluation: PreDispatchResult;
24
+ readonly ledger: DeliveryUnitLedger;
25
+ };
26
+ //# sourceMappingURL=evaluate.d.ts.map
@@ -0,0 +1,443 @@
1
+ /**
2
+ * Pre-dispatch gate for delivery-attempt circuit breaker (#3143).
3
+ *
4
+ * Loads durable unit state and returns a single deterministic decision
5
+ * before every delivery dispatch or retry.
6
+ */
7
+ import { buildTerminalHandoff, nextSafeActionFor } from "./handoff.js";
8
+ import { activeAttempts, newAttemptId } from "./ledger.js";
9
+ import { evaluateMaterialProgress } from "./material-delta.js";
10
+ import { isAllowDecision, isBlockDecision, mergePolicy, utcIso } from "./types.js";
11
+ function decisionClass(code) {
12
+ if (code.startsWith("ALLOW_"))
13
+ return "allow";
14
+ if (code.startsWith("DENY_"))
15
+ return "deny";
16
+ if (code.startsWith("BLOCK_"))
17
+ return "block";
18
+ return "escalate";
19
+ }
20
+ function buildEvent(input, ledger, reasonCode, opts) {
21
+ return {
22
+ decision: decisionClass(reasonCode),
23
+ reasonCode,
24
+ retryability: opts.retryability,
25
+ failureFingerprint: opts.fingerprint,
26
+ attemptCount: ledger.attempts.length,
27
+ failedAttemptCount: ledger.failedAttemptCount,
28
+ sameFailureCount: opts.sameFailureCount,
29
+ materialDeltaClassification: opts.materialClass,
30
+ resumeCondition: opts.resume,
31
+ overrideId: ledger.override?.overrideId ?? null,
32
+ scopeId: input.scopeId,
33
+ targetId: input.targetId,
34
+ workflowId: input.workflowId,
35
+ sourceRevision: input.sourceRevision,
36
+ };
37
+ }
38
+ function result(input, ledger, decision, reason, opts) {
39
+ const event = buildEvent(input, ledger, decision, opts);
40
+ const allowed = isAllowDecision(decision);
41
+ const handoff = !allowed && decision.startsWith("BLOCK_")
42
+ ? buildTerminalHandoff({
43
+ ledger: opts.handoffLedger ?? ledger,
44
+ decision,
45
+ now: input.now,
46
+ })
47
+ : null;
48
+ return {
49
+ decision,
50
+ allowed,
51
+ reason,
52
+ event,
53
+ handoff,
54
+ nextAttemptId: allowed ? newAttemptId() : null,
55
+ };
56
+ }
57
+ function overrideUsable(ledger, nowIso) {
58
+ const o = ledger.override;
59
+ if (o === null)
60
+ return false;
61
+ if (o.remainingAttempts <= 0)
62
+ return false;
63
+ if (o.expiresAt !== null && o.expiresAt < nowIso)
64
+ return false;
65
+ return true;
66
+ }
67
+ /**
68
+ * Evaluate whether a delivery dispatch may proceed.
69
+ *
70
+ * Pure w.r.t. the provided ledger snapshot — callers load/save durability.
71
+ *
72
+ * Operator override is applied **only** when the natural gate would `BLOCK_*`
73
+ * (not before ordinary ALLOW paths, and not for DENY_* such as duplicate active).
74
+ * That keeps bounded override quota for dispatches that truly need it.
75
+ */
76
+ export function evaluatePreDispatch(ledger, input) {
77
+ const now = utcIso(input.now);
78
+ const raw = evaluatePreDispatchNatural(ledger, input);
79
+ if (raw.allowed || !isBlockDecision(raw.decision) || !overrideUsable(ledger, now)) {
80
+ return raw;
81
+ }
82
+ const anticipated = input.anticipatedFailure ?? ledger.lastFailure;
83
+ const fingerprint = anticipated?.fingerprint ?? null;
84
+ const sameFailureCount = fingerprint !== null ? (ledger.sameFailureCounts[fingerprint] ?? 0) : 0;
85
+ const progress = evaluateMaterialProgress({
86
+ claims: input.materialDelta,
87
+ failure: anticipated,
88
+ evaluatedRevision: input.sourceRevision,
89
+ });
90
+ return result(input, ledger, "ALLOW_OVERRIDE", `audited operator override permits next attempt (natural gate: ${raw.decision})`, {
91
+ retryability: anticipated?.retryability ?? null,
92
+ fingerprint,
93
+ sameFailureCount,
94
+ materialClass: progress.classification,
95
+ resume: ledger.resumeCondition,
96
+ });
97
+ }
98
+ /**
99
+ * Natural gate without override short-circuit. Override is applied by
100
+ * {@link evaluatePreDispatch} only when this returns BLOCK_*.
101
+ */
102
+ function evaluatePreDispatchNatural(ledger, input) {
103
+ const policy = mergePolicy(input.policy);
104
+ const anticipated = input.anticipatedFailure ?? ledger.lastFailure;
105
+ const fingerprint = anticipated?.fingerprint ?? null;
106
+ const sameFailureCount = fingerprint !== null ? (ledger.sameFailureCounts[fingerprint] ?? 0) : 0;
107
+ const progress = evaluateMaterialProgress({
108
+ claims: input.materialDelta,
109
+ failure: anticipated,
110
+ evaluatedRevision: input.sourceRevision,
111
+ });
112
+ // --- Duplicate active (DENY — must run before any BLOCK path that override can lift) ---
113
+ const active = activeAttempts(ledger);
114
+ if (active.length >= policy.maxActiveAttempts) {
115
+ return result(input, ledger, "DENY_DUPLICATE_ACTIVE", `active attempt already exists (${active.map((a) => a.attemptId).join(", ")})`, {
116
+ retryability: anticipated?.retryability ?? null,
117
+ fingerprint,
118
+ sameFailureCount,
119
+ materialClass: progress.classification,
120
+ resume: ledger.resumeCondition,
121
+ });
122
+ }
123
+ // --- Usage budgets (elapsed / tool / token) ---
124
+ const elapsed = ledger.totalElapsedSeconds + (input.usage?.elapsedSeconds ?? 0);
125
+ const tools = ledger.totalToolCallCount + (input.usage?.toolCallCount ?? 0);
126
+ const tokensFromUsage = input.usage?.hostTokenCount;
127
+ const tokens = tokensFromUsage !== undefined && tokensFromUsage !== null
128
+ ? (ledger.totalHostTokenCount ?? 0) + tokensFromUsage
129
+ : ledger.totalHostTokenCount;
130
+ if (elapsed >= policy.maxElapsedSeconds) {
131
+ const resume = {
132
+ kind: "operator-override",
133
+ description: "elapsed budget exhausted; operator override or phase reset required",
134
+ satisfied: false,
135
+ };
136
+ return result(input, ledger, "BLOCK_ELAPSED_BUDGET", "elapsed-time budget exhausted", {
137
+ retryability: anticipated?.retryability ?? null,
138
+ fingerprint,
139
+ sameFailureCount,
140
+ materialClass: progress.classification,
141
+ resume,
142
+ handoffLedger: {
143
+ ...ledger,
144
+ resumeCondition: resume,
145
+ blockedDecision: "BLOCK_ELAPSED_BUDGET",
146
+ },
147
+ });
148
+ }
149
+ if (tools >= policy.maxToolCalls) {
150
+ const resume = {
151
+ kind: "operator-override",
152
+ description: "tool-call budget exhausted; operator override or phase reset required",
153
+ satisfied: false,
154
+ };
155
+ return result(input, ledger, "BLOCK_TOOL_OR_TOKEN_BUDGET", "tool-call budget exhausted", {
156
+ retryability: anticipated?.retryability ?? null,
157
+ fingerprint,
158
+ sameFailureCount,
159
+ materialClass: progress.classification,
160
+ resume,
161
+ handoffLedger: {
162
+ ...ledger,
163
+ resumeCondition: resume,
164
+ blockedDecision: "BLOCK_TOOL_OR_TOKEN_BUDGET",
165
+ },
166
+ });
167
+ }
168
+ // Token budget only when policy sets maxHostTokens AND we have token telemetry.
169
+ if (policy.maxHostTokens !== null && tokens !== null && tokens >= policy.maxHostTokens) {
170
+ const resume = {
171
+ kind: "operator-override",
172
+ description: "host token budget exhausted; operator override or phase reset required",
173
+ satisfied: false,
174
+ };
175
+ return result(input, ledger, "BLOCK_TOOL_OR_TOKEN_BUDGET", "host token budget exhausted", {
176
+ retryability: anticipated?.retryability ?? null,
177
+ fingerprint,
178
+ sameFailureCount,
179
+ materialClass: progress.classification,
180
+ resume,
181
+ handoffLedger: {
182
+ ...ledger,
183
+ resumeCondition: resume,
184
+ blockedDecision: "BLOCK_TOOL_OR_TOKEN_BUDGET",
185
+ },
186
+ });
187
+ }
188
+ // --- Resume when condition satisfied ---
189
+ if (ledger.resumeCondition?.satisfied && (input.trigger === "resume" || progress.isMaterial)) {
190
+ return result(input, ledger, "ALLOW_RESUME", "resume condition satisfied", {
191
+ retryability: anticipated?.retryability ?? null,
192
+ fingerprint,
193
+ sameFailureCount,
194
+ materialClass: progress.classification,
195
+ resume: ledger.resumeCondition,
196
+ });
197
+ }
198
+ // --- Prior block without satisfied resume ---
199
+ // (usable operator override is applied by evaluatePreDispatch wrapper)
200
+ if (ledger.blockedDecision !== null) {
201
+ if (!progress.isMaterial) {
202
+ const resume = ledger.resumeCondition ??
203
+ {
204
+ kind: "material-delta",
205
+ description: nextSafeActionFor(ledger.blockedDecision),
206
+ satisfied: false,
207
+ };
208
+ return result(input, ledger, ledger.blockedDecision, "unit remains blocked from prior decision", {
209
+ retryability: anticipated?.retryability ?? null,
210
+ fingerprint,
211
+ sameFailureCount,
212
+ materialClass: progress.classification,
213
+ resume,
214
+ handoffLedger: ledger,
215
+ });
216
+ }
217
+ }
218
+ // --- Aggregate failed-attempt budget ---
219
+ if (ledger.failedAttemptCount >= policy.maxFailedAttempts && !progress.isMaterial) {
220
+ const resume = {
221
+ kind: "material-delta",
222
+ description: "attempt budget exhausted; require relevant material delta or operator override",
223
+ satisfied: false,
224
+ };
225
+ return result(input, ledger, "BLOCK_ATTEMPT_BUDGET", "failed-attempt budget exhausted", {
226
+ retryability: anticipated?.retryability ?? null,
227
+ fingerprint,
228
+ sameFailureCount,
229
+ materialClass: progress.classification,
230
+ resume,
231
+ handoffLedger: {
232
+ ...ledger,
233
+ resumeCondition: resume,
234
+ blockedDecision: "BLOCK_ATTEMPT_BUDGET",
235
+ },
236
+ });
237
+ }
238
+ // --- First attempt / recovered unit (last failure cleared on success) ---
239
+ if (ledger.attempts.length === 0) {
240
+ return result(input, ledger, "ALLOW_FIRST_ATTEMPT", "no prior attempts for this unit", {
241
+ retryability: null,
242
+ fingerprint: null,
243
+ sameFailureCount: 0,
244
+ materialClass: progress.classification,
245
+ resume: null,
246
+ });
247
+ }
248
+ if (ledger.lastFailure === null && (anticipated === null || anticipated === undefined)) {
249
+ return result(input, ledger, "ALLOW_FIRST_ATTEMPT", "no open failure identity for this unit", {
250
+ retryability: null,
251
+ fingerprint: null,
252
+ sameFailureCount: 0,
253
+ materialClass: progress.classification,
254
+ resume: null,
255
+ });
256
+ }
257
+ // --- Material progress opens a new evaluated attempt ---
258
+ if (progress.isMaterial) {
259
+ // Still respect aggregate attempt budget unless progress resets effective path.
260
+ // Issue: relevant corrective delta allows one new evaluated attempt even after
261
+ // same-failure history — but not after hard attempt budget without override.
262
+ // Interpretation: material progress allows retry unless maxFailedAttempts already
263
+ // hit AND no override. Spec case 4: "allows one new evaluated attempt".
264
+ // Spec case 5: unrelated does not reset. Spec case 3: same failure still exhausts.
265
+ // So material progress allows unless we're over maxFailedAttempts *and* this would
266
+ // exceed by continuing a thrash — allow when material, block only at hard elapsed/tool.
267
+ if (ledger.failedAttemptCount >= policy.maxFailedAttempts) {
268
+ // Adjacent stage advancement still records progress without erasing phase budget
269
+ // when budget already exhausted — require override for further automatic dispatch.
270
+ // But case 8 says stage advancement records progress without erasing aggregate budget
271
+ // — meaning counters stay, but dispatch may still be allowed if under budget.
272
+ // When at budget, block.
273
+ const resume = {
274
+ kind: "operator-override",
275
+ description: "phase attempt budget exhausted despite material progress; override required",
276
+ satisfied: false,
277
+ };
278
+ return result(input, ledger, "BLOCK_ATTEMPT_BUDGET", "attempt budget exhausted (phase)", {
279
+ retryability: anticipated?.retryability ?? null,
280
+ fingerprint,
281
+ sameFailureCount,
282
+ materialClass: progress.classification,
283
+ resume,
284
+ handoffLedger: {
285
+ ...ledger,
286
+ resumeCondition: resume,
287
+ blockedDecision: "BLOCK_ATTEMPT_BUDGET",
288
+ },
289
+ });
290
+ }
291
+ return result(input, ledger, "ALLOW_MATERIAL_PROGRESS", progress.reason, {
292
+ retryability: anticipated?.retryability ?? null,
293
+ fingerprint,
294
+ sameFailureCount,
295
+ materialClass: progress.classification,
296
+ resume: null,
297
+ });
298
+ }
299
+ // --- No material progress: classify by last/anticipated failure ---
300
+ const retryability = anticipated?.retryability ?? "unknown";
301
+ if (retryability === "deterministic" && policy.blockDeterministicWithoutDelta) {
302
+ // First occurrence after a failure: sameFailureCount is count of completed
303
+ // identical failures. Any completed deterministic failure without delta blocks.
304
+ if (sameFailureCount >= 1 || ledger.failedAttemptCount >= 1) {
305
+ const resume = {
306
+ kind: "material-delta",
307
+ description: "deterministic failure requires relevant material delta before automatic retry",
308
+ satisfied: false,
309
+ };
310
+ const decision = sameFailureCount >= 1 ? "BLOCK_NO_MATERIAL_PROGRESS" : "BLOCK_NON_RETRYABLE";
311
+ return result(input, ledger, decision, "deterministic failure without relevant material delta", {
312
+ retryability,
313
+ fingerprint,
314
+ sameFailureCount,
315
+ materialClass: progress.classification,
316
+ resume,
317
+ handoffLedger: {
318
+ ...ledger,
319
+ resumeCondition: resume,
320
+ blockedDecision: decision,
321
+ },
322
+ });
323
+ }
324
+ }
325
+ if (retryability === "transient") {
326
+ // Count transient failures with same fingerprint (or any last transient).
327
+ const transientFails = ledger.attempts.filter((a) => a.status === "failed" &&
328
+ a.failure !== null &&
329
+ a.failure.retryability === "transient" &&
330
+ (fingerprint === null || a.failure.fingerprint === fingerprint)).length;
331
+ // maxTransientRetries = max automatic retries after a transient failure
332
+ // (1 failure → allow 1 retry when maxTransientRetries is 1).
333
+ if (transientFails <= policy.maxTransientRetries) {
334
+ return result(input, ledger, "ALLOW_TRANSIENT_RETRY", `transient retry ${transientFails}/${policy.maxTransientRetries}`, {
335
+ retryability,
336
+ fingerprint,
337
+ sameFailureCount,
338
+ materialClass: progress.classification,
339
+ resume: null,
340
+ });
341
+ }
342
+ const resume = {
343
+ kind: "material-delta",
344
+ description: "transient retry allowance exhausted; require material change or override",
345
+ satisfied: false,
346
+ };
347
+ return result(input, ledger, "BLOCK_NO_MATERIAL_PROGRESS", "transient retry allowance exhausted without recovery", {
348
+ retryability,
349
+ fingerprint,
350
+ sameFailureCount,
351
+ materialClass: progress.classification,
352
+ resume,
353
+ handoffLedger: {
354
+ ...ledger,
355
+ resumeCondition: resume,
356
+ blockedDecision: "BLOCK_NO_MATERIAL_PROGRESS",
357
+ },
358
+ });
359
+ }
360
+ // unknown
361
+ if (sameFailureCount >= policy.maxUnknownWithoutProgress) {
362
+ const resume = {
363
+ kind: "material-delta",
364
+ description: "repeated unknown failure without material progress",
365
+ satisfied: false,
366
+ };
367
+ return result(input, ledger, "BLOCK_REPEATED_UNKNOWN", `identical unknown failure count ${sameFailureCount} >= ${policy.maxUnknownWithoutProgress}`, {
368
+ retryability,
369
+ fingerprint,
370
+ sameFailureCount,
371
+ materialClass: progress.classification,
372
+ resume,
373
+ handoffLedger: {
374
+ ...ledger,
375
+ resumeCondition: resume,
376
+ blockedDecision: "BLOCK_REPEATED_UNKNOWN",
377
+ },
378
+ });
379
+ }
380
+ // First unknown after one failure: allow only if we have zero sameFailureCount
381
+ // (shouldn't happen if we failed once) — after first unknown fail, count is 1;
382
+ // threshold 2 means second identical unknown blocks, so one more attempt is allowed
383
+ // when sameFailureCount is 1? Spec: "Two identical unknown failures without material
384
+ // progress produce BLOCK_REPEATED_UNKNOWN". So after 2 completed identical unknowns,
385
+ // next dispatch blocks. When sameFailureCount is 1, we can allow one more attempt.
386
+ if (sameFailureCount < policy.maxUnknownWithoutProgress && ledger.failedAttemptCount > 0) {
387
+ if (ledger.failedAttemptCount >= policy.maxFailedAttempts) {
388
+ const resume = {
389
+ kind: "material-delta",
390
+ description: "attempt budget exhausted",
391
+ satisfied: false,
392
+ };
393
+ return result(input, ledger, "BLOCK_ATTEMPT_BUDGET", "failed-attempt budget exhausted", {
394
+ retryability,
395
+ fingerprint,
396
+ sameFailureCount,
397
+ materialClass: progress.classification,
398
+ resume,
399
+ handoffLedger: {
400
+ ...ledger,
401
+ resumeCondition: resume,
402
+ blockedDecision: "BLOCK_ATTEMPT_BUDGET",
403
+ },
404
+ });
405
+ }
406
+ // Allow another attempt toward the unknown threshold (e.g. second try after first unknown).
407
+ return result(input, ledger, "ALLOW_TRANSIENT_RETRY", `unknown failure under threshold (${sameFailureCount}/${policy.maxUnknownWithoutProgress})`, {
408
+ retryability,
409
+ fingerprint,
410
+ sameFailureCount,
411
+ materialClass: progress.classification,
412
+ resume: null,
413
+ });
414
+ }
415
+ // Fallback first path
416
+ return result(input, ledger, "ALLOW_FIRST_ATTEMPT", "default first-path allow", {
417
+ retryability: anticipated?.retryability ?? null,
418
+ fingerprint,
419
+ sameFailureCount,
420
+ materialClass: progress.classification,
421
+ resume: null,
422
+ });
423
+ }
424
+ /**
425
+ * Convenience: evaluate against ledger, and if blocked, return handoff suitable
426
+ * for persistence via markBlocked + saveUnitLedger.
427
+ */
428
+ export function evaluateAndPrepareBlock(ledger, input) {
429
+ const evaluation = evaluatePreDispatch(ledger, input);
430
+ if (!evaluation.allowed && evaluation.handoff !== null) {
431
+ return {
432
+ evaluation,
433
+ ledger: {
434
+ ...ledger,
435
+ blockedDecision: evaluation.decision,
436
+ resumeCondition: evaluation.handoff.resumeCondition,
437
+ updatedAt: utcIso(input.now),
438
+ },
439
+ };
440
+ }
441
+ return { evaluation, ledger };
442
+ }
443
+ //# sourceMappingURL=evaluate.js.map
@@ -0,0 +1,32 @@
1
+ /**
2
+ * Failure fingerprinting for delivery-attempt circuit breaker (#3143).
3
+ *
4
+ * Free-form model judgment alone is not sufficient. Prefer structured
5
+ * stage/code fields; normalize volatile identifiers out of messages.
6
+ */
7
+ import type { FailureInfo, Retryability } from "./types.js";
8
+ /**
9
+ * Normalize a free-form failure message for fingerprinting.
10
+ * Removes volatile identifiers, timestamps, paths, and secret-like values.
11
+ */
12
+ export declare function normalizeFailureMessage(message: string | null | undefined): string;
13
+ export interface FingerprintInput {
14
+ readonly stage: string;
15
+ readonly code?: string | null;
16
+ readonly message?: string | null;
17
+ readonly resourceClass?: string | null;
18
+ readonly retryability?: Retryability | null;
19
+ }
20
+ /**
21
+ * Derive a stable redacted fingerprint from structured failure fields.
22
+ * SHA-256 hex truncated to 32 chars for compact ledger storage.
23
+ */
24
+ export declare function computeFailureFingerprint(input: FingerprintInput): string;
25
+ /**
26
+ * Build a FailureInfo from structured fields.
27
+ * Defaults retryability to `unknown` when not provided.
28
+ */
29
+ export declare function buildFailureInfo(input: FingerprintInput): FailureInfo;
30
+ /** Infer retryability from structured code when adapter does not set it. */
31
+ export declare function inferRetryability(code: string | null | undefined): Retryability;
32
+ //# sourceMappingURL=fingerprint.d.ts.map