@xingular/harness 1.3.1-unstable.9 → 1.4.0-unstable
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 +42 -6
- package/NOTICE.md +26 -0
- package/builtin/skills/plugin-authoring/SKILL.md +20 -1
- package/dist/cli.js +1035 -163
- package/dist/sdk/commands/registry.d.ts +7 -0
- package/dist/sdk/commands/registry.js +1 -0
- package/dist/sdk/plugins/discover.js +80 -4
- package/dist/sdk/plugins/install.js +26 -0
- package/dist/sdk/plugins/manifest.d.ts +2 -0
- package/dist/sdk/plugins/manifest.js +20 -1
- package/dist/sdk/prompt/compile.d.ts +4 -3
- package/dist/sdk/prompt/compile.js +4 -18
- package/dist/sdk/prompt/reporting.d.ts +2 -0
- package/dist/sdk/prompt/reporting.js +282 -0
- package/dist/sdk/prompt/standard.d.ts +12 -0
- package/dist/sdk/prompt/standard.js +53 -2
- package/dist/sdk/sessions/completionCheck.d.ts +8 -10
- package/dist/sdk/sessions/completionCheck.js +21 -26
- package/dist/sdk/sessions/contracts.d.ts +90 -0
- package/dist/sdk/sessions/contracts.js +13 -0
- package/dist/sdk/sessions/goal.d.ts +145 -0
- package/dist/sdk/sessions/goal.js +405 -0
- package/dist/sdk/sessions/goalLoop.d.ts +37 -0
- package/dist/sdk/sessions/goalLoop.js +125 -0
- package/dist/sdk/sessions/goalPrompts.d.ts +4 -0
- package/dist/sdk/sessions/goalPrompts.js +93 -0
- package/dist/sdk/sessions/runStore.js +1 -0
- package/dist/sdk/sessions/runner.d.ts +7 -1
- package/dist/sdk/sessions/runner.js +13 -12
- package/dist/sdk/sessions/sessions.d.ts +10 -0
- package/dist/sdk/sessions/sessions.js +44 -4
- package/dist/sdk/sessions/stopGate.d.ts +16 -27
- package/dist/sdk/sessions/stopGate.js +32 -60
- package/dist/sdk/sessions/workSlice.d.ts +2 -0
- package/dist/sdk/sessions/workSlice.js +1 -1
- package/dist/sdk/terminal/piExtension.d.ts +2 -1
- package/dist/sdk/terminal/piExtension.js +68 -13
- package/dist/sdk/terminal/terminals.js +2 -1
- package/dist/sdk/terminal/tui/commands.d.ts +2 -0
- package/dist/sdk/terminal/tui/commands.js +3 -0
- package/dist/sdk/terminal/tui/goalCommand.d.ts +19 -0
- package/dist/sdk/terminal/tui/goalCommand.js +83 -0
- package/dist/sdk/terminal/tui/runtime.d.ts +3 -0
- package/dist/sdk/terminal/tui/runtime.js +7 -1
- package/dist/sdk/tools/askUser.js +1 -1
- package/dist/sdk/tools/todo.js +2 -1
- package/dist/terminal-opentui.js +839 -119
- package/dist/terminal-pi.js +387 -80
- package/docs/architecture/02-sessions-and-runs.md +28 -20
- package/docs/architecture/04-prompt.md +73 -52
- package/docs/architecture/06-plugins-and-capabilities.md +16 -1
- package/docs/architecture/07-tools-and-gate.md +7 -4
- package/docs/architecture/10-server-protocol.md +13 -6
- package/docs/architecture/11-terminal.md +23 -7
- package/docs/decisions/ADR-0008-flexible-reporting-guidance.md +3 -0
- package/docs/decisions/ADR-0068-scope-and-completion-report.md +5 -2
- package/docs/decisions/ADR-0071-completion-check.md +3 -2
- package/docs/decisions/ADR-0072-codex-prompts-and-goal.md +105 -0
- package/docs/decisions/ADR-0073-large-reporting-prompt.md +84 -0
- package/docs/parity.md +4 -1
- package/docs/releases/1.4.0-unstable.md +18 -0
- package/docs/releases/README.md +1 -1
- package/docs/study/00-index.md +7 -0
- package/docs/study/STUDY-0004-turn-end-controls.md +2 -0
- package/docs/study/STUDY-0005-skill-installation-guidance.md +146 -0
- package/docs/study/STUDY-0006-reporting-prompts.md +81 -0
- package/docs/study/arms/04-skills.md +6 -0
- package/docs/terminal-tui.md +17 -7
- package/package.json +1 -1
|
@@ -14,6 +14,8 @@ import { askUserTool } from "../tools/askUser.js";
|
|
|
14
14
|
import { TODO_WRITE, todoTool } from "../tools/todo.js";
|
|
15
15
|
import { createStopGateState, handoffFields, sessionCheckModel, stopCheckpointReason, stopGate, stopGateNotice, stopOutcome, stopSummary, STOP_GATE_EXTENSION } from "../sessions/stopGate.js";
|
|
16
16
|
import { launchSwitchOff } from "../sessions/completionCheck.js";
|
|
17
|
+
import { GOAL_STATE_ENTRY, GOAL_TOOLS, goalTools } from "../sessions/goal.js";
|
|
18
|
+
import { GOAL_EXTENSION, goalLoop, goalNotice } from "../sessions/goalLoop.js";
|
|
17
19
|
import { createSliceState, DEFAULT_SLICE_POLICY, parseWorkSlice, sliceContinuation, workSliceGuard } from "../sessions/workSlice.js";
|
|
18
20
|
import { DEFAULT_COMMAND_TIMEOUT_SECONDS, withCommandBudget } from "../core/commandBudget.js";
|
|
19
21
|
import { SESSION_NAME, deriveSessionName, sessionNameTool } from "../tools/sessionName.js";
|
|
@@ -50,7 +52,7 @@ async function withParentCancellation(signal, work) {
|
|
|
50
52
|
signal?.removeEventListener('abort', stop);
|
|
51
53
|
}
|
|
52
54
|
}
|
|
53
|
-
export default async function (pi, planRun, getSession) {
|
|
55
|
+
export default async function (pi, planRun, getSession, goal) {
|
|
54
56
|
await terminalCall('startup', { phase: 'Pi extension loaded' });
|
|
55
57
|
let context = null;
|
|
56
58
|
let nativeSubagents = false;
|
|
@@ -190,8 +192,8 @@ export default async function (pi, planRun, getSession) {
|
|
|
190
192
|
// `XINGULAR_WORK_SLICE=off` at launch leaves terminal replies unbounded, as before PROP-0041.
|
|
191
193
|
// One stop-gate state per terminal session; each reply resets it.
|
|
192
194
|
const gateState = createStopGateState();
|
|
193
|
-
// A
|
|
194
|
-
|
|
195
|
+
// A goal-claim check's model request counts with the reply's usage, like any other request.
|
|
196
|
+
const recordCheckUsage = (raw) => {
|
|
195
197
|
const message = raw;
|
|
196
198
|
const reported = message.usage;
|
|
197
199
|
if (!reported)
|
|
@@ -488,20 +490,72 @@ export default async function (pi, planRun, getSession) {
|
|
|
488
490
|
continue: true
|
|
489
491
|
};
|
|
490
492
|
});
|
|
491
|
-
// The stop gate (ADR-0069
|
|
492
|
-
// continuations: a terminal reply that ends with open todo items
|
|
493
|
-
//
|
|
493
|
+
// The stop gate (ADR-0069), after subagent delivery and plan repair so it sees their
|
|
494
|
+
// continuations: a terminal reply that ends with open todo items is sent back up to its cap.
|
|
495
|
+
// Plan replies are not gated.
|
|
494
496
|
if (!launchSwitchOff('XINGULAR_STOP_GATE'))
|
|
495
497
|
stopGate({
|
|
496
498
|
state: gateState,
|
|
497
499
|
active: () => !planRun?.active,
|
|
498
500
|
sliceEnded: () => sliceState.endedBy !== null,
|
|
499
501
|
failed: () => Boolean(failure),
|
|
500
|
-
check: launchSwitchOff('XINGULAR_COMPLETION_CHECK') ? null : sessionCheckModel,
|
|
501
502
|
onEvent: (event) => {
|
|
502
503
|
void terminalCall('notice', { kind: 'notify', level: 'info', extension: STOP_GATE_EXTENSION, text: stopGateNotice(event) }).catch(() => undefined);
|
|
503
504
|
}
|
|
504
505
|
})(pi);
|
|
506
|
+
// A /goal (ADR-0072): while one is active, a reply that would end continues in place. After the
|
|
507
|
+
// stop gate, so open todo items are nudged first; its claim check is the only completion check.
|
|
508
|
+
if (goal) {
|
|
509
|
+
for (const spec of goalTools(() => goal))
|
|
510
|
+
pi.registerTool({
|
|
511
|
+
name: spec.name,
|
|
512
|
+
label: spec.label,
|
|
513
|
+
description: spec.description,
|
|
514
|
+
parameters: spec.parameters(Type),
|
|
515
|
+
async execute(_id, input) {
|
|
516
|
+
const result = await spec.execute(input);
|
|
517
|
+
return { content: [{ type: 'text', text: typeof result === 'string' ? result : result.text }], details: undefined };
|
|
518
|
+
}
|
|
519
|
+
});
|
|
520
|
+
// The goal's tools are offered to the model only while a goal exists.
|
|
521
|
+
const syncGoalTools = (state) => {
|
|
522
|
+
const active = pi.getActiveTools();
|
|
523
|
+
const wanted = state !== null && state.status !== 'complete';
|
|
524
|
+
const has = GOAL_TOOLS.some((name) => active.includes(name));
|
|
525
|
+
if (wanted && !GOAL_TOOLS.every((name) => active.includes(name)))
|
|
526
|
+
pi.setActiveTools([...active, ...GOAL_TOOLS.filter((name) => !active.includes(name))]);
|
|
527
|
+
else if (!wanted && has)
|
|
528
|
+
pi.setActiveTools(active.filter((name) => !GOAL_TOOLS.includes(name)));
|
|
529
|
+
};
|
|
530
|
+
const unsubscribe = goal.subscribe((state) => {
|
|
531
|
+
try {
|
|
532
|
+
pi.appendEntry(GOAL_STATE_ENTRY, { goal: state });
|
|
533
|
+
syncGoalTools(state);
|
|
534
|
+
}
|
|
535
|
+
catch {
|
|
536
|
+
// The session is closing; the next one restores from its own branch.
|
|
537
|
+
}
|
|
538
|
+
});
|
|
539
|
+
pi.on('session_shutdown', () => { unsubscribe(); });
|
|
540
|
+
pi.on('session_start', (_event, ctx) => {
|
|
541
|
+
// A goal is restored from the branch, and waits for the person: nothing resumes by itself.
|
|
542
|
+
const entries = (ctx.sessionManager.getBranch?.() ?? []);
|
|
543
|
+
const saved = [...entries].reverse().find((entry) => entry.type === 'custom' && entry.customType === GOAL_STATE_ENTRY)?.data?.goal ?? null;
|
|
544
|
+
goal.restore(saved && (saved.status === 'active') ? { ...saved, status: 'paused', claimed: false } : saved);
|
|
545
|
+
syncGoalTools(goal.get());
|
|
546
|
+
});
|
|
547
|
+
goalLoop({
|
|
548
|
+
controller: goal,
|
|
549
|
+
check: launchSwitchOff('XINGULAR_COMPLETION_CHECK') ? null : sessionCheckModel,
|
|
550
|
+
active: () => !planRun?.active,
|
|
551
|
+
sliceEnded: () => sliceState.endedBy !== null,
|
|
552
|
+
failed: () => Boolean(failure),
|
|
553
|
+
recordUsage: recordCheckUsage,
|
|
554
|
+
onEvent: (event) => {
|
|
555
|
+
void terminalCall('notice', { kind: 'notify', level: 'info', extension: GOAL_EXTENSION, text: goalNotice(event) }).catch(() => undefined);
|
|
556
|
+
}
|
|
557
|
+
})(pi);
|
|
558
|
+
}
|
|
505
559
|
// After the stop gate, which stays silent at a slice boundary: a reply that did work and reached
|
|
506
560
|
// its budget continues as the next slice of the same run (ADR-0070).
|
|
507
561
|
if (slicePolicy.enabled)
|
|
@@ -510,11 +564,12 @@ export default async function (pi, planRun, getSession) {
|
|
|
510
564
|
state: sliceState,
|
|
511
565
|
active: () => !planRun?.active,
|
|
512
566
|
failed: () => Boolean(failure),
|
|
513
|
-
blocked: () =>
|
|
567
|
+
blocked: () => goal?.get()?.status === 'blocked',
|
|
568
|
+
goalActive: () => goal?.active() === true,
|
|
514
569
|
todo: () => gateState.todo,
|
|
515
|
-
next: () => handoffFields(gateState).next,
|
|
570
|
+
next: () => handoffFields(gateState, goal?.get() ?? null).next,
|
|
516
571
|
onContinue: (ended) => {
|
|
517
|
-
void terminalCall('checkpoint', { summary: `Work slice ${ended.slice} reached its limit after ${ended.steps} steps; the reply continues.`, ...handoffFields(gateState), files: [], checks: [], decisions: [], slice: ended.slice, reason: 'budget' }).catch(() => undefined);
|
|
572
|
+
void terminalCall('checkpoint', { summary: `Work slice ${ended.slice} reached its limit after ${ended.steps} steps; the reply continues.`, ...handoffFields(gateState, goal?.get() ?? null), files: [], checks: [], decisions: [], slice: ended.slice, reason: 'budget' }).catch(() => undefined);
|
|
518
573
|
void terminalCall('notice', { kind: 'notify', level: 'info', extension: STOP_GATE_EXTENSION, text: `Work slice ${ended.slice} reached ${ended.limit ?? 'its limit'}; continuing with slice ${ended.slice + 1} of up to ${slicePolicy.maxSlices}.` }).catch(() => undefined);
|
|
519
574
|
}
|
|
520
575
|
})(pi);
|
|
@@ -725,13 +780,13 @@ export default async function (pi, planRun, getSession) {
|
|
|
725
780
|
// A reply that ended with open todo items is not completed either (ADR-0069): it stopped for the
|
|
726
781
|
// person, or the stop gate's nudges ran out. The harness writes its checkpoint from the todo list.
|
|
727
782
|
const aborted = event.aborted || messageAborted;
|
|
728
|
-
const stop = !sliceState.endedBy && !failure && !aborted && !planRun?.active ? stopOutcome(gateState) : null;
|
|
783
|
+
const stop = !sliceState.endedBy && !failure && !aborted && !planRun?.active ? stopOutcome(gateState, goal?.get() ?? null) : null;
|
|
729
784
|
if (stop) {
|
|
730
|
-
await terminalCall('checkpoint', { summary: stopSummary(stop), ...handoffFields(gateState), files: [], checks: [], decisions: [], slice: sliceState.slice, reason: stopCheckpointReason(stop) }).catch(() => undefined);
|
|
785
|
+
await terminalCall('checkpoint', { summary: stopSummary(stop), ...handoffFields(gateState, goal?.get() ?? null), files: [], checks: [], decisions: [], slice: sliceState.slice, reason: stopCheckpointReason(stop) }).catch(() => undefined);
|
|
731
786
|
}
|
|
732
787
|
else if (sliceState.endedBy && !failure && !aborted) {
|
|
733
788
|
// The guard ended the reply and promised the model the harness would keep its state.
|
|
734
|
-
await terminalCall('checkpoint', { summary: `The reply stopped after ${sliceState.steps} steps at its work-slice boundary.`, ...handoffFields(gateState), files: [], checks: [], decisions: [], slice: sliceState.slice, reason: sliceState.endedBy }).catch(() => undefined);
|
|
789
|
+
await terminalCall('checkpoint', { summary: `The reply stopped after ${sliceState.steps} steps at its work-slice boundary.`, ...handoffFields(gateState, goal?.get() ?? null), files: [], checks: [], decisions: [], slice: sliceState.slice, reason: sliceState.endedBy }).catch(() => undefined);
|
|
735
790
|
}
|
|
736
791
|
const reason = sliceState.endedBy ? 'slice_complete' : stop?.code;
|
|
737
792
|
const code = sliceState.endedBy === 'compaction-thrash' ? 'compaction_thrash' : sliceState.endedBy === 'budget' ? 'slice_budget' : stop?.code;
|
|
@@ -28,6 +28,7 @@ import { isActiveStatus } from "../sessions/contracts.js";
|
|
|
28
28
|
import { findRun, listRuns, loadRun, readEvents, recordedSubagents, saveRun, updateRun } from "../sessions/runStore.js";
|
|
29
29
|
import { combineWorkspaceScope, inspectRunWorkspaceWrite } from "../sessions/workspaceBoundary.js";
|
|
30
30
|
import { TODO_WRITE } from "../tools/todo.js";
|
|
31
|
+
import { GOAL_TOOLS } from "../sessions/goal.js";
|
|
31
32
|
import { DEFAULT_TOOL_NAMES } from "../tools/definitions.js";
|
|
32
33
|
import { harnessTools } from "../tools/harnessTools.js";
|
|
33
34
|
import { HARNESS_PLUGINS } from "../plugins/builtin.js";
|
|
@@ -880,7 +881,7 @@ export class Terminals {
|
|
|
880
881
|
}
|
|
881
882
|
if (inspection.outside)
|
|
882
883
|
return { allowed: false, requiresApproval: true, rememberable: false, standing: 'switch', reason: inspection.reason, workspaceRoot };
|
|
883
|
-
if (name === SESSION_NAME || name === TODO_WRITE || name === SPAWN_SUBAGENT || name === WAIT_SUBAGENTS || READ_ONLY_TOOLS.has(name) || this.worldReadOnly.has(name) || this.options.apps.toolNames.includes(name) || ['bash', 'edit', 'write'].includes(name)) {
|
|
884
|
+
if (name === SESSION_NAME || name === TODO_WRITE || GOAL_TOOLS.includes(name) || name === SPAWN_SUBAGENT || name === WAIT_SUBAGENTS || READ_ONLY_TOOLS.has(name) || this.worldReadOnly.has(name) || this.options.apps.toolNames.includes(name) || ['bash', 'edit', 'write'].includes(name)) {
|
|
884
885
|
return { allowed: true, workspaceRoot };
|
|
885
886
|
}
|
|
886
887
|
return { allowed: false, requiresApproval: true, rememberable: true, reason: 'This native action requires approval.', workspaceRoot };
|
|
@@ -25,6 +25,8 @@ export interface TerminalCommandHost {
|
|
|
25
25
|
todo(): Promise<readonly TodoEntry[]>;
|
|
26
26
|
showTodo(items: readonly TodoEntry[]): void;
|
|
27
27
|
plan(brief: string): void;
|
|
28
|
+
/** Runs `/goal` and returns the text to show (ADR-0072). */
|
|
29
|
+
goal(argument: string): Promise<string>;
|
|
28
30
|
/** Asks a side question in its own tool-free session; empty text dismisses the one showing. */
|
|
29
31
|
btw(question: string): Promise<void>;
|
|
30
32
|
shutdown(): void;
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { GOAL_CUSTOM_TYPE, type GoalController } from "../../sessions/goal.js";
|
|
2
|
+
export interface GoalCommandDeps {
|
|
3
|
+
controller: GoalController;
|
|
4
|
+
/** True when no reply is running. */
|
|
5
|
+
idle(): boolean;
|
|
6
|
+
/** Sends a hidden message and starts a reply with it. */
|
|
7
|
+
start(content: string): Promise<void>;
|
|
8
|
+
/** Sends a hidden message into the running reply. */
|
|
9
|
+
steer(content: string): Promise<void>;
|
|
10
|
+
/** Records a hidden message without starting a reply. */
|
|
11
|
+
note(content: string): Promise<void>;
|
|
12
|
+
/** Stops the running reply. */
|
|
13
|
+
interrupt(): Promise<void>;
|
|
14
|
+
confirm(title: string, message: string): Promise<boolean>;
|
|
15
|
+
input(title: string, value: string): Promise<string | undefined>;
|
|
16
|
+
}
|
|
17
|
+
export { GOAL_CUSTOM_TYPE };
|
|
18
|
+
/** Runs one `/goal` and returns the text to show the person. */
|
|
19
|
+
export declare function runGoalCommand(argument: string, deps: GoalCommandDeps): Promise<string>;
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
// `/goal` in the terminal (ADR-0072): set, view, edit, pause, resume and clear a goal. Adapted from
|
|
2
|
+
// OpenAI Codex (Apache-2.0; see NOTICE.md): codex-rs/tui/src/chatwidget/slash_dispatch.rs and
|
|
3
|
+
// goal_menu.rs. The model never creates a goal: the person does, here.
|
|
4
|
+
import { GOAL_CUSTOM_TYPE, GoalError, continuationPrompt, goalSummary, objectiveUpdatedPrompt, parseGoalCommand, provenanceText } from "../../sessions/goal.js";
|
|
5
|
+
export { GOAL_CUSTOM_TYPE };
|
|
6
|
+
/** Runs one `/goal` and returns the text to show the person. */
|
|
7
|
+
export async function runGoalCommand(argument, deps) {
|
|
8
|
+
const { controller } = deps;
|
|
9
|
+
const command = parseGoalCommand(argument);
|
|
10
|
+
try {
|
|
11
|
+
switch (command.kind) {
|
|
12
|
+
case 'error': return command.message;
|
|
13
|
+
case 'show': return goalSummary(controller.get());
|
|
14
|
+
case 'set': {
|
|
15
|
+
const existing = controller.get();
|
|
16
|
+
if (existing && existing.status !== 'complete') {
|
|
17
|
+
const replace = await deps.confirm('Replace goal?', `The current goal is not finished:\n${existing.objective}\n\nReplace it with the new one and start it now?`);
|
|
18
|
+
if (!replace)
|
|
19
|
+
return 'Kept the current goal.';
|
|
20
|
+
}
|
|
21
|
+
// A reply that is running stops first: the new goal starts from a clean boundary.
|
|
22
|
+
if (!deps.idle())
|
|
23
|
+
await deps.interrupt();
|
|
24
|
+
const goal = controller.set(command.objective, command.limits);
|
|
25
|
+
await deps.start(`${provenanceText('set', goal)}\n\n${continuationPrompt(goal)}`);
|
|
26
|
+
return `Goal active\nObjective: ${goal.objective}`;
|
|
27
|
+
}
|
|
28
|
+
case 'edit': {
|
|
29
|
+
const existing = controller.get();
|
|
30
|
+
if (!existing)
|
|
31
|
+
return 'No goal to edit. Use /goal <objective>.';
|
|
32
|
+
const objective = command.objective ?? await deps.input('Edit goal', existing.objective);
|
|
33
|
+
if (!objective?.trim())
|
|
34
|
+
return 'Goal unchanged.';
|
|
35
|
+
const goal = controller.edit(objective);
|
|
36
|
+
const content = `${provenanceText('edited', goal)}\n\n${objectiveUpdatedPrompt(goal)}`;
|
|
37
|
+
if (deps.idle())
|
|
38
|
+
await deps.start(`${provenanceText('edited', goal)}\n\n${continuationPrompt(goal)}`);
|
|
39
|
+
else
|
|
40
|
+
await deps.steer(content);
|
|
41
|
+
return `Goal updated\nObjective: ${goal.objective}`;
|
|
42
|
+
}
|
|
43
|
+
case 'pause': {
|
|
44
|
+
const goal = controller.get();
|
|
45
|
+
if (!goal)
|
|
46
|
+
return 'No goal to pause.';
|
|
47
|
+
if (!deps.idle())
|
|
48
|
+
await deps.interrupt();
|
|
49
|
+
const paused = controller.pause();
|
|
50
|
+
await deps.note(provenanceText('paused', paused));
|
|
51
|
+
return `Goal ${paused?.status === 'paused' ? 'paused' : `is ${paused?.status.replace('_', ' ')}`}. Use /goal resume to continue.`;
|
|
52
|
+
}
|
|
53
|
+
case 'resume': {
|
|
54
|
+
const goal = controller.get();
|
|
55
|
+
if (!goal)
|
|
56
|
+
return 'No goal to resume.';
|
|
57
|
+
if (goal.status === 'complete')
|
|
58
|
+
return 'The goal is complete. Set a new one with /goal <objective>.';
|
|
59
|
+
const resumed = controller.resume();
|
|
60
|
+
const content = `${provenanceText('resumed', resumed)}\n\n${continuationPrompt(resumed)}`;
|
|
61
|
+
if (deps.idle())
|
|
62
|
+
await deps.start(content);
|
|
63
|
+
else
|
|
64
|
+
await deps.steer(content);
|
|
65
|
+
return 'Goal active';
|
|
66
|
+
}
|
|
67
|
+
case 'clear': {
|
|
68
|
+
if (!controller.get())
|
|
69
|
+
return 'No goal to clear.';
|
|
70
|
+
if (!deps.idle() && controller.active())
|
|
71
|
+
await deps.interrupt();
|
|
72
|
+
controller.clear();
|
|
73
|
+
await deps.note(provenanceText('cleared', null));
|
|
74
|
+
return 'Goal cleared';
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
catch (error) {
|
|
79
|
+
if (error instanceof GoalError)
|
|
80
|
+
return error.message;
|
|
81
|
+
throw error;
|
|
82
|
+
}
|
|
83
|
+
}
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { type AgentSession, type AgentSessionEventListener, type AgentSessionRuntime, type AgentSessionServices, type ExtensionUIContext } from '@earendil-works/pi-coding-agent';
|
|
2
|
+
import { GoalController } from "../../sessions/goal.js";
|
|
2
3
|
import type { SessionAccount } from "../accountsMenu.js";
|
|
3
4
|
import type { QuestionAnswer } from "../../sessions/registry.js";
|
|
4
5
|
import type { TerminalQuestion } from "./questions.js";
|
|
@@ -104,6 +105,8 @@ export interface TerminalRuntime {
|
|
|
104
105
|
installationDeviceId(): Promise<string>;
|
|
105
106
|
reload(): Promise<void>;
|
|
106
107
|
interrupt(): Promise<void>;
|
|
108
|
+
/** This session's /goal (ADR-0072). */
|
|
109
|
+
readonly goal: GoalController;
|
|
107
110
|
dispose(): Promise<void>;
|
|
108
111
|
}
|
|
109
112
|
/** Saved native history stays in the prepared project folder, including symlink targets. */
|
|
@@ -2,6 +2,7 @@ import { createAgentSession, createAgentSessionRuntime, DefaultResourceLoader, M
|
|
|
2
2
|
import { closeSync, existsSync, openSync, readSync, realpathSync } from 'node:fs';
|
|
3
3
|
import { isAbsolute, join, relative, resolve, sep } from 'node:path';
|
|
4
4
|
import terminalExtension from "../piExtension.js";
|
|
5
|
+
import { GoalController } from "../../sessions/goal.js";
|
|
5
6
|
import { terminalCredentialStore } from "../credentialStore.js";
|
|
6
7
|
import { normalModeTools, normalToolsForBranch, planModeTools, PLAN_TOOLS_ENTRY, QUESTION_TOOL_NAMES } from "./planMode.js";
|
|
7
8
|
import { claimTerminalControl, controlCall } from "../control.js";
|
|
@@ -159,6 +160,7 @@ export async function createTerminalRuntime(args, host) {
|
|
|
159
160
|
return sessionUiContext(host.ui, () => !disposed && generation === uiGeneration);
|
|
160
161
|
};
|
|
161
162
|
const planRun = { active: false, allowedTools: new Set() };
|
|
163
|
+
const goal = new GoalController();
|
|
162
164
|
let currentMode = 'normal';
|
|
163
165
|
let pendingApply = false;
|
|
164
166
|
const choiceStore = {
|
|
@@ -223,7 +225,7 @@ export async function createTerminalRuntime(args, host) {
|
|
|
223
225
|
additionalSkillPaths: prepared.skills, additionalPromptTemplatePaths: prepared.prompts,
|
|
224
226
|
additionalThemePaths: prepared.themes,
|
|
225
227
|
extensionFactories: [
|
|
226
|
-
{ name: 'harness-terminal-bridge', factory: pi => terminalExtension(pi, planRun, () => native.session), hidden: true },
|
|
228
|
+
{ name: 'harness-terminal-bridge', factory: pi => terminalExtension(pi, planRun, () => native.session, goal), hidden: true },
|
|
227
229
|
// A model picked in the terminal reaches Pi as this event. A restore is not a pick.
|
|
228
230
|
{ name: 'harness-mode-choices', factory: pi => { pi.on('model_select', event => { if (event.source !== 'restore')
|
|
229
231
|
void modeChoices.record(recordingMode(), { model: `${event.model.provider}/${event.model.id}` }); }); }, hidden: true },
|
|
@@ -462,7 +464,11 @@ export async function createTerminalRuntime(args, host) {
|
|
|
462
464
|
} });
|
|
463
465
|
host.changed();
|
|
464
466
|
}),
|
|
467
|
+
goal,
|
|
465
468
|
async interrupt() {
|
|
469
|
+
// The person stopping a running goal pauses it, as in Codex; /goal resume continues.
|
|
470
|
+
if (goal.active() && !native.session.isIdle)
|
|
471
|
+
goal.pause();
|
|
466
472
|
// Bash executions use their own abort controllers, outside Pi's agent tool loop.
|
|
467
473
|
native.session.abortBash();
|
|
468
474
|
// Socket-backed questions and approvals must settle before Pi can finish aborting tools.
|
|
@@ -19,7 +19,7 @@ export function askUserTool(run) {
|
|
|
19
19
|
name: ASK_USER,
|
|
20
20
|
label: 'Ask the user',
|
|
21
21
|
description: 'Ask the user one to five related questions in one request and wait until they answer the batch. Give each question a concise 1–2 word label and aim to keep each question to 25 words or fewer. ' +
|
|
22
|
-
'Every question must offer 1–6 short choices, each with a helpful description of 25 words or fewer, and at least one choice must be marked recommended: true. Recommendations are visual only: they never preselect an answer, and submitted choices keep their original labels. Mark at most one recommended choice for a single-choice question, or several when multiSelect is true. Every question also includes a free-text “Something else” answer. Set multiSelect when several can apply. When a UI diagram helps compare choices, set uiSketch: true and give every choice a detailed, bounded ASCII wireframe. Use ASCII tables and panel borders to show the affected element with relevant neighboring UI, readable labels and spacing. Keep each panel’s outer `+` corners in fixed columns, include side edges at those same columns on every content row, and run horizontal borders corner to corner; if a title sits in a top border, leave at least two dashes on both sides. Keep framing consistent across alternatives and concentrate detail on what changes. Do not substitute a symbolic logo or one-line illustration. The preview supplies its outer border; draw internal borders only for actual UI panels. Use it when you need a decision, not for small talk.',
|
|
22
|
+
'Every question must offer 1–6 short choices, each with a helpful description of 25 words or fewer, and at least one choice must be marked recommended: true. Recommendations are visual only: they never preselect an answer, and submitted choices keep their original labels. Mark at most one recommended choice for a single-choice question, or several when multiSelect is true. Every question also includes a free-text “Something else” answer. Set multiSelect when several can apply. When a UI diagram helps compare choices, set uiSketch: true and give every choice a detailed, bounded ASCII wireframe. Use ASCII tables and panel borders to show the affected element with relevant neighboring UI, readable labels and spacing. Keep each panel’s outer `+` corners in fixed columns, include side edges at those same columns on every content row, and run horizontal borders corner to corner; if a title sits in a top border, leave at least two dashes on both sides. Keep framing consistent across alternatives and concentrate detail on what changes. Do not substitute a symbolic logo or one-line illustration. The preview supplies its outer border; draw internal borders only for actual UI panels. Use it when you need a decision the request and the files do not answer, not for small talk and never to ask permission for work inside the request. If no answer arrives, continue with your best judgment instead of asking again or treating the turn as blocked.',
|
|
23
23
|
parameters: (Type) => Type.Object({
|
|
24
24
|
questions: Type.Array(Type.Object({
|
|
25
25
|
summary: Type.String({ description: 'A concise label of one or two words' }),
|
package/dist/sdk/tools/todo.js
CHANGED
|
@@ -4,7 +4,8 @@ import { todoItemSchema, MAX_TODO_ITEMS } from "../sessions/contracts.js";
|
|
|
4
4
|
export const TODO_WRITE = 'todo_write';
|
|
5
5
|
/** The trigger the prompt and the tool description share, so progress guidance cannot drift. */
|
|
6
6
|
export const TODO_GUIDANCE = 'If the work has several distinct steps or will take more than one tool call, call todo_write before your first other tool call with the steps you know so far, and keep the list current as steps start and finish. ' +
|
|
7
|
-
'
|
|
7
|
+
'The list shows the person your progress; it is not the scope of the work. The request, and any document it points to, is the scope: add steps as the work shows them, list the steps of a handoff or plan instead of folding them into one "remaining work" item, and never stop because the list is short or done. ' +
|
|
8
|
+
'Keep the list true at all times: mark a step completed only once it is done and checked, and never mark unfinished work completed. An open step is work to do in this reply, not something to report. Before you end the turn, compare the request, not the list, with what is done and checked, and do whatever is still yours to do. ' +
|
|
8
9
|
'The person follows your work from it, and a subagent’s todo list shows as its progress in the person’s list of subagents.';
|
|
9
10
|
/** The event a run writes; `checklist.updated` is the pre-rename name, still read for old runs. */
|
|
10
11
|
const TODO_EVENTS = new Set(['todo.updated', 'checklist.updated']);
|