@quantmind-br/pi-goal-x 0.0.0-stage → 0.35.1
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/LICENSE +21 -0
- package/README.md +131 -2
- package/assets/badge-dark.svg +18 -0
- package/assets/badge-light.svg +18 -0
- package/docs/advanced-usage.md +106 -0
- package/docs/agent-flow-design.md +306 -0
- package/docs/agentic-runtime-prd.md +773 -0
- package/docs/architecture.md +368 -0
- package/docs/controls.md +140 -0
- package/docs/pi-autoresearch-survey.md +50 -0
- package/docs/pi-goal-x-unified-dashboard-plan.md +1667 -0
- package/docs/prompt-caching.md +31 -0
- package/docs/unified-dashboard.md +286 -0
- package/extensions/auditor-selector.ts +74 -0
- package/extensions/goal-accounting.ts +107 -0
- package/extensions/goal-activity.ts +202 -0
- package/extensions/goal-auditor.ts +480 -0
- package/extensions/goal-commands.ts +866 -0
- package/extensions/goal-compaction.ts +197 -0
- package/extensions/goal-completion.ts +173 -0
- package/extensions/goal-context.ts +12 -0
- package/extensions/goal-contract.ts +58 -0
- package/extensions/goal-controls.ts +202 -0
- package/extensions/goal-core-tools.ts +549 -0
- package/extensions/goal-core.ts +78 -0
- package/extensions/goal-detail.ts +65 -0
- package/extensions/goal-dialog-controls.ts +31 -0
- package/extensions/goal-draft.ts +244 -0
- package/extensions/goal-drafting.ts +535 -0
- package/extensions/goal-events.ts +692 -0
- package/extensions/goal-format.ts +376 -0
- package/extensions/goal-interruption.ts +53 -0
- package/extensions/goal-ledger-index.ts +68 -0
- package/extensions/goal-ledger.ts +1007 -0
- package/extensions/goal-live-retention.ts +239 -0
- package/extensions/goal-oracle.ts +346 -0
- package/extensions/goal-policy.ts +370 -0
- package/extensions/goal-pool.ts +103 -0
- package/extensions/goal-prompt-cache.ts +80 -0
- package/extensions/goal-questionnaire.ts +1073 -0
- package/extensions/goal-record.ts +388 -0
- package/extensions/goal-recovery.ts +256 -0
- package/extensions/goal-runtime.ts +277 -0
- package/extensions/goal-scheduler-state.ts +100 -0
- package/extensions/goal-scheduler.ts +436 -0
- package/extensions/goal-service.ts +843 -0
- package/extensions/goal-session-health.ts +130 -0
- package/extensions/goal-session-safety.ts +65 -0
- package/extensions/goal-settings.ts +1370 -0
- package/extensions/goal-state.ts +1069 -0
- package/extensions/goal-status.ts +305 -0
- package/extensions/goal-task-confirmation.ts +174 -0
- package/extensions/goal-task-count.ts +59 -0
- package/extensions/goal-task-derive.ts +46 -0
- package/extensions/goal-task-index.ts +57 -0
- package/extensions/goal-task-tools.ts +595 -0
- package/extensions/goal-tool-inputs.ts +102 -0
- package/extensions/goal-tool-names.ts +73 -0
- package/extensions/goal-tools.ts +16 -0
- package/extensions/goal-widget.ts +455 -0
- package/extensions/goal.ts +42 -0
- package/extensions/network-error-backoff.ts +55 -0
- package/extensions/prompts/goal-prompts.ts +330 -0
- package/extensions/storage/atomic-rename.ts +52 -0
- package/extensions/storage/goal-files.ts +695 -0
- package/extensions/storage/goal-lock.ts +131 -0
- package/extensions/storage/goal-root.ts +95 -0
- package/extensions/widgets/auditor-dashboard-model.ts +174 -0
- package/extensions/widgets/dialog-scaffold.ts +26 -0
- package/extensions/widgets/goal-controls-dialog.ts +329 -0
- package/extensions/widgets/goal-dashboard-model.ts +530 -0
- package/extensions/widgets/goal-dashboard-renderer.ts +674 -0
- package/extensions/widgets/goal-escape-dialog.ts +148 -0
- package/extensions/widgets/goal-notifications.ts +9 -0
- package/extensions/widgets/goal-widget.ts +723 -0
- package/extensions/widgets/task-list-overlay.ts +409 -0
- package/extensions/widgets/text-cache.ts +30 -0
- package/package.json +117 -4
- package/scripts/recover-session-checkpoints.mjs +289 -0
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
import type {GoalContinuation} from "./goal-scheduler-state.ts";
|
|
2
|
+
|
|
3
|
+
export interface TaskProgressInput {
|
|
4
|
+
task_id: string;
|
|
5
|
+
status: "start" | "complete" | "skipped" | "pending";
|
|
6
|
+
evidence?: string;
|
|
7
|
+
reason?: string;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
type Result = {ok: true; updates: TaskProgressInput[]; batch: boolean} | {ok: false; message: string};
|
|
11
|
+
const fields = ["task_id", "status", "evidence", "reason"] as const;
|
|
12
|
+
const statuses = new Set(["start", "complete", "skipped", "pending"]);
|
|
13
|
+
const object = (value: unknown): value is Record<string, unknown> => !!value && typeof value === "object" && !Array.isArray(value);
|
|
14
|
+
|
|
15
|
+
/** Normalize transport noise without discarding a meaningful task instruction. */
|
|
16
|
+
export function normalizeTaskProgress(input: unknown): Result {
|
|
17
|
+
const fail = (message: string): Result => ({ok: false, message});
|
|
18
|
+
if (!object(input)) return fail("Provide task_id and status, or an updates batch.");
|
|
19
|
+
const normalize = (raw: Record<string, unknown>): Partial<TaskProgressInput> | undefined => {
|
|
20
|
+
const result: Record<string, string> = {};
|
|
21
|
+
for (const field of fields) {
|
|
22
|
+
const value = raw[field];
|
|
23
|
+
if (value === undefined) continue;
|
|
24
|
+
if (typeof value !== "string") return;
|
|
25
|
+
if (value.trim()) result[field] = value.trim();
|
|
26
|
+
}
|
|
27
|
+
if (result.status && !statuses.has(result.status)) return;
|
|
28
|
+
return result as Partial<TaskProgressInput>;
|
|
29
|
+
};
|
|
30
|
+
const complete = (value: Partial<TaskProgressInput>): value is TaskProgressInput => !!value.task_id && !!value.status;
|
|
31
|
+
const root = normalize(input);
|
|
32
|
+
if (!root) return fail("Task fields must be strings; status must be start, complete, skipped, or pending.");
|
|
33
|
+
if (input.updates === undefined || (Array.isArray(input.updates) && input.updates.length === 0)) {
|
|
34
|
+
return complete(root) ? {ok: true, updates: [root], batch: false}
|
|
35
|
+
: fail("Provide task_id and status, or a non-empty updates batch.");
|
|
36
|
+
}
|
|
37
|
+
if (!Array.isArray(input.updates) || input.updates.length > 100) return fail("updates must contain 1–100 valid task updates.");
|
|
38
|
+
const updates: TaskProgressInput[] = [];
|
|
39
|
+
for (const entry of input.updates) {
|
|
40
|
+
if (!object(entry) || Object.keys(entry).some(key => !fields.includes(key as typeof fields[number]))) return fail("updates must contain 1–100 valid task updates.");
|
|
41
|
+
const normalized = normalize(entry);
|
|
42
|
+
if (!normalized || !complete(normalized)) return fail("updates must contain 1–100 valid task updates.");
|
|
43
|
+
updates.push(normalized);
|
|
44
|
+
}
|
|
45
|
+
if (!updates.some(entry => fields.every(field => root[field] === undefined || root[field] === entry[field]))) {
|
|
46
|
+
return fail("Top-level task fields conflict with updates. Put each instruction in updates and omit the top-level task fields.");
|
|
47
|
+
}
|
|
48
|
+
return {ok: true, updates, batch: true};
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
type GoalUpdatePlan =
|
|
52
|
+
| {action: "complete"; completionSummary?: string}
|
|
53
|
+
| {action: "blocked"; reason: string; attemptedActions: string[]}
|
|
54
|
+
| {action: "paused"; reason: string; suggestedAction?: string}
|
|
55
|
+
| {action: "continuation"; continuation: GoalContinuation};
|
|
56
|
+
|
|
57
|
+
/** Classify once before the caller flushes or changes any state. */
|
|
58
|
+
export function planUpdateGoal(input: unknown): {ok: true; plan: GoalUpdatePlan} | {ok: false; message: string} {
|
|
59
|
+
const fail = (message: string) => ({ok: false as const, message});
|
|
60
|
+
const keys = ["status", "continuation", "reason", "attempted_actions", "suggested_action", "completion_summary"];
|
|
61
|
+
if (!object(input) || Object.keys(input).some(key => !keys.includes(key))) return fail("Provide a lifecycle status or continuation using the declared fields.");
|
|
62
|
+
const strings: Record<string, string> = {};
|
|
63
|
+
for (const key of ["status", "reason", "suggested_action", "completion_summary"]) {
|
|
64
|
+
const value = input[key];
|
|
65
|
+
if (value === undefined) continue;
|
|
66
|
+
if (typeof value !== "string") return fail(`${key} must be a string.`);
|
|
67
|
+
if (value.trim()) strings[key] = value;
|
|
68
|
+
}
|
|
69
|
+
const status = strings.status;
|
|
70
|
+
if (status && !["complete", "blocked", "paused"].includes(status)) return fail("status must be complete, blocked, or paused.");
|
|
71
|
+
const rawActions = input.attempted_actions;
|
|
72
|
+
if (rawActions !== undefined && (!Array.isArray(rawActions) || rawActions.length > 8 || rawActions.some(a => typeof a !== "string" || a.length > 240))) return fail("attempted_actions must contain at most eight strings of at most 240 characters.");
|
|
73
|
+
const attemptedActions: string[] = (rawActions ?? []).filter((action: string) => action.trim());
|
|
74
|
+
let continuation: GoalContinuation | undefined;
|
|
75
|
+
if (input.continuation !== undefined) {
|
|
76
|
+
const raw = input.continuation;
|
|
77
|
+
if (!object(raw)) return fail("continuation must be a ready or wait object.");
|
|
78
|
+
if (raw.kind === "ready" && Object.keys(raw).length === 1) {
|
|
79
|
+
if (!status) continuation = {kind: "ready"};
|
|
80
|
+
} else if (raw.kind === "wait" && Object.keys(raw).every(key => ["kind", "reason", "deadline", "wait_id", "polling"].includes(key))) {
|
|
81
|
+
if (typeof raw.reason !== "string" || !raw.reason.trim() || raw.reason.length > 2000 || typeof raw.deadline !== "string" || (raw.wait_id !== undefined && typeof raw.wait_id !== "string")) return fail("continuation.wait requires reason and deadline strings; wait_id must be a string.");
|
|
82
|
+
let polling: {interval_seconds: number; max_checks: number} | undefined;
|
|
83
|
+
if (raw.polling !== undefined) {
|
|
84
|
+
const p = raw.polling;
|
|
85
|
+
if (!object(p) || Object.keys(p).some(key => !["interval_seconds", "max_checks"].includes(key)) || typeof p.interval_seconds !== "number" || !Number.isSafeInteger(p.interval_seconds) || p.interval_seconds < 1 || p.interval_seconds > 2147483 || typeof p.max_checks !== "number" || !Number.isSafeInteger(p.max_checks) || p.max_checks < 1) return fail("continuation.polling requires positive whole interval_seconds and max_checks.");
|
|
86
|
+
polling = {interval_seconds: p.interval_seconds, max_checks: p.max_checks};
|
|
87
|
+
}
|
|
88
|
+
continuation = {kind: "wait", reason: raw.reason, deadline: raw.deadline, ...(raw.wait_id === undefined ? {} : {wait_id: raw.wait_id}), ...(polling ? {polling} : {})};
|
|
89
|
+
} else return fail("continuation must be payload-free ready or a valid wait; unexpected continuation fields are not filler.");
|
|
90
|
+
}
|
|
91
|
+
if (status && continuation) return fail("status conflicts with continuation.wait. Choose one action; a payload-free ready may accompany status as filler.");
|
|
92
|
+
if (!status && !continuation) return fail("Provide one lifecycle status or continuation.");
|
|
93
|
+
const allowed = status === "complete" ? ["completion_summary"] : status === "blocked" ? ["reason", "attempted_actions"] : status === "paused" ? ["reason", "suggested_action"] : [];
|
|
94
|
+
const supplied = [...Object.keys(strings).filter(key => key !== "status"), ...(attemptedActions.length ? ["attempted_actions"] : [])];
|
|
95
|
+
const conflicts = supplied.filter(key => !allowed.includes(key));
|
|
96
|
+
if (conflicts.length) return fail(`${status ? `status=${status}` : "continuation"} conflicts with ${conflicts.join(", ")}. Use only the fields for that action; empty strings and arrays may remain filler.`);
|
|
97
|
+
if ((status === "blocked" || status === "paused") && !strings.reason) return fail(`update_goal({ status: "${status}" }) requires a "reason" describing why work cannot continue.`);
|
|
98
|
+
if (continuation) return {ok: true, plan: {action: "continuation", continuation}};
|
|
99
|
+
if (status === "blocked") return {ok: true, plan: {action: "blocked", reason: strings.reason!, attemptedActions}};
|
|
100
|
+
if (status === "paused") return {ok: true, plan: {action: "paused", reason: strings.reason!, suggestedAction: strings.suggested_action}};
|
|
101
|
+
return {ok: true, plan: {action: "complete", completionSummary: strings.completion_summary}};
|
|
102
|
+
}
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
export const CREATE_GOAL_TOOL_NAME = "create_goal";
|
|
2
|
+
export const GET_GOAL_TOOL_NAME = "get_goal";
|
|
3
|
+
export const UPDATE_GOAL_TOOL_NAME = "update_goal";
|
|
4
|
+
export const SET_GOAL_TASKS_TOOL_NAME = "set_goal_tasks";
|
|
5
|
+
export const UPDATE_GOAL_TASK_TOOL_NAME = "update_goal_task";
|
|
6
|
+
export const QUESTION_TOOL_NAME = "goal_question";
|
|
7
|
+
export const QUESTIONNAIRE_TOOL_NAME = "goal_questionnaire";
|
|
8
|
+
export const PROPOSE_DRAFT_TOOL_NAME = "propose_goal_draft";
|
|
9
|
+
|
|
10
|
+
/** The stable core model surface: three tools, installed without phase-dependent sync. */
|
|
11
|
+
export const CORE_GOAL_TOOL_NAMES = [CREATE_GOAL_TOOL_NAME, GET_GOAL_TOOL_NAME, UPDATE_GOAL_TOOL_NAME] as const;
|
|
12
|
+
|
|
13
|
+
/** The two consolidated task tools advertised when tasks are enabled. */
|
|
14
|
+
export const TASK_TOOL_NAMES = [SET_GOAL_TASKS_TOOL_NAME, UPDATE_GOAL_TASK_TOOL_NAME] as const;
|
|
15
|
+
|
|
16
|
+
/** Fixed task-enabled profile: all five registered goal tools. */
|
|
17
|
+
export const FIVE_GOAL_TOOLS = [...CORE_GOAL_TOOL_NAMES, ...TASK_TOOL_NAMES] as const;
|
|
18
|
+
|
|
19
|
+
/** Fixed task-disabled profile: the three core tools. */
|
|
20
|
+
export const CORE_GOAL_TOOLS = CORE_GOAL_TOOL_NAMES;
|
|
21
|
+
|
|
22
|
+
/** Always advertised; executors require an active user-started draft. */
|
|
23
|
+
export const DRAFTING_GOAL_TOOLS = [
|
|
24
|
+
QUESTION_TOOL_NAME,
|
|
25
|
+
QUESTIONNAIRE_TOOL_NAME,
|
|
26
|
+
PROPOSE_DRAFT_TOOL_NAME,
|
|
27
|
+
] as const;
|
|
28
|
+
|
|
29
|
+
/** Every goal tool this extension registers (used by installGoalToolProfile). */
|
|
30
|
+
export const ALL_REGISTERED_GOAL_TOOLS = [...FIVE_GOAL_TOOLS, ...DRAFTING_GOAL_TOOLS] as const;
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Legacy work-tool classification plus the common host work tools. Scheduling
|
|
34
|
+
* does not require membership: idle pacing uses the narrower progress set.
|
|
35
|
+
*/
|
|
36
|
+
export const GOAL_WORK_TOOL_NAMES = [
|
|
37
|
+
UPDATE_GOAL_TOOL_NAME,
|
|
38
|
+
SET_GOAL_TASKS_TOOL_NAME,
|
|
39
|
+
UPDATE_GOAL_TASK_TOOL_NAME,
|
|
40
|
+
CREATE_GOAL_TOOL_NAME,
|
|
41
|
+
GET_GOAL_TOOL_NAME,
|
|
42
|
+
"write",
|
|
43
|
+
"edit",
|
|
44
|
+
"bash",
|
|
45
|
+
"read",
|
|
46
|
+
"grep",
|
|
47
|
+
"find",
|
|
48
|
+
"ls",
|
|
49
|
+
] as const;
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* The subset of GOAL_WORK_TOOL_NAMES that indicates actual progress (excludes
|
|
53
|
+
* read-only surface tools such as get_goal and create_goal).
|
|
54
|
+
*/
|
|
55
|
+
export const GOAL_PROGRESS_TOOL_NAMES = [
|
|
56
|
+
UPDATE_GOAL_TOOL_NAME,
|
|
57
|
+
UPDATE_GOAL_TASK_TOOL_NAME,
|
|
58
|
+
"write",
|
|
59
|
+
"edit",
|
|
60
|
+
"bash",
|
|
61
|
+
"read",
|
|
62
|
+
"grep",
|
|
63
|
+
"find",
|
|
64
|
+
"ls",
|
|
65
|
+
] as const;
|
|
66
|
+
|
|
67
|
+
/** Tools the model may still call on a stopped turn (state reads only). */
|
|
68
|
+
export const POST_STOP_ALLOWED_TOOLS = ["get_goal"] as const;
|
|
69
|
+
|
|
70
|
+
/** Keep the cached tool/rules prefix independent of goal and draft state. */
|
|
71
|
+
export function applicableGoalTools(tasksEnabled: boolean): string[] {
|
|
72
|
+
return [...(tasksEnabled ? FIVE_GOAL_TOOLS : CORE_GOAL_TOOLS), ...DRAFTING_GOAL_TOOLS];
|
|
73
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import type { GoalCore } from "./goal-state.ts";
|
|
2
|
+
import { registerCoreTools } from "./goal-core-tools.ts";
|
|
3
|
+
import { registerTaskTools } from "./goal-task-tools.ts";
|
|
4
|
+
import { registerDraftingTools } from "./goal-drafting.ts";
|
|
5
|
+
import { runGoalCompletionFlow } from "./goal-completion.ts";
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Registration composition only (Stage 5 module split): the three core tools,
|
|
9
|
+
* the two task tools, and the shared completion flow live in their own
|
|
10
|
+
* modules; goal.ts calls this installer once at extension load.
|
|
11
|
+
*/
|
|
12
|
+
export function registerGoalTools(core: GoalCore): void {
|
|
13
|
+
registerCoreTools(core, { runGoalCompletionFlow });
|
|
14
|
+
registerTaskTools(core);
|
|
15
|
+
registerDraftingTools(core);
|
|
16
|
+
}
|
|
@@ -0,0 +1,455 @@
|
|
|
1
|
+
import { GOAL_KEY_ACTIONS, GOAL_COMMAND_INPUTS, bindingKeys, keyActionBinding, type GoalCommandId } from "./goal-controls.ts";
|
|
2
|
+
import { matchesKey, getKeybindings } from "@earendil-works/pi-tui";
|
|
3
|
+
import type { ExtensionContext } from "@earendil-works/pi-coding-agent";
|
|
4
|
+
import { cloneGoal, createGoal, nowIso, type GoalTask } from "./goal-record.ts";
|
|
5
|
+
import { checkSubtasksComplete, findTaskInTree } from "./goal-policy.ts";
|
|
6
|
+
import { loadGoalSettings } from "./goal-settings.ts";
|
|
7
|
+
import { serializeGoalFile } from "./storage/goal-files.ts";
|
|
8
|
+
import type { AuditorWidgetProgress } from "./widgets/goal-widget.ts";
|
|
9
|
+
import type { GoalCore } from "./goal-state.ts";
|
|
10
|
+
|
|
11
|
+
const DEBUG_GOALS_DIR = ".pi/goals/debug";
|
|
12
|
+
const KEY_ACTION_BY_PATH = new Map(GOAL_KEY_ACTIONS.map(action => [action.path.slice(1).join("."), action]));
|
|
13
|
+
const COMMAND_KEYS = GOAL_KEY_ACTIONS.filter(action => action.path[1] === "commands");
|
|
14
|
+
const EXPANDED_NAVIGATION = [["up", "up"], ["down", "down"], ["pageUp", "pageUp"], ["pageDown", "pageDown"], ["first", "home"], ["last", "end"]] as const;
|
|
15
|
+
const COMPACT_NAVIGATION = [["scrollUp", "up"], ["scrollDown", "down"], ["scrollPageUp", "pageUp"], ["scrollPageDown", "pageDown"], ["scrollFirst", "home"], ["scrollLast", "end"]] as const;
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
function isKittyRepeatOrRelease(data: string): boolean {
|
|
19
|
+
return data.startsWith("\x1b[") && /^[0-9:;]+:[23][u~ABCDHF]$/.test(data.slice(2));
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Terminal input keybindings (Escape pause/abort-audit, Ctrl+Shift+T dashboard
|
|
24
|
+
* overlay, and the hidden debug-mode bindings) plus the debug goal/task/audit
|
|
25
|
+
* helpers. Re-registered at session start/tree navigation; the handler reads
|
|
26
|
+
* live state through the core at event time.
|
|
27
|
+
*/
|
|
28
|
+
let debugGoalCounter = 0;
|
|
29
|
+
let debugMockAuditTimer: ReturnType<typeof setInterval> | null = null;
|
|
30
|
+
|
|
31
|
+
/** Debug helpers are inert unless PI_GOAL_DEBUG is set (P1-13). */
|
|
32
|
+
function isDebugEnabled(): boolean {
|
|
33
|
+
const value = process.env.PI_GOAL_DEBUG;
|
|
34
|
+
return value === "true" || value === "1";
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/** Render task lines for the debug proposal dialog. */
|
|
38
|
+
function formatModeLabelDebug(sisyphus: boolean): string {
|
|
39
|
+
return sisyphus ? "Sisyphus (prompt/criteria style)" : "Normal goal";
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function formatPrefixedLinesDebug(content: string): string[] {
|
|
43
|
+
const lines: string[] = [];
|
|
44
|
+
for (const rawLine of content.split("\n")) {
|
|
45
|
+
const trimmed = rawLine.trim();
|
|
46
|
+
if (!trimmed) continue;
|
|
47
|
+
if (trimmed.startsWith("│")) {
|
|
48
|
+
lines.push(rawLine);
|
|
49
|
+
} else {
|
|
50
|
+
lines.push(`│ ${rawLine}`);
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
return lines;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function formatSectionDebug(title: string, content: string): string[] {
|
|
57
|
+
const body = formatPrefixedLinesDebug(content);
|
|
58
|
+
return ["", `─── ${title} ───`, "", ...body];
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* F3: toggle one task through the goal-service mutation boundary with the
|
|
64
|
+
* same gates as update_goal_task: completing a pending task requires its
|
|
65
|
+
* subtasks complete first and (unless contracts are disabled) evidence when
|
|
66
|
+
* it carries a verification contract — the evidence is collected through the
|
|
67
|
+
* UI input dialog; completing a complete task reopens it to pending (a
|
|
68
|
+
* deliberate TUI-only relaxation of the tool surface's immutability, for
|
|
69
|
+
* explicit human corrections).
|
|
70
|
+
*/
|
|
71
|
+
export async function toggleTaskViaService(core: GoalCore, ctx: ExtensionContext, goalId: string, taskId: string): Promise<{ ok: boolean; message?: string }> {
|
|
72
|
+
const goal = core.goalsById.get(goalId);
|
|
73
|
+
if (!goal) return { ok: false, message: `Goal ${goalId} not found in this session.` };
|
|
74
|
+
if (goal.status !== "active") return { ok: false, message: `Task updates apply only to an active goal (current status: ${goal.status}).` };
|
|
75
|
+
if (!goal.taskList) return { ok: false, message: "The goal has no task list." };
|
|
76
|
+
const settings = loadGoalSettings(ctx.cwd);
|
|
77
|
+
const task = findTaskInTree(goal.taskList.tasks, taskId);
|
|
78
|
+
if (!task) return { ok: false, message: `Task "${taskId}" not found.` };
|
|
79
|
+
const now = nowIso();
|
|
80
|
+
|
|
81
|
+
if (task.status === "pending") {
|
|
82
|
+
let evidence: string | undefined;
|
|
83
|
+
if (!settings.disableContracts && task.verificationContract) {
|
|
84
|
+
const input = await ctx.ui.input(`Evidence for task ${taskId}`, "");
|
|
85
|
+
if (input === undefined || !input.trim()) {
|
|
86
|
+
return { ok: false, message: `Task "${taskId}" has a verification contract; provide evidence to complete it.` };
|
|
87
|
+
}
|
|
88
|
+
evidence = input.trim().slice(0, 200);
|
|
89
|
+
}
|
|
90
|
+
const subtaskGate = checkSubtasksComplete(task);
|
|
91
|
+
if (subtaskGate) return { ok: false, message: subtaskGate };
|
|
92
|
+
const result = core.goalService.updateTask(ctx, {
|
|
93
|
+
focusToken: core.focusedOperationToken(goalId),
|
|
94
|
+
taskId,
|
|
95
|
+
validate: (t) => {
|
|
96
|
+
if (t.status === "complete") return { ok: false, message: `Task "${taskId}" is already complete.` };
|
|
97
|
+
return { ok: true };
|
|
98
|
+
},
|
|
99
|
+
update: (t) => ({ ...t, status: "complete" as const, completedAt: now, evidence }),
|
|
100
|
+
ledger: (written) => [{ type: "task_complete", goalId: written.id, taskId, evidence, at: written.updatedAt }],
|
|
101
|
+
});
|
|
102
|
+
if (!result.ok) return { ok: false, message: result.message };
|
|
103
|
+
return { ok: true };
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
if (task.status === "complete") {
|
|
107
|
+
const result = core.goalService.updateTask(ctx, {
|
|
108
|
+
focusToken: core.focusedOperationToken(goalId),
|
|
109
|
+
taskId,
|
|
110
|
+
validate: () => ({ ok: true }),
|
|
111
|
+
update: (t) => ({ ...t, status: "pending" as const, completedAt: undefined, evidence: undefined }),
|
|
112
|
+
ledger: (written) => [{ type: "task_reopened", goalId: written.id, taskId, at: written.updatedAt }],
|
|
113
|
+
});
|
|
114
|
+
if (!result.ok) return { ok: false, message: result.message };
|
|
115
|
+
return { ok: true };
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
return { ok: false, message: `Task "${taskId}" is skipped; change it with /goal-tweak or update_goal_task.` };
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
export function syncTerminalInputPause(core: GoalCore, ctx: ExtensionContext): void {
|
|
122
|
+
if (!ctx.hasUI) return;
|
|
123
|
+
core.terminalInputUnsubscribe?.();
|
|
124
|
+
const goalCwd = typeof ctx.cwd === "string" ? ctx.cwd : process.cwd();
|
|
125
|
+
core.terminalInputUnsubscribe = ctx.ui.onTerminalInput((data) => {
|
|
126
|
+
// The active dialog owns every key, including key capture and Escape.
|
|
127
|
+
if (core.goalModalDepth > 0) return undefined;
|
|
128
|
+
if (core.goalWidgetComponentRef.current?.isMainEditorFocused?.() === false) return undefined;
|
|
129
|
+
const settings = loadGoalSettings(goalCwd);
|
|
130
|
+
const matches = (path: string): boolean => {
|
|
131
|
+
const action = KEY_ACTION_BY_PATH.get(path)!;
|
|
132
|
+
return bindingKeys(keyActionBinding(settings, action)).some(key => matchesKey(data, key));
|
|
133
|
+
};
|
|
134
|
+
if (isKittyRepeatOrRelease(data)) {
|
|
135
|
+
const applicable = GOAL_KEY_ACTIONS.filter(action => action.context === "global" ||
|
|
136
|
+
(core.auditProgress ? action.context === "audit" : core.isDashboardExpanded() ? action.context === "expanded" : action.context === "compact"));
|
|
137
|
+
return applicable.some(action => bindingKeys(keyActionBinding(settings, action)).some(key => matchesKey(data, key))) ? { consume: true } : undefined;
|
|
138
|
+
}
|
|
139
|
+
if (core.auditProgress && matches("interruptAudit")) { core.abortAudit(ctx); return { consume: true }; }
|
|
140
|
+
if (core.isDashboardExpanded() && matches("dashboard.collapse")) { core.toggleDashboardExpanded(); return { consume: true }; }
|
|
141
|
+
if (matches("pause") && ((core.state.goal?.status === "active" && core.state.goal.autoContinue) || (core.state.goal?.status === "paused" && !ctx.isIdle()))) {
|
|
142
|
+
core.interruption.pause(ctx);
|
|
143
|
+
// Preserve Pi's ordinary Escape handling for the default combined stop.
|
|
144
|
+
return matchesKey(data, "escape") && settings.pauseOrder === "together" ? undefined : { consume: true };
|
|
145
|
+
}
|
|
146
|
+
// Follow Pi's configured interrupt key.
|
|
147
|
+
let hostInterrupt = matchesKey(data, "escape");
|
|
148
|
+
try {
|
|
149
|
+
const manager = getKeybindings();
|
|
150
|
+
const keys = manager.getKeys("app.interrupt");
|
|
151
|
+
if (keys.length) hostInterrupt = keys.some(key => matchesKey(data, key));
|
|
152
|
+
} catch { /* Embeddings without an app keymap retain Escape. */ }
|
|
153
|
+
if (hostInterrupt && !matches("pause") && core.state.goal?.status === "active" && core.state.goal.autoContinue) {
|
|
154
|
+
core.interruption.interruptTurn(ctx); return { consume: true };
|
|
155
|
+
}
|
|
156
|
+
if (matches("dashboard.toggleExpand")) { core.toggleDashboardExpanded(); return { consume: true }; }
|
|
157
|
+
if (matches("toggleAuditor")) {
|
|
158
|
+
core.toggleGoalAuditor(ctx); core.goalWidgetComponentRef.current?.invalidate(); return { consume: true };
|
|
159
|
+
}
|
|
160
|
+
if (core.isDashboardExpanded()) {
|
|
161
|
+
for (const [name, key] of EXPANDED_NAVIGATION) if (matches(`dashboard.${name}`) && core.goalWidgetComponentRef.current?.handleNavigationKey(key)) return { consume: true };
|
|
162
|
+
} else {
|
|
163
|
+
for (const [name, key] of COMPACT_NAVIGATION) if (matches(`dashboard.${name}`) && core.goalWidgetComponentRef.current?.handleCompactScrollKey(key)) return { consume: true };
|
|
164
|
+
}
|
|
165
|
+
for (const action of COMMAND_KEYS) {
|
|
166
|
+
if (!bindingKeys(keyActionBinding(settings, action)).some(key => matchesKey(data, key))) continue;
|
|
167
|
+
const id = action.path[2] as GoalCommandId;
|
|
168
|
+
const revision = core.focusRevision;
|
|
169
|
+
const owner = core.sessionOwner;
|
|
170
|
+
void (async () => {
|
|
171
|
+
core.enterGoalModal();
|
|
172
|
+
try {
|
|
173
|
+
const prompt = GOAL_COMMAND_INPUTS[id];
|
|
174
|
+
const args = prompt ? await ctx.ui.input(prompt) : "";
|
|
175
|
+
if (core.focusRevision !== revision || args === undefined || (prompt && !args.trim())) return;
|
|
176
|
+
await core.invokeGoalCommand?.(id, args, ctx);
|
|
177
|
+
} finally { if (core.sessionOwner === owner) core.exitGoalModal(); }
|
|
178
|
+
})().catch(error => {if (core.sessionOwner === owner) ctx.ui.notify(String(error), "error");});
|
|
179
|
+
return { consume: true };
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
// Debug keybindings are inert unless PI_GOAL_DEBUG is set (P1-13).
|
|
183
|
+
if (!isDebugEnabled()) return undefined;
|
|
184
|
+
|
|
185
|
+
// ── Debug mode keybindings (hidden from normal view) ────────────────
|
|
186
|
+
|
|
187
|
+
// Ctrl+Shift+X — toggle debug mode on/off
|
|
188
|
+
if (matchesKey(data, "ctrl+shift+x")) {
|
|
189
|
+
core.debugMode = !core.debugMode;
|
|
190
|
+
ctx.ui.notify(core.debugMode ? "Debug mode ON" : "Debug mode OFF", "info");
|
|
191
|
+
core.goalWidgetComponentRef.current?.invalidate();
|
|
192
|
+
return { consume: true };
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
// Only process the following debug keybindings when debug mode is active
|
|
196
|
+
if (!core.debugMode) return undefined;
|
|
197
|
+
|
|
198
|
+
// Ctrl+Shift+N — create a test goal
|
|
199
|
+
if (matchesKey(data, "ctrl+shift+n")) {
|
|
200
|
+
createDebugGoal(ctx);
|
|
201
|
+
return { consume: true };
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
// Ctrl+Shift+T — inject sample tasks into current goal
|
|
205
|
+
if (matchesKey(data, "ctrl+shift+t")) {
|
|
206
|
+
injectDebugTasks(ctx);
|
|
207
|
+
return { consume: true };
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
// Ctrl+Shift+R — start mock completion audit
|
|
211
|
+
if (matchesKey(data, "ctrl+shift+r")) {
|
|
212
|
+
startMockAudit(ctx);
|
|
213
|
+
return { consume: true };
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
// Ctrl+Shift+O — open proposal dialog with sample data
|
|
217
|
+
if (matchesKey(data, "ctrl+shift+o")) {
|
|
218
|
+
openDebugProposal(ctx);
|
|
219
|
+
return { consume: true };
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
return undefined;
|
|
223
|
+
});
|
|
224
|
+
|
|
225
|
+
/** Toggle a test goal: create (first press) or remove (second press) */
|
|
226
|
+
function createDebugGoal(ctx: ExtensionContext): void {
|
|
227
|
+
if (!isDebugEnabled()) return;
|
|
228
|
+
const prev = core.state.goal;
|
|
229
|
+
if (prev && prev.id.startsWith("debug-")) {
|
|
230
|
+
// Toggle off — remove debug goal entirely (no archive, full delete)
|
|
231
|
+
const filePath = `${DEBUG_GOALS_DIR}/debug_goal.md`;
|
|
232
|
+
core.goalService.removeDebugFile(ctx, filePath);
|
|
233
|
+
const prevId = prev.id;
|
|
234
|
+
core.state.goal = null;
|
|
235
|
+
if (core.focusedGoalId === prevId) {
|
|
236
|
+
core.goalsById.delete(prevId);
|
|
237
|
+
core.assignFocusedGoalId(null);
|
|
238
|
+
}
|
|
239
|
+
core.clearStoppedRuntimeState();
|
|
240
|
+
core.updateUI(ctx);
|
|
241
|
+
ctx.ui.notify("Debug goal removed", "info");
|
|
242
|
+
return;
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
// Toggle on — create a new debug goal, write to temp dir
|
|
246
|
+
debugGoalCounter++;
|
|
247
|
+
const goal = createGoal({
|
|
248
|
+
objective: "=== Goal ===\nObjective: Debug test goal",
|
|
249
|
+
autoContinue: true,
|
|
250
|
+
sisyphus: false,
|
|
251
|
+
});
|
|
252
|
+
goal.id = `debug-${nowIso().replace(/[:.]/g, "-")}-${debugGoalCounter}`;
|
|
253
|
+
goal.createdAt = nowIso();
|
|
254
|
+
goal.updatedAt = nowIso();
|
|
255
|
+
goal.activePath = `${DEBUG_GOALS_DIR}/debug_goal.md`;
|
|
256
|
+
core.goalService.writeDebugFile(ctx, goal.activePath, serializeGoalFile(goal));
|
|
257
|
+
core.setGoal(goal, ctx, false, "created"); // no persist (we already wrote the file)
|
|
258
|
+
ctx.ui.notify(`Debug goal created: ${goal.id}`, "info");
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
/** Inject 3-4 sample tasks into the current goal */
|
|
262
|
+
function injectDebugTasks(ctx: ExtensionContext): void {
|
|
263
|
+
if (!isDebugEnabled()) return;
|
|
264
|
+
if (!core.state.goal) {
|
|
265
|
+
ctx.ui.notify("No goal to inject tasks into; create one first (Ctrl+Shift+N)", "warning");
|
|
266
|
+
return;
|
|
267
|
+
}
|
|
268
|
+
const now = nowIso();
|
|
269
|
+
const tasks: GoalTask[] = [
|
|
270
|
+
{
|
|
271
|
+
id: "t1",
|
|
272
|
+
title: "Set up project structure",
|
|
273
|
+
status: "complete",
|
|
274
|
+
completedAt: now,
|
|
275
|
+
subtasks: [
|
|
276
|
+
{ id: "t1a", title: "Initialize repo", status: "complete", completedAt: now },
|
|
277
|
+
{ id: "t1b", title: "Add build config", status: "pending" },
|
|
278
|
+
],
|
|
279
|
+
},
|
|
280
|
+
{
|
|
281
|
+
id: "t2",
|
|
282
|
+
title: "Implement core feature",
|
|
283
|
+
status: "pending",
|
|
284
|
+
},
|
|
285
|
+
{
|
|
286
|
+
id: "t3",
|
|
287
|
+
title: "Write tests",
|
|
288
|
+
status: "pending",
|
|
289
|
+
},
|
|
290
|
+
];
|
|
291
|
+
const next = cloneGoal(core.state.goal);
|
|
292
|
+
next.taskList = { tasks, blockCompletion: false, proposedAt: now };
|
|
293
|
+
next.updatedAt = now;
|
|
294
|
+
core.setGoal(next, ctx);
|
|
295
|
+
ctx.ui.notify("Sample tasks injected (3 tasks, 1 completed)", "info");
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
/** Stop mock audit timer if running */
|
|
299
|
+
function stopMockAuditTimer(): void {
|
|
300
|
+
if (debugMockAuditTimer) {
|
|
301
|
+
clearInterval(debugMockAuditTimer);
|
|
302
|
+
debugMockAuditTimer = null;
|
|
303
|
+
}
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
/** Start a mock completion audit that transitions through phases */
|
|
307
|
+
function startMockAudit(ctx: ExtensionContext): void {
|
|
308
|
+
if (!isDebugEnabled()) return;
|
|
309
|
+
stopMockAuditTimer();
|
|
310
|
+
const startedAt = Date.now();
|
|
311
|
+
const phases: { phase: AuditorWidgetProgress["phase"]; atMs: number; label: string; percentage: number }[] = [
|
|
312
|
+
{ phase: "tool_executing", atMs: 0, label: "Checking test results...", percentage: 10 },
|
|
313
|
+
{ phase: "tool_executing", atMs: 800, label: "Verifying requirements...", percentage: 30 },
|
|
314
|
+
{ phase: "thinking", atMs: 1800, label: "Evaluating completion criteria...", percentage: 60 },
|
|
315
|
+
{ phase: "producing_report", atMs: 3200, label: "Writing audit report...", percentage: 85 },
|
|
316
|
+
{ phase: "done", atMs: 4800, label: "Audit complete", percentage: 100 },
|
|
317
|
+
];
|
|
318
|
+
core.auditProgress = {
|
|
319
|
+
recentOutput: [],
|
|
320
|
+
phase: "running",
|
|
321
|
+
elapsedMs: 0,
|
|
322
|
+
};
|
|
323
|
+
core.goalWidgetComponentRef.current?.invalidate();
|
|
324
|
+
|
|
325
|
+
debugMockAuditTimer = setInterval(() => {
|
|
326
|
+
const elapsed = Date.now() - startedAt;
|
|
327
|
+
let currentPhase: AuditorWidgetProgress["phase"] = "done";
|
|
328
|
+
let currentLabel = "Audit complete";
|
|
329
|
+
let currentPct = 100;
|
|
330
|
+
for (let i = phases.length - 1; i >= 0; i--) {
|
|
331
|
+
if (elapsed >= phases[i]!.atMs) {
|
|
332
|
+
currentPhase = phases[i]!.phase;
|
|
333
|
+
currentLabel = phases[i]!.label;
|
|
334
|
+
currentPct = phases[i]!.percentage;
|
|
335
|
+
break;
|
|
336
|
+
}
|
|
337
|
+
}
|
|
338
|
+
core.auditProgress = {
|
|
339
|
+
phase: currentPhase,
|
|
340
|
+
label: currentLabel,
|
|
341
|
+
percentage: currentPct,
|
|
342
|
+
elapsedMs: elapsed,
|
|
343
|
+
recentOutput: core.auditProgress?.recentOutput ?? [],
|
|
344
|
+
};
|
|
345
|
+
if (currentPhase === "done") {
|
|
346
|
+
if (core.auditProgress) core.auditProgress.recentOutput = [
|
|
347
|
+
"✓ All requirements verified",
|
|
348
|
+
"✓ Tests pass: 310/310",
|
|
349
|
+
"✓ No truncation cap remaining",
|
|
350
|
+
];
|
|
351
|
+
stopMockAuditTimer();
|
|
352
|
+
// Auto-clear audit after 3 more seconds
|
|
353
|
+
setTimeout(() => {
|
|
354
|
+
core.auditProgress = null;
|
|
355
|
+
core.goalWidgetComponentRef.current?.invalidate();
|
|
356
|
+
}, 3000);
|
|
357
|
+
}
|
|
358
|
+
core.goalWidgetComponentRef.current?.invalidate();
|
|
359
|
+
}, 100);
|
|
360
|
+
debugMockAuditTimer.unref?.();
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
function renderDebugTaskLines(tasks: GoalTask[], indent = 0): string[] {
|
|
364
|
+
const prefix = " ".repeat(indent);
|
|
365
|
+
const lines: string[] = [];
|
|
366
|
+
for (const t of tasks) {
|
|
367
|
+
const marker = t.status === "complete" ? "[x]" : t.status === "skipped" ? "[~]" : "[ ]";
|
|
368
|
+
const lw = t.lightweightSubtasks ? " (lightweight)" : "";
|
|
369
|
+
lines.push(`${prefix}${marker} ${t.id}: ${t.title}${lw}`);
|
|
370
|
+
if (t.subtasks && t.subtasks.length > 0) {
|
|
371
|
+
lines.push(...renderDebugTaskLines(t.subtasks, indent + 1));
|
|
372
|
+
}
|
|
373
|
+
}
|
|
374
|
+
return lines;
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
/** Show the proposal dialog using real goal state — no hardcoded text */
|
|
378
|
+
function openDebugProposal(ctx: ExtensionContext): void {
|
|
379
|
+
if (!isDebugEnabled()) return;
|
|
380
|
+
// Build a fresh debug goal + tasks in memory for the dialog
|
|
381
|
+
debugGoalCounter++;
|
|
382
|
+
const goal = createGoal({
|
|
383
|
+
objective: `=== Goal ===
|
|
384
|
+
Objective: Add collapsible task sections to the goal widget so large task lists are navigable
|
|
385
|
+
|
|
386
|
+
Success criteria:
|
|
387
|
+
- Tasks are grouped into sections by status (pending, active, complete) with visible section headers
|
|
388
|
+
- Each section header is toggleable — clicking it expands or collapses that section
|
|
389
|
+
- When collapsed, the section shows a header line only with a task count badge
|
|
390
|
+
- When expanded, tasks render with normal indentation and per-line styling
|
|
391
|
+
- Default state: pending section expanded, active and complete sections collapsed
|
|
392
|
+
- Section state is tracked per-render (no persistence needed)
|
|
393
|
+
- All 310 existing tests still pass
|
|
394
|
+
|
|
395
|
+
Boundaries:
|
|
396
|
+
- In scope: GoalWidgetComponent.render() grouping logic, section header toggling, expand/collapse state per render cycle
|
|
397
|
+
- Out of scope: task reordering, drag-and-drop, keyboard navigation for sections, persistence of section state across pi restarts
|
|
398
|
+
- Out of scope: modifying GoalTask or GoalRecord types
|
|
399
|
+
|
|
400
|
+
Constraints:
|
|
401
|
+
- Render width must respect the existing width parameter — no hardcoded widths
|
|
402
|
+
- Section collapse state is a render-only map, not stored in goal record
|
|
403
|
+
- Collapse toggle must be keyboard-accessible via existing widget interaction model
|
|
404
|
+
- Do not change the GoalWidgetComponent public API (constructor options, render signature)
|
|
405
|
+
- Section headers must use theme.fg("accent", ...) consistent with existing render patterns
|
|
406
|
+
|
|
407
|
+
Verification contract:
|
|
408
|
+
- Run npm test and confirm 310/310 pass (0 failures)
|
|
409
|
+
- Read render method and confirm task grouping logic exists
|
|
410
|
+
- Read expand/collapse toggle handler and confirm it inverts section state
|
|
411
|
+
- Confirm collapsed sections only render the header line with task count
|
|
412
|
+
- Confirm expanded sections render tasks with correct indentation and styling`,
|
|
413
|
+
autoContinue: true,
|
|
414
|
+
sisyphus: false,
|
|
415
|
+
});
|
|
416
|
+
goal.id = `debug-${nowIso().replace(/[:.]/g, "-")}-${debugGoalCounter}`;
|
|
417
|
+
goal.createdAt = nowIso();
|
|
418
|
+
goal.updatedAt = nowIso();
|
|
419
|
+
|
|
420
|
+
const now = nowIso();
|
|
421
|
+
const tasks: GoalTask[] = [
|
|
422
|
+
{
|
|
423
|
+
id: "t1",
|
|
424
|
+
title: "Set up project structure",
|
|
425
|
+
status: "complete",
|
|
426
|
+
completedAt: now,
|
|
427
|
+
subtasks: [
|
|
428
|
+
{ id: "t1a", title: "Initialize repo", status: "complete", completedAt: now },
|
|
429
|
+
{ id: "t1b", title: "Add build config", status: "pending" },
|
|
430
|
+
],
|
|
431
|
+
},
|
|
432
|
+
{
|
|
433
|
+
id: "t2",
|
|
434
|
+
title: "Implement core feature",
|
|
435
|
+
status: "pending",
|
|
436
|
+
subtasks: [
|
|
437
|
+
{ id: "t2a", title: "Status grouping logic", status: "pending" },
|
|
438
|
+
{ id: "t2b", title: "Section header component", status: "pending" },
|
|
439
|
+
{ id: "t2c", title: "Expand/collapse state", status: "pending" },
|
|
440
|
+
{ id: "t2d", title: "Task count badge", status: "pending" },
|
|
441
|
+
],
|
|
442
|
+
},
|
|
443
|
+
{ id: "t3", title: "Update tests", status: "pending" },
|
|
444
|
+
{ id: "t4", title: "Manual TUI verification", status: "pending" },
|
|
445
|
+
];
|
|
446
|
+
goal.taskList = { tasks, blockCompletion: false, proposedAt: now };
|
|
447
|
+
// Development-only diagnostic: the legacy proposal dialog is
|
|
448
|
+
// gone; the injected sample task list is inspected through the goal
|
|
449
|
+
// file and the debug task overlay (Ctrl+Shift+T).
|
|
450
|
+
ctx.ui.notify(
|
|
451
|
+
`Injected sample task list into debug goal ${goal.id}; inspect the goal file or use the debug task overlay.`,
|
|
452
|
+
"info",
|
|
453
|
+
);
|
|
454
|
+
}
|
|
455
|
+
}
|