kcp-harness 0.1.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 (46) hide show
  1. package/LICENSE +201 -0
  2. package/dist/audit.d.ts +73 -0
  3. package/dist/audit.js +142 -0
  4. package/dist/budget-ledger.d.ts +85 -0
  5. package/dist/budget-ledger.js +100 -0
  6. package/dist/classifier.d.ts +35 -0
  7. package/dist/classifier.js +177 -0
  8. package/dist/cli.d.ts +2 -0
  9. package/dist/cli.js +177 -0
  10. package/dist/config.d.ts +65 -0
  11. package/dist/config.js +91 -0
  12. package/dist/downstream.d.ts +50 -0
  13. package/dist/downstream.js +167 -0
  14. package/dist/governor.d.ts +34 -0
  15. package/dist/governor.js +201 -0
  16. package/dist/index.d.ts +12 -0
  17. package/dist/index.js +21 -0
  18. package/dist/integrations/claude-code.d.ts +2 -0
  19. package/dist/integrations/claude-code.js +95 -0
  20. package/dist/integrations/cline.d.ts +2 -0
  21. package/dist/integrations/cline.js +70 -0
  22. package/dist/integrations/continue.d.ts +2 -0
  23. package/dist/integrations/continue.js +39 -0
  24. package/dist/integrations/copilot.d.ts +2 -0
  25. package/dist/integrations/copilot.js +69 -0
  26. package/dist/integrations/crush.d.ts +2 -0
  27. package/dist/integrations/crush.js +48 -0
  28. package/dist/integrations/cursor.d.ts +2 -0
  29. package/dist/integrations/cursor.js +65 -0
  30. package/dist/integrations/generate.d.ts +13 -0
  31. package/dist/integrations/generate.js +60 -0
  32. package/dist/integrations/openclaw.d.ts +2 -0
  33. package/dist/integrations/openclaw.js +67 -0
  34. package/dist/integrations/types.d.ts +51 -0
  35. package/dist/integrations/types.js +78 -0
  36. package/dist/integrations/windsurf.d.ts +2 -0
  37. package/dist/integrations/windsurf.js +67 -0
  38. package/dist/kcp-bridge.d.ts +2 -0
  39. package/dist/kcp-bridge.js +85 -0
  40. package/dist/proxy.d.ts +37 -0
  41. package/dist/proxy.js +394 -0
  42. package/dist/session.d.ts +50 -0
  43. package/dist/session.js +82 -0
  44. package/dist/temporal-watch.d.ts +63 -0
  45. package/dist/temporal-watch.js +147 -0
  46. package/package.json +56 -0
