pi-harness-runtime 1.1.394 → 1.1.396
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/AGENTS.md +135 -4
- package/harness/glm-quota-logger.ts +17 -0
- package/harness/langchain/planner-logger.ts +15 -0
- package/harness/service-diagnostics.ts +32 -1
- package/index.ts +92 -3
- package/package.json +1 -1
- package/packages/a2a-adapter/package.json +1 -1
- package/packages/agent-policy/package.json +1 -1
- package/packages/architecture-generator/package.json +1 -1
- package/packages/auth/package.json +1 -1
- package/packages/autonomous-refactor/package.json +1 -1
- package/packages/autonomous-runtime/package.json +1 -1
- package/packages/cache-strategy/package.json +1 -1
- package/packages/capability-registry/package.json +1 -1
- package/packages/checkpoint/package.json +1 -1
- package/packages/cli-plugin-sdk/package.json +1 -1
- package/packages/clipboard/package.json +1 -1
- package/packages/clipboard-plugin/package.json +1 -1
- package/packages/code-generation/package.json +1 -1
- package/packages/code-review/package.json +1 -1
- package/packages/codex-adapter/package.json +1 -1
- package/packages/codex-watcher/package.json +1 -1
- package/packages/config-capture/package.json +1 -1
- package/packages/connection-runtime/package.json +1 -1
- package/packages/context-compiler/package.json +1 -1
- package/packages/context-discovery/package.json +1 -1
- package/packages/context-manager/package.json +1 -1
- package/packages/contracts/dist/workspace-env-format.d.ts +87 -0
- package/packages/contracts/dist/workspace-env-format.js +174 -6
- package/packages/contracts/dist/workspace-env.js +120 -9
- package/packages/contracts/package.json +1 -1
- package/packages/contracts/src/workspace-env-format.ts +209 -6
- package/packages/contracts/src/workspace-env.ts +128 -7
- package/packages/contracts/test/workspace-env.test.ts +177 -0
- package/packages/cookie-sanitizer/package.json +1 -1
- package/packages/cost-optimizer/package.json +1 -1
- package/packages/dependency-analyzer/package.json +1 -1
- package/packages/django-plugin/package.json +1 -1
- package/packages/doc-generator/package.json +1 -1
- package/packages/durable-review/package.json +1 -1
- package/packages/e2e-tester/package.json +1 -1
- package/packages/evaluation-engine/package.json +1 -1
- package/packages/evaluation-runner/package.json +1 -1
- package/packages/event-bus/package.json +1 -1
- package/packages/event-sink/package.json +1 -1
- package/packages/event-store/package.json +1 -1
- package/packages/experience-replay/package.json +1 -1
- package/packages/feedback-collector/package.json +1 -1
- package/packages/file-copy-helper/package.json +1 -1
- package/packages/framework-detector/package.json +1 -1
- package/packages/framework-plugin-sdk/package.json +1 -1
- package/packages/frappe-plugin/package.json +1 -1
- package/packages/generic-web-plugin/package.json +1 -1
- package/packages/health-monitor/package.json +1 -1
- package/packages/intent-analyzer/package.json +1 -1
- package/packages/jev-judge/dist/continuation-controller.d.ts +47 -0
- package/packages/jev-judge/dist/continuation-controller.d.ts.map +1 -1
- package/packages/jev-judge/dist/continuation-controller.js +142 -16
- package/packages/jev-judge/dist/continuation-controller.js.map +1 -1
- package/packages/jev-judge/dist/index.d.ts +6 -0
- package/packages/jev-judge/dist/index.d.ts.map +1 -1
- package/packages/jev-judge/dist/index.js +6 -0
- package/packages/jev-judge/dist/index.js.map +1 -1
- package/packages/jev-judge/dist/validation-ledger.d.ts +129 -0
- package/packages/jev-judge/dist/validation-ledger.d.ts.map +1 -0
- package/packages/jev-judge/dist/validation-ledger.js +234 -0
- package/packages/jev-judge/dist/validation-ledger.js.map +1 -0
- package/packages/jev-judge/dist/validation-plan.d.ts +176 -0
- package/packages/jev-judge/dist/validation-plan.d.ts.map +1 -0
- package/packages/jev-judge/dist/validation-plan.js +293 -0
- package/packages/jev-judge/dist/validation-plan.js.map +1 -0
- package/packages/jev-judge/dist/validation-tie-breaker.d.ts +48 -0
- package/packages/jev-judge/dist/validation-tie-breaker.d.ts.map +1 -0
- package/packages/jev-judge/dist/validation-tie-breaker.js +83 -0
- package/packages/jev-judge/dist/validation-tie-breaker.js.map +1 -0
- package/packages/jev-judge/package.json +1 -1
- package/packages/jev-judge/src/continuation-controller.ts +179 -20
- package/packages/jev-judge/src/index.ts +41 -0
- package/packages/jev-judge/src/validation-ledger.ts +290 -0
- package/packages/jev-judge/src/validation-plan.ts +406 -0
- package/packages/jev-judge/src/validation-tie-breaker.ts +123 -0
- package/packages/jev-judge/tests/continuation-controller.test.ts +167 -0
- package/packages/jev-judge/tests/validation-ledger.test.ts +181 -0
- package/packages/jev-judge/tests/validation-plan.test.ts +170 -0
- package/packages/jev-judge/tests/validation-tie-breaker.test.ts +88 -0
- package/packages/knowledge-graph/package.json +1 -1
- package/packages/knowledge-retrieval/package.json +1 -1
- package/packages/laravel-plugin/package.json +1 -1
- package/packages/learning-engine/package.json +1 -1
- package/packages/logging/README.md +37 -0
- package/packages/logging/dist/buffer.d.ts +33 -0
- package/packages/logging/dist/buffer.d.ts.map +1 -0
- package/packages/logging/dist/buffer.js +60 -0
- package/packages/logging/dist/buffer.js.map +1 -0
- package/packages/logging/dist/config.d.ts +40 -0
- package/packages/logging/dist/config.d.ts.map +1 -0
- package/packages/logging/dist/config.js +114 -0
- package/packages/logging/dist/config.js.map +1 -0
- package/packages/logging/dist/index.d.ts +32 -0
- package/packages/logging/dist/index.d.ts.map +1 -0
- package/packages/logging/dist/index.js +31 -0
- package/packages/logging/dist/index.js.map +1 -0
- package/packages/logging/dist/redact.d.ts +43 -0
- package/packages/logging/dist/redact.d.ts.map +1 -0
- package/packages/logging/dist/redact.js +220 -0
- package/packages/logging/dist/redact.js.map +1 -0
- package/packages/logging/dist/sampler.d.ts +37 -0
- package/packages/logging/dist/sampler.d.ts.map +1 -0
- package/packages/logging/dist/sampler.js +78 -0
- package/packages/logging/dist/sampler.js.map +1 -0
- package/packages/logging/dist/shipper.d.ts +82 -0
- package/packages/logging/dist/shipper.d.ts.map +1 -0
- package/packages/logging/dist/shipper.js +238 -0
- package/packages/logging/dist/shipper.js.map +1 -0
- package/packages/logging/dist/singleton.d.ts +38 -0
- package/packages/logging/dist/singleton.d.ts.map +1 -0
- package/packages/logging/dist/singleton.js +95 -0
- package/packages/logging/dist/singleton.js.map +1 -0
- package/packages/logging/dist/spool.d.ts +39 -0
- package/packages/logging/dist/spool.d.ts.map +1 -0
- package/packages/logging/dist/spool.js +110 -0
- package/packages/logging/dist/spool.js.map +1 -0
- package/packages/logging/dist/transports/r2.d.ts +31 -0
- package/packages/logging/dist/transports/r2.d.ts.map +1 -0
- package/packages/logging/dist/transports/r2.js +60 -0
- package/packages/logging/dist/transports/r2.js.map +1 -0
- package/packages/logging/dist/transports/webhook.d.ts +22 -0
- package/packages/logging/dist/transports/webhook.d.ts.map +1 -0
- package/packages/logging/dist/transports/webhook.js +37 -0
- package/packages/logging/dist/transports/webhook.js.map +1 -0
- package/packages/logging/dist/types.d.ts +73 -0
- package/packages/logging/dist/types.d.ts.map +1 -0
- package/packages/logging/dist/types.js +19 -0
- package/packages/logging/dist/types.js.map +1 -0
- package/packages/logging/package.json +37 -0
- package/packages/logging/src/buffer.ts +77 -0
- package/packages/logging/src/config.ts +163 -0
- package/packages/logging/src/index.ts +87 -0
- package/packages/logging/src/redact.ts +271 -0
- package/packages/logging/src/sampler.ts +97 -0
- package/packages/logging/src/shipper.ts +281 -0
- package/packages/logging/src/singleton.ts +142 -0
- package/packages/logging/src/spool.ts +139 -0
- package/packages/logging/src/transports/r2.ts +86 -0
- package/packages/logging/src/transports/webhook.ts +54 -0
- package/packages/logging/src/types.ts +85 -0
- package/packages/logging/test/buffer.test.ts +44 -0
- package/packages/logging/test/config.test.ts +97 -0
- package/packages/logging/test/integration.test.ts +135 -0
- package/packages/logging/test/redact.test.ts +109 -0
- package/packages/logging/test/sampler.test.ts +57 -0
- package/packages/logging/test/shipper.test.ts +152 -0
- package/packages/logging/test/spool.test.ts +93 -0
- package/packages/mcp-adapter/package.json +1 -1
- package/packages/memory-engine/package.json +1 -1
- package/packages/milestone-manager/package.json +1 -1
- package/packages/model-registry/package.json +1 -1
- package/packages/nextjs-plugin/package.json +1 -1
- package/packages/notification/package.json +1 -1
- package/packages/observability/package.json +1 -1
- package/packages/okf-indexer/package.json +1 -1
- package/packages/paperclip-integration/package.json +1 -1
- package/packages/performance-optimizer/package.json +1 -1
- package/packages/permission-mode/package.json +1 -1
- package/packages/privilege-broker/package.json +1 -1
- package/packages/project-analyzer/package.json +1 -1
- package/packages/project-bootstrap/package.json +1 -1
- package/packages/projection-engine/package.json +1 -1
- package/packages/prompt-compiler/package.json +1 -1
- package/packages/prompt-versioning/package.json +1 -1
- package/packages/provider-adapter-sdk/package.json +1 -1
- package/packages/provider-router/package.json +1 -1
- package/packages/provider-selector/package.json +1 -1
- package/packages/providers/package.json +1 -1
- package/packages/qdrant-skills/package.json +1 -1
- package/packages/quota-manager/package.json +1 -1
- package/packages/rate-limiter/package.json +1 -1
- package/packages/react-vite-plugin/package.json +1 -1
- package/packages/relay/package.json +1 -1
- package/packages/release-manager/package.json +1 -1
- package/packages/requirement-compiler/package.json +1 -1
- package/packages/runtime/package.json +1 -1
- package/packages/scheduler/package.json +1 -1
- package/packages/scheduler-adapter/package.json +1 -1
- package/packages/session/package.json +1 -1
- package/packages/session-api/package.json +1 -1
- package/packages/session-export/package.json +1 -1
- package/packages/shared-context/package.json +1 -1
- package/packages/skill-mcp-client/package.json +1 -1
- package/packages/skill-registry/package.json +1 -1
- package/packages/skills/package.json +1 -1
- package/packages/sprint-planner/package.json +1 -1
- package/packages/subscription-engine/package.json +1 -1
- package/packages/task-compiler/package.json +1 -1
- package/packages/telegram/package.json +1 -1
- package/packages/tencentdb-memory/package.json +1 -1
- package/packages/tencentdb-sync/package.json +1 -1
- package/packages/test-data-generator/package.json +1 -1
- package/packages/test-generator/package.json +1 -1
- package/packages/todo-bd-sync/package.json +1 -1
- package/packages/token-estimation/package.json +1 -1
- package/packages/token-optimizer/package.json +1 -1
- package/packages/trajectory/package.json +1 -1
- package/packages/tui/package.json +1 -1
- package/packages/types/package.json +1 -1
- package/packages/voice-api/package.json +1 -1
- package/packages/webmcp-tester/package.json +1 -1
- package/packages/wiki/package.json +1 -1
- package/packages/workflow-events/package.json +1 -1
- package/packages/workspace-scanner/package.json +1 -1
- package/packages/worktree/package.json +1 -1
- package/packages/write-review/package.json +1 -1
- package/scripts/unified-webhook-server.ts +102 -0
- package/scripts/validation-run.ts +191 -0
|
@@ -0,0 +1,406 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Validation Plan - staged (fast -> full) test tiers for a todo.
|
|
3
|
+
*
|
|
4
|
+
* The harness NEVER guesses a tier from a command string. A tier is declared
|
|
5
|
+
* by the task author in the todo metadata, so the plan stays truthful and
|
|
6
|
+
* reviewable:
|
|
7
|
+
*
|
|
8
|
+
* ```json
|
|
9
|
+
* {
|
|
10
|
+
* "metadata": {
|
|
11
|
+
* "validationPlan": {
|
|
12
|
+
* "fast": { "command": "bun test tests/fast", "expectedDurationMs": 18000, "datasetSize": 100 },
|
|
13
|
+
* "full": { "command": "bun test tests/full", "expectedDurationMs": 2400000, "datasetSize": 10000 },
|
|
14
|
+
* "budgetMinutes": 90,
|
|
15
|
+
* "finalTierRequired": true
|
|
16
|
+
* }
|
|
17
|
+
* }
|
|
18
|
+
* }
|
|
19
|
+
* ```
|
|
20
|
+
*
|
|
21
|
+
* Every field is optional. A todo without a plan keeps the current behaviour
|
|
22
|
+
* (no validation context is injected), so small tasks are unaffected.
|
|
23
|
+
*/
|
|
24
|
+
|
|
25
|
+
import type { TodoItem } from "./todo-provider.js";
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Which side of the ladder a tier sits on.
|
|
29
|
+
*
|
|
30
|
+
* - `fast`: representative, cheap, safe to run after every edit
|
|
31
|
+
* - `full`: complete, expensive, run once as the final acceptance check
|
|
32
|
+
*/
|
|
33
|
+
export type ValidationTierKind = "fast" | "full";
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* One declared validation tier.
|
|
37
|
+
*/
|
|
38
|
+
export interface ValidationTier {
|
|
39
|
+
kind: ValidationTierKind;
|
|
40
|
+
/** Exact command for this tier. Never rewritten by the harness. */
|
|
41
|
+
command?: string;
|
|
42
|
+
/** Human label, e.g. "fixture regression". */
|
|
43
|
+
label?: string;
|
|
44
|
+
/** Expected cost of the tier in milliseconds (measured or estimated). */
|
|
45
|
+
expectedDurationMs?: number;
|
|
46
|
+
/** Dataset size the tier exercises, when it is data-volume bound. */
|
|
47
|
+
datasetSize?: number;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* State of one tier for one todo item.
|
|
52
|
+
*/
|
|
53
|
+
export interface ValidationTierState {
|
|
54
|
+
/** `not-run` when no run was recorded for this tier. */
|
|
55
|
+
status: "passed" | "failed" | "timeout" | "error" | "skipped" | "not-run";
|
|
56
|
+
/** Timestamp of the latest run. */
|
|
57
|
+
at?: string;
|
|
58
|
+
/** Duration of the latest run. */
|
|
59
|
+
durationMs?: number;
|
|
60
|
+
/** Command of the latest run. */
|
|
61
|
+
command?: string;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* Aggregated validation state of a todo item.
|
|
66
|
+
*/
|
|
67
|
+
export interface ValidationState {
|
|
68
|
+
/** True when the item declares a validation plan. */
|
|
69
|
+
hasPlan: boolean;
|
|
70
|
+
/** State of the cheap tier. */
|
|
71
|
+
fast: ValidationTierState;
|
|
72
|
+
/** State of the expensive tier. */
|
|
73
|
+
full: ValidationTierState;
|
|
74
|
+
/** True when the plan declares the full tier as the final acceptance check. */
|
|
75
|
+
finalTierRequired: boolean;
|
|
76
|
+
/**
|
|
77
|
+
* True when the final tier is required but has not passed yet.
|
|
78
|
+
*
|
|
79
|
+
* A todo in this state must not be reported as complete.
|
|
80
|
+
*/
|
|
81
|
+
finalTierPending: boolean;
|
|
82
|
+
/** Soft time budget of the plan, in minutes. */
|
|
83
|
+
budgetMinutes?: number;
|
|
84
|
+
/** True when the plan requires a final tier but does not declare one. */
|
|
85
|
+
planIncomplete: boolean;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* The full staged validation plan for a todo.
|
|
90
|
+
*/
|
|
91
|
+
export interface ValidationPlan {
|
|
92
|
+
/** Cheap representative tier, used while the code is changing. */
|
|
93
|
+
fast?: ValidationTier;
|
|
94
|
+
/** Expensive complete tier, used once the implementation is stable. */
|
|
95
|
+
full?: ValidationTier;
|
|
96
|
+
/** Extra guidance for the agent (e.g. "fast tier must cover aggregation"). */
|
|
97
|
+
notes?: string;
|
|
98
|
+
/** Soft time budget for this todo, in minutes. */
|
|
99
|
+
budgetMinutes?: number;
|
|
100
|
+
/** When true, the todo must not be completed before the full tier passed. */
|
|
101
|
+
finalTierRequired?: boolean;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
function asRecord(value: unknown): Record<string, unknown> | null {
|
|
105
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) return null;
|
|
106
|
+
return value as Record<string, unknown>;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
function firstString(source: Record<string, unknown>, keys: string[]): string | undefined {
|
|
110
|
+
for (const key of keys) {
|
|
111
|
+
const value = source[key];
|
|
112
|
+
if (typeof value === "string" && value.trim().length > 0) return value.trim();
|
|
113
|
+
}
|
|
114
|
+
return undefined;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
function firstNumber(source: Record<string, unknown>, keys: string[]): number | undefined {
|
|
118
|
+
for (const key of keys) {
|
|
119
|
+
const value = source[key];
|
|
120
|
+
if (typeof value === "number" && Number.isFinite(value)) return value;
|
|
121
|
+
}
|
|
122
|
+
return undefined;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
/**
|
|
126
|
+
* Parse a duration into milliseconds.
|
|
127
|
+
*
|
|
128
|
+
* Strings must carry a unit (`45s`, `40m`, `1h`, `1h30m`). A bare number is
|
|
129
|
+
* milliseconds when it is at least 1000 and seconds below that, because a
|
|
130
|
+
* bare "40" is ambiguous between milliseconds and minutes.
|
|
131
|
+
*/
|
|
132
|
+
export function parseDurationMs(value: unknown): number | undefined {
|
|
133
|
+
if (typeof value === "number") {
|
|
134
|
+
if (!Number.isFinite(value)) return undefined;
|
|
135
|
+
return value >= 1000 ? value : Math.round(value * 1000);
|
|
136
|
+
}
|
|
137
|
+
if (typeof value !== "string") return undefined;
|
|
138
|
+
const text = value.trim().toLowerCase();
|
|
139
|
+
if (!text) return undefined;
|
|
140
|
+
|
|
141
|
+
const match = /^(?:(\d+(?:\.\d+)?)h)?\s*(?:(\d+(?:\.\d+)?)m)?\s*(?:(\d+(?:\.\d+)?)s)?$/.exec(text);
|
|
142
|
+
if (!match || (!match[1] && !match[2] && !match[3])) return undefined;
|
|
143
|
+
const hours = match[1] ? Number(match[1]) : 0;
|
|
144
|
+
const minutes = match[2] ? Number(match[2]) : 0;
|
|
145
|
+
const seconds = match[3] ? Number(match[3]) : 0;
|
|
146
|
+
if (hours === 0 && minutes === 0 && seconds === 0) return undefined;
|
|
147
|
+
return Math.round((hours * 3600 + minutes * 60 + seconds) * 1000);
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
/**
|
|
151
|
+
* Format a duration for a human-readable status line.
|
|
152
|
+
*/
|
|
153
|
+
export function formatDuration(ms: number): string {
|
|
154
|
+
if (!Number.isFinite(ms) || ms < 0) return "0s";
|
|
155
|
+
const seconds = Math.round(ms / 1000);
|
|
156
|
+
if (seconds < 60) return `${seconds}s`;
|
|
157
|
+
const minutes = Math.round(seconds / 60);
|
|
158
|
+
if (minutes < 60) return `${minutes}m`;
|
|
159
|
+
const hours = Math.floor(minutes / 60);
|
|
160
|
+
const rest = minutes % 60;
|
|
161
|
+
return rest === 0 ? `${hours}h` : `${hours}h ${rest}m`;
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
/**
|
|
165
|
+
* Parse one tier declaration.
|
|
166
|
+
*
|
|
167
|
+
* Accepts a plain command string (`"bun test fast"`) or an object with
|
|
168
|
+
* `command`/`label`/`expectedDurationMs`/`datasetSize` fields.
|
|
169
|
+
*/
|
|
170
|
+
export function parseTier(value: unknown, kind: ValidationTierKind): ValidationTier | undefined {
|
|
171
|
+
if (typeof value === "string") {
|
|
172
|
+
const command = value.trim();
|
|
173
|
+
if (!command) return undefined;
|
|
174
|
+
return { kind, command };
|
|
175
|
+
}
|
|
176
|
+
const record = asRecord(value);
|
|
177
|
+
if (!record) return undefined;
|
|
178
|
+
const command = firstString(record, ["command", "cmd", "run"]);
|
|
179
|
+
const label = firstString(record, ["label", "name", "description"]);
|
|
180
|
+
const expectedDurationMs = firstNumber(record, ["expectedDurationMs", "expectedCostMs", "durationMs"])
|
|
181
|
+
?? parseDurationMs(record.expectedDuration ?? record.expectedCost ?? record.duration);
|
|
182
|
+
const datasetSize = firstNumber(record, ["datasetSize", "records", "rows"]);
|
|
183
|
+
if (!command && !label && expectedDurationMs === undefined && datasetSize === undefined) {
|
|
184
|
+
return undefined;
|
|
185
|
+
}
|
|
186
|
+
return { kind, command, label, expectedDurationMs, datasetSize };
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
/**
|
|
190
|
+
* Read the validation plan of a todo.
|
|
191
|
+
*
|
|
192
|
+
* Returns `null` when the todo declares no plan: ordinary tasks must keep the
|
|
193
|
+
* current behaviour.
|
|
194
|
+
*/
|
|
195
|
+
export function parseValidationPlan(item: TodoItem | null | undefined): ValidationPlan | null {
|
|
196
|
+
const metadata = asRecord(item?.metadata);
|
|
197
|
+
if (!metadata) return null;
|
|
198
|
+
const raw = asRecord(metadata.validationPlan)
|
|
199
|
+
?? asRecord(metadata.validation)
|
|
200
|
+
?? asRecord(metadata.validation_plan);
|
|
201
|
+
if (!raw) return null;
|
|
202
|
+
|
|
203
|
+
const tiers = asRecord(raw.tiers);
|
|
204
|
+
const plan: ValidationPlan = {};
|
|
205
|
+
|
|
206
|
+
const fast = parseTier(raw.fast, "fast")
|
|
207
|
+
?? parseTier(raw.fastCommand ?? raw.fastTier ?? tiers?.fast, "fast");
|
|
208
|
+
const full = parseTier(raw.full, "full")
|
|
209
|
+
?? parseTier(raw.fullCommand ?? raw.fullTier ?? tiers?.full, "full");
|
|
210
|
+
if (fast) plan.fast = fast;
|
|
211
|
+
if (full) plan.full = full;
|
|
212
|
+
|
|
213
|
+
const notes = firstString(raw, ["notes", "note"]);
|
|
214
|
+
if (notes) plan.notes = notes;
|
|
215
|
+
|
|
216
|
+
const budgetMinutes = firstNumber(raw, ["budgetMinutes", "budgetMins", "timeBudgetMinutes"]);
|
|
217
|
+
if (budgetMinutes !== undefined) plan.budgetMinutes = budgetMinutes;
|
|
218
|
+
|
|
219
|
+
if (typeof raw.finalTierRequired === "boolean") plan.finalTierRequired = raw.finalTierRequired;
|
|
220
|
+
|
|
221
|
+
if (!plan.fast && !plan.full && plan.notes === undefined && plan.budgetMinutes === undefined) {
|
|
222
|
+
return null;
|
|
223
|
+
}
|
|
224
|
+
return plan;
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
/**
|
|
228
|
+
* Describe one tier in a single line: command, expected cost, dataset size.
|
|
229
|
+
*/
|
|
230
|
+
export function describeTier(tier: ValidationTier): string {
|
|
231
|
+
const what = tier.command ? `\`${tier.command}\`` : tier.label ?? "unspecified tier";
|
|
232
|
+
const extras: string[] = [];
|
|
233
|
+
if (tier.expectedDurationMs !== undefined) extras.push(`~${formatDuration(tier.expectedDurationMs)}`);
|
|
234
|
+
if (tier.datasetSize !== undefined) extras.push(`${tier.datasetSize} records`);
|
|
235
|
+
return extras.length > 0 ? `${what} (${extras.join(", ")})` : what;
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
/**
|
|
239
|
+
* Status of the soft time budget of a plan.
|
|
240
|
+
*/
|
|
241
|
+
export interface ValidationBudgetStatus {
|
|
242
|
+
/** The budget in milliseconds, when the plan declares one. */
|
|
243
|
+
budgetMs?: number;
|
|
244
|
+
/** Time already spent on the item, when it is known. */
|
|
245
|
+
elapsedMs?: number;
|
|
246
|
+
/** Time left before the budget is exhausted. */
|
|
247
|
+
remainingMs?: number;
|
|
248
|
+
/** True when the budget is spent. */
|
|
249
|
+
exhausted: boolean;
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
/**
|
|
253
|
+
* Compute the budget status of a plan.
|
|
254
|
+
*
|
|
255
|
+
* There is no implicit budget: only a declared `budgetMinutes` arms it. An
|
|
256
|
+
* invented budget could pause legitimate long work, so the harness never
|
|
257
|
+
* guesses one.
|
|
258
|
+
*/
|
|
259
|
+
export function validationBudgetStatus(
|
|
260
|
+
plan: ValidationPlan | null | undefined,
|
|
261
|
+
elapsedMs?: number,
|
|
262
|
+
): ValidationBudgetStatus {
|
|
263
|
+
if (!plan || plan.budgetMinutes === undefined) {
|
|
264
|
+
return { elapsedMs, exhausted: false };
|
|
265
|
+
}
|
|
266
|
+
const budgetMs = Math.max(0, plan.budgetMinutes * 60_000);
|
|
267
|
+
const elapsed = elapsedMs ?? 0;
|
|
268
|
+
const remainingMs = Math.max(0, budgetMs - elapsed);
|
|
269
|
+
return {
|
|
270
|
+
budgetMs,
|
|
271
|
+
elapsedMs: elapsed,
|
|
272
|
+
remainingMs,
|
|
273
|
+
exhausted: elapsed >= budgetMs,
|
|
274
|
+
};
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
/**
|
|
278
|
+
* Aggregate recorded runs into the validation state of an item.
|
|
279
|
+
*
|
|
280
|
+
* The runs are expected in chronological order; the latest run of a tier
|
|
281
|
+
* defines its state.
|
|
282
|
+
*/
|
|
283
|
+
export function summarizeValidationState(
|
|
284
|
+
plan: ValidationPlan | null | undefined,
|
|
285
|
+
runs: { ts: string; tier: ValidationTierKind; status: string }[],
|
|
286
|
+
): ValidationState {
|
|
287
|
+
const notRun: ValidationTierState = { status: "not-run" };
|
|
288
|
+
if (!plan) {
|
|
289
|
+
return {
|
|
290
|
+
hasPlan: false,
|
|
291
|
+
fast: { ...notRun },
|
|
292
|
+
full: { ...notRun },
|
|
293
|
+
finalTierRequired: false,
|
|
294
|
+
finalTierPending: false,
|
|
295
|
+
planIncomplete: false,
|
|
296
|
+
};
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
const stateOf = (tier: ValidationTierKind): ValidationTierState => {
|
|
300
|
+
const matching = runs.filter((run) => run.tier === tier);
|
|
301
|
+
if (matching.length === 0) return { ...notRun };
|
|
302
|
+
// The ledger is append-only, so the array order is the chronological
|
|
303
|
+
// order: with an equal timestamp the LATER run wins.
|
|
304
|
+
const latest = matching.reduce((a, b) => (a.ts > b.ts ? a : b));
|
|
305
|
+
const state: ValidationTierState = { status: latest.status as ValidationTierState["status"] };
|
|
306
|
+
state.at = latest.ts;
|
|
307
|
+
return state;
|
|
308
|
+
};
|
|
309
|
+
|
|
310
|
+
const finalTierRequired = plan.finalTierRequired === true;
|
|
311
|
+
const planIncomplete = finalTierRequired && plan.full === undefined;
|
|
312
|
+
const full = stateOf("full");
|
|
313
|
+
|
|
314
|
+
const state: ValidationState = {
|
|
315
|
+
hasPlan: true,
|
|
316
|
+
fast: stateOf("fast"),
|
|
317
|
+
full,
|
|
318
|
+
finalTierRequired,
|
|
319
|
+
finalTierPending: finalTierRequired && plan.full !== undefined && full.status !== "passed",
|
|
320
|
+
planIncomplete,
|
|
321
|
+
};
|
|
322
|
+
if (plan.budgetMinutes !== undefined) state.budgetMinutes = plan.budgetMinutes;
|
|
323
|
+
return state;
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
/**
|
|
327
|
+
* Render the validation context that is injected into a continuation message.
|
|
328
|
+
*
|
|
329
|
+
* The text is deliberately short: it is added to the steer payload that the
|
|
330
|
+
* agent reads when the harness continues a todo on its own.
|
|
331
|
+
*/
|
|
332
|
+
export function formatValidationContext(
|
|
333
|
+
plan: ValidationPlan,
|
|
334
|
+
opts: { elapsedMs?: number; state?: ValidationState } = {},
|
|
335
|
+
): string {
|
|
336
|
+
const lines: string[] = [];
|
|
337
|
+
const tiers: string[] = [];
|
|
338
|
+
if (plan.fast) tiers.push(`fast=${describeTier(plan.fast)}`);
|
|
339
|
+
if (plan.full) tiers.push(`full=${describeTier(plan.full)}`);
|
|
340
|
+
|
|
341
|
+
if (tiers.length === 0 && plan.notes === undefined && plan.budgetMinutes === undefined) {
|
|
342
|
+
return "";
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
lines.push(`VALIDATION PLAN (staged): ${tiers.join(" | ") || "no tiers declared"}`);
|
|
346
|
+
|
|
347
|
+
if (plan.fast && plan.full) {
|
|
348
|
+
lines.push(
|
|
349
|
+
"Run the fast tier after every edit; run the full tier once when the implementation is stable. " +
|
|
350
|
+
"Do not repeat the full tier after an edit that cannot affect the behavior it covers.",
|
|
351
|
+
);
|
|
352
|
+
}
|
|
353
|
+
if (plan.finalTierRequired) {
|
|
354
|
+
lines.push(
|
|
355
|
+
"The full tier is the final acceptance check: do not complete this todo while it is pending or failed.",
|
|
356
|
+
);
|
|
357
|
+
}
|
|
358
|
+
if (plan.budgetMinutes !== undefined) {
|
|
359
|
+
const budgetMs = plan.budgetMinutes * 60_000;
|
|
360
|
+
const elapsed = opts.elapsedMs;
|
|
361
|
+
const elapsedPart = elapsed === undefined
|
|
362
|
+
? ""
|
|
363
|
+
: ` (elapsed ${formatDuration(elapsed)}, remaining ${formatDuration(Math.max(0, budgetMs - elapsed))})`;
|
|
364
|
+
lines.push(
|
|
365
|
+
`Time budget: ${plan.budgetMinutes}m${elapsedPart}. When the budget is reached: stop, report what passed ` +
|
|
366
|
+
"and what is still pending, and do not mark the todo complete.",
|
|
367
|
+
);
|
|
368
|
+
}
|
|
369
|
+
if (plan.notes) lines.push(`Notes: ${plan.notes}`);
|
|
370
|
+
|
|
371
|
+
// The recorded runs tell the agent what already happened, so it neither
|
|
372
|
+
// repeats a tier that passed nor forgets a pending final tier.
|
|
373
|
+
const state = opts.state;
|
|
374
|
+
if (state?.hasPlan) {
|
|
375
|
+
if (state.fast.status !== "not-run") {
|
|
376
|
+
lines.push(`Fast tier: ${state.fast.status}${state.fast.at ? ` (last run ${state.fast.at})` : ""}.`);
|
|
377
|
+
}
|
|
378
|
+
if (state.full.status !== "not-run") {
|
|
379
|
+
lines.push(`Full tier: ${state.full.status}${state.full.at ? ` (last run ${state.full.at})` : ""}.`);
|
|
380
|
+
}
|
|
381
|
+
if (state.full.status === "passed") {
|
|
382
|
+
lines.push(
|
|
383
|
+
"The final tier already passed. Do not repeat it unless the next change can affect what it covers.",
|
|
384
|
+
);
|
|
385
|
+
}
|
|
386
|
+
if (state.planIncomplete) {
|
|
387
|
+
lines.push(
|
|
388
|
+
"The plan requires a final tier but declares none: report that instead of completing the todo silently.",
|
|
389
|
+
);
|
|
390
|
+
}
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
return lines.join("\n");
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
/**
|
|
397
|
+
* Convenience wrapper: the validation context of a todo, or an empty string.
|
|
398
|
+
*/
|
|
399
|
+
export function buildValidationSteerSuffix(
|
|
400
|
+
item: TodoItem | null | undefined,
|
|
401
|
+
opts: { elapsedMs?: number; state?: ValidationState } = {},
|
|
402
|
+
): string {
|
|
403
|
+
const plan = parseValidationPlan(item);
|
|
404
|
+
if (!plan) return "";
|
|
405
|
+
return formatValidationContext(plan, opts);
|
|
406
|
+
}
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Validation tie-breaker - a typed verdict about one todo item.
|
|
3
|
+
*
|
|
4
|
+
* The judge (Jev) and the harness both need the same question answered with
|
|
5
|
+
* a value instead of prose: "may this item count as done?".
|
|
6
|
+
*
|
|
7
|
+
* The verdict is derived from the declared plan and the recorded runs:
|
|
8
|
+
*
|
|
9
|
+
* - `no-plan` the item declares no validation plan
|
|
10
|
+
* - `complete` the final tier passed, or none was required
|
|
11
|
+
* - `final-tier-pending` the final tier is required and has not passed yet
|
|
12
|
+
* - `final-tier-failed` the final tier ran and did not pass
|
|
13
|
+
* - `plan-incomplete` the plan requires a final tier but declares none
|
|
14
|
+
*
|
|
15
|
+
* A `final-tier-pending` or `final-tier-failed` verdict means the item must
|
|
16
|
+
* not be reported as complete.
|
|
17
|
+
*/
|
|
18
|
+
|
|
19
|
+
import type { TodoItem } from "./todo-provider.js";
|
|
20
|
+
import type { ValidationState } from "./validation-plan.js";
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Typed verdict about the validation state of one item.
|
|
24
|
+
*/
|
|
25
|
+
export type ValidationVerdict =
|
|
26
|
+
| "no-plan"
|
|
27
|
+
| "complete"
|
|
28
|
+
| "final-tier-pending"
|
|
29
|
+
| "final-tier-failed"
|
|
30
|
+
| "plan-incomplete";
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* The verdict plus the context a caller needs to report it.
|
|
34
|
+
*/
|
|
35
|
+
export interface ValidationTieBreaker {
|
|
36
|
+
verdict: ValidationVerdict;
|
|
37
|
+
/** Human readable reason for the verdict. */
|
|
38
|
+
reason: string;
|
|
39
|
+
/** The item the verdict is about. */
|
|
40
|
+
itemId?: number;
|
|
41
|
+
/** Subject of the item. */
|
|
42
|
+
subject?: string;
|
|
43
|
+
/** Status of the final tier, when one ran. */
|
|
44
|
+
tierStatus?: string;
|
|
45
|
+
/** True when the item must not be reported as complete. */
|
|
46
|
+
blocksCompletion: boolean;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Derive the verdict from the validation state of an item.
|
|
51
|
+
*/
|
|
52
|
+
export function tieBreakValidation(
|
|
53
|
+
item: TodoItem | null | undefined,
|
|
54
|
+
state: ValidationState,
|
|
55
|
+
): ValidationTieBreaker {
|
|
56
|
+
const base = {
|
|
57
|
+
itemId: item?.id,
|
|
58
|
+
subject: item?.subject,
|
|
59
|
+
};
|
|
60
|
+
|
|
61
|
+
if (!state.hasPlan) {
|
|
62
|
+
return {
|
|
63
|
+
...base,
|
|
64
|
+
verdict: "no-plan",
|
|
65
|
+
reason: "no validation plan declared",
|
|
66
|
+
blocksCompletion: false,
|
|
67
|
+
};
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
if (state.planIncomplete) {
|
|
71
|
+
return {
|
|
72
|
+
...base,
|
|
73
|
+
verdict: "plan-incomplete",
|
|
74
|
+
reason: "the plan requires a final tier but declares none",
|
|
75
|
+
blocksCompletion: true,
|
|
76
|
+
};
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
if (!state.finalTierRequired) {
|
|
80
|
+
return {
|
|
81
|
+
...base,
|
|
82
|
+
verdict: "complete",
|
|
83
|
+
reason: "no final tier required",
|
|
84
|
+
blocksCompletion: false,
|
|
85
|
+
};
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
if (state.full.status === "passed") {
|
|
89
|
+
return {
|
|
90
|
+
...base,
|
|
91
|
+
verdict: "complete",
|
|
92
|
+
reason: "the final tier passed",
|
|
93
|
+
tierStatus: state.full.status,
|
|
94
|
+
blocksCompletion: false,
|
|
95
|
+
};
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
if (state.full.status === "not-run") {
|
|
99
|
+
return {
|
|
100
|
+
...base,
|
|
101
|
+
verdict: "final-tier-pending",
|
|
102
|
+
reason: "the final tier has not run yet",
|
|
103
|
+
tierStatus: state.full.status,
|
|
104
|
+
blocksCompletion: true,
|
|
105
|
+
};
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
return {
|
|
109
|
+
...base,
|
|
110
|
+
verdict: "final-tier-failed",
|
|
111
|
+
reason: `the final tier did not pass (${state.full.status})`,
|
|
112
|
+
tierStatus: state.full.status,
|
|
113
|
+
blocksCompletion: true,
|
|
114
|
+
};
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
/**
|
|
118
|
+
* Format a verdict for a log line or a status line.
|
|
119
|
+
*/
|
|
120
|
+
export function formatTieBreaker(result: ValidationTieBreaker): string {
|
|
121
|
+
const subject = result.subject ? `${result.subject}: ` : "";
|
|
122
|
+
return `${subject}${result.verdict} (${result.reason})`;
|
|
123
|
+
}
|
|
@@ -8,8 +8,12 @@
|
|
|
8
8
|
*/
|
|
9
9
|
|
|
10
10
|
import { describe, it, expect } from "bun:test";
|
|
11
|
+
import { mkdtempSync, rmSync } from "node:fs";
|
|
12
|
+
import { tmpdir } from "node:os";
|
|
13
|
+
import { join } from "node:path";
|
|
11
14
|
import { TodoContinuationController } from "../src/continuation-controller.js";
|
|
12
15
|
import { MockTodoProvider, type TodoContext } from "../src/todo-provider.js";
|
|
16
|
+
import { ValidationLedger } from "../src/validation-ledger.js";
|
|
13
17
|
|
|
14
18
|
function makeCtx(overrides: Partial<TodoContext> = {}): TodoContext {
|
|
15
19
|
return {
|
|
@@ -1227,3 +1231,166 @@ describe("TodoContinuationController - guard lifecycle", () => {
|
|
|
1227
1231
|
expect(sends).toBe(4);
|
|
1228
1232
|
});
|
|
1229
1233
|
});
|
|
1234
|
+
|
|
1235
|
+
describe("TodoContinuationController - staged validation context", () => {
|
|
1236
|
+
it("adds the declared validation plan to the continuation message", async () => {
|
|
1237
|
+
const provider = new MockTodoProvider();
|
|
1238
|
+
provider.addItem({
|
|
1239
|
+
subject: "seed data",
|
|
1240
|
+
status: "in_progress",
|
|
1241
|
+
metadata: {
|
|
1242
|
+
validationPlan: {
|
|
1243
|
+
fast: { command: "bun test tests/fast", expectedDurationMs: 18_000, datasetSize: 100 },
|
|
1244
|
+
full: { command: "bun test tests/full", expectedDurationMs: 2_400_000, datasetSize: 10_000 },
|
|
1245
|
+
budgetMinutes: 90,
|
|
1246
|
+
finalTierRequired: true,
|
|
1247
|
+
},
|
|
1248
|
+
},
|
|
1249
|
+
} as never);
|
|
1250
|
+
|
|
1251
|
+
const sent: string[] = [];
|
|
1252
|
+
const controller = makeController({
|
|
1253
|
+
provider,
|
|
1254
|
+
dispatch: async (content) => {
|
|
1255
|
+
sent.push(content);
|
|
1256
|
+
},
|
|
1257
|
+
});
|
|
1258
|
+
|
|
1259
|
+
const result = await controller.steerToContinue(1);
|
|
1260
|
+
expect(result.dispatched).toBe(true);
|
|
1261
|
+
expect(sent.length).toBe(1);
|
|
1262
|
+
|
|
1263
|
+
const message = sent[0];
|
|
1264
|
+
expect(message).toContain("Continue todo item ID 1.");
|
|
1265
|
+
expect(message).toContain("VALIDATION PLAN (staged)");
|
|
1266
|
+
expect(message).toContain("bun test tests/fast");
|
|
1267
|
+
expect(message).toContain("bun test tests/full");
|
|
1268
|
+
expect(message).toContain("Run the fast tier after every edit");
|
|
1269
|
+
expect(message).toContain("Time budget: 90m");
|
|
1270
|
+
});
|
|
1271
|
+
|
|
1272
|
+
it("leaves the message untouched when no plan is declared", async () => {
|
|
1273
|
+
const provider = new MockTodoProvider();
|
|
1274
|
+
provider.addItem({ subject: "keep going", status: "in_progress" } as never);
|
|
1275
|
+
|
|
1276
|
+
const sent: string[] = [];
|
|
1277
|
+
const controller = makeController({
|
|
1278
|
+
provider,
|
|
1279
|
+
dispatch: async (content) => {
|
|
1280
|
+
sent.push(content);
|
|
1281
|
+
},
|
|
1282
|
+
});
|
|
1283
|
+
|
|
1284
|
+
await controller.steerToContinue(1);
|
|
1285
|
+
expect(sent[0]).toBe("Continue todo item ID 1.");
|
|
1286
|
+
expect(sent[0]).not.toContain("VALIDATION PLAN");
|
|
1287
|
+
});
|
|
1288
|
+
});
|
|
1289
|
+
|
|
1290
|
+
describe("TodoContinuationController - validation budget and ledger", () => {
|
|
1291
|
+
it("pauses the item when the declared time budget is spent", async () => {
|
|
1292
|
+
const provider = new MockTodoProvider();
|
|
1293
|
+
provider.addItem({
|
|
1294
|
+
subject: "budgeted work",
|
|
1295
|
+
status: "in_progress",
|
|
1296
|
+
metadata: {
|
|
1297
|
+
validationPlan: {
|
|
1298
|
+
fast: { command: "bun test tests/fast" },
|
|
1299
|
+
budgetMinutes: 0,
|
|
1300
|
+
},
|
|
1301
|
+
},
|
|
1302
|
+
} as never);
|
|
1303
|
+
|
|
1304
|
+
const sent: string[] = [];
|
|
1305
|
+
const controller = makeController({
|
|
1306
|
+
provider,
|
|
1307
|
+
dispatch: async (content) => {
|
|
1308
|
+
sent.push(content);
|
|
1309
|
+
},
|
|
1310
|
+
});
|
|
1311
|
+
|
|
1312
|
+
const result = await controller.decide();
|
|
1313
|
+
expect(result.action).toBe("stop");
|
|
1314
|
+
expect(result.reason).toContain("budget exhausted");
|
|
1315
|
+
expect(result.budget?.exhausted).toBe(true);
|
|
1316
|
+
expect(sent.length).toBe(0);
|
|
1317
|
+
});
|
|
1318
|
+
|
|
1319
|
+
it("does not put a continuation on the wire when the budget is spent", async () => {
|
|
1320
|
+
const provider = new MockTodoProvider();
|
|
1321
|
+
provider.addItem({
|
|
1322
|
+
subject: "budgeted work",
|
|
1323
|
+
status: "in_progress",
|
|
1324
|
+
metadata: {
|
|
1325
|
+
validationPlan: {
|
|
1326
|
+
fast: { command: "bun test tests/fast" },
|
|
1327
|
+
budgetMinutes: 0.001, // 60ms
|
|
1328
|
+
},
|
|
1329
|
+
},
|
|
1330
|
+
} as never);
|
|
1331
|
+
|
|
1332
|
+
const sent: string[] = [];
|
|
1333
|
+
const controller = makeController({
|
|
1334
|
+
provider,
|
|
1335
|
+
dispatch: async (content) => {
|
|
1336
|
+
sent.push(content);
|
|
1337
|
+
},
|
|
1338
|
+
});
|
|
1339
|
+
|
|
1340
|
+
// The first continuation is inside the budget.
|
|
1341
|
+
const first = await controller.steerToContinue(1);
|
|
1342
|
+
expect(first.dispatched).toBe(true);
|
|
1343
|
+
expect(sent.length).toBe(1);
|
|
1344
|
+
|
|
1345
|
+
// After the budget is spent the item pauses at a safe boundary.
|
|
1346
|
+
await new Promise((resolve) => setTimeout(resolve, 90));
|
|
1347
|
+
controller.releaseContinuationGuard("settled");
|
|
1348
|
+
const second = await controller.steerToContinue(1);
|
|
1349
|
+
expect(second.dispatched).toBe(false);
|
|
1350
|
+
expect(second.reason).toContain("budget exhausted");
|
|
1351
|
+
expect(sent.length).toBe(1);
|
|
1352
|
+
});
|
|
1353
|
+
|
|
1354
|
+
it("reports the recorded tier state in the continuation message", async () => {
|
|
1355
|
+
const dir = mkdtempSync(join(tmpdir(), "validation-ledger-"));
|
|
1356
|
+
try {
|
|
1357
|
+
const provider = new MockTodoProvider();
|
|
1358
|
+
provider.addItem({
|
|
1359
|
+
subject: "staged work",
|
|
1360
|
+
status: "in_progress",
|
|
1361
|
+
metadata: {
|
|
1362
|
+
validationPlan: {
|
|
1363
|
+
fast: { command: "bun test tests/fast" },
|
|
1364
|
+
full: { command: "bun test tests/full" },
|
|
1365
|
+
finalTierRequired: true,
|
|
1366
|
+
},
|
|
1367
|
+
},
|
|
1368
|
+
} as never);
|
|
1369
|
+
|
|
1370
|
+
const ledger = new ValidationLedger({ path: join(dir, "runs.jsonl") });
|
|
1371
|
+
ledger.record({ tier: "fast", status: "passed", itemId: 1 });
|
|
1372
|
+
|
|
1373
|
+
const sent: string[] = [];
|
|
1374
|
+
const controller = new TodoContinuationController(makeCtx(), {
|
|
1375
|
+
dispatch: async (content) => {
|
|
1376
|
+
sent.push(content);
|
|
1377
|
+
},
|
|
1378
|
+
todoProvider: provider,
|
|
1379
|
+
validationLedger: ledger,
|
|
1380
|
+
});
|
|
1381
|
+
|
|
1382
|
+
await controller.steerToContinue(1);
|
|
1383
|
+
expect(sent[0]).toContain("Fast tier: passed");
|
|
1384
|
+
expect(sent[0]).toContain("do not complete this todo while it is pending or failed");
|
|
1385
|
+
|
|
1386
|
+
// Once the final tier passed, the message says so.
|
|
1387
|
+
ledger.record({ tier: "full", status: "passed", itemId: 1 });
|
|
1388
|
+
controller.releaseContinuationGuard("settled");
|
|
1389
|
+
await controller.steerToContinue(1);
|
|
1390
|
+
expect(sent[1]).toContain("Full tier: passed");
|
|
1391
|
+
expect(sent[1]).toContain("The final tier already passed");
|
|
1392
|
+
} finally {
|
|
1393
|
+
rmSync(dir, { recursive: true, force: true });
|
|
1394
|
+
}
|
|
1395
|
+
});
|
|
1396
|
+
});
|