@xingular/harness 1.4.0-unstable → 1.5.1-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 +41 -1
- package/NOTICE.md +9 -7
- package/dist/cli.js +160 -456
- package/dist/sdk/prompt/compile.d.ts +4 -2
- package/dist/sdk/prompt/compile.js +10 -8
- package/dist/sdk/prompt/reporting.d.ts +2 -2
- package/dist/sdk/prompt/reporting.js +40 -250
- package/dist/sdk/prompt/standard.d.ts +4 -1
- package/dist/sdk/prompt/standard.js +6 -2
- package/dist/sdk/sessions/completionCheck.d.ts +4 -2
- package/dist/sdk/sessions/completionCheck.js +19 -1
- package/dist/sdk/sessions/contracts.js +2 -2
- package/dist/sdk/sessions/goalStop.d.ts +27 -0
- package/dist/sdk/sessions/goalStop.js +41 -0
- package/dist/sdk/sessions/runner.d.ts +0 -3
- package/dist/sdk/sessions/runner.js +16 -9
- package/dist/sdk/sessions/sessions.js +6 -36
- package/dist/sdk/sessions/workSlice.js +1 -1
- package/dist/sdk/terminal/opentui/model.d.ts +1 -0
- package/dist/sdk/terminal/opentui/model.js +3 -0
- package/dist/sdk/terminal/piExtension.js +18 -31
- package/dist/sdk/terminal/terminals.js +1 -1
- package/dist/sdk/tools/askUser.js +1 -1
- package/dist/sdk/tools/todo.js +4 -3
- package/dist/terminal-opentui.js +112 -201
- package/dist/terminal-pi.js +262 -338
- package/docs/architecture/02-sessions-and-runs.md +13 -15
- package/docs/architecture/04-prompt.md +41 -35
- package/docs/architecture/07-tools-and-gate.md +2 -2
- package/docs/architecture/10-server-protocol.md +6 -5
- package/docs/architecture/11-terminal.md +7 -7
- package/docs/decisions/ADR-0008-flexible-reporting-guidance.md +2 -2
- package/docs/decisions/ADR-0069-stop-gate-and-finish.md +8 -2
- package/docs/decisions/ADR-0070-terminal-slice-continuation.md +4 -1
- package/docs/decisions/ADR-0072-codex-prompts-and-goal.md +3 -0
- package/docs/decisions/ADR-0073-large-reporting-prompt.md +6 -1
- package/docs/decisions/ADR-0074-report-content-not-report-blocks.md +90 -0
- package/docs/decisions/ADR-0075-no-todo-gate.md +84 -0
- package/docs/decisions/ADR-0076-codex-final-answer-verbatim.md +93 -0
- package/docs/releases/1.4.0-unstable.md +9 -2
- package/docs/releases/1.5.0-unstable.md +32 -0
- package/docs/releases/1.5.1-unstable.md +11 -0
- package/docs/study/00-index.md +5 -1
- package/docs/study/STUDY-0004-turn-end-controls.md +7 -0
- package/docs/study/STUDY-0006-reporting-prompts.md +48 -10
- package/docs/study/STUDY-0007-claude-code-prompt.md +162 -0
- package/docs/study/sources/claude-code.md +47 -39
- package/package.json +1 -1
- package/dist/sdk/sessions/stopGate.d.ts +0 -78
- package/dist/sdk/sessions/stopGate.js +0 -143
|
@@ -1,143 +0,0 @@
|
|
|
1
|
-
import { TODO_WRITE } from "../tools/todo.js";
|
|
2
|
-
import { validatedTodo } from "./compactionCheckpoint.js";
|
|
3
|
-
import { answerText } from "./completionCheck.js";
|
|
4
|
-
import { MAX_CHECKPOINT_DONE } from "./contracts.js";
|
|
5
|
-
export const STOP_GATE_EXTENSION = 'stop-gate';
|
|
6
|
-
export const STOP_GATE_CUSTOM_TYPE = 'xingular-stop-gate';
|
|
7
|
-
/** Nudges per reply before the reply is handed back to the person. */
|
|
8
|
-
export const MAX_STOP_GATE_NUDGES = 3;
|
|
9
|
-
/** Calls that are bookkeeping, not work: they neither count as progress nor make a reply checkable. */
|
|
10
|
-
const BOOKKEEPING_TOOLS = new Set([TODO_WRITE, 'session_name']);
|
|
11
|
-
export function createStopGateState(initialTodo = []) {
|
|
12
|
-
return { todo: validatedTodo(initialTodo) ?? [], request: '', nudges: 0, callsSinceNudge: 0, callsInReply: 0 };
|
|
13
|
-
}
|
|
14
|
-
/** A new reply starts from the inherited todo list, with no nudges. */
|
|
15
|
-
export function resetStopGateState(state, initialTodo = [], request = '') {
|
|
16
|
-
state.todo = validatedTodo(initialTodo) ?? [];
|
|
17
|
-
state.request = request;
|
|
18
|
-
state.nudges = 0;
|
|
19
|
-
state.callsSinceNudge = 0;
|
|
20
|
-
state.callsInReply = 0;
|
|
21
|
-
}
|
|
22
|
-
/** The todo items a reply has not finished. */
|
|
23
|
-
export function openItems(todo) {
|
|
24
|
-
return todo.filter((item) => item.status !== 'completed');
|
|
25
|
-
}
|
|
26
|
-
/**
|
|
27
|
-
* `goal` is the session's /goal, when it has one: a goal that ended `blocked` needs the person,
|
|
28
|
-
* and one that ended on a limit, a pause or a stall leaves work (ADR-0072).
|
|
29
|
-
*/
|
|
30
|
-
export function stopOutcome(state, goal) {
|
|
31
|
-
const open = openItems(state.todo);
|
|
32
|
-
if (goal?.status === 'blocked')
|
|
33
|
-
return { code: 'needs_person', reason: goal.lastReason || 'The goal is blocked.', open };
|
|
34
|
-
if (goal && (goal.status === 'budget_limited' || goal.status === 'usage_limited' || goal.status === 'paused')) {
|
|
35
|
-
return { code: 'unfinished', reason: goal.lastReason || `The goal is ${goal.status.replace('_', ' ')}.`, open };
|
|
36
|
-
}
|
|
37
|
-
if (open.length)
|
|
38
|
-
return { code: 'unfinished', reason: '', open };
|
|
39
|
-
return null;
|
|
40
|
-
}
|
|
41
|
-
/** What a checkpoint learns from the reply's todo list and the goal. */
|
|
42
|
-
export function handoffFields(state, goal) {
|
|
43
|
-
const todo = state?.todo ?? [];
|
|
44
|
-
const next = openItems(todo)[0]?.text || '';
|
|
45
|
-
const blocked = goal?.status === 'blocked' ? (goal.lastReason || 'The goal is blocked.').slice(0, 500) : '';
|
|
46
|
-
return {
|
|
47
|
-
done: todo.filter((item) => item.status === 'completed').slice(0, MAX_CHECKPOINT_DONE).map((item) => ({ item: item.text.slice(0, 300), evidence: [] })),
|
|
48
|
-
next: next.slice(0, 500),
|
|
49
|
-
...(blocked ? { blocked } : {})
|
|
50
|
-
};
|
|
51
|
-
}
|
|
52
|
-
/** Why a checkpoint written for a stop outcome was written. */
|
|
53
|
-
export const stopCheckpointReason = (outcome) => outcome.code === 'needs_person' ? 'requested' : 'stuck';
|
|
54
|
-
/** The summary a stop outcome's checkpoint falls back to when the reply left no text. */
|
|
55
|
-
export function stopSummary(outcome) {
|
|
56
|
-
if (outcome.code === 'needs_person')
|
|
57
|
-
return `The reply stopped for the person: ${outcome.reason}`.slice(0, 1_200);
|
|
58
|
-
if (outcome.reason)
|
|
59
|
-
return `The reply ended with work left: ${outcome.reason}`.slice(0, 1_200);
|
|
60
|
-
return `The reply ended with ${outcome.open.length} open todo item${outcome.open.length === 1 ? '' : 's'}.`;
|
|
61
|
-
}
|
|
62
|
-
/** The text the model reads when the todo gate sends its reply back. */
|
|
63
|
-
export function nudgeText(count, max, open) {
|
|
64
|
-
const list = open.slice(0, 12).map((item) => `- ${item.text}`).join('\n');
|
|
65
|
-
const more = open.length > 12 ? `\n- …and ${open.length - 12} more` : '';
|
|
66
|
-
return [
|
|
67
|
-
`Harness stop gate (${count}/${max}): your reply ended with ${open.length} open todo item${open.length === 1 ? '' : 's'}:`,
|
|
68
|
-
`${list}${more}`,
|
|
69
|
-
'Continue with the next open item now. Mark an item completed only after it is done and checked.',
|
|
70
|
-
'The list shows progress; it is not the limit of the work. If the request needs more than the list says, add it and do it.',
|
|
71
|
-
'Decisions inside the request are yours. Stop only for something only the person can do (a deploy, a secret, remote data, money, missing access), and then say exactly what it is.'
|
|
72
|
-
].join('\n');
|
|
73
|
-
}
|
|
74
|
-
/** One line for the run record per stop-gate nudge. */
|
|
75
|
-
export function stopGateNotice(event) {
|
|
76
|
-
return `Stop gate ${event.count}: the reply ended with ${event.open.length} open todo item${event.open.length === 1 ? '' : 's'}; continuing.`;
|
|
77
|
-
}
|
|
78
|
-
export function stopGate(options) {
|
|
79
|
-
const { state } = options;
|
|
80
|
-
const max = options.max ?? MAX_STOP_GATE_NUDGES;
|
|
81
|
-
return (pi) => {
|
|
82
|
-
// Pi emits agent_start again for every continuation, so a reply resets only on a person's message.
|
|
83
|
-
pi.on('before_agent_start', (event) => {
|
|
84
|
-
if (event.messageType !== undefined && event.messageType !== 'user')
|
|
85
|
-
return;
|
|
86
|
-
resetStopGateState(state, options.initialTodo, options.request?.() || (typeof event.prompt === 'string' ? event.prompt : ''));
|
|
87
|
-
});
|
|
88
|
-
pi.on('tool_call', (event) => {
|
|
89
|
-
if (BOOKKEEPING_TOOLS.has(event.toolName))
|
|
90
|
-
return;
|
|
91
|
-
state.callsSinceNudge += 1;
|
|
92
|
-
state.callsInReply += 1;
|
|
93
|
-
});
|
|
94
|
-
pi.on('tool_result', (event) => {
|
|
95
|
-
if (event.toolName !== TODO_WRITE || event.isError)
|
|
96
|
-
return;
|
|
97
|
-
const items = validatedTodo(event.input?.items);
|
|
98
|
-
if (items)
|
|
99
|
-
state.todo = items;
|
|
100
|
-
});
|
|
101
|
-
pi.on('agent_before_settle', (event, ctx) => {
|
|
102
|
-
if (event.outcome !== 'completed' || event.continue || ctx?.signal?.aborted)
|
|
103
|
-
return;
|
|
104
|
-
if (options.active && !options.active())
|
|
105
|
-
return;
|
|
106
|
-
if (options.sliceEnded?.() || options.failed?.())
|
|
107
|
-
return;
|
|
108
|
-
if (state.nudges >= max)
|
|
109
|
-
return;
|
|
110
|
-
// A nudge that produced no work will not be followed by one that does: hand it back.
|
|
111
|
-
if (state.nudges > 0 && state.callsSinceNudge === 0)
|
|
112
|
-
return;
|
|
113
|
-
const open = openItems(state.todo);
|
|
114
|
-
if (!open.length)
|
|
115
|
-
return;
|
|
116
|
-
state.nudges += 1;
|
|
117
|
-
state.callsSinceNudge = 0;
|
|
118
|
-
options.onEvent?.({ kind: 'todo', count: state.nudges, open });
|
|
119
|
-
return {
|
|
120
|
-
entries: [...event.entries, { type: 'custom_message', customType: STOP_GATE_CUSTOM_TYPE, content: nudgeText(state.nudges, max, open), display: true }],
|
|
121
|
-
continue: true
|
|
122
|
-
};
|
|
123
|
-
});
|
|
124
|
-
};
|
|
125
|
-
}
|
|
126
|
-
/** The model a goal-claim check asks: the session's own model, at low effort, through Pi's registry. */
|
|
127
|
-
export function sessionCheckModel(ctx) {
|
|
128
|
-
const session = (ctx ?? {});
|
|
129
|
-
const model = session.model;
|
|
130
|
-
const registry = session.modelRegistry;
|
|
131
|
-
if (!model || !registry)
|
|
132
|
-
return null;
|
|
133
|
-
// Some providers (opencode-go) reject a request without a session id.
|
|
134
|
-
const sessionId = session.sessionManager?.getSessionId?.();
|
|
135
|
-
return async (system, user, signal) => {
|
|
136
|
-
const context = { systemPrompt: system, messages: [{ role: 'user', content: user, timestamp: Date.now() }] };
|
|
137
|
-
const message = await registry.streamSimple(model, context, { ...(model.reasoning ? { reasoning: 'low' } : {}), ...(sessionId ? { sessionId } : {}), maxTokens: 4_000, signal }).result();
|
|
138
|
-
const failed = message.stopReason;
|
|
139
|
-
if (failed === 'error' || failed === 'aborted')
|
|
140
|
-
throw new Error(message.errorMessage || `the check request ended: ${failed}`);
|
|
141
|
-
return { text: answerText(message), message };
|
|
142
|
-
};
|
|
143
|
-
}
|