@linimin/pi-letscook 0.1.47 → 0.1.49
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/CHANGELOG.md +12 -0
- package/README.md +7 -7
- package/extensions/completion/driver.ts +7 -9
- package/extensions/completion/index.ts +28 -10
- package/extensions/completion/prompt-surfaces.ts +1 -0
- package/extensions/completion/proposal.ts +66 -6
- package/extensions/completion/role-runner.ts +1 -0
- package/package.json +1 -1
- package/scripts/context-proposal-test.sh +20 -19
- package/scripts/refocus-test.sh +9 -8
- package/scripts/release-check.sh +9 -10
- package/scripts/smoke-test.sh +7 -6
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,18 @@
|
|
|
2
2
|
|
|
3
3
|
## Unreleased
|
|
4
4
|
|
|
5
|
+
## 0.1.49
|
|
6
|
+
|
|
7
|
+
### Changed
|
|
8
|
+
|
|
9
|
+
- restored optional `/cook <hint>` support as a soft intent hint that biases context analysis, proposal ranking, active-workflow disambiguation, and next-round startup without bypassing fail-closed routing or the approval-only Start/Cancel gate
|
|
10
|
+
|
|
11
|
+
## 0.1.48
|
|
12
|
+
|
|
13
|
+
### Fixed
|
|
14
|
+
|
|
15
|
+
- stopped injecting active completion workflow routing into ordinary non-`/cook` main-chat turns after a repo had been activated earlier in the same Pi process, so stale canonical stop-wave state can no longer pull unrelated user requests into `completion-regrounder` unless a fresh `/cook` driver prompt is actually in flight
|
|
16
|
+
|
|
5
17
|
## 0.1.47
|
|
6
18
|
|
|
7
19
|
### Changed
|
package/README.md
CHANGED
|
@@ -38,26 +38,26 @@ Primary entrypoint:
|
|
|
38
38
|
/cook
|
|
39
39
|
```
|
|
40
40
|
|
|
41
|
-
Use
|
|
41
|
+
Use `/cook` after you discuss the mission in the main chat. You can run it bare or pass a short hint such as `/cook login redirect`. The same command can:
|
|
42
42
|
|
|
43
43
|
- start a brand-new workflow from recent discussion
|
|
44
44
|
- continue the current workflow when recent discussion still matches it, or when discussion is too weak or ambiguous to justify a refocus
|
|
45
45
|
- surface a conservative refocus chooser when recent discussion clearly points to a different workflow
|
|
46
46
|
- start the next workflow round after the previous one is `done`
|
|
47
47
|
|
|
48
|
-
`/cook` expects recent main-chat discussion to describe concrete repo changes. README/CHANGELOG updates still count as concrete repo changes, but assistant-produced summaries and plan/spec/design-doc/proposal-only artifacts do not. `/cook <
|
|
48
|
+
`/cook` expects recent main-chat discussion to describe concrete repo changes. README/CHANGELOG updates still count as concrete repo changes, but assistant-produced summaries and plan/spec/design-doc/proposal-only artifacts do not. `/cook <hint>` acts as a high-priority intent hint that helps proposal derivation interpret the recent discussion, but it still goes through the same fail-closed routing and approval-only Start/Cancel confirmation flow.
|
|
49
49
|
|
|
50
50
|
On startup and next-round flows, if recent discussion is missing, weak, ambiguous, assistant-produced, or only describes planning artifacts instead of concrete repo changes, `/cook` fails closed, leaves canonical `.agent/**` state unchanged, and tells you to clarify the mission in the main chat before rerunning `/cook`.
|
|
51
51
|
|
|
52
52
|
## How `/cook` works
|
|
53
53
|
|
|
54
|
-
|
|
54
|
+
`/cook` supports both bare discussion-driven startup and optional inline intent hints.
|
|
55
55
|
|
|
56
56
|
| Repo state | `/cook` behavior |
|
|
57
57
|
|---|---|
|
|
58
|
-
| No workflow yet | Summarizes recent main-chat discussion into a startup proposal, weighting the latest clear implementation intent ahead of older background discussion,
|
|
59
|
-
| Active workflow exists | Reads the current mission plus recent non-command main-chat discussion. Matching or unclear discussion resumes from canonical `.agent/**` state. Clear replacement discussion about different concrete repo changes opens a chooser first, then only rewrites canonical state after the follow-on **Start** confirmation. If recent discussion implies more than one plausible replacement mission, `/cook` keeps the current workflow parked behind a multi-candidate chooser instead of silently resuming or guessing. Assistant/summary artifacts or plan/spec/design-doc/proposal-only context do not refocus the workflow.
|
|
60
|
-
| Previous workflow is `done` | Starts the next round from recent main-chat discussion, then asks for approval with **Start** or **Cancel**. Weak, ambiguous, assistant-produced, or planning-artifact-only discussion fails closed without rewriting canonical state and tells you to clarify the mission in the main chat before rerunning
|
|
58
|
+
| No workflow yet | Summarizes recent main-chat discussion into a startup proposal, weighting the latest clear implementation intent ahead of older background discussion. Optional `/cook <hint>` text is treated as a high-priority cue for how to interpret that discussion, not as an unconditional mission override. The result still asks for approval with **Start** or **Cancel**. If the discussion is weak, ambiguous, assistant-produced, or only a plan/spec/design-doc/proposal artifact instead of concrete repo changes, `/cook` fails closed without writing `.agent/**` state and tells you to clarify the mission in the main chat before rerunning `/cook`. |
|
|
59
|
+
| Active workflow exists | Reads the current mission plus recent non-command main-chat discussion. Matching or unclear discussion resumes from canonical `.agent/**` state. Clear replacement discussion about different concrete repo changes opens a chooser first, then only rewrites canonical state after the follow-on **Start** confirmation. If recent discussion implies more than one plausible replacement mission, `/cook` keeps the current workflow parked behind a multi-candidate chooser instead of silently resuming or guessing. Optional `/cook <hint>` text biases that routing and candidate ranking toward the hinted implementation intent without bypassing the chooser or final confirmation. Assistant/summary artifacts or plan/spec/design-doc/proposal-only context do not refocus the workflow. |
|
|
60
|
+
| Previous workflow is `done` | Starts the next round from recent main-chat discussion, then asks for approval with **Start** or **Cancel**. Optional `/cook <hint>` text biases next-round proposal derivation toward the hinted intent while still preserving fail-closed behavior. Weak, ambiguous, assistant-produced, or planning-artifact-only discussion fails closed without rewriting canonical state and tells you to clarify the mission in the main chat before rerunning `/cook`. Recent discussion that only restates already-completed or already-verified work also fails closed instead of reopening the finished mission. |
|
|
61
61
|
|
|
62
62
|
## Approval-only confirmation and fail-closed behavior
|
|
63
63
|
|
|
@@ -67,7 +67,7 @@ All startup, next-round, and replacement proposals are **approval-only**:
|
|
|
67
67
|
- actions are only **Start** and **Cancel**
|
|
68
68
|
- **Cancel** is side-effect free: discuss changes in the main chat and rerun `/cook`
|
|
69
69
|
|
|
70
|
-
When `/cook` cannot derive a clear startup, next-round, or replacement proposal for concrete repo changes from recent main-chat discussion, it fails closed instead of guessing. That means no canonical `.agent/**` state is created or rewritten until the discussion is clarified in the main chat and you rerun `/cook`. Tracked docs-only work such as README/CHANGELOG updates is still execution-ready, but assistant-produced summaries and plan/spec/design-doc/proposal-only artifacts are not enough to start or refocus a workflow on their own. `/cook <
|
|
70
|
+
When `/cook` cannot derive a clear startup, next-round, or replacement proposal for concrete repo changes from recent main-chat discussion, it fails closed instead of guessing. That means no canonical `.agent/**` state is created or rewritten until the discussion is clarified in the main chat and you rerun `/cook`. Tracked docs-only work such as README/CHANGELOG updates is still execution-ready, but assistant-produced summaries and plan/spec/design-doc/proposal-only artifacts are not enough to start or refocus a workflow on their own. Optional `/cook <hint>` text can bias proposal ranking, but it still fails closed when repo truth or recent discussion does not support a clear executable mission.
|
|
71
71
|
|
|
72
72
|
When an active workflow already exists and recent discussion suggests a different workflow, `/cook` shows a separate chooser first. The chooser can stay conservative or list multiple candidate replacements when the latest discussion contains more than one plausible implementation goal:
|
|
73
73
|
|
|
@@ -106,7 +106,7 @@ export type CompletionDriverDeps = {
|
|
|
106
106
|
missionAnchor?: string,
|
|
107
107
|
) => string;
|
|
108
108
|
completionResumePrompt: (taskType: string, evaluationProfile: string) => string;
|
|
109
|
-
deriveCookContextProposal: (ctx: DriverContext, projectName: string) => Promise<ContextProposal | undefined>;
|
|
109
|
+
deriveCookContextProposal: (ctx: DriverContext, projectName: string, hintText?: string) => Promise<ContextProposal | undefined>;
|
|
110
110
|
confirmContextProposal: (
|
|
111
111
|
ctx: { hasUI: boolean; ui: any },
|
|
112
112
|
proposal: ContextProposal,
|
|
@@ -325,10 +325,11 @@ async function assessActiveWorkflowProposalRouting(
|
|
|
325
325
|
ctx: DriverContext,
|
|
326
326
|
snapshot: CompletionStateSnapshot,
|
|
327
327
|
deps: CompletionDriverDeps,
|
|
328
|
+
hintText?: string,
|
|
328
329
|
): Promise<ActiveWorkflowProposalAssessment> {
|
|
329
330
|
const currentMission = currentMissionAnchor(snapshot);
|
|
330
331
|
const projectName = path.basename(snapshot.files.root);
|
|
331
|
-
const proposal = await deps.deriveCookContextProposal(ctx, projectName);
|
|
332
|
+
const proposal = await deps.deriveCookContextProposal(ctx, projectName, hintText);
|
|
332
333
|
if (!proposal) {
|
|
333
334
|
const assessment: ActiveWorkflowProposalAssessment = {
|
|
334
335
|
action: "unclear",
|
|
@@ -533,10 +534,7 @@ export function registerCookCommand(pi: ExtensionAPI, deps: CompletionDriverDeps
|
|
|
533
534
|
pi.registerCommand("cook", {
|
|
534
535
|
description: deps.cookCommandSpec.description,
|
|
535
536
|
handler: async (args, ctx) => {
|
|
536
|
-
|
|
537
|
-
deps.emitCommandText(ctx, deps.bareOnlyGuidance, "info");
|
|
538
|
-
return;
|
|
539
|
-
}
|
|
537
|
+
const explicitHint = args.trim().length > 0 ? args.trim() : undefined;
|
|
540
538
|
let goal: string | undefined;
|
|
541
539
|
const cwd = deps.getCtxCwd(ctx);
|
|
542
540
|
let snapshot = await loadCompletionSnapshot(cwd);
|
|
@@ -548,7 +546,7 @@ export function registerCookCommand(pi: ExtensionAPI, deps: CompletionDriverDeps
|
|
|
548
546
|
if (!snapshot) {
|
|
549
547
|
const root = findRepoRoot(cwd) ?? cwd;
|
|
550
548
|
const projectName = path.basename(root);
|
|
551
|
-
const proposal = await deps.deriveCookContextProposal(ctx, projectName);
|
|
549
|
+
const proposal = await deps.deriveCookContextProposal(ctx, projectName, explicitHint);
|
|
552
550
|
if (!proposal) {
|
|
553
551
|
deps.emitCommandText(ctx, buildCookStructuredDiscussionFailureMessage(deps), "info");
|
|
554
552
|
return;
|
|
@@ -587,7 +585,7 @@ export function registerCookCommand(pi: ExtensionAPI, deps: CompletionDriverDeps
|
|
|
587
585
|
if (!goal) {
|
|
588
586
|
if (workflowDone) {
|
|
589
587
|
const projectName = path.basename(snapshot.files.root);
|
|
590
|
-
const proposal = await deps.deriveCookContextProposal(ctx, projectName);
|
|
588
|
+
const proposal = await deps.deriveCookContextProposal(ctx, projectName, explicitHint);
|
|
591
589
|
if (!proposal) {
|
|
592
590
|
deps.emitCommandText(ctx, buildCookStructuredDiscussionFailureMessage(deps, "The previous completion workflow is already done."), "info");
|
|
593
591
|
return;
|
|
@@ -606,7 +604,7 @@ export function registerCookCommand(pi: ExtensionAPI, deps: CompletionDriverDeps
|
|
|
606
604
|
snapshot = (await loadCompletionSnapshot(snapshot.files.root)) ?? snapshot;
|
|
607
605
|
deps.emitCommandText(ctx, `Started a new completion workflow round from recent discussion: ${decision.missionAnchor}`, "info");
|
|
608
606
|
} else {
|
|
609
|
-
const assessment = await assessActiveWorkflowProposalRouting(ctx, snapshot, deps);
|
|
607
|
+
const assessment = await assessActiveWorkflowProposalRouting(ctx, snapshot, deps, explicitHint);
|
|
610
608
|
if (!assessment.proposal || assessment.action === "continue") {
|
|
611
609
|
await resumeActiveWorkflowFromCanonicalState(pi, ctx, snapshot, deps);
|
|
612
610
|
return;
|
|
@@ -207,7 +207,7 @@ function maybeWriteTestSnapshot(targetPath: string | undefined, content: string)
|
|
|
207
207
|
|
|
208
208
|
const COOK_MAIN_CHAT_RERUN_GUIDANCE = "Discuss changes in the main chat and rerun /cook.";
|
|
209
209
|
const COOK_BARE_ONLY_GUIDANCE =
|
|
210
|
-
"/cook
|
|
210
|
+
"/cook supports optional inline hints as high-priority intent cues, but mission selection still comes from recent discussion, repo truth, and the approval-only confirmation flow.";
|
|
211
211
|
const COOK_STRUCTURED_DISCUSSION_FAILURE_DETAIL =
|
|
212
212
|
"/cook failed closed because recent discussion did not produce a clear execution-ready Mission/Scope/Constraints/Acceptance proposal for concrete repo changes. Clarify the concrete repo changes in the main chat and rerun /cook.";
|
|
213
213
|
|
|
@@ -242,8 +242,19 @@ function hasCompletionRoutingActivation(snapshot: CompletionStateSnapshot | unde
|
|
|
242
242
|
return activatedCompletionRoutingRoots.has(path.resolve(snapshot.files.root));
|
|
243
243
|
}
|
|
244
244
|
|
|
245
|
-
function
|
|
246
|
-
return
|
|
245
|
+
function latestUserOrCustomTurnText(ctx: { sessionManager?: any }): string | undefined {
|
|
246
|
+
return collectRecentDiscussionEntries(ctx as { sessionManager: any }, { isRecord, asString, isStaleContextError }, 1)[0]?.text;
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
function isCompletionDriverPromptTurn(ctx: { sessionManager?: any }): boolean {
|
|
250
|
+
const latest = latestUserOrCustomTurnText(ctx);
|
|
251
|
+
if (!latest) return false;
|
|
252
|
+
if (!/^\/skill:completion-protocol\b/.test(latest)) return false;
|
|
253
|
+
return /(?:Start or continue the completion workflow for this repo\.|Resume the completion workflow from canonical state\.)/.test(latest);
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
function shouldInjectCompletionWorkflowContext(snapshot: CompletionStateSnapshot | undefined, ctx: { sessionManager?: any }): boolean {
|
|
257
|
+
return hasCompletionRoutingActivation(snapshot) && isCompletionDriverPromptTurn(ctx);
|
|
247
258
|
}
|
|
248
259
|
|
|
249
260
|
function buildDoneWorkflowBoundaryReminder(snapshot: CompletionStateSnapshot): string {
|
|
@@ -366,6 +377,7 @@ async function promptContextProposalConfirmationAction(
|
|
|
366
377
|
async function deriveCookContextProposal(
|
|
367
378
|
ctx: { cwd: string; hasUI: boolean; ui: any; sessionManager: any; model?: any; modelRegistry?: any },
|
|
368
379
|
projectName: string,
|
|
380
|
+
hintText?: string,
|
|
369
381
|
): Promise<ContextProposal | undefined> {
|
|
370
382
|
const recentEntries = collectRecentDiscussionEntries(ctx, { isRecord, asString, isStaleContextError });
|
|
371
383
|
const snapshot = await loadCompletionSnapshot(getCtxCwd(ctx));
|
|
@@ -381,9 +393,11 @@ async function deriveCookContextProposal(
|
|
|
381
393
|
`verification summary: ${asString(snapshot.verificationEvidence?.summary) ?? "(none)"}`,
|
|
382
394
|
]
|
|
383
395
|
: [];
|
|
396
|
+
if (hintText) workflowContextLines.push(`cook hint: ${hintText}`);
|
|
384
397
|
return await deriveCookContextProposalFromRecentDiscussion(projectName, recentEntries, {
|
|
385
398
|
asString,
|
|
386
399
|
asStringArray,
|
|
400
|
+
hintText,
|
|
387
401
|
workflowContext: snapshot
|
|
388
402
|
? {
|
|
389
403
|
currentMissionAnchor:
|
|
@@ -397,12 +411,15 @@ async function deriveCookContextProposal(
|
|
|
397
411
|
continuationPolicy: asString(snapshot.state?.continuation_policy),
|
|
398
412
|
}
|
|
399
413
|
: undefined,
|
|
400
|
-
analyzeContextProposal: async (entries) =>
|
|
414
|
+
analyzeContextProposal: async (entries, derivedHintText) =>
|
|
401
415
|
await analyzeContextProposalWithAgent({
|
|
402
416
|
ctx,
|
|
403
417
|
projectName,
|
|
404
418
|
recentEntries: entries,
|
|
405
|
-
workflowContextLines
|
|
419
|
+
workflowContextLines:
|
|
420
|
+
derivedHintText && !workflowContextLines.includes(`cook hint: ${derivedHintText}`)
|
|
421
|
+
? [...workflowContextLines, `cook hint: ${derivedHintText}`]
|
|
422
|
+
: workflowContextLines,
|
|
406
423
|
liveRoleActivityByRoot,
|
|
407
424
|
completionStatusKey: COMPLETION_STATUS_KEY,
|
|
408
425
|
safeUiCall,
|
|
@@ -905,7 +922,7 @@ export default function completionExtension(pi: ExtensionAPI) {
|
|
|
905
922
|
structuredDiscussionFailureDetail: COOK_STRUCTURED_DISCUSSION_FAILURE_DETAIL,
|
|
906
923
|
mainChatRerunGuidance: COOK_MAIN_CHAT_RERUN_GUIDANCE,
|
|
907
924
|
cookCommandSpec: {
|
|
908
|
-
description: "
|
|
925
|
+
description: "/cook workflow: start, continue, refocus, or start the next round (optional hint supported)",
|
|
909
926
|
},
|
|
910
927
|
buildContextProposalContinuationReason,
|
|
911
928
|
completionKickoff,
|
|
@@ -939,7 +956,7 @@ export default function completionExtension(pi: ExtensionAPI) {
|
|
|
939
956
|
await refreshCompletionStatus({ ctx, ...statusSurfaceArgs });
|
|
940
957
|
if (shouldTestAutoContinueOnSessionStart()) {
|
|
941
958
|
const snapshot = await loadCompletionSnapshot(getCtxCwd(ctx));
|
|
942
|
-
if (hasCompletionRoutingActivation(snapshot)) {
|
|
959
|
+
if (hasCompletionRoutingActivation(snapshot) && isCompletionDriverPromptTurn(ctx)) {
|
|
943
960
|
await autoContinueWorkflowIfNeeded(pi, ctx, driverDeps);
|
|
944
961
|
}
|
|
945
962
|
}
|
|
@@ -955,19 +972,20 @@ export default function completionExtension(pi: ExtensionAPI) {
|
|
|
955
972
|
await fsp.rm(snapshot.files.compactionMarkerPath, { force: true });
|
|
956
973
|
}
|
|
957
974
|
await refreshCompletionStatus({ ctx, ...statusSurfaceArgs });
|
|
958
|
-
if (hasCompletionRoutingActivation(snapshot)) {
|
|
975
|
+
if (hasCompletionRoutingActivation(snapshot) && isCompletionDriverPromptTurn(ctx)) {
|
|
959
976
|
await autoContinueWorkflowIfNeeded(pi, ctx, driverDeps);
|
|
960
977
|
}
|
|
961
978
|
});
|
|
962
979
|
|
|
963
980
|
pi.on("before_agent_start", async (_event, ctx) => {
|
|
964
981
|
const loaded = await loadCompletionDataForReminder(getCtxCwd(ctx));
|
|
965
|
-
|
|
982
|
+
const driverPromptTurn = isCompletionDriverPromptTurn(ctx);
|
|
983
|
+
if (loaded && driverPromptTurn) {
|
|
966
984
|
const rootKey = completionRootKey(loaded.snapshot, getCtxCwd(ctx));
|
|
967
985
|
const fingerprint = completionContinuationFingerprint(loaded.snapshot);
|
|
968
986
|
if (fingerprint) markQueuedDriverPromptInFlight(rootKey, fingerprint);
|
|
969
987
|
}
|
|
970
|
-
if (!loaded || !shouldInjectCompletionWorkflowContext(loaded.snapshot)) return;
|
|
988
|
+
if (!loaded || !shouldInjectCompletionWorkflowContext(loaded.snapshot, ctx)) return;
|
|
971
989
|
const additions = isWorkflowDone(loaded.snapshot)
|
|
972
990
|
? [buildDoneWorkflowBoundaryReminder(loaded.snapshot)]
|
|
973
991
|
: [composeSystemReminder(loaded.snapshot, loaded.sliceHistory, loaded.stopHistory)];
|
|
@@ -201,6 +201,7 @@ export function buildContextProposalAnalystPrompt(projectName: string, discussio
|
|
|
201
201
|
"Infer the current implementation mission from the discussion.",
|
|
202
202
|
"Prefer the latest clear user implementation intent over older background context.",
|
|
203
203
|
"Treat stale, completed, or explicitly negated topics as context to ignore unless the latest discussion clearly reopens them.",
|
|
204
|
+
"If canonical workflow context includes a /cook hint, use it as a high-priority cue for how to interpret the recent discussion without treating it as an unconditional override.",
|
|
204
205
|
];
|
|
205
206
|
if (contextLines.length > 0) lines.push("", "Canonical workflow context:", ...contextLines);
|
|
206
207
|
lines.push("", "Recent discussion:", discussion || "(none)");
|
|
@@ -342,6 +342,11 @@ export function serializeRecentDiscussionEntries(entries: RecentDiscussionEntry[
|
|
|
342
342
|
.join("\n\n");
|
|
343
343
|
}
|
|
344
344
|
|
|
345
|
+
function contextHintEntry(hintText: string | undefined): RecentDiscussionEntry[] {
|
|
346
|
+
const normalized = normalizeProposalLine(hintText ?? "");
|
|
347
|
+
return normalized ? [{ role: "user", text: `Hint: ${normalized}` }] : [];
|
|
348
|
+
}
|
|
349
|
+
|
|
345
350
|
const RECENT_DISCUSSION_IMPLEMENTATION_INTENT_REGEX =
|
|
346
351
|
/(?:\b(?:fix|update|add|remove|restore|refactor|ship|support|wire|route|rewrite|replace|preserve|filter|separate|refresh|reroute|suppress|align|convert|reconcile|repair|correct|implement|build|land|block|allow|keep|edit|document|write)\b|(?:修正|修復|修复|更新|新增|移除|恢復|恢复|重構|重构|調整|调整|過濾|过滤|分離|分离|刷新|替換|替换|抑制|對齊|对齐|實作|实现|落地|修補|修补|阻止|允許|允许|轉換|转换|保留|保持))/iu;
|
|
347
352
|
|
|
@@ -716,13 +721,63 @@ function missionTextOverlapsTopic(mission: string, topic: string): boolean {
|
|
|
716
721
|
return overlap.length >= Math.min(2, Math.min(missionTokens.length, topicTokens.length));
|
|
717
722
|
}
|
|
718
723
|
|
|
719
|
-
function proposalOverlapsTopic(proposal: ContextProposal, topic: string): boolean {
|
|
724
|
+
function proposalOverlapsTopic(proposal: ContextProposal | ContextProposalAlternate, topic: string): boolean {
|
|
720
725
|
if (!topic.trim()) return false;
|
|
721
726
|
if (missionTextOverlapsTopic(proposal.mission, topic)) return true;
|
|
722
727
|
const bodyTexts = [proposal.basisPreview, ...proposal.scope, ...proposal.constraints, ...proposal.acceptance].filter(Boolean);
|
|
723
728
|
return bodyTexts.some((text) => missionTextOverlapsTopic(text, topic) || missionTextOverlapsTopic(topic, text));
|
|
724
729
|
}
|
|
725
730
|
|
|
731
|
+
function hintOverlapScore(text: string, hintText: string): number {
|
|
732
|
+
const normalizedText = normalizeMissionAnchorText(text).toLowerCase();
|
|
733
|
+
const normalizedHint = normalizeMissionAnchorText(hintText).toLowerCase();
|
|
734
|
+
if (!normalizedText || !normalizedHint) return 0;
|
|
735
|
+
if (normalizedText === normalizedHint) return 10;
|
|
736
|
+
if (normalizedText.includes(normalizedHint) || normalizedHint.includes(normalizedText)) return 6;
|
|
737
|
+
const textTokens = missionAnchorSemanticTokens(normalizedText);
|
|
738
|
+
const hintTokens = missionAnchorSemanticTokens(normalizedHint);
|
|
739
|
+
if (textTokens.length === 0 || hintTokens.length === 0) return 0;
|
|
740
|
+
const hintSet = new Set(hintTokens);
|
|
741
|
+
const overlap = textTokens.filter((token) => hintSet.has(token));
|
|
742
|
+
if (overlap.length === 0) return 0;
|
|
743
|
+
return overlap.length / Math.max(textTokens.length, hintTokens.length);
|
|
744
|
+
}
|
|
745
|
+
|
|
746
|
+
function proposalHintScore(proposal: ContextProposal | ContextProposalAlternate, hintText: string): number {
|
|
747
|
+
return (
|
|
748
|
+
hintOverlapScore(proposal.mission, hintText) * 4 +
|
|
749
|
+
proposal.scope.reduce((sum, item) => sum + hintOverlapScore(item, hintText) * 2, 0) +
|
|
750
|
+
proposal.constraints.reduce((sum, item) => sum + hintOverlapScore(item, hintText), 0) +
|
|
751
|
+
proposal.acceptance.reduce((sum, item) => sum + hintOverlapScore(item, hintText), 0)
|
|
752
|
+
);
|
|
753
|
+
}
|
|
754
|
+
|
|
755
|
+
function selectHintPreferredProposal(proposal: ContextProposal | undefined, hintText: string | undefined): ContextProposal | undefined {
|
|
756
|
+
if (!proposal || !hintText) return proposal;
|
|
757
|
+
const candidates = [proposal, ...(proposal.alternateProposals ?? [])].filter((candidate, index, list) =>
|
|
758
|
+
list.findIndex((other) => missionAnchorsStrictlyEquivalent(other.mission, candidate.mission)) === index,
|
|
759
|
+
);
|
|
760
|
+
if (candidates.length <= 1) return proposal;
|
|
761
|
+
const scored = candidates.map((candidate, index) => ({ candidate, index, score: proposalHintScore(candidate, hintText) }));
|
|
762
|
+
const best = scored.reduce((current, item) => (item.score > current.score ? item : current), scored[0]);
|
|
763
|
+
if (best.score <= 0 || best.index === 0) return proposal;
|
|
764
|
+
const selected = best.candidate;
|
|
765
|
+
const alternates = candidates
|
|
766
|
+
.filter((_, index) => index !== best.index)
|
|
767
|
+
.map((candidate) => ({ ...candidate, analysis: finalizeContextProposalAnalysis(candidate.analysis, [candidate.goalText, candidate.mission]) }));
|
|
768
|
+
return {
|
|
769
|
+
...selected,
|
|
770
|
+
alternateProposals: alternates,
|
|
771
|
+
analysis: finalizeContextProposalAnalysis(
|
|
772
|
+
{
|
|
773
|
+
...selected.analysis,
|
|
774
|
+
alternateMissions: alternates.map((candidate) => candidate.mission),
|
|
775
|
+
},
|
|
776
|
+
[selected.goalText, selected.mission, hintText, ...alternates.map((candidate) => candidate.mission)],
|
|
777
|
+
),
|
|
778
|
+
};
|
|
779
|
+
}
|
|
780
|
+
|
|
726
781
|
function extractSuppressedNegatedTopics(proposal: ContextProposal): string[] {
|
|
727
782
|
return uniqueProposalItems(
|
|
728
783
|
proposal.constraints.filter((item) => looksLikeConstraint(item) && CONTEXT_PROPOSAL_IMPLEMENTATION_SOURCE_REGEX.test(normalizeProposalLine(item))),
|
|
@@ -1186,20 +1241,25 @@ export async function deriveCookContextProposalFromRecentDiscussion(
|
|
|
1186
1241
|
projectName: string,
|
|
1187
1242
|
recentEntries: RecentDiscussionEntry[],
|
|
1188
1243
|
deps: ProposalParseDeps & {
|
|
1189
|
-
analyzeContextProposal?: (recentEntries: RecentDiscussionEntry[]) => Promise<ContextProposal | undefined>;
|
|
1244
|
+
analyzeContextProposal?: (recentEntries: RecentDiscussionEntry[], hintText?: string) => Promise<ContextProposal | undefined>;
|
|
1190
1245
|
workflowContext?: ContextProposalWorkflowContext;
|
|
1246
|
+
hintText?: string;
|
|
1191
1247
|
},
|
|
1192
1248
|
): Promise<ContextProposal | undefined> {
|
|
1193
|
-
|
|
1194
|
-
|
|
1195
|
-
|
|
1249
|
+
const effectiveEntries = [...contextHintEntry(deps.hintText), ...recentEntries];
|
|
1250
|
+
if (effectiveEntries.length === 0) return undefined;
|
|
1251
|
+
for (const candidateEntries of recentDiscussionWindows(effectiveEntries, deps.stripCodeBlocks)) {
|
|
1252
|
+
const analyzed = selectHintPreferredProposal(
|
|
1253
|
+
applyWorkflowContextToProposal(await deps.analyzeContextProposal?.(candidateEntries, deps.hintText), deps.workflowContext, deps) ?? undefined,
|
|
1254
|
+
deps.hintText,
|
|
1255
|
+
);
|
|
1196
1256
|
if (analyzed) return analyzed;
|
|
1197
1257
|
const structured = applyWorkflowContextToProposal(
|
|
1198
1258
|
extractContextProposalFromStructuredSession(candidateEntries, projectName, deps),
|
|
1199
1259
|
deps.workflowContext,
|
|
1200
1260
|
deps,
|
|
1201
1261
|
);
|
|
1202
|
-
if (structured) return structured;
|
|
1262
|
+
if (structured) return selectHintPreferredProposal(structured, deps.hintText);
|
|
1203
1263
|
}
|
|
1204
1264
|
return undefined;
|
|
1205
1265
|
}
|
|
@@ -79,6 +79,7 @@ const CONTEXT_PROPOSAL_ANALYST_SYSTEM_PROMPT = [
|
|
|
79
79
|
"You may additionally include optional keys alternate_missions, completed_topics, and negated_topics when they are clearly supported by the discussion and canonical workflow context.",
|
|
80
80
|
"mission must be a concise implementation mission anchor sentence.",
|
|
81
81
|
"Prefer the latest clear user implementation intent over older background context when they differ.",
|
|
82
|
+
"If canonical workflow context includes a /cook hint, treat it as a high-priority disambiguation signal, but do not let it bypass clear contradictory repo truth or approval-only confirmation.",
|
|
82
83
|
"Do not reopen work that the canonical workflow context says is done, completed, historical, or already covered unless the latest discussion clearly asks to revisit it.",
|
|
83
84
|
"Treat stale, weakly related, or explicitly negated topics as noise instead of mission scope.",
|
|
84
85
|
"scope must contain only work items that directly support the mission.",
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@linimin/pi-letscook",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.49",
|
|
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,
|
|
@@ -1101,8 +1101,8 @@ assert plan['plan_basis'] == 'user_refocus', 'plan_basis should reset to user_re
|
|
|
1101
1101
|
assert active['status'] == 'idle', 'active-slice should reset to idle for the next workflow round'
|
|
1102
1102
|
PY
|
|
1103
1103
|
|
|
1104
|
-
# Active workflow: /cook <
|
|
1105
|
-
# and
|
|
1104
|
+
# Active workflow: /cook <hint> should bias active-workflow proposal derivation,
|
|
1105
|
+
# still route through the chooser, and leave canonical state unchanged when the user cancels.
|
|
1106
1106
|
ACTIVE_INLINE_REJECTION_ROUTING="$TMPDIR/context-proposal-active-inline-arg-routing.json"
|
|
1107
1107
|
ACTIVE_INLINE_REJECTION_PROPOSAL="$TMPDIR/context-proposal-active-inline-arg-proposal.json"
|
|
1108
1108
|
ACTIVE_INLINE_REJECTION_CHOOSER="$TMPDIR/context-proposal-active-inline-arg-chooser.json"
|
|
@@ -1138,9 +1138,9 @@ import sys
|
|
|
1138
1138
|
from pathlib import Path
|
|
1139
1139
|
|
|
1140
1140
|
output = Path(sys.argv[1]).read_text() + Path(sys.argv[2]).read_text()
|
|
1141
|
-
routing = Path(sys.argv[3])
|
|
1141
|
+
routing = json.loads(Path(sys.argv[3]).read_text())
|
|
1142
1142
|
proposal = Path(sys.argv[4])
|
|
1143
|
-
chooser = Path(sys.argv[5])
|
|
1143
|
+
chooser = json.loads(Path(sys.argv[5]).read_text())
|
|
1144
1144
|
before = json.loads(Path(sys.argv[6]).read_text())
|
|
1145
1145
|
tracked = [
|
|
1146
1146
|
Path('.agent/mission.md'),
|
|
@@ -1151,16 +1151,17 @@ tracked = [
|
|
|
1151
1151
|
Path('.agent/verification-evidence.json'),
|
|
1152
1152
|
]
|
|
1153
1153
|
|
|
1154
|
-
assert '
|
|
1155
|
-
assert
|
|
1156
|
-
assert
|
|
1157
|
-
assert
|
|
1154
|
+
assert routing['action'] == 'refocus', 'active /cook <hint> should run active-workflow routing'
|
|
1155
|
+
assert routing['proposedMissionAnchor'] == 'Replacement mission for the active workflow.', 'active /cook <hint> should bias toward the hinted replacement mission'
|
|
1156
|
+
assert json.loads(proposal.read_text())['mission'] == 'Replacement mission for the active workflow.', 'active /cook <hint> should carry the hinted mission into the final replacement proposal'
|
|
1157
|
+
assert chooser['choices'][1].startswith('Start new workflow from recent discussion'), 'active /cook <hint> should open the existing-workflow chooser'
|
|
1158
|
+
assert 'Cancelled replacement workflow proposal.' in output or 'Cancelled existing workflow confirmation.' in output, 'active /cook <hint> cancel should report cancellation'
|
|
1158
1159
|
after = {path.name: path.read_text() for path in tracked}
|
|
1159
|
-
assert before == after, 'active /cook <
|
|
1160
|
+
assert before == after, 'active /cook <hint> cancel should leave canonical files unchanged'
|
|
1160
1161
|
PY
|
|
1161
1162
|
|
|
1162
|
-
# Completed workflow: /cook <
|
|
1163
|
-
#
|
|
1163
|
+
# Completed workflow: /cook <hint> should bias next-round proposal derivation and still leave canonical state
|
|
1164
|
+
# unchanged when the user cancels the approval-only proposal.
|
|
1164
1165
|
mark_done
|
|
1165
1166
|
|
|
1166
1167
|
DONE_INLINE_REJECTION_ROUTING="$TMPDIR/context-proposal-done-inline-arg-routing.json"
|
|
@@ -1198,7 +1199,7 @@ from pathlib import Path
|
|
|
1198
1199
|
|
|
1199
1200
|
output = Path(sys.argv[1]).read_text() + Path(sys.argv[2]).read_text()
|
|
1200
1201
|
routing = Path(sys.argv[3])
|
|
1201
|
-
proposal = Path(sys.argv[4])
|
|
1202
|
+
proposal = json.loads(Path(sys.argv[4]).read_text())
|
|
1202
1203
|
chooser = Path(sys.argv[5])
|
|
1203
1204
|
before = json.loads(Path(sys.argv[6]).read_text())
|
|
1204
1205
|
tracked = [
|
|
@@ -1210,14 +1211,14 @@ tracked = [
|
|
|
1210
1211
|
Path('.agent/verification-evidence.json'),
|
|
1211
1212
|
]
|
|
1212
1213
|
state_before = json.loads(before['state.json'])
|
|
1213
|
-
assert state_before['current_phase'] == 'done', 'done /cook <
|
|
1214
|
-
assert state_before['project_done'] is True, 'done /cook <
|
|
1215
|
-
assert
|
|
1216
|
-
assert
|
|
1217
|
-
assert not
|
|
1218
|
-
assert
|
|
1214
|
+
assert state_before['current_phase'] == 'done', 'done /cook <hint> should start from a completed workflow'
|
|
1215
|
+
assert state_before['project_done'] is True, 'done /cook <hint> should start from project_done=true'
|
|
1216
|
+
assert not routing.exists(), 'done /cook <hint> should not run active-workflow routing while starting the next round'
|
|
1217
|
+
assert proposal['mission'] == 'Update README guidance for the next workflow round.', 'done /cook <hint> should bias next-round mission derivation toward the hint'
|
|
1218
|
+
assert not chooser.exists(), 'done /cook <hint> should not open the existing-workflow chooser when starting the next round'
|
|
1219
|
+
assert 'Cancelled next workflow round proposal.' in output, 'done /cook <hint> cancel should report next-round proposal cancellation'
|
|
1219
1220
|
after = {path.name: path.read_text() for path in tracked}
|
|
1220
|
-
assert before == after, 'done /cook <
|
|
1221
|
+
assert before == after, 'done /cook <hint> cancel should leave canonical files unchanged'
|
|
1221
1222
|
PY
|
|
1222
1223
|
|
|
1223
1224
|
# Completed workflow again: /cook with no goal should be able to use model-assisted
|
package/scripts/refocus-test.sh
CHANGED
|
@@ -117,9 +117,9 @@ import sys
|
|
|
117
117
|
from pathlib import Path
|
|
118
118
|
|
|
119
119
|
output = Path(sys.argv[1]).read_text() + Path(sys.argv[2]).read_text()
|
|
120
|
-
routing = Path(sys.argv[3])
|
|
120
|
+
routing = json.loads(Path(sys.argv[3]).read_text())
|
|
121
121
|
proposal = Path(sys.argv[4])
|
|
122
|
-
chooser = Path(sys.argv[5])
|
|
122
|
+
chooser = json.loads(Path(sys.argv[5]).read_text())
|
|
123
123
|
initial_mission = sys.argv[6]
|
|
124
124
|
before = json.loads(Path(sys.argv[7]).read_text())
|
|
125
125
|
tracked = [
|
|
@@ -131,13 +131,14 @@ tracked = [
|
|
|
131
131
|
Path('.agent/verification-evidence.json'),
|
|
132
132
|
]
|
|
133
133
|
current_state = json.loads(before['state.json'])
|
|
134
|
-
assert current_state['mission_anchor'] == initial_mission, 'active /cook <
|
|
135
|
-
assert '
|
|
136
|
-
assert
|
|
137
|
-
assert not proposal.exists(), 'active /cook <
|
|
138
|
-
assert
|
|
134
|
+
assert current_state['mission_anchor'] == initial_mission, 'active /cook <hint> should start from the current mission anchor'
|
|
135
|
+
assert routing['action'] == 'refocus', 'active /cook <hint> should route through active-workflow replacement assessment'
|
|
136
|
+
assert routing['proposedMissionAnchor'] == 'Replacement mission that should stay in the main chat.', 'active /cook <hint> should preserve the hinted replacement mission'
|
|
137
|
+
assert not proposal.exists(), 'active /cook <hint> chooser cancel should not open final proposal confirmation'
|
|
138
|
+
assert chooser['choices'][1].startswith('Start new workflow from recent discussion'), 'active /cook <hint> should open the existing-workflow chooser'
|
|
139
|
+
assert 'Cancelled existing workflow confirmation.' in output, 'active /cook <hint> chooser cancel should report cancellation'
|
|
139
140
|
after = {path.name: path.read_text() for path in tracked}
|
|
140
|
-
assert before == after, 'active /cook <
|
|
141
|
+
assert before == after, 'active /cook <hint> chooser cancel should leave canonical files unchanged'
|
|
141
142
|
PY
|
|
142
143
|
|
|
143
144
|
SESSION_INITIAL_REFOCUS="$TMPDIR/session-initial-bare-refocus.jsonl"
|
package/scripts/release-check.sh
CHANGED
|
@@ -14,9 +14,9 @@ from pathlib import Path
|
|
|
14
14
|
|
|
15
15
|
checks = {
|
|
16
16
|
"README.md": [
|
|
17
|
-
"
|
|
18
|
-
"`/cook <
|
|
19
|
-
"clarify the mission in the main chat before rerunning
|
|
17
|
+
"`/cook` supports both bare discussion-driven startup and optional inline intent hints.",
|
|
18
|
+
"`/cook <hint>` acts as a high-priority intent hint that helps proposal derivation interpret the recent discussion",
|
|
19
|
+
"clarify the mission in the main chat before rerunning `/cook`",
|
|
20
20
|
"Matching or unclear discussion resumes from canonical `.agent/**` state.",
|
|
21
21
|
"approval-only Start/Cancel gate",
|
|
22
22
|
"Start new workflow from recent discussion",
|
|
@@ -24,26 +24,25 @@ checks = {
|
|
|
24
24
|
"README/CHANGELOG updates still count as concrete repo changes",
|
|
25
25
|
"assistant-produced summaries and plan/spec/design-doc/proposal-only artifacts do not",
|
|
26
26
|
"Assistant/summary artifacts or plan/spec/design-doc/proposal-only context do not refocus the workflow.",
|
|
27
|
-
"`/cook <
|
|
27
|
+
"Optional `/cook <hint>` text biases that routing and candidate ranking toward the hinted implementation intent",
|
|
28
28
|
],
|
|
29
29
|
"CHANGELOG.md": [
|
|
30
|
+
"restored optional `/cook <hint>` support as a soft intent hint that biases context analysis, proposal ranking, active-workflow disambiguation, and next-round startup without bypassing fail-closed routing or the approval-only Start/Cancel gate",
|
|
30
31
|
"removed inline `/cook <text>` argument support so bare `/cook` is now the only supported workflow entrypoint",
|
|
31
|
-
"packaged release parity fail closed when command arguments are passed instead of discussion driving proposal derivation",
|
|
32
32
|
"historically allowed `/cook <hint>` as an analyst-only high-priority prompt",
|
|
33
|
-
"that inline-argument path has since been removed so bare `/cook` is now the only supported entrypoint",
|
|
34
33
|
],
|
|
35
34
|
"extensions/completion/index.ts": [
|
|
36
|
-
'description: "
|
|
35
|
+
'description: "/cook workflow: start, continue, refocus, or start the next round (optional hint supported)"',
|
|
37
36
|
'const COOK_BARE_ONLY_GUIDANCE =',
|
|
38
|
-
'"/cook
|
|
37
|
+
'"/cook supports optional inline hints as high-priority intent cues, but mission selection still comes from recent discussion, repo truth, and the approval-only confirmation flow."',
|
|
39
38
|
'"/cook failed closed because recent discussion did not produce a clear execution-ready Mission/Scope/Constraints/Acceptance proposal for concrete repo changes. Clarify the concrete repo changes in the main chat and rerun /cook."',
|
|
40
39
|
],
|
|
41
40
|
}
|
|
42
41
|
|
|
43
42
|
forbidden = {
|
|
44
|
-
"README.md": ["compatibility" + " shim", "
|
|
43
|
+
"README.md": ["compatibility" + " shim", "optional inline /cook hint"],
|
|
45
44
|
"CHANGELOG.md": ["compatibility" + " shim"],
|
|
46
|
-
"extensions/completion/index.ts": ["temporary" + " compatibility" + " shim, pass /cook", "
|
|
45
|
+
"extensions/completion/index.ts": ["temporary" + " compatibility" + " shim, pass /cook", "optional inline /cook hint"],
|
|
47
46
|
}
|
|
48
47
|
|
|
49
48
|
for path, needles in checks.items():
|
package/scripts/smoke-test.sh
CHANGED
|
@@ -76,19 +76,20 @@ pi -e "$PKG_ROOT" -p "/cook smoke-test mission" \
|
|
|
76
76
|
>"$TMPDIR/pi-completion-smoke-inline-arg.out" 2>"$TMPDIR/pi-completion-smoke-inline-arg.err"
|
|
77
77
|
|
|
78
78
|
python3 - "$TMPDIR/pi-completion-smoke-inline-arg.out" "$TMPDIR/pi-completion-smoke-inline-arg.err" "$INLINE_REJECTION_ROUTING_SNAPSHOT" "$INLINE_REJECTION_PROPOSAL_SNAPSHOT" "$INLINE_REJECTION_CHOOSER_SNAPSHOT" <<'PY'
|
|
79
|
+
import json
|
|
79
80
|
import sys
|
|
80
81
|
from pathlib import Path
|
|
81
82
|
|
|
82
83
|
output = Path(sys.argv[1]).read_text() + Path(sys.argv[2]).read_text()
|
|
83
84
|
routing = Path(sys.argv[3])
|
|
84
|
-
proposal = Path(sys.argv[4])
|
|
85
|
+
proposal = json.loads(Path(sys.argv[4]).read_text())
|
|
85
86
|
chooser = Path(sys.argv[5])
|
|
86
87
|
|
|
87
|
-
assert not Path('.agent').exists(), 'startup /cook <
|
|
88
|
-
assert not routing.exists(), 'startup /cook <
|
|
89
|
-
assert
|
|
90
|
-
assert not chooser.exists(), 'startup /cook <
|
|
91
|
-
assert '
|
|
88
|
+
assert not Path('.agent').exists(), 'startup /cook <hint> cancel should leave canonical state untouched'
|
|
89
|
+
assert not routing.exists(), 'startup /cook <hint> should not open active-workflow routing before a workflow exists'
|
|
90
|
+
assert proposal['mission'] == 'Smoke-test inline hint startup mission.', 'startup /cook <hint> should bias proposal derivation toward the hinted mission'
|
|
91
|
+
assert not chooser.exists(), 'startup /cook <hint> should not open the existing-workflow chooser before a workflow exists'
|
|
92
|
+
assert 'Cancelled recent-discussion workflow proposal.' in output, 'startup /cook <hint> cancel should report proposal cancellation'
|
|
92
93
|
PY
|
|
93
94
|
|
|
94
95
|
write_session "$BOOTSTRAP_SESSION" "$ROOT" "$BOOTSTRAP_DISCUSSION"
|