@xingular/harness 1.3.1-unstable.8 → 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 +122 -5
- package/NOTICE.md +26 -0
- package/builtin/skills/plugin-authoring/SKILL.md +20 -1
- package/dist/cli.js +2424 -1237
- package/dist/sdk/accounts/accounts.d.ts +26 -20
- package/dist/sdk/accounts/accounts.js +100 -63
- package/dist/sdk/agents/builtin.js +2 -0
- package/dist/sdk/commands/registry.d.ts +7 -0
- package/dist/sdk/commands/registry.js +1 -0
- package/dist/sdk/core/commandBudget.js +7 -2
- package/dist/sdk/core/workspaceWriteOperations.d.ts +24 -0
- package/dist/sdk/core/workspaceWriteOperations.js +21 -0
- package/dist/sdk/plugins/builtin.js +0 -4
- 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 -1
- package/dist/sdk/prompt/compile.js +8 -21
- 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/checkpoint.d.ts +7 -0
- package/dist/sdk/sessions/checkpoint.js +41 -0
- package/dist/sdk/sessions/compactionCheckpoint.d.ts +2 -0
- package/dist/sdk/sessions/compactionCheckpoint.js +2 -1
- package/dist/sdk/sessions/completionCheck.d.ts +49 -0
- package/dist/sdk/sessions/completionCheck.js +143 -0
- package/dist/sdk/sessions/continuation.js +1 -1
- package/dist/sdk/sessions/contracts.d.ts +97 -1
- package/dist/sdk/sessions/contracts.js +17 -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/planMode.d.ts +31 -0
- package/dist/sdk/sessions/planMode.js +48 -3
- package/dist/sdk/sessions/registry.d.ts +6 -1
- package/dist/sdk/sessions/registry.js +4 -4
- package/dist/sdk/sessions/runStore.js +1 -0
- package/dist/sdk/sessions/runner.d.ts +9 -0
- package/dist/sdk/sessions/runner.js +65 -13
- package/dist/sdk/sessions/sessions.d.ts +10 -0
- package/dist/sdk/sessions/sessions.js +87 -10
- package/dist/sdk/sessions/stopGate.d.ts +78 -0
- package/dist/sdk/sessions/stopGate.js +143 -0
- package/dist/sdk/sessions/subagentService.d.ts +2 -0
- package/dist/sdk/sessions/subagentService.js +1 -1
- package/dist/sdk/sessions/workSlice.d.ts +61 -12
- package/dist/sdk/sessions/workSlice.js +102 -33
- package/dist/sdk/terminal/controller.d.ts +2 -2
- package/dist/sdk/terminal/controller.js +14 -4
- package/dist/sdk/terminal/credentialStore.d.ts +4 -0
- package/dist/sdk/terminal/credentialStore.js +33 -0
- package/dist/sdk/terminal/piExtension.d.ts +2 -1
- package/dist/sdk/terminal/piExtension.js +160 -27
- package/dist/sdk/terminal/terminals.js +27 -4
- 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/planMode.d.ts +1 -1
- package/dist/sdk/terminal/tui/planMode.js +1 -1
- package/dist/sdk/terminal/tui/runtime.d.ts +3 -0
- package/dist/sdk/terminal/tui/runtime.js +21 -4
- package/dist/sdk/tools/askUser.js +1 -1
- package/dist/sdk/tools/todo.js +2 -1
- package/dist/terminal-opentui.js +1740 -627
- package/dist/terminal-pi.js +996 -407
- package/docs/architecture/02-sessions-and-runs.md +47 -11
- package/docs/architecture/04-prompt.md +83 -16
- package/docs/architecture/06-plugins-and-capabilities.md +16 -1
- package/docs/architecture/07-tools-and-gate.md +7 -2
- package/docs/architecture/09-accounts-and-connectors.md +9 -0
- package/docs/architecture/10-server-protocol.md +13 -0
- package/docs/architecture/11-terminal.md +38 -12
- package/docs/architecture/proposals/PROP-0041-bounded-work-slices.md +2 -0
- package/docs/decisions/ADR-0008-flexible-reporting-guidance.md +3 -0
- package/docs/decisions/ADR-0063-unfinished-work-handoffs.md +2 -0
- package/docs/decisions/ADR-0068-scope-and-completion-report.md +60 -0
- package/docs/decisions/ADR-0069-stop-gate-and-finish.md +130 -0
- package/docs/decisions/ADR-0070-terminal-slice-continuation.md +46 -0
- package/docs/decisions/ADR-0071-completion-check.md +104 -0
- 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.3.1-unstable.9.md +54 -0
- package/docs/releases/1.4.0-unstable.md +18 -0
- package/docs/releases/README.md +1 -1
- package/docs/study/00-index.md +10 -0
- package/docs/study/STUDY-0004-turn-end-controls.md +123 -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
- package/dist/sdk/tools/checkpoint.d.ts +0 -27
- package/dist/sdk/tools/checkpoint.js +0 -154
|
@@ -2,7 +2,8 @@ import { renderCompactionCheckpoint } from "../prompt/compile.js";
|
|
|
2
2
|
import { TODO_WRITE } from "../tools/todo.js";
|
|
3
3
|
import { todoItemSchema, MAX_TODO_ITEMS } from "./contracts.js";
|
|
4
4
|
export const COMPACTION_CHECKPOINT_EXTENSION = 'compaction-checkpoint';
|
|
5
|
-
|
|
5
|
+
/** A todo list as `todo_write` received it, validated; `null` when any item is unusable. */
|
|
6
|
+
export function validatedTodo(value) {
|
|
6
7
|
if (!Array.isArray(value) || value.length > MAX_TODO_ITEMS)
|
|
7
8
|
return null;
|
|
8
9
|
const items = [];
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
export type CheckVerdictKind = 'done' | 'continue';
|
|
2
|
+
export interface CheckVerdict {
|
|
3
|
+
verdict: CheckVerdictKind;
|
|
4
|
+
reason: string;
|
|
5
|
+
/** The next concrete step, for `continue`. */
|
|
6
|
+
next: string;
|
|
7
|
+
}
|
|
8
|
+
/** What the check sends; kept small so a check costs a fraction of a round. */
|
|
9
|
+
export declare const MAX_CHECK_TRANSCRIPT = 24000;
|
|
10
|
+
export declare const CHECK_TIMEOUT_MS = 60000;
|
|
11
|
+
/** The fixed instructions of the check. Answering is all it does: it has no tools. */
|
|
12
|
+
export declare const CHECK_SYSTEM_PROMPT = "You verify whether an AI coding agent has achieved a goal a person gave it. You do not do the work and you have no tools. You read the goal objective and a transcript of what the agent did and said, and you answer with one JSON object and nothing else:\n\n{\"verdict\":\"done\"|\"continue\",\"reason\":\"one or two sentences\",\"next\":\"the next concrete step, for continue\"}\n\nTreat completion as unproven. Derive the concrete requirements from the objective and from any file, plan or spec it points to that the transcript shows. For every explicit requirement, numbered item, named artifact, command, test, gate and deliverable, look in the transcript for evidence that it was achieved and checked: command output, test results, file contents the agent read or wrote. Evidence that is weak, indirect or missing counts as not achieved. The agent saying the goal is complete is not evidence. Match the scope of the check to the scope of the requirement: a narrow check does not support a broad claim.\n\nAnswer \"done\" only when every requirement has evidence in the transcript. Quote the key evidence in \"reason\". Answer \"continue\" when any requirement is unmet, unchecked or unclear; name the unmet requirements in \"reason\" and the next step in \"next\". If the transcript does not contain clear evidence that the objective is met, answer \"continue\" with the reason \"insufficient evidence in transcript\".\n\nJudge only from the objective and the transcript. Keep \"reason\" and \"next\" short and specific.";
|
|
13
|
+
/**
|
|
14
|
+
* The reply's transcript: from the person's request onward, clipped to `limit` with the newest
|
|
15
|
+
* messages kept. The request is found as the last user message that contains its opening text;
|
|
16
|
+
* without it, the tail of the conversation stands in.
|
|
17
|
+
*/
|
|
18
|
+
export declare function replyTranscript(messages: readonly unknown[], request: string, limit?: number): string;
|
|
19
|
+
/** The check's user message. */
|
|
20
|
+
export declare function checkPrompt(objective: string, transcript: string): string;
|
|
21
|
+
/** The verdict in the check's text, or `null` when there is none: the first JSON object that parses. */
|
|
22
|
+
export declare function parseVerdict(text: string): CheckVerdict | null;
|
|
23
|
+
/** A model answer as the check needs it: its text, and what it cost for the run's usage. */
|
|
24
|
+
export interface CheckAnswer {
|
|
25
|
+
text: string;
|
|
26
|
+
/** The provider's message, passed to the usage recorder unchanged. */
|
|
27
|
+
message?: unknown;
|
|
28
|
+
}
|
|
29
|
+
/** Asks a model once. Implemented over Pi's model registry by the caller. */
|
|
30
|
+
export type CheckModel = (system: string, user: string, signal: AbortSignal) => Promise<CheckAnswer>;
|
|
31
|
+
/**
|
|
32
|
+
* Runs one check. Never throws: a timeout, an error or an answer without a verdict returns
|
|
33
|
+
* `{ verdict: null }`, and the claim is accepted as it stands.
|
|
34
|
+
*/
|
|
35
|
+
export declare function runCompletionCheck(ask: CheckModel, input: {
|
|
36
|
+
objective: string;
|
|
37
|
+
messages: readonly unknown[];
|
|
38
|
+
}, options?: {
|
|
39
|
+
signal?: AbortSignal;
|
|
40
|
+
timeoutMs?: number;
|
|
41
|
+
}): Promise<{
|
|
42
|
+
verdict: CheckVerdict | null;
|
|
43
|
+
answer: CheckAnswer | null;
|
|
44
|
+
error?: string;
|
|
45
|
+
}>;
|
|
46
|
+
/** The text of a model's answer message. */
|
|
47
|
+
export declare function answerText(message: unknown): string;
|
|
48
|
+
/** `XINGULAR_STOP_GATE=off` / `XINGULAR_COMPLETION_CHECK=off` at launch switch a layer off, for experiments. The second one accepts every goal claim unchecked. */
|
|
49
|
+
export declare function launchSwitchOff(name: 'XINGULAR_STOP_GATE' | 'XINGULAR_COMPLETION_CHECK'): boolean;
|
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
/** What the check sends; kept small so a check costs a fraction of a round. */
|
|
2
|
+
export const MAX_CHECK_TRANSCRIPT = 24_000;
|
|
3
|
+
const MAX_TOOL_RESULT = 600;
|
|
4
|
+
const MAX_TOOL_ARGS = 300;
|
|
5
|
+
const MAX_TEXT = 2_000;
|
|
6
|
+
export const CHECK_TIMEOUT_MS = 60_000;
|
|
7
|
+
/** The fixed instructions of the check. Answering is all it does: it has no tools. */
|
|
8
|
+
export const CHECK_SYSTEM_PROMPT = `You verify whether an AI coding agent has achieved a goal a person gave it. You do not do the work and you have no tools. You read the goal objective and a transcript of what the agent did and said, and you answer with one JSON object and nothing else:
|
|
9
|
+
|
|
10
|
+
{"verdict":"done"|"continue","reason":"one or two sentences","next":"the next concrete step, for continue"}
|
|
11
|
+
|
|
12
|
+
Treat completion as unproven. Derive the concrete requirements from the objective and from any file, plan or spec it points to that the transcript shows. For every explicit requirement, numbered item, named artifact, command, test, gate and deliverable, look in the transcript for evidence that it was achieved and checked: command output, test results, file contents the agent read or wrote. Evidence that is weak, indirect or missing counts as not achieved. The agent saying the goal is complete is not evidence. Match the scope of the check to the scope of the requirement: a narrow check does not support a broad claim.
|
|
13
|
+
|
|
14
|
+
Answer "done" only when every requirement has evidence in the transcript. Quote the key evidence in "reason". Answer "continue" when any requirement is unmet, unchecked or unclear; name the unmet requirements in "reason" and the next step in "next". If the transcript does not contain clear evidence that the objective is met, answer "continue" with the reason "insufficient evidence in transcript".
|
|
15
|
+
|
|
16
|
+
Judge only from the objective and the transcript. Keep "reason" and "next" short and specific.`;
|
|
17
|
+
const clip = (text, limit) => (text.length <= limit ? text : `${text.slice(0, limit - 1)}…`);
|
|
18
|
+
const textOf = (content) => {
|
|
19
|
+
if (typeof content === 'string')
|
|
20
|
+
return content;
|
|
21
|
+
if (!Array.isArray(content))
|
|
22
|
+
return '';
|
|
23
|
+
return content.filter((part) => part?.type === 'text' && typeof part.text === 'string').map((part) => part.text).join('\n');
|
|
24
|
+
};
|
|
25
|
+
/** One message as the check reads it: visible text, tool calls and clipped tool results. Thinking is left out. */
|
|
26
|
+
function renderMessage(message) {
|
|
27
|
+
if (message.role === 'user') {
|
|
28
|
+
const text = textOf(message.content);
|
|
29
|
+
// The goal's own continuation prompts repeat the same long audit text; the check needs only the objective.
|
|
30
|
+
if (text.startsWith('Continue working toward the active thread goal.'))
|
|
31
|
+
return '';
|
|
32
|
+
return `[User or harness]: ${clip(text, MAX_TEXT)}`;
|
|
33
|
+
}
|
|
34
|
+
if (message.role === 'toolResult')
|
|
35
|
+
return `[Tool result${message.toolName ? ` ${message.toolName}` : ''}${message.isError ? ', error' : ''}]: ${clip(textOf(message.content), MAX_TOOL_RESULT)}`;
|
|
36
|
+
if (message.role === 'assistant') {
|
|
37
|
+
const parts = Array.isArray(message.content) ? message.content : [];
|
|
38
|
+
const lines = [];
|
|
39
|
+
for (const part of parts) {
|
|
40
|
+
if (part?.type === 'text' && part.text)
|
|
41
|
+
lines.push(`[Agent]: ${clip(part.text, MAX_TEXT)}`);
|
|
42
|
+
else if (part?.type === 'toolCall')
|
|
43
|
+
lines.push(`[Agent tool call] ${part.name ?? 'tool'}(${clip(JSON.stringify(part.arguments ?? {}), MAX_TOOL_ARGS)})`);
|
|
44
|
+
}
|
|
45
|
+
return lines.join('\n');
|
|
46
|
+
}
|
|
47
|
+
return '';
|
|
48
|
+
}
|
|
49
|
+
/**
|
|
50
|
+
* The reply's transcript: from the person's request onward, clipped to `limit` with the newest
|
|
51
|
+
* messages kept. The request is found as the last user message that contains its opening text;
|
|
52
|
+
* without it, the tail of the conversation stands in.
|
|
53
|
+
*/
|
|
54
|
+
export function replyTranscript(messages, request, limit = MAX_CHECK_TRANSCRIPT) {
|
|
55
|
+
const list = messages;
|
|
56
|
+
const probe = request.trim().slice(0, 80);
|
|
57
|
+
let start = 0;
|
|
58
|
+
if (probe) {
|
|
59
|
+
for (let index = list.length - 1; index >= 0; index -= 1) {
|
|
60
|
+
if (list[index]?.role === 'user' && textOf(list[index].content).includes(probe)) {
|
|
61
|
+
start = index;
|
|
62
|
+
break;
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
const rendered = list.slice(start).map(renderMessage).filter(Boolean);
|
|
67
|
+
const kept = [];
|
|
68
|
+
let size = 0;
|
|
69
|
+
for (let index = rendered.length - 1; index >= 0; index -= 1) {
|
|
70
|
+
const line = rendered[index];
|
|
71
|
+
if (size + line.length + 1 > limit) {
|
|
72
|
+
kept.unshift('[…earlier messages of this reply omitted…]');
|
|
73
|
+
break;
|
|
74
|
+
}
|
|
75
|
+
kept.unshift(line);
|
|
76
|
+
size += line.length + 1;
|
|
77
|
+
}
|
|
78
|
+
return kept.join('\n');
|
|
79
|
+
}
|
|
80
|
+
/** The check's user message. */
|
|
81
|
+
export function checkPrompt(objective, transcript) {
|
|
82
|
+
return [
|
|
83
|
+
`## The goal's objective\n${clip(objective.trim() || '(not recorded)', 4_000)}`,
|
|
84
|
+
`## What the agent did\n${transcript || '(nothing recorded)'}`,
|
|
85
|
+
'## Your answer\nOne JSON object: verdict, reason, next.'
|
|
86
|
+
].join('\n\n');
|
|
87
|
+
}
|
|
88
|
+
/** The verdict in the check's text, or `null` when there is none: the first JSON object that parses. */
|
|
89
|
+
export function parseVerdict(text) {
|
|
90
|
+
const starts = [];
|
|
91
|
+
for (let index = text.indexOf('{'); index >= 0; index = text.indexOf('{', index + 1))
|
|
92
|
+
starts.push(index);
|
|
93
|
+
for (const start of starts) {
|
|
94
|
+
for (let end = text.indexOf('}', start); end >= 0; end = text.indexOf('}', end + 1)) {
|
|
95
|
+
try {
|
|
96
|
+
const value = JSON.parse(text.slice(start, end + 1));
|
|
97
|
+
const verdict = value.verdict;
|
|
98
|
+
if (verdict !== 'done' && verdict !== 'continue')
|
|
99
|
+
continue;
|
|
100
|
+
return {
|
|
101
|
+
verdict,
|
|
102
|
+
reason: clip(typeof value.reason === 'string' ? value.reason.trim() : '', 500),
|
|
103
|
+
next: clip(typeof value.next === 'string' ? value.next.trim() : '', 500)
|
|
104
|
+
};
|
|
105
|
+
}
|
|
106
|
+
catch {
|
|
107
|
+
// Not a complete object yet; try a longer span.
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
return null;
|
|
112
|
+
}
|
|
113
|
+
/**
|
|
114
|
+
* Runs one check. Never throws: a timeout, an error or an answer without a verdict returns
|
|
115
|
+
* `{ verdict: null }`, and the claim is accepted as it stands.
|
|
116
|
+
*/
|
|
117
|
+
export async function runCompletionCheck(ask, input, options = {}) {
|
|
118
|
+
const controller = new AbortController();
|
|
119
|
+
const abort = () => controller.abort();
|
|
120
|
+
options.signal?.addEventListener('abort', abort, { once: true });
|
|
121
|
+
const timer = setTimeout(abort, options.timeoutMs ?? CHECK_TIMEOUT_MS);
|
|
122
|
+
try {
|
|
123
|
+
const user = checkPrompt(input.objective, replyTranscript(input.messages, ''));
|
|
124
|
+
const answer = await ask(CHECK_SYSTEM_PROMPT, user, controller.signal);
|
|
125
|
+
return { verdict: parseVerdict(answer.text), answer };
|
|
126
|
+
}
|
|
127
|
+
catch (error) {
|
|
128
|
+
return { verdict: null, answer: null, error: controller.signal.aborted ? 'the completion check timed out' : error instanceof Error ? error.message : String(error) };
|
|
129
|
+
}
|
|
130
|
+
finally {
|
|
131
|
+
clearTimeout(timer);
|
|
132
|
+
options.signal?.removeEventListener('abort', abort);
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
/** The text of a model's answer message. */
|
|
136
|
+
export function answerText(message) {
|
|
137
|
+
return textOf(message?.content);
|
|
138
|
+
}
|
|
139
|
+
/** `XINGULAR_STOP_GATE=off` / `XINGULAR_COMPLETION_CHECK=off` at launch switch a layer off, for experiments. The second one accepts every goal claim unchecked. */
|
|
140
|
+
export function launchSwitchOff(name) {
|
|
141
|
+
const value = process.env[name]?.trim().toLowerCase();
|
|
142
|
+
return value === 'off' || value === '0' || value === 'false';
|
|
143
|
+
}
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
// run with `continueFrom`, tells the model where the earlier one stopped.
|
|
4
4
|
import { loadRun as loadCapabilities } from "../capabilities/runSnapshot.js";
|
|
5
5
|
import { todoOf, describeTodo } from "../tools/todo.js";
|
|
6
|
-
import { checkpointOf, describeCheckpoint } from "
|
|
6
|
+
import { checkpointOf, describeCheckpoint } from "./checkpoint.js";
|
|
7
7
|
import { isActiveStatus } from "./contracts.js";
|
|
8
8
|
import { findHistory } from "./history.js";
|
|
9
9
|
import { findRun, pendingDecisions, readEvents, recordedSubagents } from "./runStore.js";
|
|
@@ -310,6 +310,8 @@ export declare const runStopCodeSchema: z.ZodEnum<{
|
|
|
310
310
|
user_abort: "user_abort";
|
|
311
311
|
slice_budget: "slice_budget";
|
|
312
312
|
compaction_thrash: "compaction_thrash";
|
|
313
|
+
needs_person: "needs_person";
|
|
314
|
+
unfinished: "unfinished";
|
|
313
315
|
provider_error: "provider_error";
|
|
314
316
|
stalled: "stalled";
|
|
315
317
|
owner_gone: "owner_gone";
|
|
@@ -690,6 +692,8 @@ export declare const runEventPayloadSchemas: {
|
|
|
690
692
|
user_abort: "user_abort";
|
|
691
693
|
slice_budget: "slice_budget";
|
|
692
694
|
compaction_thrash: "compaction_thrash";
|
|
695
|
+
needs_person: "needs_person";
|
|
696
|
+
unfinished: "unfinished";
|
|
693
697
|
provider_error: "provider_error";
|
|
694
698
|
stalled: "stalled";
|
|
695
699
|
owner_gone: "owner_gone";
|
|
@@ -754,6 +758,44 @@ export type RunEvent = z.infer<typeof runEventSchema>;
|
|
|
754
758
|
* the harness stores and returns them, and reads only `mode` (`chat` is an agent's own
|
|
755
759
|
* conversation, `code` is a session in a project).
|
|
756
760
|
*/
|
|
761
|
+
/** A run's /goal as the run record keeps it (ADR-0072). */
|
|
762
|
+
export interface GoalModeRecord {
|
|
763
|
+
goalId: string;
|
|
764
|
+
objective: string;
|
|
765
|
+
status: 'active' | 'paused' | 'blocked' | 'usage_limited' | 'budget_limited' | 'complete';
|
|
766
|
+
limits: {
|
|
767
|
+
rounds: number;
|
|
768
|
+
hours: number;
|
|
769
|
+
};
|
|
770
|
+
rounds: number;
|
|
771
|
+
timeUsedSeconds: number;
|
|
772
|
+
createdAt: number;
|
|
773
|
+
updatedAt: number;
|
|
774
|
+
claimed: boolean;
|
|
775
|
+
lastReason: string;
|
|
776
|
+
}
|
|
777
|
+
export declare const goalModeSchema: z.ZodObject<{
|
|
778
|
+
goalId: z.ZodString;
|
|
779
|
+
objective: z.ZodString;
|
|
780
|
+
status: z.ZodEnum<{
|
|
781
|
+
blocked: "blocked";
|
|
782
|
+
active: "active";
|
|
783
|
+
paused: "paused";
|
|
784
|
+
usage_limited: "usage_limited";
|
|
785
|
+
budget_limited: "budget_limited";
|
|
786
|
+
complete: "complete";
|
|
787
|
+
}>;
|
|
788
|
+
limits: z.ZodObject<{
|
|
789
|
+
rounds: z.ZodNumber;
|
|
790
|
+
hours: z.ZodNumber;
|
|
791
|
+
}, z.core.$strip>;
|
|
792
|
+
rounds: z.ZodNumber;
|
|
793
|
+
timeUsedSeconds: z.ZodNumber;
|
|
794
|
+
createdAt: z.ZodNumber;
|
|
795
|
+
updatedAt: z.ZodNumber;
|
|
796
|
+
claimed: z.ZodBoolean;
|
|
797
|
+
lastReason: z.ZodString;
|
|
798
|
+
}, z.core.$strip>;
|
|
757
799
|
export interface RunRecord {
|
|
758
800
|
resumedFromRunId: string | null;
|
|
759
801
|
/** Opaque run lineage supplied by the embedding application. */
|
|
@@ -776,6 +818,8 @@ export interface RunRecord {
|
|
|
776
818
|
workspaceRoot?: string | null;
|
|
777
819
|
worktreeId: string | null;
|
|
778
820
|
goal: string | null;
|
|
821
|
+
/** The run's /goal (ADR-0072): its objective, status, rounds and time. Absent for a run without one. */
|
|
822
|
+
goalMode?: GoalModeRecord | null;
|
|
779
823
|
attachmentCount: number;
|
|
780
824
|
startedAt: string;
|
|
781
825
|
endedAt: string | null;
|
|
@@ -826,6 +870,8 @@ export declare const runRecordSchema: z.ZodObject<{
|
|
|
826
870
|
user_abort: "user_abort";
|
|
827
871
|
slice_budget: "slice_budget";
|
|
828
872
|
compaction_thrash: "compaction_thrash";
|
|
873
|
+
needs_person: "needs_person";
|
|
874
|
+
unfinished: "unfinished";
|
|
829
875
|
provider_error: "provider_error";
|
|
830
876
|
stalled: "stalled";
|
|
831
877
|
owner_gone: "owner_gone";
|
|
@@ -833,7 +879,7 @@ export declare const runRecordSchema: z.ZodObject<{
|
|
|
833
879
|
terminal_attention: "terminal_attention";
|
|
834
880
|
terminal_closed: "terminal_closed";
|
|
835
881
|
parent_stopped: "parent_stopped";
|
|
836
|
-
}>>>, z.ZodTransform<"completed" | "failed" | "user_abort" | "slice_budget" | "compaction_thrash" | "provider_error" | "stalled" | "owner_gone" | "harness_restart" | "terminal_attention" | "terminal_closed" | "parent_stopped" | null, "completed" | "failed" | "user_abort" | "slice_budget" | "compaction_thrash" | "provider_error" | "stalled" | "owner_gone" | "harness_restart" | "terminal_attention" | "terminal_closed" | "parent_stopped" | null | undefined>>;
|
|
882
|
+
}>>>, z.ZodTransform<"completed" | "failed" | "user_abort" | "slice_budget" | "compaction_thrash" | "needs_person" | "unfinished" | "provider_error" | "stalled" | "owner_gone" | "harness_restart" | "terminal_attention" | "terminal_closed" | "parent_stopped" | null, "completed" | "failed" | "user_abort" | "slice_budget" | "compaction_thrash" | "needs_person" | "unfinished" | "provider_error" | "stalled" | "owner_gone" | "harness_restart" | "terminal_attention" | "terminal_closed" | "parent_stopped" | null | undefined>>;
|
|
837
883
|
id: z.ZodString;
|
|
838
884
|
projectId: z.ZodPipe<z.ZodOptional<z.ZodNullable<z.ZodString>>, z.ZodTransform<string | null, string | null | undefined>>;
|
|
839
885
|
mode: z.ZodString;
|
|
@@ -849,6 +895,56 @@ export declare const runRecordSchema: z.ZodObject<{
|
|
|
849
895
|
workspaceRoot: z.ZodOptional<z.ZodPipe<z.ZodOptional<z.ZodNullable<z.ZodString>>, z.ZodTransform<string | null, string | null | undefined>>>;
|
|
850
896
|
worktreeId: z.ZodPipe<z.ZodOptional<z.ZodNullable<z.ZodString>>, z.ZodTransform<string | null, string | null | undefined>>;
|
|
851
897
|
goal: z.ZodPipe<z.ZodOptional<z.ZodNullable<z.ZodString>>, z.ZodTransform<string | null, string | null | undefined>>;
|
|
898
|
+
goalMode: z.ZodOptional<z.ZodPipe<z.ZodOptional<z.ZodNullable<z.ZodObject<{
|
|
899
|
+
goalId: z.ZodString;
|
|
900
|
+
objective: z.ZodString;
|
|
901
|
+
status: z.ZodEnum<{
|
|
902
|
+
blocked: "blocked";
|
|
903
|
+
active: "active";
|
|
904
|
+
paused: "paused";
|
|
905
|
+
usage_limited: "usage_limited";
|
|
906
|
+
budget_limited: "budget_limited";
|
|
907
|
+
complete: "complete";
|
|
908
|
+
}>;
|
|
909
|
+
limits: z.ZodObject<{
|
|
910
|
+
rounds: z.ZodNumber;
|
|
911
|
+
hours: z.ZodNumber;
|
|
912
|
+
}, z.core.$strip>;
|
|
913
|
+
rounds: z.ZodNumber;
|
|
914
|
+
timeUsedSeconds: z.ZodNumber;
|
|
915
|
+
createdAt: z.ZodNumber;
|
|
916
|
+
updatedAt: z.ZodNumber;
|
|
917
|
+
claimed: z.ZodBoolean;
|
|
918
|
+
lastReason: z.ZodString;
|
|
919
|
+
}, z.core.$strip>>>, z.ZodTransform<{
|
|
920
|
+
goalId: string;
|
|
921
|
+
objective: string;
|
|
922
|
+
status: "blocked" | "active" | "paused" | "usage_limited" | "budget_limited" | "complete";
|
|
923
|
+
limits: {
|
|
924
|
+
rounds: number;
|
|
925
|
+
hours: number;
|
|
926
|
+
};
|
|
927
|
+
rounds: number;
|
|
928
|
+
timeUsedSeconds: number;
|
|
929
|
+
createdAt: number;
|
|
930
|
+
updatedAt: number;
|
|
931
|
+
claimed: boolean;
|
|
932
|
+
lastReason: string;
|
|
933
|
+
} | null, {
|
|
934
|
+
goalId: string;
|
|
935
|
+
objective: string;
|
|
936
|
+
status: "blocked" | "active" | "paused" | "usage_limited" | "budget_limited" | "complete";
|
|
937
|
+
limits: {
|
|
938
|
+
rounds: number;
|
|
939
|
+
hours: number;
|
|
940
|
+
};
|
|
941
|
+
rounds: number;
|
|
942
|
+
timeUsedSeconds: number;
|
|
943
|
+
createdAt: number;
|
|
944
|
+
updatedAt: number;
|
|
945
|
+
claimed: boolean;
|
|
946
|
+
lastReason: string;
|
|
947
|
+
} | null | undefined>>>;
|
|
852
948
|
attachmentCount: z.ZodDefault<z.ZodNumber>;
|
|
853
949
|
startedAt: z.ZodString;
|
|
854
950
|
endedAt: z.ZodPipe<z.ZodOptional<z.ZodNullable<z.ZodString>>, z.ZodTransform<string | null, string | null | undefined>>;
|
|
@@ -382,6 +382,10 @@ export const runStopCodeSchema = z.enum([
|
|
|
382
382
|
'slice_budget',
|
|
383
383
|
/** Three compactions in one reply ended it. */
|
|
384
384
|
'compaction_thrash',
|
|
385
|
+
/** The completion check found that the work left needs the person (ADR-0071). */
|
|
386
|
+
'needs_person',
|
|
387
|
+
/** The reply ended with work left after the stop gate's nudges ran out or produced no work (ADR-0069, ADR-0071). */
|
|
388
|
+
'unfinished',
|
|
385
389
|
/** The model request failed (auth, quota, rate limit, network, unsupported model). */
|
|
386
390
|
'provider_error',
|
|
387
391
|
/** The run stopped reporting progress or heartbeats. */
|
|
@@ -461,6 +465,18 @@ export const runEventSchema = z
|
|
|
461
465
|
context.addIssue({ code: 'custom', path: ['payload'], message: `Invalid payload for ${event.type}` });
|
|
462
466
|
}
|
|
463
467
|
});
|
|
468
|
+
export const goalModeSchema = z.object({
|
|
469
|
+
goalId: z.string(),
|
|
470
|
+
objective: z.string(),
|
|
471
|
+
status: z.enum(['active', 'paused', 'blocked', 'usage_limited', 'budget_limited', 'complete']),
|
|
472
|
+
limits: z.object({ rounds: z.number().int().positive(), hours: z.number().positive() }),
|
|
473
|
+
rounds: z.number().int().nonnegative(),
|
|
474
|
+
timeUsedSeconds: z.number().nonnegative(),
|
|
475
|
+
createdAt: z.number(),
|
|
476
|
+
updatedAt: z.number(),
|
|
477
|
+
claimed: z.boolean(),
|
|
478
|
+
lastReason: z.string()
|
|
479
|
+
});
|
|
464
480
|
const text = z.string().nullish().transform((value) => value ?? null);
|
|
465
481
|
export const runRecordSchema = z.object({
|
|
466
482
|
resumedFromRunId: text,
|
|
@@ -480,6 +496,7 @@ export const runRecordSchema = z.object({
|
|
|
480
496
|
workspaceRoot: text.optional(),
|
|
481
497
|
worktreeId: text,
|
|
482
498
|
goal: text,
|
|
499
|
+
goalMode: goalModeSchema.nullish().transform((value) => value ?? null).optional(),
|
|
483
500
|
attachmentCount: z.number().int().nonnegative().default(0),
|
|
484
501
|
startedAt: z.string(),
|
|
485
502
|
endedAt: text,
|
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
import type { ToolSpec } from "../tools/types.js";
|
|
2
|
+
export declare const GET_GOAL = "get_goal";
|
|
3
|
+
export declare const UPDATE_GOAL = "update_goal";
|
|
4
|
+
export declare const GOAL_TOOLS: readonly ["get_goal", "update_goal"];
|
|
5
|
+
export declare const GOAL_CUSTOM_TYPE = "xingular-goal";
|
|
6
|
+
export declare const GOAL_STATE_ENTRY = "xingular-goal-state";
|
|
7
|
+
/** Codex limits a goal objective to this many characters. */
|
|
8
|
+
export declare const MAX_GOAL_OBJECTIVE = 4000;
|
|
9
|
+
export declare const DEFAULT_GOAL_ROUNDS = 30;
|
|
10
|
+
export declare const DEFAULT_GOAL_HOURS = 8;
|
|
11
|
+
/** Consecutive automatic rounds without activity, or with only failed commands, before a goal is blocked. */
|
|
12
|
+
export declare const GOAL_STRIKES = 3;
|
|
13
|
+
export type GoalStatus = 'active' | 'paused' | 'blocked' | 'usage_limited' | 'budget_limited' | 'complete';
|
|
14
|
+
export interface GoalLimits {
|
|
15
|
+
rounds: number;
|
|
16
|
+
hours: number;
|
|
17
|
+
}
|
|
18
|
+
export interface GoalState {
|
|
19
|
+
/** Changes when a goal is replaced, kept when its objective is edited. */
|
|
20
|
+
goalId: string;
|
|
21
|
+
objective: string;
|
|
22
|
+
status: GoalStatus;
|
|
23
|
+
limits: GoalLimits;
|
|
24
|
+
/** Automatic continuations started so far. */
|
|
25
|
+
rounds: number;
|
|
26
|
+
timeUsedSeconds: number;
|
|
27
|
+
createdAt: number;
|
|
28
|
+
updatedAt: number;
|
|
29
|
+
/** The model called update_goal(complete) and the harness has not checked the claim yet. */
|
|
30
|
+
claimed: boolean;
|
|
31
|
+
/** The latest verdict on a completion claim, while it matters. */
|
|
32
|
+
lastReason: string;
|
|
33
|
+
}
|
|
34
|
+
/** Thrown for a call the model may not make; the message goes back to the model as the tool result. */
|
|
35
|
+
export declare class GoalError extends Error {
|
|
36
|
+
}
|
|
37
|
+
export declare const withLimits: (limits?: Partial<GoalLimits>) => GoalLimits;
|
|
38
|
+
/** `59s`, `1m`, `1h 30m`, `2d 23h 42m`: Codex's elapsed-time format. */
|
|
39
|
+
export declare function formatElapsed(seconds: number): string;
|
|
40
|
+
export declare const continuationPrompt: (goal: GoalState) => string;
|
|
41
|
+
export declare const budgetLimitPrompt: (goal: GoalState) => string;
|
|
42
|
+
export declare const objectiveUpdatedPrompt: (goal: GoalState) => string;
|
|
43
|
+
/** The goal as the model's tools report it. */
|
|
44
|
+
export declare function goalReport(goal: GoalState | null): Record<string, unknown>;
|
|
45
|
+
/** The text a goal-aware session shows after it ends in `complete`. */
|
|
46
|
+
export declare const completionReport: (goal: GoalState) => string;
|
|
47
|
+
/** What one finished round did, as the loop saw it. */
|
|
48
|
+
export interface RoundOutcome {
|
|
49
|
+
/** The round produced text, reasoning, or a tool call. */
|
|
50
|
+
activity: boolean;
|
|
51
|
+
/** A command failed and no tool succeeded in the round. */
|
|
52
|
+
failedCommands: boolean;
|
|
53
|
+
/** The round ended with a provider error (not a usage limit). */
|
|
54
|
+
error?: boolean;
|
|
55
|
+
/** The round ended with a usage-limit error. */
|
|
56
|
+
usageLimit?: boolean;
|
|
57
|
+
}
|
|
58
|
+
export interface GoalControllerOptions {
|
|
59
|
+
now?: () => number;
|
|
60
|
+
/** Called after every change with the new state (null once cleared), to persist and show it. */
|
|
61
|
+
onChange?: (state: GoalState | null) => void;
|
|
62
|
+
}
|
|
63
|
+
/**
|
|
64
|
+
* One session's goal. All changes go through it, so the status rules live in one place:
|
|
65
|
+
* the model may only claim `complete`, report `blocked` or, on the person's word, `paused`;
|
|
66
|
+
* the person sets, edits, pauses, resumes and clears; the system alone limits.
|
|
67
|
+
*/
|
|
68
|
+
export declare class GoalController {
|
|
69
|
+
private readonly options;
|
|
70
|
+
private state;
|
|
71
|
+
private idleStrikes;
|
|
72
|
+
private failureStrikes;
|
|
73
|
+
private startedAt;
|
|
74
|
+
private readonly now;
|
|
75
|
+
private readonly listeners;
|
|
76
|
+
constructor(options?: GoalControllerOptions);
|
|
77
|
+
get(): GoalState | null;
|
|
78
|
+
active(): boolean;
|
|
79
|
+
/** Restores a persisted goal; the in-memory strike counters start fresh. */
|
|
80
|
+
restore(state: GoalState | null): void;
|
|
81
|
+
/** Hears every change (a persisted copy, the footer, the model's tools). Returns the way to stop. */
|
|
82
|
+
subscribe(listener: (state: GoalState | null) => void): () => void;
|
|
83
|
+
private changed;
|
|
84
|
+
private touch;
|
|
85
|
+
/** The person sets a goal. Replaces a finished one; the caller confirms replacing an unfinished one. */
|
|
86
|
+
set(objective: string, limits?: Partial<GoalLimits>): GoalState;
|
|
87
|
+
/** The person edits the objective: the same goal, its usage kept. A finished goal becomes active again. */
|
|
88
|
+
edit(objective: string): GoalState;
|
|
89
|
+
pause(): GoalState | null;
|
|
90
|
+
resume(): GoalState;
|
|
91
|
+
clear(): boolean;
|
|
92
|
+
/** A round (a turn charged to the goal) begins. */
|
|
93
|
+
beginRound(): void;
|
|
94
|
+
/** Charges the round's wall time to the goal. Called at every stop, so an idle gap is not billed. */
|
|
95
|
+
endRound(): void;
|
|
96
|
+
/** The model's `update_goal`. Returns the tool result. */
|
|
97
|
+
update(status: string): {
|
|
98
|
+
text: string;
|
|
99
|
+
goal: GoalState;
|
|
100
|
+
};
|
|
101
|
+
/** The claim was checked and holds. */
|
|
102
|
+
accept(reason?: string): GoalState;
|
|
103
|
+
/** The claim was checked and does not hold: the goal stays active with the reason. */
|
|
104
|
+
reject(reason: string): GoalState;
|
|
105
|
+
/** The system limits the goal. */
|
|
106
|
+
limit(status: 'budget_limited' | 'usage_limited' | 'blocked', reason?: string): GoalState;
|
|
107
|
+
/** Counts one automatic round; returns the status the system moved the goal to, if any. */
|
|
108
|
+
settleRound(outcome: RoundOutcome): GoalStatus | null;
|
|
109
|
+
/** True once the goal has used its rounds or hours. */
|
|
110
|
+
overLimit(): boolean;
|
|
111
|
+
/** Starts the next automatic round. */
|
|
112
|
+
nextRound(): GoalState;
|
|
113
|
+
}
|
|
114
|
+
export declare function goalTools(goal: () => GoalController | null): ToolSpec[];
|
|
115
|
+
export type GoalCommand = {
|
|
116
|
+
kind: 'show';
|
|
117
|
+
} | {
|
|
118
|
+
kind: 'set';
|
|
119
|
+
objective: string;
|
|
120
|
+
limits: Partial<GoalLimits>;
|
|
121
|
+
} | {
|
|
122
|
+
kind: 'edit';
|
|
123
|
+
objective: string | null;
|
|
124
|
+
} | {
|
|
125
|
+
kind: 'pause';
|
|
126
|
+
} | {
|
|
127
|
+
kind: 'resume';
|
|
128
|
+
} | {
|
|
129
|
+
kind: 'clear';
|
|
130
|
+
} | {
|
|
131
|
+
kind: 'error';
|
|
132
|
+
message: string;
|
|
133
|
+
};
|
|
134
|
+
export declare const GOAL_USAGE = "Usage: /goal [<objective>|edit|pause|resume|clear]";
|
|
135
|
+
/**
|
|
136
|
+
* Parses the argument of `/goal`. Subcommands match only as the whole text, so "pause now" is
|
|
137
|
+
* an objective. `--rounds N` and `--hours H` before the objective set this goal's caps.
|
|
138
|
+
*/
|
|
139
|
+
export declare function parseGoalCommand(argument: string): GoalCommand;
|
|
140
|
+
/** The text of a bare `/goal`. */
|
|
141
|
+
export declare function goalSummary(goal: GoalState | null): string;
|
|
142
|
+
/** The footer label (Codex's wording, with rounds in place of tokens). */
|
|
143
|
+
export declare function goalFooter(goal: GoalState | null): string | null;
|
|
144
|
+
/** The hidden message that records who changed the goal, so a pause can be checked as the person's. */
|
|
145
|
+
export declare function provenanceText(action: 'set' | 'paused' | 'cleared' | 'resumed' | 'edited', goal: GoalState | null): string;
|