@linimin/pi-letscook 0.1.68 → 0.1.70
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/.agent/README.md +2 -3
- package/.agent/verify_completion_control_plane.sh +21 -34
- package/CHANGELOG.md +16 -0
- package/README.md +23 -26
- package/agents/completion-bootstrapper.md +1 -2
- package/agents/completion-regrounder.md +10 -16
- package/extensions/completion/driver.ts +69 -136
- package/extensions/completion/index.ts +94 -81
- package/extensions/completion/policy-guards.ts +1 -1
- package/extensions/completion/prompt-surfaces.ts +63 -161
- package/extensions/completion/proposal.ts +26 -61
- package/extensions/completion/role-runner.ts +161 -57
- package/extensions/completion/state-store.ts +43 -85
- package/extensions/completion/types.ts +2 -3
- package/package.json +1 -1
- package/scripts/active-slice-contract-test.sh +21 -0
- package/scripts/canonical-evidence-artifact-test.sh +57 -65
- package/scripts/context-proposal-test.sh +1430 -310
- package/scripts/legacy-cleanup-test.sh +1 -1
- package/scripts/refocus-test.sh +459 -185
- package/scripts/release-check.sh +14 -15
- package/scripts/role-runner-contract-test.sh +4 -2
- package/scripts/smoke-test.sh +36 -78
- package/skills/completion-protocol/SKILL.md +8 -9
- package/skills/completion-protocol/references/completion.md +2 -37
- package/skills/cook-handoff-boundary/SKILL.md +19 -18
|
@@ -11,7 +11,7 @@ import {
|
|
|
11
11
|
type ContextProposal,
|
|
12
12
|
type RecentDiscussionEntry,
|
|
13
13
|
} from "./proposal";
|
|
14
|
-
import { contextProposalAnalystProgressLines } from "./prompt-surfaces";
|
|
14
|
+
import { contextProposalAnalystProgressLines, primaryAgentHandoffProgressLines } from "./prompt-surfaces";
|
|
15
15
|
import {
|
|
16
16
|
applyLiveRoleEvent,
|
|
17
17
|
buildInlineRunningLines,
|
|
@@ -97,22 +97,23 @@ const STARTUP_ANALYST_ROLE = "cook-proposal-analyst";
|
|
|
97
97
|
const ANALYST_HEARTBEAT_MS = 5_000;
|
|
98
98
|
|
|
99
99
|
const PRIMARY_AGENT_HANDOFF_SYSTEM_PROMPT = [
|
|
100
|
-
"You are the primary agent preparing an explicit /cook
|
|
101
|
-
"Return either exactly one fenced ```cook_handoff JSON block or one brief plain sentence explaining why no concrete
|
|
102
|
-
"If you can prepare a
|
|
103
|
-
"
|
|
104
|
-
"
|
|
105
|
-
"If a bounded first slice, likely implementation surfaces, or likely verification commands are already obvious, include first_slice_goal, first_slice_non_goals, implementation_surfaces, verification_commands, and why_this_slice_first as optional hints only. They are not required when the overall startup plan is already concrete enough to begin workflow planning.",
|
|
106
|
-
"Prefer the latest user-authored task context plus canonical workflow context over older assistant-authored previews or stale planning text.",
|
|
107
|
-
"Do not directly reuse an old preview capsule as-is; either synthesize a fresh startup plan from the current task context or return a brief plain sentence saying no concrete startup plan should replace canonical state yet.",
|
|
108
|
-
"If canonical workflow context already exists and the latest discussion does not clearly ask to replace the mission or start the next round, return a brief plain sentence instead of inventing a replacement startup plan.",
|
|
109
|
-
"Do not make /cook infer or rediscover the mission later; author the startup plan now from the primary-agent view of the task.",
|
|
100
|
+
"You are the primary agent preparing an explicit /cook handoff after the user already chose workflow mode.",
|
|
101
|
+
"Return either exactly one fenced ```cook_handoff JSON block or one brief plain sentence explaining why no concrete handoff can be prepared.",
|
|
102
|
+
"If you can prepare a handoff, the JSON must use kind cook_handoff, source primary_agent, and handoff_kind implementation_workflow_handoff.",
|
|
103
|
+
"When the user has clearly accepted a concrete assistant-proposed slice, carry that slice forward into the handoff instead of broadening or re-guessing the mission.",
|
|
104
|
+
"Do not make /cook infer or rediscover the mission from recent discussion later; author the handoff now from the primary-agent view of the task.",
|
|
110
105
|
"Do not emit markdown commentary before or after the capsule.",
|
|
111
|
-
"If the task is not concrete enough for workflow
|
|
106
|
+
"If the task is not concrete enough for implementation workflow, do not invent the slice.",
|
|
107
|
+
"A valid implementation-ready handoff must include mission, scope, constraints or non_goals, acceptance, risks, notes, first_slice_goal, first_slice_non_goals, implementation_surfaces, verification_commands, and why_this_slice_first.",
|
|
112
108
|
].join(" ");
|
|
113
109
|
const PRIMARY_AGENT_HANDOFF_ROLE = "cook-primary-agent-handoff";
|
|
114
110
|
|
|
115
|
-
|
|
111
|
+
type CookStartupOverlayOptions = {
|
|
112
|
+
title: string;
|
|
113
|
+
footer: string;
|
|
114
|
+
};
|
|
115
|
+
|
|
116
|
+
class CookStartupOverlay extends Container {
|
|
116
117
|
private readonly border: DynamicBorder;
|
|
117
118
|
private readonly title: Text;
|
|
118
119
|
private readonly body: Text;
|
|
@@ -120,7 +121,10 @@ class StartupAnalystOverlay extends Container {
|
|
|
120
121
|
private lines: string[] = [];
|
|
121
122
|
onAbort?: () => void;
|
|
122
123
|
|
|
123
|
-
constructor(
|
|
124
|
+
constructor(
|
|
125
|
+
private readonly theme: any,
|
|
126
|
+
private readonly options: CookStartupOverlayOptions,
|
|
127
|
+
) {
|
|
124
128
|
super();
|
|
125
129
|
this.border = new DynamicBorder((s: string) => this.theme.fg("accent", s));
|
|
126
130
|
this.title = new Text("", 1, 0);
|
|
@@ -140,9 +144,9 @@ class StartupAnalystOverlay extends Container {
|
|
|
140
144
|
}
|
|
141
145
|
|
|
142
146
|
private updateDisplay(): void {
|
|
143
|
-
this.title.setText(this.theme.fg("accent", this.theme.bold(
|
|
147
|
+
this.title.setText(this.theme.fg("accent", this.theme.bold(this.options.title)));
|
|
144
148
|
this.body.setText(formatInlineRunningText(this.theme, this.lines, { primaryAssistant: true }));
|
|
145
|
-
this.footer.setText(this.theme.fg("muted",
|
|
149
|
+
this.footer.setText(this.theme.fg("muted", this.options.footer));
|
|
146
150
|
}
|
|
147
151
|
|
|
148
152
|
override handleInput(data: string): void {
|
|
@@ -196,12 +200,15 @@ async function runContextProposalAnalystSubprocess(params: AnalyzeContextProposa
|
|
|
196
200
|
const args: string[] = ["--mode", "json", "-p", "--no-session", "--no-extensions", "--append-system-prompt", systemPromptTemp.filePath, "--model", modelArg, prompt];
|
|
197
201
|
const invocation = getPiInvocation(args);
|
|
198
202
|
const liveActivity = createLiveRoleActivity(STARTUP_ANALYST_ROLE);
|
|
203
|
+
liveActivity.toolActivity = undefined;
|
|
204
|
+
liveActivity.toolRecentActivity = [];
|
|
205
|
+
liveActivity.recentActivity = [];
|
|
199
206
|
liveActivity.progress = "Analyzing recent discussion";
|
|
200
207
|
liveActivity.currentAction = "Reading recent discussion and preparing a startup proposal";
|
|
201
208
|
liveActivity.assistantSummary = liveActivity.progress;
|
|
202
209
|
liveActivity.recentActivity = pushRecentActivity(liveActivity.recentActivity, `assistant: ${liveActivity.progress}`);
|
|
203
210
|
const messages: RoleMessage[] = [];
|
|
204
|
-
let overlay:
|
|
211
|
+
let overlay: CookStartupOverlay | undefined;
|
|
205
212
|
let finishOverlay: ((value: string | undefined) => void) | undefined;
|
|
206
213
|
let overlaySettled = false;
|
|
207
214
|
const settleOverlay = (value: string | undefined) => {
|
|
@@ -317,7 +324,10 @@ async function runContextProposalAnalystSubprocess(params: AnalyzeContextProposa
|
|
|
317
324
|
if (ui) {
|
|
318
325
|
return await ui.custom<string | undefined>((_tui, theme, _kb, done) => {
|
|
319
326
|
finishOverlay = done;
|
|
320
|
-
overlay = new
|
|
327
|
+
overlay = new CookStartupOverlay(theme, {
|
|
328
|
+
title: "/cook proposal analyst",
|
|
329
|
+
footer: "Esc/Ctrl+C cancel • This analysis runs before /cook writes canonical workflow state",
|
|
330
|
+
});
|
|
321
331
|
overlay.setLines(contextProposalAnalystProgressLines(liveActivity, buildInlineRunningLines));
|
|
322
332
|
run().then(settleOverlay).catch(() => settleOverlay(undefined));
|
|
323
333
|
return overlay;
|
|
@@ -346,8 +356,7 @@ function buildPrimaryAgentHandoffPrompt(projectName: string, recentEntries: Rece
|
|
|
346
356
|
lines.push(
|
|
347
357
|
"",
|
|
348
358
|
"Task:",
|
|
349
|
-
"The user explicitly invoked /cook. Prepare the primary-agent
|
|
350
|
-
"If the latest discussion does not justify a concrete new startup plan, return a brief plain sentence instead of speculative JSON.",
|
|
359
|
+
"The user explicitly invoked /cook. Prepare the primary-agent handoff that /cook should consume immediately for Start/Cancel confirmation.",
|
|
351
360
|
);
|
|
352
361
|
return lines.join("\n");
|
|
353
362
|
}
|
|
@@ -358,52 +367,147 @@ async function runPrimaryAgentHandoffSubprocess(params: GenerateCookHandoffWithA
|
|
|
358
367
|
if (!modelArg) return undefined;
|
|
359
368
|
const cwd = params.getCtxCwd(ctx);
|
|
360
369
|
const runCwd = findCompletionRoot(cwd) ?? findRepoRoot(cwd) ?? cwd;
|
|
370
|
+
const rootKey = completionRootKey(undefined, cwd);
|
|
361
371
|
const prompt = buildPrimaryAgentHandoffPrompt(projectName, recentEntries, params.workflowContextLines ?? []);
|
|
362
372
|
const systemPromptTemp = await writeTempFile(runCwd, "pi-cook-primary-agent-handoff-", PRIMARY_AGENT_HANDOFF_SYSTEM_PROMPT);
|
|
363
373
|
const args: string[] = ["--mode", "json", "-p", "--no-session", "--no-extensions", "--append-system-prompt", systemPromptTemp.filePath, "--model", modelArg, prompt];
|
|
364
374
|
const invocation = getPiInvocation(args);
|
|
365
375
|
const liveActivity = createLiveRoleActivity(PRIMARY_AGENT_HANDOFF_ROLE);
|
|
366
|
-
liveActivity.
|
|
367
|
-
liveActivity.
|
|
368
|
-
liveActivity.
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
376
|
+
liveActivity.toolActivity = undefined;
|
|
377
|
+
liveActivity.toolRecentActivity = [];
|
|
378
|
+
liveActivity.recentActivity = [];
|
|
379
|
+
liveActivity.currentAction = "Reading current task context";
|
|
380
|
+
liveActivity.rationale = "Loading canonical workflow context";
|
|
381
|
+
liveActivity.nextStep = "Synthesizing startup plan";
|
|
382
|
+
liveActivity.verifying = "Waiting for model response...";
|
|
383
|
+
let overlay: CookStartupOverlay | undefined;
|
|
384
|
+
let finishOverlay: ((value: string | undefined) => void) | undefined;
|
|
385
|
+
let overlaySettled = false;
|
|
386
|
+
const settleOverlay = (value: string | undefined) => {
|
|
387
|
+
if (overlaySettled) return;
|
|
388
|
+
overlaySettled = true;
|
|
389
|
+
finishOverlay?.(value);
|
|
390
|
+
};
|
|
391
|
+
const updateActivity = (fresh = false) => {
|
|
392
|
+
if (fresh) liveActivity.updatedAt = nowMs();
|
|
393
|
+
params.liveRoleActivityByRoot.set(rootKey, cloneLiveRoleActivity(liveActivity, { status: "running" }));
|
|
394
|
+
void refreshCompletionStatus({
|
|
395
|
+
ctx,
|
|
396
|
+
liveRoleActivityByRoot: params.liveRoleActivityByRoot,
|
|
397
|
+
completionStatusKey: params.completionStatusKey,
|
|
398
|
+
safeUiCall: params.safeUiCall,
|
|
399
|
+
getCtxCwd: params.getCtxCwd,
|
|
400
|
+
getCtxHasUI: params.getCtxHasUI,
|
|
401
|
+
getCtxUi: params.getCtxUi,
|
|
402
|
+
});
|
|
403
|
+
overlay?.setLines(primaryAgentHandoffProgressLines(liveActivity, buildInlineRunningLines));
|
|
404
|
+
};
|
|
405
|
+
const heartbeat = setInterval(() => updateActivity(false), ANALYST_HEARTBEAT_MS);
|
|
406
|
+
const run = async (): Promise<string | undefined> => {
|
|
407
|
+
try {
|
|
408
|
+
updateActivity(true);
|
|
409
|
+
const output = await new Promise<string | undefined>((resolve) => {
|
|
410
|
+
const proc = spawn(invocation.command, invocation.args, {
|
|
411
|
+
cwd: runCwd,
|
|
412
|
+
env: process.env,
|
|
413
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
414
|
+
shell: false,
|
|
415
|
+
});
|
|
416
|
+
let settled = false;
|
|
417
|
+
const resolveOnce = (value: string | undefined) => {
|
|
418
|
+
if (settled) return;
|
|
419
|
+
settled = true;
|
|
420
|
+
resolve(value);
|
|
421
|
+
};
|
|
422
|
+
const abort = () => {
|
|
423
|
+
proc.kill("SIGTERM");
|
|
424
|
+
resolveOnce(undefined);
|
|
425
|
+
};
|
|
426
|
+
const handleSigint = () => abort();
|
|
427
|
+
let buffer = "";
|
|
428
|
+
const messages: RoleMessage[] = [];
|
|
429
|
+
const processLine = (line: string) => {
|
|
430
|
+
if (!line.trim()) return;
|
|
431
|
+
try {
|
|
432
|
+
const event = JSON.parse(line) as JsonRecord;
|
|
433
|
+
if (applyLiveRoleEvent(liveActivity, event, messages)) updateActivity(true);
|
|
434
|
+
} catch {
|
|
435
|
+
// ignore malformed lines
|
|
436
|
+
}
|
|
437
|
+
};
|
|
438
|
+
proc.stdout.on("data", (chunk) => {
|
|
439
|
+
buffer += chunk.toString();
|
|
440
|
+
const lines = buffer.split("\n");
|
|
441
|
+
buffer = lines.pop() ?? "";
|
|
442
|
+
for (const line of lines) processLine(line);
|
|
443
|
+
});
|
|
444
|
+
proc.stderr.on("data", (_chunk) => {
|
|
445
|
+
// ignore handoff stderr unless the subprocess exits without assistant output
|
|
446
|
+
});
|
|
447
|
+
proc.on("close", (code) => {
|
|
448
|
+
process.off("SIGINT", handleSigint);
|
|
449
|
+
if (buffer.trim()) processLine(buffer);
|
|
450
|
+
resolveOnce(code === 0 ? liveActivity.lastAssistantText?.trim() || undefined : undefined);
|
|
451
|
+
});
|
|
452
|
+
proc.on("error", () => {
|
|
453
|
+
process.off("SIGINT", handleSigint);
|
|
454
|
+
resolveOnce(undefined);
|
|
455
|
+
});
|
|
456
|
+
process.once("SIGINT", handleSigint);
|
|
457
|
+
if (overlay) {
|
|
458
|
+
overlay.onAbort = () => {
|
|
459
|
+
process.off("SIGINT", handleSigint);
|
|
460
|
+
abort();
|
|
461
|
+
};
|
|
386
462
|
}
|
|
387
|
-
};
|
|
388
|
-
proc.stdout.on("data", (chunk) => {
|
|
389
|
-
buffer += chunk.toString();
|
|
390
|
-
const lines = buffer.split("\n");
|
|
391
|
-
buffer = lines.pop() ?? "";
|
|
392
|
-
for (const line of lines) processLine(line);
|
|
393
463
|
});
|
|
394
|
-
|
|
395
|
-
|
|
464
|
+
params.liveRoleActivityByRoot.set(rootKey, cloneLiveRoleActivity(liveActivity, { status: output ? "ok" : "error" }));
|
|
465
|
+
await refreshCompletionStatus({
|
|
466
|
+
ctx,
|
|
467
|
+
liveRoleActivityByRoot: params.liveRoleActivityByRoot,
|
|
468
|
+
completionStatusKey: params.completionStatusKey,
|
|
469
|
+
safeUiCall: params.safeUiCall,
|
|
470
|
+
getCtxCwd: params.getCtxCwd,
|
|
471
|
+
getCtxHasUI: params.getCtxHasUI,
|
|
472
|
+
getCtxUi: params.getCtxUi,
|
|
396
473
|
});
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
474
|
+
return output;
|
|
475
|
+
} finally {
|
|
476
|
+
clearInterval(heartbeat);
|
|
477
|
+
setTimeout(() => {
|
|
478
|
+
const current = params.liveRoleActivityByRoot.get(rootKey);
|
|
479
|
+
if (current && current.role === PRIMARY_AGENT_HANDOFF_ROLE && current.status !== "running") {
|
|
480
|
+
params.liveRoleActivityByRoot.delete(rootKey);
|
|
481
|
+
void refreshCompletionStatus({
|
|
482
|
+
ctx,
|
|
483
|
+
liveRoleActivityByRoot: params.liveRoleActivityByRoot,
|
|
484
|
+
completionStatusKey: params.completionStatusKey,
|
|
485
|
+
safeUiCall: params.safeUiCall,
|
|
486
|
+
getCtxCwd: params.getCtxCwd,
|
|
487
|
+
getCtxHasUI: params.getCtxHasUI,
|
|
488
|
+
getCtxUi: params.getCtxUi,
|
|
489
|
+
});
|
|
490
|
+
}
|
|
491
|
+
}, 10_000);
|
|
492
|
+
await fsp.rm(systemPromptTemp.dir, { recursive: true, force: true });
|
|
493
|
+
}
|
|
494
|
+
};
|
|
495
|
+
if (params.getCtxHasUI(ctx)) {
|
|
496
|
+
const ui = params.getCtxUi(ctx);
|
|
497
|
+
if (ui) {
|
|
498
|
+
return await ui.custom<string | undefined>((_tui, theme, _kb, done) => {
|
|
499
|
+
finishOverlay = done;
|
|
500
|
+
overlay = new CookStartupOverlay(theme, {
|
|
501
|
+
title: "/cook startup plan",
|
|
502
|
+
footer: "Esc/Ctrl+C cancel • This startup-plan synthesis runs before /cook writes canonical workflow state",
|
|
503
|
+
});
|
|
504
|
+
overlay.setLines(primaryAgentHandoffProgressLines(liveActivity, buildInlineRunningLines));
|
|
505
|
+
run().then(settleOverlay).catch(() => settleOverlay(undefined));
|
|
506
|
+
return overlay;
|
|
400
507
|
});
|
|
401
|
-
|
|
402
|
-
});
|
|
403
|
-
return output;
|
|
404
|
-
} finally {
|
|
405
|
-
await fsp.rm(systemPromptTemp.dir, { recursive: true, force: true });
|
|
508
|
+
}
|
|
406
509
|
}
|
|
510
|
+
return await run();
|
|
407
511
|
}
|
|
408
512
|
|
|
409
513
|
export async function generateCookHandoffWithAgent(params: GenerateCookHandoffWithAgentParams): Promise<string | undefined> {
|
|
@@ -414,7 +518,7 @@ export async function generateCookHandoffWithAgent(params: GenerateCookHandoffWi
|
|
|
414
518
|
try {
|
|
415
519
|
return await runPrimaryAgentHandoffSubprocess(params);
|
|
416
520
|
} catch (error) {
|
|
417
|
-
console.warn("[completion] primary-agent
|
|
521
|
+
console.warn("[completion] primary-agent handoff generation failed", error);
|
|
418
522
|
return undefined;
|
|
419
523
|
}
|
|
420
524
|
}
|
|
@@ -3,7 +3,6 @@ import { spawnSync } from "node:child_process";
|
|
|
3
3
|
import { promises as fsp } from "node:fs";
|
|
4
4
|
import * as os from "node:os";
|
|
5
5
|
import * as path from "node:path";
|
|
6
|
-
import { buildApprovedStartupPlanMarkdown } from "./prompt-surfaces";
|
|
7
6
|
import type { CompletionStateSnapshot, JsonRecord } from "./types";
|
|
8
7
|
|
|
9
8
|
const PROTOCOL_ID = "completion";
|
|
@@ -46,10 +45,9 @@ export function resolveFiles(root: string) {
|
|
|
46
45
|
statePath: path.join(agentDir, "state.json"),
|
|
47
46
|
planPath: path.join(agentDir, "plan.json"),
|
|
48
47
|
activePath: path.join(agentDir, "active-slice.json"),
|
|
49
|
-
startupPlanPath: path.join(agentDir, "startup-plan.json"),
|
|
50
|
-
startupPlanMarkdownPath: path.join(agentDir, "startup-plan.md"),
|
|
51
48
|
sliceHistoryPath: path.join(agentDir, "slice-history.jsonl"),
|
|
52
49
|
stopHistoryPath: path.join(agentDir, "stop-check-history.jsonl"),
|
|
50
|
+
startupBriefPath: path.join(agentDir, "startup-brief.json"),
|
|
53
51
|
verificationEvidencePath: path.join(agentDir, "verification-evidence.json"),
|
|
54
52
|
compactionMarkerPath: path.join(tmpDir, "post-compaction-recovery.json"),
|
|
55
53
|
};
|
|
@@ -144,7 +142,7 @@ export async function loadCompletionSnapshot(startCwd: string): Promise<Completi
|
|
|
144
142
|
const state = await readJson(files.statePath);
|
|
145
143
|
const plan = await readJson(files.planPath);
|
|
146
144
|
const active = await readJson(files.activePath);
|
|
147
|
-
const
|
|
145
|
+
const startupBrief = await readJson(files.startupBriefPath);
|
|
148
146
|
const verificationEvidence = await readJson(files.verificationEvidencePath);
|
|
149
147
|
return {
|
|
150
148
|
files,
|
|
@@ -152,7 +150,7 @@ export async function loadCompletionSnapshot(startCwd: string): Promise<Completi
|
|
|
152
150
|
state,
|
|
153
151
|
plan,
|
|
154
152
|
active,
|
|
155
|
-
|
|
153
|
+
startupBrief,
|
|
156
154
|
verificationEvidence,
|
|
157
155
|
activeSlice: findActiveSlice(plan, active),
|
|
158
156
|
};
|
|
@@ -233,15 +231,25 @@ export function buildProfileRecord(args: {
|
|
|
233
231
|
};
|
|
234
232
|
}
|
|
235
233
|
|
|
234
|
+
function buildWorkflowSessionId(): string {
|
|
235
|
+
return `wf-${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 8)}`;
|
|
236
|
+
}
|
|
237
|
+
|
|
236
238
|
export function defaultState(
|
|
237
239
|
missionAnchor: string,
|
|
238
240
|
routing?: { taskType?: string; evaluationProfile?: string; continuationReason?: string },
|
|
239
241
|
advisoryStartupBrief?: JsonRecord,
|
|
240
242
|
): JsonRecord {
|
|
243
|
+
const confirmedAt = asString(advisoryStartupBrief?.captured_at) ?? new Date().toISOString();
|
|
241
244
|
return {
|
|
242
245
|
schema_version: 1,
|
|
243
246
|
mission_anchor: missionAnchor,
|
|
244
247
|
advisory_startup_brief: advisoryStartupBrief ?? null,
|
|
248
|
+
workflow_entry_status: "active",
|
|
249
|
+
workflow_entry_source: "/cook",
|
|
250
|
+
workflow_entry_confirmed_at: confirmedAt,
|
|
251
|
+
workflow_session_id: buildWorkflowSessionId(),
|
|
252
|
+
startup_brief_path: ".agent/startup-brief.json",
|
|
245
253
|
current_phase: "reground",
|
|
246
254
|
continuation_policy: "continue",
|
|
247
255
|
continuation_reason: routing?.continuationReason ?? "Fresh completion bootstrap requires canonical re-ground",
|
|
@@ -280,32 +288,6 @@ export function defaultPlan(
|
|
|
280
288
|
};
|
|
281
289
|
}
|
|
282
290
|
|
|
283
|
-
export function defaultStartupPlan(
|
|
284
|
-
missionAnchor: string,
|
|
285
|
-
routing?: { taskType?: string; evaluationProfile?: string },
|
|
286
|
-
approvedStartupPlan?: JsonRecord,
|
|
287
|
-
): JsonRecord {
|
|
288
|
-
return approvedStartupPlan ?? {
|
|
289
|
-
schema_version: 1,
|
|
290
|
-
artifact_type: "completion-startup-plan",
|
|
291
|
-
status: "approved",
|
|
292
|
-
source: "recent_discussion",
|
|
293
|
-
captured_at: null,
|
|
294
|
-
mission_anchor: missionAnchor,
|
|
295
|
-
goal_text: `Mission: ${missionAnchor}`,
|
|
296
|
-
task_type: routing?.taskType ?? DEFAULT_TASK_TYPE,
|
|
297
|
-
evaluation_profile: routing?.evaluationProfile ?? DEFAULT_EVALUATION_PROFILE,
|
|
298
|
-
scope: [],
|
|
299
|
-
constraints: [],
|
|
300
|
-
acceptance: [],
|
|
301
|
-
risks: [],
|
|
302
|
-
notes: ["No approved startup plan has been recorded yet."],
|
|
303
|
-
planned_surfaces: [],
|
|
304
|
-
verification_intent: [],
|
|
305
|
-
sequencing_hints: [],
|
|
306
|
-
};
|
|
307
|
-
}
|
|
308
|
-
|
|
309
291
|
export function defaultActiveSlice(
|
|
310
292
|
missionAnchor: string,
|
|
311
293
|
routing?: { taskType?: string; evaluationProfile?: string },
|
|
@@ -334,6 +316,32 @@ export function defaultActiveSlice(
|
|
|
334
316
|
};
|
|
335
317
|
}
|
|
336
318
|
|
|
319
|
+
export function defaultStartupBrief(
|
|
320
|
+
missionAnchor: string,
|
|
321
|
+
routing?: { taskType?: string; evaluationProfile?: string },
|
|
322
|
+
advisoryStartupBrief?: JsonRecord,
|
|
323
|
+
): JsonRecord {
|
|
324
|
+
return {
|
|
325
|
+
schema_version: 1,
|
|
326
|
+
artifact_type: "completion-startup-brief",
|
|
327
|
+
source: asString(advisoryStartupBrief?.source) ?? "primary_agent",
|
|
328
|
+
confirmed: true,
|
|
329
|
+
confirmed_at: asString(advisoryStartupBrief?.captured_at) ?? new Date().toISOString(),
|
|
330
|
+
mission: asString(advisoryStartupBrief?.mission) ?? missionAnchor,
|
|
331
|
+
goal_text: asString(advisoryStartupBrief?.goal_text) ?? `Mission: ${missionAnchor}`,
|
|
332
|
+
scope: asStringArray(advisoryStartupBrief?.scope),
|
|
333
|
+
constraints: asStringArray(advisoryStartupBrief?.constraints),
|
|
334
|
+
acceptance: asStringArray(advisoryStartupBrief?.acceptance),
|
|
335
|
+
risks: asStringArray(advisoryStartupBrief?.risks),
|
|
336
|
+
notes:
|
|
337
|
+
asStringArray(advisoryStartupBrief?.notes).length > 0
|
|
338
|
+
? asStringArray(advisoryStartupBrief?.notes)
|
|
339
|
+
: ["No additional startup notes were preserved for this workflow entry."],
|
|
340
|
+
task_type: asString(advisoryStartupBrief?.task_type) ?? routing?.taskType ?? DEFAULT_TASK_TYPE,
|
|
341
|
+
evaluation_profile: asString(advisoryStartupBrief?.evaluation_profile) ?? routing?.evaluationProfile ?? DEFAULT_EVALUATION_PROFILE,
|
|
342
|
+
};
|
|
343
|
+
}
|
|
344
|
+
|
|
337
345
|
export function defaultVerificationEvidence(): JsonRecord {
|
|
338
346
|
return {
|
|
339
347
|
schema_version: 1,
|
|
@@ -352,7 +360,7 @@ export function defaultVerificationEvidence(): JsonRecord {
|
|
|
352
360
|
}
|
|
353
361
|
|
|
354
362
|
export function buildAgentReadme(projectName: string): string {
|
|
355
|
-
return `# Completion Control Plane\n\nThis repository uses the \`completion\` workflow for long-running coding tasks.\n\n## Canonical tracked contract files\n\n- \`.agent/README.md\`\n- \`.agent/mission.md\`\n- \`.agent/profile.json\`\n- \`.agent/verify_completion_stop.sh\`\n- \`.agent/verify_completion_control_plane.sh\`\n\n## Ignored canonical execution state\n\n- \`.agent/state.json\`\n- \`.agent/startup-
|
|
363
|
+
return `# Completion Control Plane\n\nThis repository uses the \`completion\` workflow for long-running coding tasks.\n\n## Canonical tracked contract files\n\n- \`.agent/README.md\`\n- \`.agent/mission.md\`\n- \`.agent/profile.json\`\n- \`.agent/verify_completion_stop.sh\`\n- \`.agent/verify_completion_control_plane.sh\`\n\n## Ignored canonical execution state\n\n- \`.agent/state.json\`\n- \`.agent/startup-brief.json\`\n- \`.agent/plan.json\`\n- \`.agent/active-slice.json\`\n- \`.agent/slice-history.jsonl\`\n- \`.agent/stop-check-history.jsonl\`\n- \`.agent/verification-evidence.json\`\n- \`.agent/*.log\`\n- \`.agent/tmp/\`\n\n\`.agent/startup-brief.json\` preserves the confirmed \`/cook\` startup intent as canonical intake for re-grounding. It does not replace \`.agent/plan.json\` or \`.agent/active-slice.json\`, which remain under regrounder authority.\n\n\`.agent/verification-evidence.json\` is the durable canonical record of deterministic verification for the selected slice or current HEAD. Recovery, review, audit, and stop-check reminder surfaces consume it instead of temp-only artifacts or conversational summaries when it is populated.\n\nThe source of truth for long-running completion work is canonical \`.agent/**\` state plus current repo truth.\n\nProject: ${projectName}\n`;
|
|
356
364
|
}
|
|
357
365
|
|
|
358
366
|
export function buildMission(projectName: string, missionAnchor: string): string {
|
|
@@ -455,23 +463,15 @@ function trackedDiffFiles(fromCommit, toCommit) {
|
|
|
455
463
|
|
|
456
464
|
const profile = readJson('.agent/profile.json');
|
|
457
465
|
const state = readJson('.agent/state.json');
|
|
458
|
-
const startupPlan = readJson('.agent/startup-plan.json');
|
|
459
466
|
const plan = readJson('.agent/plan.json');
|
|
460
467
|
const active = readJson('.agent/active-slice.json');
|
|
461
468
|
const evidence = readJson('.agent/verification-evidence.json');
|
|
462
|
-
let startupPlanMarkdown = '';
|
|
463
|
-
try {
|
|
464
|
-
startupPlanMarkdown = fs.readFileSync('.agent/startup-plan.md', 'utf8');
|
|
465
|
-
} catch (error) {
|
|
466
|
-
fail('.agent/startup-plan.md must be present and readable: ' + error.message);
|
|
467
|
-
}
|
|
468
469
|
|
|
469
470
|
ensureTrackedContractFiles();
|
|
470
471
|
|
|
471
472
|
for (const [file, record] of [
|
|
472
473
|
['.agent/profile.json', profile],
|
|
473
474
|
['.agent/state.json', state],
|
|
474
|
-
['.agent/startup-plan.json', startupPlan],
|
|
475
475
|
['.agent/plan.json', plan],
|
|
476
476
|
['.agent/active-slice.json', active],
|
|
477
477
|
]) {
|
|
@@ -482,38 +482,12 @@ for (const [file, record] of [
|
|
|
482
482
|
const taskType = asString(profile.task_type);
|
|
483
483
|
const evaluationProfile = asString(profile.evaluation_profile);
|
|
484
484
|
if (asString(state.task_type) !== taskType) fail('.agent/state.json task_type must match .agent/profile.json task_type');
|
|
485
|
-
if (asString(startupPlan.task_type) !== taskType) fail('.agent/startup-plan.json task_type must match .agent/profile.json task_type');
|
|
486
485
|
if (asString(plan.task_type) !== taskType) fail('.agent/plan.json task_type must match .agent/profile.json task_type');
|
|
487
486
|
if (asString(active.task_type) !== taskType) fail('.agent/active-slice.json task_type must match .agent/profile.json task_type');
|
|
488
487
|
if (asString(state.evaluation_profile) !== evaluationProfile) fail('.agent/state.json evaluation_profile must match .agent/profile.json evaluation_profile');
|
|
489
|
-
if (asString(startupPlan.evaluation_profile) !== evaluationProfile) fail('.agent/startup-plan.json evaluation_profile must match .agent/profile.json evaluation_profile');
|
|
490
488
|
if (asString(plan.evaluation_profile) !== evaluationProfile) fail('.agent/plan.json evaluation_profile must match .agent/profile.json evaluation_profile');
|
|
491
489
|
if (asString(active.evaluation_profile) !== evaluationProfile) fail('.agent/active-slice.json evaluation_profile must match .agent/profile.json evaluation_profile');
|
|
492
490
|
|
|
493
|
-
if (asString(startupPlan.artifact_type) !== 'completion-startup-plan') {
|
|
494
|
-
fail('.agent/startup-plan.json artifact_type must be completion-startup-plan');
|
|
495
|
-
}
|
|
496
|
-
if (asString(startupPlan.status) !== 'approved') {
|
|
497
|
-
fail('.agent/startup-plan.json status must be approved');
|
|
498
|
-
}
|
|
499
|
-
const startupPlanMissionAnchor = asString(startupPlan.mission_anchor);
|
|
500
|
-
if (!startupPlanMissionAnchor) fail('.agent/startup-plan.json mission_anchor must be present');
|
|
501
|
-
if (startupPlanMissionAnchor !== asString(state.mission_anchor)) fail('.agent/startup-plan.json mission_anchor must match .agent/state.json mission_anchor');
|
|
502
|
-
if (startupPlanMissionAnchor !== asString(plan.mission_anchor)) fail('.agent/startup-plan.json mission_anchor must match .agent/plan.json mission_anchor');
|
|
503
|
-
if (startupPlanMissionAnchor !== asString(active.mission_anchor)) fail('.agent/startup-plan.json mission_anchor must match .agent/active-slice.json mission_anchor');
|
|
504
|
-
if (!asString(startupPlan.goal_text)) fail('.agent/startup-plan.json goal_text must be present');
|
|
505
|
-
for (const field of ['scope', 'constraints', 'acceptance', 'risks', 'notes', 'planned_surfaces', 'verification_intent', 'sequencing_hints']) {
|
|
506
|
-
if (!Array.isArray(startupPlan[field])) fail('.agent/startup-plan.json is missing ' + field);
|
|
507
|
-
}
|
|
508
|
-
if (startupPlanMarkdown.trim().length === 0) fail('.agent/startup-plan.md must not be empty');
|
|
509
|
-
if (startupPlanMissionAnchor && !startupPlanMarkdown.includes(startupPlanMissionAnchor)) {
|
|
510
|
-
fail('.agent/startup-plan.md must mention the startup-plan mission_anchor');
|
|
511
|
-
}
|
|
512
|
-
const startupPlanGoalText = asString(startupPlan.goal_text);
|
|
513
|
-
if (startupPlanGoalText && !startupPlanMarkdown.includes(startupPlanGoalText)) {
|
|
514
|
-
fail('.agent/startup-plan.md must render the startup-plan goal_text');
|
|
515
|
-
}
|
|
516
|
-
|
|
517
491
|
if (asString(evidence.artifact_type) !== 'completion-verification-evidence') {
|
|
518
492
|
fail('.agent/verification-evidence.json artifact_type must be completion-verification-evidence');
|
|
519
493
|
}
|
|
@@ -660,12 +634,7 @@ export type ScaffoldResult = {
|
|
|
660
634
|
export async function scaffoldCompletionFiles(
|
|
661
635
|
root: string,
|
|
662
636
|
missionAnchor: string,
|
|
663
|
-
options?: {
|
|
664
|
-
analysis?: { taskType?: string; evaluationProfile?: string };
|
|
665
|
-
continuationReason?: string;
|
|
666
|
-
advisoryStartupBrief?: JsonRecord;
|
|
667
|
-
approvedStartupPlan?: JsonRecord;
|
|
668
|
-
},
|
|
637
|
+
options?: { analysis?: { taskType?: string; evaluationProfile?: string }; continuationReason?: string; advisoryStartupBrief?: JsonRecord },
|
|
669
638
|
): Promise<ScaffoldResult> {
|
|
670
639
|
const files = resolveFiles(root);
|
|
671
640
|
const created: string[] = [];
|
|
@@ -675,10 +644,6 @@ export async function scaffoldCompletionFiles(
|
|
|
675
644
|
const projectName = path.basename(root);
|
|
676
645
|
const docsSurfaces = await detectDocsSurfaces(root);
|
|
677
646
|
const verifierCommand = await detectVerifierCommand(root);
|
|
678
|
-
const startupPlanRecord =
|
|
679
|
-
options?.approvedStartupPlan ??
|
|
680
|
-
(await readJson(files.startupPlanPath)) ??
|
|
681
|
-
defaultStartupPlan(missionAnchor, { taskType: options?.analysis?.taskType, evaluationProfile: options?.analysis?.evaluationProfile });
|
|
682
647
|
const trackedFiles: Array<{ path: string; content: string; executable?: boolean }> = [
|
|
683
648
|
{ path: path.join(files.agentDir, "README.md"), content: buildAgentReadme(projectName) },
|
|
684
649
|
{ path: path.join(files.agentDir, "mission.md"), content: buildMission(projectName, missionAnchor) },
|
|
@@ -693,12 +658,8 @@ export async function scaffoldCompletionFiles(
|
|
|
693
658
|
content: `${JSON.stringify(defaultState(missionAnchor, { taskType: options?.analysis?.taskType, evaluationProfile: options?.analysis?.evaluationProfile, continuationReason: options?.continuationReason }, options?.advisoryStartupBrief), null, 2)}\n`,
|
|
694
659
|
},
|
|
695
660
|
{
|
|
696
|
-
path: files.
|
|
697
|
-
content: `${JSON.stringify(
|
|
698
|
-
},
|
|
699
|
-
{
|
|
700
|
-
path: files.startupPlanMarkdownPath,
|
|
701
|
-
content: buildApprovedStartupPlanMarkdown(startupPlanRecord as any),
|
|
661
|
+
path: files.startupBriefPath,
|
|
662
|
+
content: `${JSON.stringify(defaultStartupBrief(missionAnchor, { taskType: options?.analysis?.taskType, evaluationProfile: options?.analysis?.evaluationProfile }, options?.advisoryStartupBrief), null, 2)}\n`,
|
|
702
663
|
},
|
|
703
664
|
{ path: files.planPath, content: `${JSON.stringify(defaultPlan(missionAnchor, { taskType: options?.analysis?.taskType, evaluationProfile: options?.analysis?.evaluationProfile }), null, 2)}\n` },
|
|
704
665
|
{ path: files.activePath, content: `${JSON.stringify(defaultActiveSlice(missionAnchor, { taskType: options?.analysis?.taskType, evaluationProfile: options?.analysis?.evaluationProfile }), null, 2)}\n` },
|
|
@@ -721,7 +682,6 @@ export function currentTaskType(snapshot: CompletionStateSnapshot): string | und
|
|
|
721
682
|
return (
|
|
722
683
|
asString(snapshot.active?.task_type) ??
|
|
723
684
|
asString(snapshot.state?.task_type) ??
|
|
724
|
-
asString(snapshot.startupPlan?.task_type) ??
|
|
725
685
|
asString(snapshot.plan?.task_type) ??
|
|
726
686
|
asString(snapshot.profile?.task_type)
|
|
727
687
|
);
|
|
@@ -731,7 +691,6 @@ export function currentEvaluationProfile(snapshot: CompletionStateSnapshot): str
|
|
|
731
691
|
return (
|
|
732
692
|
asString(snapshot.active?.evaluation_profile) ??
|
|
733
693
|
asString(snapshot.state?.evaluation_profile) ??
|
|
734
|
-
asString(snapshot.startupPlan?.evaluation_profile) ??
|
|
735
694
|
asString(snapshot.plan?.evaluation_profile) ??
|
|
736
695
|
asString(snapshot.profile?.evaluation_profile)
|
|
737
696
|
);
|
|
@@ -740,7 +699,6 @@ export function currentEvaluationProfile(snapshot: CompletionStateSnapshot): str
|
|
|
740
699
|
export function currentMissionAnchor(snapshot: CompletionStateSnapshot): string {
|
|
741
700
|
return (
|
|
742
701
|
asString(snapshot.state?.mission_anchor) ??
|
|
743
|
-
asString(snapshot.startupPlan?.mission_anchor) ??
|
|
744
702
|
asString(snapshot.plan?.mission_anchor) ??
|
|
745
703
|
asString(snapshot.active?.mission_anchor) ??
|
|
746
704
|
path.basename(snapshot.files.root)
|
|
@@ -18,10 +18,9 @@ export type CompletionFiles = {
|
|
|
18
18
|
statePath: string;
|
|
19
19
|
planPath: string;
|
|
20
20
|
activePath: string;
|
|
21
|
-
startupPlanPath: string;
|
|
22
|
-
startupPlanMarkdownPath: string;
|
|
23
21
|
sliceHistoryPath: string;
|
|
24
22
|
stopHistoryPath: string;
|
|
23
|
+
startupBriefPath: string;
|
|
25
24
|
verificationEvidencePath: string;
|
|
26
25
|
compactionMarkerPath: string;
|
|
27
26
|
};
|
|
@@ -32,7 +31,7 @@ export type CompletionStateSnapshot = {
|
|
|
32
31
|
state?: JsonRecord;
|
|
33
32
|
plan?: JsonRecord;
|
|
34
33
|
active?: JsonRecord;
|
|
35
|
-
|
|
34
|
+
startupBrief?: JsonRecord;
|
|
36
35
|
verificationEvidence?: JsonRecord;
|
|
37
36
|
activeSlice?: JsonRecord;
|
|
38
37
|
};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@linimin/pi-letscook",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.70",
|
|
4
4
|
"description": "Pi package for long-running completion workflows with canonical .agent state, role-based subagents, continuity, and verification helpers.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"private": false,
|
|
@@ -242,6 +242,11 @@ acceptance = [
|
|
|
242
242
|
state = {
|
|
243
243
|
'schema_version': 1,
|
|
244
244
|
'mission_anchor': mission,
|
|
245
|
+
'workflow_entry_status': 'active',
|
|
246
|
+
'workflow_entry_source': '/cook',
|
|
247
|
+
'workflow_entry_confirmed_at': '2026-05-03T00:00:00Z',
|
|
248
|
+
'workflow_session_id': 'active-slice-fixture-session',
|
|
249
|
+
'startup_brief_path': '.agent/startup-brief.json',
|
|
245
250
|
'current_phase': 'implement',
|
|
246
251
|
'continuation_policy': 'continue',
|
|
247
252
|
'continuation_reason': 'Fixture for active-slice contract regression coverage.',
|
|
@@ -316,6 +321,22 @@ active = {
|
|
|
316
321
|
}
|
|
317
322
|
|
|
318
323
|
Path('.agent/state.json').write_text(json.dumps(state, indent=2) + '\n')
|
|
324
|
+
Path('.agent/startup-brief.json').write_text(json.dumps({
|
|
325
|
+
'schema_version': 1,
|
|
326
|
+
'artifact_type': 'completion-startup-brief',
|
|
327
|
+
'source': 'primary_agent',
|
|
328
|
+
'confirmed': True,
|
|
329
|
+
'confirmed_at': '2026-05-03T00:00:00Z',
|
|
330
|
+
'mission': mission,
|
|
331
|
+
'goal_text': f'Mission: {mission}',
|
|
332
|
+
'scope': ['Exercise active-slice contract parity.'],
|
|
333
|
+
'constraints': ['Keep the fixture scoped to active-slice contract coverage.'],
|
|
334
|
+
'acceptance': acceptance,
|
|
335
|
+
'risks': ['Fixture drift can hide active-slice contract regressions.'],
|
|
336
|
+
'notes': ['Fixture startup brief for active-slice contract regression coverage.'],
|
|
337
|
+
'task_type': task_type,
|
|
338
|
+
'evaluation_profile': evaluation_profile,
|
|
339
|
+
}, indent=2) + '\n')
|
|
319
340
|
Path('.agent/plan.json').write_text(json.dumps(plan, indent=2) + '\n')
|
|
320
341
|
Path('.agent/active-slice.json').write_text(json.dumps(active, indent=2) + '\n')
|
|
321
342
|
Path('.agent/verification-evidence.json').write_text(json.dumps({
|