@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.
Files changed (101) hide show
  1. package/CHANGELOG.md +122 -5
  2. package/NOTICE.md +26 -0
  3. package/builtin/skills/plugin-authoring/SKILL.md +20 -1
  4. package/dist/cli.js +2424 -1237
  5. package/dist/sdk/accounts/accounts.d.ts +26 -20
  6. package/dist/sdk/accounts/accounts.js +100 -63
  7. package/dist/sdk/agents/builtin.js +2 -0
  8. package/dist/sdk/commands/registry.d.ts +7 -0
  9. package/dist/sdk/commands/registry.js +1 -0
  10. package/dist/sdk/core/commandBudget.js +7 -2
  11. package/dist/sdk/core/workspaceWriteOperations.d.ts +24 -0
  12. package/dist/sdk/core/workspaceWriteOperations.js +21 -0
  13. package/dist/sdk/plugins/builtin.js +0 -4
  14. package/dist/sdk/plugins/discover.js +80 -4
  15. package/dist/sdk/plugins/install.js +26 -0
  16. package/dist/sdk/plugins/manifest.d.ts +2 -0
  17. package/dist/sdk/plugins/manifest.js +20 -1
  18. package/dist/sdk/prompt/compile.d.ts +4 -1
  19. package/dist/sdk/prompt/compile.js +8 -21
  20. package/dist/sdk/prompt/reporting.d.ts +2 -0
  21. package/dist/sdk/prompt/reporting.js +282 -0
  22. package/dist/sdk/prompt/standard.d.ts +12 -0
  23. package/dist/sdk/prompt/standard.js +53 -2
  24. package/dist/sdk/sessions/checkpoint.d.ts +7 -0
  25. package/dist/sdk/sessions/checkpoint.js +41 -0
  26. package/dist/sdk/sessions/compactionCheckpoint.d.ts +2 -0
  27. package/dist/sdk/sessions/compactionCheckpoint.js +2 -1
  28. package/dist/sdk/sessions/completionCheck.d.ts +49 -0
  29. package/dist/sdk/sessions/completionCheck.js +143 -0
  30. package/dist/sdk/sessions/continuation.js +1 -1
  31. package/dist/sdk/sessions/contracts.d.ts +97 -1
  32. package/dist/sdk/sessions/contracts.js +17 -0
  33. package/dist/sdk/sessions/goal.d.ts +145 -0
  34. package/dist/sdk/sessions/goal.js +405 -0
  35. package/dist/sdk/sessions/goalLoop.d.ts +37 -0
  36. package/dist/sdk/sessions/goalLoop.js +125 -0
  37. package/dist/sdk/sessions/goalPrompts.d.ts +4 -0
  38. package/dist/sdk/sessions/goalPrompts.js +93 -0
  39. package/dist/sdk/sessions/planMode.d.ts +31 -0
  40. package/dist/sdk/sessions/planMode.js +48 -3
  41. package/dist/sdk/sessions/registry.d.ts +6 -1
  42. package/dist/sdk/sessions/registry.js +4 -4
  43. package/dist/sdk/sessions/runStore.js +1 -0
  44. package/dist/sdk/sessions/runner.d.ts +9 -0
  45. package/dist/sdk/sessions/runner.js +65 -13
  46. package/dist/sdk/sessions/sessions.d.ts +10 -0
  47. package/dist/sdk/sessions/sessions.js +87 -10
  48. package/dist/sdk/sessions/stopGate.d.ts +78 -0
  49. package/dist/sdk/sessions/stopGate.js +143 -0
  50. package/dist/sdk/sessions/subagentService.d.ts +2 -0
  51. package/dist/sdk/sessions/subagentService.js +1 -1
  52. package/dist/sdk/sessions/workSlice.d.ts +61 -12
  53. package/dist/sdk/sessions/workSlice.js +102 -33
  54. package/dist/sdk/terminal/controller.d.ts +2 -2
  55. package/dist/sdk/terminal/controller.js +14 -4
  56. package/dist/sdk/terminal/credentialStore.d.ts +4 -0
  57. package/dist/sdk/terminal/credentialStore.js +33 -0
  58. package/dist/sdk/terminal/piExtension.d.ts +2 -1
  59. package/dist/sdk/terminal/piExtension.js +160 -27
  60. package/dist/sdk/terminal/terminals.js +27 -4
  61. package/dist/sdk/terminal/tui/commands.d.ts +2 -0
  62. package/dist/sdk/terminal/tui/commands.js +3 -0
  63. package/dist/sdk/terminal/tui/goalCommand.d.ts +19 -0
  64. package/dist/sdk/terminal/tui/goalCommand.js +83 -0
  65. package/dist/sdk/terminal/tui/planMode.d.ts +1 -1
  66. package/dist/sdk/terminal/tui/planMode.js +1 -1
  67. package/dist/sdk/terminal/tui/runtime.d.ts +3 -0
  68. package/dist/sdk/terminal/tui/runtime.js +21 -4
  69. package/dist/sdk/tools/askUser.js +1 -1
  70. package/dist/sdk/tools/todo.js +2 -1
  71. package/dist/terminal-opentui.js +1740 -627
  72. package/dist/terminal-pi.js +996 -407
  73. package/docs/architecture/02-sessions-and-runs.md +47 -11
  74. package/docs/architecture/04-prompt.md +83 -16
  75. package/docs/architecture/06-plugins-and-capabilities.md +16 -1
  76. package/docs/architecture/07-tools-and-gate.md +7 -2
  77. package/docs/architecture/09-accounts-and-connectors.md +9 -0
  78. package/docs/architecture/10-server-protocol.md +13 -0
  79. package/docs/architecture/11-terminal.md +38 -12
  80. package/docs/architecture/proposals/PROP-0041-bounded-work-slices.md +2 -0
  81. package/docs/decisions/ADR-0008-flexible-reporting-guidance.md +3 -0
  82. package/docs/decisions/ADR-0063-unfinished-work-handoffs.md +2 -0
  83. package/docs/decisions/ADR-0068-scope-and-completion-report.md +60 -0
  84. package/docs/decisions/ADR-0069-stop-gate-and-finish.md +130 -0
  85. package/docs/decisions/ADR-0070-terminal-slice-continuation.md +46 -0
  86. package/docs/decisions/ADR-0071-completion-check.md +104 -0
  87. package/docs/decisions/ADR-0072-codex-prompts-and-goal.md +105 -0
  88. package/docs/decisions/ADR-0073-large-reporting-prompt.md +84 -0
  89. package/docs/parity.md +4 -1
  90. package/docs/releases/1.3.1-unstable.9.md +54 -0
  91. package/docs/releases/1.4.0-unstable.md +18 -0
  92. package/docs/releases/README.md +1 -1
  93. package/docs/study/00-index.md +10 -0
  94. package/docs/study/STUDY-0004-turn-end-controls.md +123 -0
  95. package/docs/study/STUDY-0005-skill-installation-guidance.md +146 -0
  96. package/docs/study/STUDY-0006-reporting-prompts.md +81 -0
  97. package/docs/study/arms/04-skills.md +6 -0
  98. package/docs/terminal-tui.md +17 -7
  99. package/package.json +1 -1
  100. package/dist/sdk/tools/checkpoint.d.ts +0 -27
  101. 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
- function validatedTodo(value) {
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 "../tools/checkpoint.js";
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;