package/dist/proxy.js ADDED
@@ -0,0 +1,394 @@
1
+ // MCP proxy server — the harness's main entry point.
2
+ //
3
+ // The proxy sits between an agent (upstream) and downstream MCP servers.
4
+ // It presents itself as a single MCP server to the agent, aggregating
5
+ // tools from all downstream servers plus the harness's own governance tools.
6
+ //
7
+ // Every tool call flows through the pipeline:
8
+ // classify → govern → execute → audit
9
+ //
10
+ // Governed calls are routed through the kcp-agent planner before the
11
+ // downstream server is contacted. The agent can't bypass governance
12
+ // because it only has access to the proxy's stdio — not the downstream
13
+ // servers' stdio directly.
14
+ import { createInterface } from "node:readline";
15
+ import { classify } from "./classifier.js";
16
+ import { govern } from "./governor.js";
17
+ import { AuditLog, buildEvent, buildLifecycleEvent, buildDriftEvent, } from "./audit.js";
18
+ import { createSession, nextSequence } from "./session.js";
19
+ import { DownstreamManager } from "./downstream.js";
20
+ import { callKcpTool } from "./kcp-bridge.js";
21
+ const HARNESS_VERSION = "0.1.0";
22
+ const PROTOCOL_VERSION = "2025-06-18";
23
+ const rpcResult = (id, result) => ({
24
+ jsonrpc: "2.0",
25
+ id: id ?? null,
26
+ result,
27
+ });
28
+ const rpcError = (id, code, message) => ({
29
+ jsonrpc: "2.0",
30
+ id: id ?? null,
31
+ error: { code, message },
32
+ });
33
+ // -- Harness-specific tools --------------------------------------------------
34
+ const HARNESS_TOOLS = [
35
+ {
36
+ name: "harness_status",
37
+ description: "Show the harness's current governance state: session ID, approved plans, " +
38
+ "known units, budget spent, and audit log path.",
39
+ inputSchema: { type: "object", properties: {} },
40
+ },
41
+ {
42
+ name: "harness_session",
43
+ description: "Show the current session's approved plans and their selected units. " +
44
+ "Use this to see what knowledge the governance layer has approved.",
45
+ inputSchema: { type: "object", properties: {} },
46
+ },
47
+ {
48
+ name: "harness_budget",
49
+ description: "Show the session budget ledger: ceiling, running totals, remaining budget, " +
50
+ "and itemized spend history.",
51
+ inputSchema: { type: "object", properties: {} },
52
+ },
53
+ {
54
+ name: "harness_temporal_check",
55
+ description: "Check all approved plans for temporal drift: re-evaluate against current " +
56
+ "time and report any plans that would produce different results now.",
57
+ inputSchema: { type: "object", properties: {} },
58
+ },
59
+ ];
60
+ /** KCP tool names that the harness routes to kcp-agent directly. */
61
+ const KCP_TOOLS = new Set(["kcp_plan", "kcp_load", "kcp_trace", "kcp_validate", "kcp_replay"]);
62
+ export class HarnessProxy {
63
+ config;
64
+ downstream;
65
+ audit;
66
+ session;
67
+ constructor(options) {
68
+ this.config = options.config;
69
+ this.downstream = new DownstreamManager();
70
+ this.audit = options.audit ?? new AuditLog(options.config.audit.path);
71
+ // Derive budget ceiling from policy
72
+ const policy = options.config.governance.policy;
73
+ const ceiling = policy.budget
74
+ ? { amount: policy.budget.amount, currency: policy.budget.currency ?? "USDC" }
75
+ : undefined;
76
+ this.session = createSession(ceiling);
77
+ }
78
+ /** Start the proxy: spawn downstream servers and begin serving. */
79
+ async start() {
80
+ // Emit session start event
81
+ const seq = nextSequence(this.session);
82
+ this.audit.emit(buildLifecycleEvent(this.session.id, seq, "session_start", {
83
+ domains: this.config.governance.domains.length,
84
+ downstream: this.config.downstream.map((d) => d.name),
85
+ budget: this.session.ledger.getCeiling(),
86
+ }));
87
+ // Spawn and initialize downstream servers
88
+ for (const ds of this.config.downstream) {
89
+ try {
90
+ await this.downstream.add(ds);
91
+ }
92
+ catch (e) {
93
+ const msg = e instanceof Error ? e.message : String(e);
94
+ process.stderr.write(`[kcp-harness] failed to spawn downstream ${ds.name}: ${msg}\n`);
95
+ }
96
+ }
97
+ }
98
+ /** Shut down the proxy and all downstream connections. */
99
+ async stop() {
100
+ // Emit session end event
101
+ const seq = nextSequence(this.session);
102
+ this.audit.emit(buildLifecycleEvent(this.session.id, seq, "session_end", {
103
+ totalCalls: this.session.sequence,
104
+ budgetSnapshot: this.session.ledger.snapshot(),
105
+ plansCount: this.session.plans.size,
106
+ knownUnitsCount: this.session.known.size,
107
+ }));
108
+ await this.downstream.shutdown();
109
+ }
110
+ /** Handle one JSON-RPC message from the agent. */
111
+ async handleMessage(msg) {
112
+ switch (msg.method) {
113
+ case "initialize":
114
+ return rpcResult(msg.id, {
115
+ protocolVersion: typeof msg.params?.["protocolVersion"] === "string"
116
+ ? msg.params["protocolVersion"]
117
+ : PROTOCOL_VERSION,
118
+ capabilities: { tools: {} },
119
+ serverInfo: { name: "kcp-harness", version: HARNESS_VERSION },
120
+ });
121
+ case "ping":
122
+ return rpcResult(msg.id, {});
123
+ case "tools/list":
124
+ return rpcResult(msg.id, { tools: this.listTools() });
125
+ case "tools/call": {
126
+ const name = String(msg.params?.["name"] ?? "");
127
+ const args = (msg.params?.["arguments"] ?? {});
128
+ return this.handleToolCall(msg.id, name, args);
129
+ }
130
+ default:
131
+ // Notifications (no id) are acknowledged silently
132
+ if (msg.id === undefined || msg.method?.startsWith("notifications/"))
133
+ return null;
134
+ return rpcError(msg.id, -32601, `method not found: ${msg.method}`);
135
+ }
136
+ }
137
+ /** List all available tools (downstream + harness + KCP). */
138
+ listTools() {
139
+ const downstreamTools = this.downstream.allTools();
140
+ // Import KCP tools from kcp-agent
141
+ const kcpTools = [
142
+ {
143
+ name: "kcp_plan",
144
+ description: "Produce a deterministic, inspectable load plan for a task against a KCP knowledge.yaml. " +
145
+ "Routes through the governance harness for compliance tracking.",
146
+ inputSchema: {
147
+ type: "object",
148
+ properties: {
149
+ task: { type: "string", description: "The task to plan knowledge loading for" },
150
+ manifest: { type: "string", description: "Path, directory, or HTTPS URL of a knowledge.yaml" },
151
+ },
152
+ required: ["task", "manifest"],
153
+ },
154
+ },
155
+ {
156
+ name: "kcp_load",
157
+ description: "Plan and load knowledge content through the governance harness. " +
158
+ "Automatically tracks session dedup and budget.",
159
+ inputSchema: {
160
+ type: "object",
161
+ properties: {
162
+ task: { type: "string", description: "The task to plan knowledge loading for" },
163
+ manifest: { type: "string", description: "Path, directory, or HTTPS URL of a knowledge.yaml" },
164
+ },
165
+ required: ["task", "manifest"],
166
+ },
167
+ },
168
+ {
169
+ name: "kcp_trace",
170
+ description: "Produce a decision trace: every unit annotated with gate cascade verdicts.",
171
+ inputSchema: {
172
+ type: "object",
173
+ properties: {
174
+ task: { type: "string", description: "The task to trace" },
175
+ manifest: { type: "string", description: "Path, directory, or HTTPS URL of a knowledge.yaml" },
176
+ },
177
+ required: ["task", "manifest"],
178
+ },
179
+ },
180
+ {
181
+ name: "kcp_validate",
182
+ description: "Validate (lint) a knowledge.yaml.",
183
+ inputSchema: {
184
+ type: "object",
185
+ properties: {
186
+ manifest: { type: "string", description: "Path, directory, or HTTPS URL of a knowledge.yaml" },
187
+ },
188
+ required: ["manifest"],
189
+ },
190
+ },
191
+ {
192
+ name: "kcp_replay",
193
+ description: "Cross-examine a saved plan artifact against live manifests.",
194
+ inputSchema: {
195
+ type: "object",
196
+ properties: {
197
+ artifact: { description: "The plan artifact JSON" },
198
+ },
199
+ required: ["artifact"],
200
+ },
201
+ },
202
+ ];
203
+ return [...HARNESS_TOOLS, ...kcpTools, ...downstreamTools];
204
+ }
205
+ /** Handle a tool call through the governance pipeline. */
206
+ async handleToolCall(id, toolName, args) {
207
+ const startTime = Date.now();
208
+ const seq = nextSequence(this.session);
209
+ try {
210
+ // Step 1: Classify
211
+ const classification = classify(toolName, args, this.config.governance.domains);
212
+ // Step 2: Govern (for governed calls)
213
+ let governance;
214
+ if (classification.governed) {
215
+ governance = await govern(classification, toolName, args, this.session, this.config.governance.policy);
216
+ if (!governance.approved) {
217
+ // Blocked — emit audit and return error
218
+ const event = buildEvent(this.session.id, seq, toolName, args, classification, governance, "blocked", Date.now() - startTime);
219
+ this.audit.emit(event);
220
+ return rpcResult(id, {
221
+ content: [
222
+ {
223
+ type: "text",
224
+ text: `[kcp-harness] BLOCKED: ${governance.reason}`,
225
+ },
226
+ ],
227
+ isError: true,
228
+ });
229
+ }
230
+ }
231
+ // Step 3: Execute
232
+ let result;
233
+ if (HARNESS_TOOLS.some((t) => t.name === toolName)) {
234
+ // Harness-internal tool
235
+ result = await this.handleHarnessTool(toolName, args);
236
+ }
237
+ else if (KCP_TOOLS.has(toolName)) {
238
+ // KCP tool — delegate to kcp-agent via the bridge
239
+ const text = await callKcpTool(toolName, args);
240
+ result = { content: [{ type: "text", text }], isError: false };
241
+ }
242
+ else {
243
+ // Downstream tool — forward to the owning downstream server
244
+ result = await this.downstream.callTool(toolName, args);
245
+ }
246
+ // Step 4: Audit
247
+ const outcome = classification.governed ? "approved" : "pass-through";
248
+ if (this.config.governance.policy.audit_all || classification.governed) {
249
+ const event = buildEvent(this.session.id, seq, toolName, args, classification, governance, outcome, Date.now() - startTime);
250
+ this.audit.emit(event);
251
+ }
252
+ return rpcResult(id, result);
253
+ }
254
+ catch (e) {
255
+ const msg = e instanceof Error ? e.message : String(e);
256
+ // Audit the error
257
+ const classification = classify(toolName, args, this.config.governance.domains);
258
+ const event = buildEvent(this.session.id, seq, toolName, args, classification, undefined, "error", Date.now() - startTime, msg);
259
+ this.audit.emit(event);
260
+ return rpcResult(id, {
261
+ content: [{ type: "text", text: `[kcp-harness] error: ${msg}` }],
262
+ isError: true,
263
+ });
264
+ }
265
+ }
266
+ /** Handle harness-internal tools. */
267
+ async handleHarnessTool(name, _args) {
268
+ switch (name) {
269
+ case "harness_status": {
270
+ const status = {
271
+ session: {
272
+ id: this.session.id,
273
+ startedAt: this.session.startedAt,
274
+ sequence: this.session.sequence,
275
+ plansCount: this.session.plans.size,
276
+ knownUnitsCount: this.session.known.size,
277
+ budgetSpent: this.session.budgetSpent,
278
+ },
279
+ governance: {
280
+ domains: this.config.governance.domains.length,
281
+ policy: this.config.governance.policy,
282
+ },
283
+ downstream: this.config.downstream.map((d) => d.name),
284
+ audit: { path: this.audit.getPath() },
285
+ };
286
+ return {
287
+ content: [{ type: "text", text: JSON.stringify(status, null, 2) }],
288
+ isError: false,
289
+ };
290
+ }
291
+ case "harness_session": {
292
+ const plans = {};
293
+ for (const [manifest, approved] of this.session.plans) {
294
+ plans[manifest] = {
295
+ task: approved.task,
296
+ approvedAt: approved.approvedAt,
297
+ selected: approved.plan.selected.map((u) => ({
298
+ id: u.id,
299
+ path: u.path,
300
+ score: u.score,
301
+ loadEligible: u.loadEligible,
302
+ })),
303
+ skipped: approved.plan.skipped.length,
304
+ };
305
+ }
306
+ return {
307
+ content: [{ type: "text", text: JSON.stringify({ sessionId: this.session.id, plans }, null, 2) }],
308
+ isError: false,
309
+ };
310
+ }
311
+ case "harness_budget": {
312
+ const snapshot = this.session.ledger.snapshot();
313
+ const entries = this.session.ledger.getEntries();
314
+ return {
315
+ content: [{
316
+ type: "text",
317
+ text: JSON.stringify({
318
+ ...snapshot,
319
+ entries: entries.map((e) => ({
320
+ seq: e.seq,
321
+ timestamp: e.timestamp,
322
+ manifest: e.source.manifest,
323
+ unitId: e.source.unitId,
324
+ amount: e.cost.amount,
325
+ currency: e.cost.currency,
326
+ method: e.cost.method,
327
+ runningTotal: e.runningTotal,
328
+ })),
329
+ }, null, 2),
330
+ }],
331
+ isError: false,
332
+ };
333
+ }
334
+ case "harness_temporal_check": {
335
+ const watchResult = await this.session.temporalWatch.check();
336
+ // Emit drift audit events for any drifted plans
337
+ for (const drift of watchResult.drifted) {
338
+ const driftSeq = nextSequence(this.session);
339
+ this.audit.emit(buildDriftEvent(this.session.id, driftSeq, drift));
340
+ }
341
+ return {
342
+ content: [{
343
+ type: "text",
344
+ text: JSON.stringify({
345
+ checked: watchResult.checked,
346
+ stable: watchResult.stable,
347
+ drifted: watchResult.drifted.map((d) => ({
348
+ manifest: d.manifest,
349
+ task: d.task,
350
+ summary: d.summary,
351
+ moves: d.diff?.moves.length ?? 0,
352
+ scoreChanges: d.diff?.scoreChanges.length ?? 0,
353
+ })),
354
+ errors: watchResult.errors,
355
+ }, null, 2),
356
+ }],
357
+ isError: false,
358
+ };
359
+ }
360
+ default:
361
+ return {
362
+ content: [{ type: "text", text: `unknown harness tool: ${name}` }],
363
+ isError: true,
364
+ };
365
+ }
366
+ }
367
+ /** Expose session for testing. */
368
+ getSession() {
369
+ return this.session;
370
+ }
371
+ }
372
+ /** Serve the harness proxy over stdio until stdin closes. */
373
+ export async function serveProxy(config) {
374
+ const proxy = new HarnessProxy({ config });
375
+ await proxy.start();
376
+ const rl = createInterface({ input: process.stdin, terminal: false });
377
+ for await (const line of rl) {
378
+ const trimmed = line.trim();
379
+ if (!trimmed)
380
+ continue;
381
+ let msg;
382
+ try {
383
+ msg = JSON.parse(trimmed);
384
+ }
385
+ catch {
386
+ process.stdout.write(JSON.stringify(rpcError(null, -32700, "parse error")) + "\n");
387
+ continue;
388
+ }
389
+ const response = await proxy.handleMessage(msg);
390
+ if (response)
391
+ process.stdout.write(JSON.stringify(response) + "\n");
392
+ }
393
+ await proxy.stop();
394
+ }
@@ -0,0 +1,50 @@
1
+ import type { AgentPlan } from "kcp-agent";
2
+ import { BudgetLedger, type BudgetCeiling } from "./budget-ledger.js";
3
+ import { TemporalWatch } from "./temporal-watch.js";
4
+ /** An approved plan — the set of units the agent is allowed to access. */
5
+ export interface ApprovedPlan {
6
+ /** The manifest this plan came from. */
7
+ manifest: string;
8
+ /** The task the plan was created for. */
9
+ task: string;
10
+ /** The full plan from kcp-agent. */
11
+ plan: AgentPlan;
12
+ /** Timestamp when the plan was approved. */
13
+ approvedAt: string;
14
+ }
15
+ /** Session state for one agent connection. */
16
+ export interface SessionState {
17
+ /** Unique session identifier. */
18
+ id: string;
19
+ /** Approved plans indexed by manifest source. */
20
+ plans: Map<string, ApprovedPlan>;
21
+ /** Units the session has loaded (id → sha256) for dedup. */
22
+ known: Map<string, string>;
23
+ /** Cumulative money budget spent this session. */
24
+ budgetSpent: number;
25
+ /** Monotonic sequence counter for audit events. */
26
+ sequence: number;
27
+ /** Session start time (ISO 8601). */
28
+ startedAt: string;
29
+ /** Budget ledger — itemized spend tracking. */
30
+ ledger: BudgetLedger;
31
+ /** Temporal watcher — detects plan drift. */
32
+ temporalWatch: TemporalWatch;
33
+ }
34
+ /** Create a fresh session state. */
35
+ export declare function createSession(budgetCeiling?: BudgetCeiling): SessionState;
36
+ /** Register an approved plan in the session. */
37
+ export declare function addPlan(session: SessionState, manifest: string, task: string, plan: AgentPlan): void;
38
+ /** Check if a file path is in any approved plan's selected units. */
39
+ export declare function isPathApproved(session: SessionState, path: string): ApprovedPlan | undefined;
40
+ /** Record a loaded unit's sha256 for session dedup. */
41
+ export declare function recordLoaded(session: SessionState, id: string, sha256: string): void;
42
+ /** Get the known[] set for passing to kcp_load. */
43
+ export declare function getKnown(session: SessionState): Array<{
44
+ id: string;
45
+ sha256: string;
46
+ }>;
47
+ /** Record budget expenditure. */
48
+ export declare function recordSpend(session: SessionState, amount: number): void;
49
+ /** Get next sequence number (auto-increments). */
50
+ export declare function nextSequence(session: SessionState): number;
@@ -0,0 +1,82 @@
1
+ // Session state — tracks approved plans, known units, and budget across calls.
2
+ //
3
+ // The session is the harness's ephemeral governance state. It lives for one
4
+ // agent session (process lifetime) and is never persisted. All durable state
5
+ // goes to the audit log.
6
+ //
7
+ // Key responsibilities:
8
+ // 1. Track which plans the agent has established (approved unit sets)
9
+ // 2. Track known[] for session dedup (units already loaded)
10
+ // 3. Track cumulative budget spend
11
+ // 4. Assign monotonic sequence numbers for audit correlation
12
+ import { randomUUID } from "node:crypto";
13
+ import { BudgetLedger } from "./budget-ledger.js";
14
+ import { TemporalWatch } from "./temporal-watch.js";
15
+ /** Create a fresh session state. */
16
+ export function createSession(budgetCeiling) {
17
+ return {
18
+ id: randomUUID(),
19
+ plans: new Map(),
20
+ known: new Map(),
21
+ budgetSpent: 0,
22
+ sequence: 0,
23
+ startedAt: new Date().toISOString(),
24
+ ledger: new BudgetLedger(budgetCeiling),
25
+ temporalWatch: new TemporalWatch(),
26
+ };
27
+ }
28
+ /** Register an approved plan in the session. */
29
+ export function addPlan(session, manifest, task, plan) {
30
+ session.plans.set(manifest, { manifest, task, plan, approvedAt: new Date().toISOString() });
31
+ }
32
+ /** Check if a file path is in any approved plan's selected units. */
33
+ export function isPathApproved(session, path) {
34
+ for (const approved of session.plans.values()) {
35
+ for (const unit of approved.plan.selected) {
36
+ if (unit.loadEligible && pathMatchesUnit(path, unit, approved.manifest)) {
37
+ return approved;
38
+ }
39
+ }
40
+ }
41
+ return undefined;
42
+ }
43
+ /** Check if a unit path matches a target file path (relative resolution). */
44
+ function pathMatchesUnit(targetPath, unit, manifestSource) {
45
+ const unitPath = unit.path;
46
+ // Direct match
47
+ if (targetPath === unitPath)
48
+ return true;
49
+ if (targetPath.endsWith("/" + unitPath))
50
+ return true;
51
+ if (targetPath.endsWith(unitPath))
52
+ return true;
53
+ // Resolve relative to manifest directory
54
+ if (manifestSource) {
55
+ const manifestDir = manifestSource.replace(/\/[^/]*$/, "");
56
+ const resolved = manifestDir + "/" + unitPath;
57
+ if (targetPath === resolved)
58
+ return true;
59
+ if (normalizePath(targetPath) === normalizePath(resolved))
60
+ return true;
61
+ }
62
+ return false;
63
+ }
64
+ function normalizePath(p) {
65
+ return p.replace(/\/+/g, "/").replace(/^\.\//, "");
66
+ }
67
+ /** Record a loaded unit's sha256 for session dedup. */
68
+ export function recordLoaded(session, id, sha256) {
69
+ session.known.set(id, sha256);
70
+ }
71
+ /** Get the known[] set for passing to kcp_load. */
72
+ export function getKnown(session) {
73
+ return Array.from(session.known.entries()).map(([id, sha256]) => ({ id, sha256 }));
74
+ }
75
+ /** Record budget expenditure. */
76
+ export function recordSpend(session, amount) {
77
+ session.budgetSpent += amount;
78
+ }
79
+ /** Get next sequence number (auto-increments). */
80
+ export function nextSequence(session) {
81
+ return ++session.sequence;
82
+ }
@@ -0,0 +1,63 @@
1
+ import { type AgentPlan, type PlanDiff, type FollowOptions } from "kcp-agent";
2
+ /** A plan registered for temporal watching. */
3
+ export interface WatchedPlan {
4
+ /** The manifest source. */
5
+ manifest: string;
6
+ /** The task the plan was created for. */
7
+ task: string;
8
+ /** The approved plan. */
9
+ plan: AgentPlan;
10
+ /** FollowOptions used to create the plan (for re-evaluation). */
11
+ followOptions: FollowOptions;
12
+ /** When the plan was registered. */
13
+ registeredAt: string;
14
+ /** Last time this plan was checked. */
15
+ lastChecked?: string;
16
+ }
17
+ /** Result of checking a single plan for temporal drift. */
18
+ export interface DriftResult {
19
+ /** The manifest that was checked. */
20
+ manifest: string;
21
+ /** The task. */
22
+ task: string;
23
+ /** Whether the plan has drifted. */
24
+ drifted: boolean;
25
+ /** The diff (if drifted). */
26
+ diff?: PlanDiff;
27
+ /** The new plan (if drifted). */
28
+ newPlan?: AgentPlan;
29
+ /** Human-readable summary. */
30
+ summary: string;
31
+ /** Timestamp of the check. */
32
+ checkedAt: string;
33
+ }
34
+ /** Result of checking all watched plans. */
35
+ export interface WatchResult {
36
+ /** Total plans checked. */
37
+ checked: number;
38
+ /** Plans that have drifted. */
39
+ drifted: DriftResult[];
40
+ /** Plans that are still valid. */
41
+ stable: number;
42
+ /** Plans that couldn't be re-evaluated (manifest unavailable, etc.). */
43
+ errors: Array<{
44
+ manifest: string;
45
+ error: string;
46
+ }>;
47
+ }
48
+ /** Temporal plan watcher. */
49
+ export declare class TemporalWatch {
50
+ private watched;
51
+ /** Register a plan for temporal watching. */
52
+ register(manifest: string, task: string, plan: AgentPlan, followOptions: FollowOptions): void;
53
+ /** Remove a plan from watching (e.g., after invalidation). */
54
+ unregister(manifest: string): void;
55
+ /** Check all watched plans for temporal drift. */
56
+ check(): Promise<WatchResult>;
57
+ /** Check a single plan for temporal drift. */
58
+ checkOne(watched: WatchedPlan): Promise<DriftResult>;
59
+ /** Get all watched plans. */
60
+ getWatched(): ReadonlyMap<string, WatchedPlan>;
61
+ /** Get a watched plan by manifest. */
62
+ get(manifest: string): WatchedPlan | undefined;
63
+ }