jeopi-cli 16.2.29 → 16.3.0
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 +26 -0
- package/dist/cli.js +2618 -2553
- package/dist/types/config/settings-schema.d.ts +12 -2
- package/dist/types/goals/evidence-gate.d.ts +62 -0
- package/dist/types/goals/index.d.ts +1 -0
- package/dist/types/goals/runtime.d.ts +29 -0
- package/dist/types/goals/verifier.d.ts +9 -0
- package/dist/types/session/agent-session.d.ts +22 -7
- package/package.json +12 -12
- package/src/autoresearch/index.ts +60 -6
- package/src/config/settings-schema.ts +14 -2
- package/src/goals/evidence-gate.ts +137 -0
- package/src/goals/index.ts +1 -0
- package/src/goals/runtime.ts +86 -0
- package/src/goals/verifier.ts +100 -0
- package/src/prompts/agents/goal-verifier.md +51 -0
- package/src/prompts/goals/goal-continuation.md +1 -0
- package/src/prompts/tools/goal.md +1 -1
- package/src/sdk.ts +2 -0
- package/src/session/agent-session.ts +84 -8
- package/src/task/agents.ts +2 -0
|
@@ -189,12 +189,12 @@ export declare const SETTINGS_SCHEMA: {
|
|
|
189
189
|
};
|
|
190
190
|
readonly "advisor.includeThinking": {
|
|
191
191
|
readonly type: "boolean";
|
|
192
|
-
readonly default:
|
|
192
|
+
readonly default: false;
|
|
193
193
|
readonly ui: {
|
|
194
194
|
readonly tab: "model";
|
|
195
195
|
readonly group: "Advisor";
|
|
196
196
|
readonly label: "Advisor Sees Thinking";
|
|
197
|
-
readonly description: "Include the primary agent's thinking blocks in advisor transcript updates. Some provider classifiers (e.g. Anthropic's reasoning_extraction) refuse payloads that replay thinking as text; the advisor
|
|
197
|
+
readonly description: "Include the primary agent's thinking blocks in advisor transcript updates. Off by default: a reviewer that sees the maker's own reasoning trail tends to anchor on conclusions the maker already committed to instead of bringing an independent angle (the advisor's own directive is to 'bring the angle they skipped, never re-run reasoning they already have'); the advisor still sees full messages, tool calls, results, and diffs. Some provider classifiers (e.g. Anthropic's reasoning_extraction) also refuse payloads that replay thinking as text if enabled; the advisor auto-disables this for the session after such a refusal.";
|
|
198
198
|
readonly condition: "advisorEnabled";
|
|
199
199
|
};
|
|
200
200
|
};
|
|
@@ -4056,6 +4056,16 @@ export declare const SETTINGS_SCHEMA: {
|
|
|
4056
4056
|
readonly description: "Run modes where active goals may auto-continue between turns";
|
|
4057
4057
|
};
|
|
4058
4058
|
};
|
|
4059
|
+
readonly "goal.verifyCompletion": {
|
|
4060
|
+
readonly type: "boolean";
|
|
4061
|
+
readonly default: true;
|
|
4062
|
+
readonly ui: {
|
|
4063
|
+
readonly tab: "tasks";
|
|
4064
|
+
readonly group: "Modes";
|
|
4065
|
+
readonly label: "Verify Goal Completion";
|
|
4066
|
+
readonly description: "Spawn an independent goal-verifier sub-agent to grade a completion claim against the objective and diff before accepting it";
|
|
4067
|
+
};
|
|
4068
|
+
};
|
|
4059
4069
|
readonly "title.refreshOnReplan": {
|
|
4060
4070
|
readonly type: "boolean";
|
|
4061
4071
|
readonly default: true;
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Deterministic evidence gate for goal completion — the code-level counterpart to
|
|
3
|
+
* the prompt-side completion audit (`prompts/goals/goal-continuation.md`).
|
|
4
|
+
*
|
|
5
|
+
* `assertSubstantiveCompletionEvidence` (completion-evidence.ts) only checks that
|
|
6
|
+
* the completion *claim text* is non-generic; it cannot see whether the work was
|
|
7
|
+
* actually verified. This gate closes that "gate theater" failure mode with
|
|
8
|
+
* runtime signals the session already observes: a goal turn that MUTATED project
|
|
9
|
+
* files can self-report complete only when a verification signal (test / build /
|
|
10
|
+
* typecheck / lint run) was observed AFTER the last mutation.
|
|
11
|
+
*
|
|
12
|
+
* Design mirrors gajae-code's `classifyDoneGate` + jeo-code's `applyEvidenceGate`:
|
|
13
|
+
* - Pure classification over recorded signals — no re-scan of the transcript.
|
|
14
|
+
* - Single-pushback latch: the first blocked `complete` bounces with a corrective
|
|
15
|
+
* message; a second `complete` with no new evidence passes (the escape hatch for
|
|
16
|
+
* docs/config-only changes where no verification command applies). Any new
|
|
17
|
+
* mutation recorded after a bounce re-arms the gate.
|
|
18
|
+
*/
|
|
19
|
+
/** Tool names whose successful execution counts as a project-file mutation. */
|
|
20
|
+
export declare const MUTATION_TOOL_NAMES: Record<string, true>;
|
|
21
|
+
/**
|
|
22
|
+
* Commands (or their output head) that count as a verification signal: a test,
|
|
23
|
+
* build, typecheck, or lint invocation. Matches command text and the first
|
|
24
|
+
* `VERIFY_OUTPUT_SCAN_LIMIT` chars of output — enough to catch a runner's banner
|
|
25
|
+
* without rescanning megabytes of logs.
|
|
26
|
+
*/
|
|
27
|
+
export declare const VERIFY_SIGNAL_RE: RegExp;
|
|
28
|
+
/** Output prefix length scanned for verification signals. */
|
|
29
|
+
export declare const VERIFY_OUTPUT_SCAN_LIMIT = 2000;
|
|
30
|
+
/** True when a bash/eval command (or the head of its output) proves a verification ran. */
|
|
31
|
+
export declare function isVerificationSignal(command: string, output?: string): boolean;
|
|
32
|
+
export type EvidenceGateState = "pass" | "unverified" | "stale-verification";
|
|
33
|
+
export interface EvidenceGateVerdict {
|
|
34
|
+
state: EvidenceGateState;
|
|
35
|
+
/** When true, `complete` should be bounced once with `message`. */
|
|
36
|
+
block: boolean;
|
|
37
|
+
/** Corrective message for the bounce; empty when `state === "pass"`. */
|
|
38
|
+
message: string;
|
|
39
|
+
}
|
|
40
|
+
/**
|
|
41
|
+
* Tracks mutation/verification evidence for the active goal. Sequence-ordered so
|
|
42
|
+
* "verified, then mutated again" is detected as stale evidence regardless of how
|
|
43
|
+
* many turns separate the two.
|
|
44
|
+
*/
|
|
45
|
+
export declare class GoalEvidenceTracker {
|
|
46
|
+
#private;
|
|
47
|
+
/** Reset all evidence (new goal, or goal replaced). */
|
|
48
|
+
reset(): void;
|
|
49
|
+
/** Record a successful project-file mutation (edit/write/ast_edit). */
|
|
50
|
+
recordMutation(): void;
|
|
51
|
+
/** Record a successful verification run (test/build/typecheck/lint). */
|
|
52
|
+
recordVerification(): void;
|
|
53
|
+
get sawMutation(): boolean;
|
|
54
|
+
get sawVerification(): boolean;
|
|
55
|
+
/** True when the last mutation happened after the last verification. */
|
|
56
|
+
get verificationStale(): boolean;
|
|
57
|
+
/**
|
|
58
|
+
* Classify a `complete` attempt. First blocked attempt arms the bypass latch;
|
|
59
|
+
* the next attempt (with no new mutation in between) passes.
|
|
60
|
+
*/
|
|
61
|
+
classifyCompletion(): EvidenceGateVerdict;
|
|
62
|
+
}
|
|
@@ -1,4 +1,11 @@
|
|
|
1
|
+
import { GoalEvidenceTracker } from "./evidence-gate";
|
|
1
2
|
import type { Goal, GoalBudgetSteering, GoalModeState, GoalRuntimeEvent, GoalTokenUsage } from "./state";
|
|
3
|
+
export interface GoalCompletionVerdict {
|
|
4
|
+
verdict: "okay" | "iterate" | "reject";
|
|
5
|
+
justification: string;
|
|
6
|
+
summary?: string;
|
|
7
|
+
requiredFixes: string[];
|
|
8
|
+
}
|
|
2
9
|
export interface GoalRuntimeHost {
|
|
3
10
|
getState(): GoalModeState | undefined;
|
|
4
11
|
setState(state: GoalModeState | undefined): void;
|
|
@@ -10,6 +17,16 @@ export interface GoalRuntimeHost {
|
|
|
10
17
|
content: string;
|
|
11
18
|
deliverAs?: "steer" | "followUp" | "nextTurn";
|
|
12
19
|
}): Promise<void>;
|
|
20
|
+
/**
|
|
21
|
+
* Spawn an independent grader sub-agent to verdict a completion claim against
|
|
22
|
+
* the objective and the actual repo diff (article gap: "the agent that wrote
|
|
23
|
+
* the code is not the agent that grades it"). The sub-agent sees only the
|
|
24
|
+
* objective + evidence text passed here plus its own repo inspection — never
|
|
25
|
+
* this session's transcript/reasoning. Optional: hosts that don't wire this
|
|
26
|
+
* (e.g. test harnesses, `goal.verifyCompletion` disabled) skip independent
|
|
27
|
+
* grading and fall back to the deterministic evidence gate alone.
|
|
28
|
+
*/
|
|
29
|
+
verifyCompletion?(objective: string, evidence: string): Promise<GoalCompletionVerdict>;
|
|
13
30
|
now?(): number;
|
|
14
31
|
}
|
|
15
32
|
export interface GoalTurnSnapshot {
|
|
@@ -37,6 +54,18 @@ export declare class GoalRuntime {
|
|
|
37
54
|
constructor(host: GoalRuntimeHost);
|
|
38
55
|
get snapshot(): GoalRuntimeSnapshot;
|
|
39
56
|
clearAccounting(): void;
|
|
57
|
+
/** Deterministic completion-evidence tracker for the active goal (see evidence-gate.ts). */
|
|
58
|
+
get evidence(): GoalEvidenceTracker;
|
|
59
|
+
/**
|
|
60
|
+
* Record a successful tool execution as completion evidence for the active goal.
|
|
61
|
+
* Mutation tools (edit/write/ast_edit) arm the completion gate; bash/eval runs
|
|
62
|
+
* whose command or output head carries a verification signal (test/build/
|
|
63
|
+
* typecheck/lint) satisfy it. No-op when no goal is accounting.
|
|
64
|
+
*/
|
|
65
|
+
recordToolEvidence(toolName: string, detail?: {
|
|
66
|
+
command?: string;
|
|
67
|
+
outputHead?: string;
|
|
68
|
+
}): void;
|
|
40
69
|
onTurnStart(turnId: string, baselineUsage: GoalTokenUsage): void;
|
|
41
70
|
onToolCompleted(toolName: string): Promise<void>;
|
|
42
71
|
onGoalToolCompleted(): Promise<void>;
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import type { ToolSession } from "../tools";
|
|
2
|
+
import type { GoalCompletionVerdict } from "./runtime";
|
|
3
|
+
/**
|
|
4
|
+
* Grade a goal-completion claim with the independent `goal-verifier` sub-agent.
|
|
5
|
+
* Never throws: a spawn failure (depth limit, disabled agent, no model) fails
|
|
6
|
+
* open to `iterate` so it costs one corrective bounce, the same as a genuine
|
|
7
|
+
* gap — never a silent skip of independent grading, never a permanent block.
|
|
8
|
+
*/
|
|
9
|
+
export declare function verifyGoalCompletion(session: ToolSession, objective: string, evidence: string): Promise<GoalCompletionVerdict>;
|
|
@@ -35,7 +35,7 @@ import type { ExtensionRunner } from "../extensibility/extensions";
|
|
|
35
35
|
import type { CompactOptions, ContextUsage } from "../extensibility/extensions/types";
|
|
36
36
|
import type { Skill, SkillWarning } from "../extensibility/skills";
|
|
37
37
|
import { type FileSlashCommand } from "../extensibility/slash-commands";
|
|
38
|
-
import { GoalRuntime } from "../goals/runtime";
|
|
38
|
+
import { type GoalCompletionVerdict, GoalRuntime } from "../goals/runtime";
|
|
39
39
|
import type { Goal, GoalModeState } from "../goals/state";
|
|
40
40
|
import type { HindsightSessionState } from "../hindsight/state";
|
|
41
41
|
import { type IrcMessage } from "../irc/bus";
|
|
@@ -124,13 +124,19 @@ export type AgentSessionEvent = AgentEvent | {
|
|
|
124
124
|
export type AgentSessionEventListener = (event: AgentSessionEvent) => void;
|
|
125
125
|
/**
|
|
126
126
|
* Budget for callers on the user-visible `/quit` / `/exit` shutdown path that
|
|
127
|
-
* want to cap how long they wait for `MnemopiSessionState.dispose()`
|
|
128
|
-
*
|
|
129
|
-
*
|
|
130
|
-
*
|
|
131
|
-
*
|
|
127
|
+
* want to cap how long they wait for `MnemopiSessionState.dispose()` (and the
|
|
128
|
+
* Hindsight retain-queue flush, see the dispose() call site) to finish writing
|
|
129
|
+
* this session's memory before the process exits. Consolidate fires fresh LLM
|
|
130
|
+
* fact extractions, each a 1-3s round-trip; the retain-queue flush is a single
|
|
131
|
+
* HTTP POST. The prior 1500ms budget was tighter than a single extraction
|
|
132
|
+
* round-trip, so it lost the race on effectively every session with any
|
|
133
|
+
* memory-worthy content — "write before walking away" in name only. 8s still
|
|
134
|
+
* bounds worst-case UI unresponsiveness on /exit while giving a small
|
|
135
|
+
* consolidation batch (1-3 facts) a realistic chance to land before the
|
|
136
|
+
* process exits. Callers that keep the process/session host alive must omit
|
|
137
|
+
* it so dispose still awaits the full write pipeline unbounded.
|
|
132
138
|
*/
|
|
133
|
-
export declare const SHUTDOWN_CONSOLIDATE_BUDGET_MS =
|
|
139
|
+
export declare const SHUTDOWN_CONSOLIDATE_BUDGET_MS = 8000;
|
|
134
140
|
export interface AgentSessionDisposeOptions {
|
|
135
141
|
mnemopiConsolidateTimeoutMs?: number;
|
|
136
142
|
/**
|
|
@@ -225,6 +231,15 @@ export interface AgentSessionConfig {
|
|
|
225
231
|
}>;
|
|
226
232
|
/** Rebuild the SSH tool from current capability discovery results. */
|
|
227
233
|
reloadSshTool?: () => Promise<AgentTool | null>;
|
|
234
|
+
/**
|
|
235
|
+
* Independent-verifier bridge for goal-mode completion (see goals/verifier.ts).
|
|
236
|
+
* Spawns the bundled `goal-verifier` sub-agent to grade a completion claim against
|
|
237
|
+
* the objective and the actual repo diff — a fresh session/context, no exposure to
|
|
238
|
+
* this session's transcript. Wired from sdk.ts (needs the constructed `toolSession`).
|
|
239
|
+
* Undefined when `goal.verifyCompletion` is off or the caller has no tool session
|
|
240
|
+
* (e.g. minimal SDK embedders) — GoalRuntime skips independent grading in that case.
|
|
241
|
+
*/
|
|
242
|
+
verifyGoalCompletion?: (objective: string, evidence: string) => Promise<GoalCompletionVerdict>;
|
|
228
243
|
requestedToolNames?: ReadonlySet<string>;
|
|
229
244
|
/**
|
|
230
245
|
* Optional accessor for live MCP server instructions. Read by the session's
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"type": "module",
|
|
3
3
|
"name": "jeopi-cli",
|
|
4
|
-
"version": "16.
|
|
4
|
+
"version": "16.3.0",
|
|
5
5
|
"description": "Coding agent CLI with read, bash, edit, write tools and session management",
|
|
6
6
|
"homepage": "https://github.com/akillness/jeopi",
|
|
7
7
|
"author": "Can Boluk",
|
|
@@ -57,17 +57,17 @@
|
|
|
57
57
|
"@agentclientprotocol/sdk": "0.25.0",
|
|
58
58
|
"@babel/parser": "^7.29.7",
|
|
59
59
|
"@mozilla/readability": "^0.6.0",
|
|
60
|
-
"jeopi-hashline": "16.
|
|
61
|
-
"jeopi-stats": "16.
|
|
62
|
-
"jeopi-agent-core": "16.
|
|
63
|
-
"jeopi-ai": "16.
|
|
64
|
-
"jeopi-catalog": "16.
|
|
65
|
-
"jeopi-mnemopi": "16.
|
|
66
|
-
"jeopi-natives": "16.
|
|
67
|
-
"jeopi-tui": "16.
|
|
68
|
-
"jeopi-utils": "16.
|
|
69
|
-
"jeopi-wire": "16.
|
|
70
|
-
"jeopi-snapcompact": "16.
|
|
60
|
+
"jeopi-hashline": "16.3.0",
|
|
61
|
+
"jeopi-stats": "16.3.0",
|
|
62
|
+
"jeopi-agent-core": "16.3.0",
|
|
63
|
+
"jeopi-ai": "16.3.0",
|
|
64
|
+
"jeopi-catalog": "16.3.0",
|
|
65
|
+
"jeopi-mnemopi": "16.3.0",
|
|
66
|
+
"jeopi-natives": "16.3.0",
|
|
67
|
+
"jeopi-tui": "16.3.0",
|
|
68
|
+
"jeopi-utils": "16.3.0",
|
|
69
|
+
"jeopi-wire": "16.3.0",
|
|
70
|
+
"jeopi-snapcompact": "16.3.0",
|
|
71
71
|
"@opentelemetry/api": "^1.9.1",
|
|
72
72
|
"@opentelemetry/context-async-hooks": "^2.7.1",
|
|
73
73
|
"@opentelemetry/exporter-trace-otlp-proto": "^0.218.0",
|
|
@@ -30,6 +30,16 @@ import type { AutoresearchRuntime, ExperimentResult, PendingRunSummary } from ".
|
|
|
30
30
|
|
|
31
31
|
const EXPERIMENT_TOOL_NAMES = ["init_experiment", "run_experiment", "log_experiment", "update_notes"];
|
|
32
32
|
|
|
33
|
+
/**
|
|
34
|
+
* Cross-process auto-resume window: a pending (run-but-never-logged) experiment
|
|
35
|
+
* completed within this long ago is resumed automatically on the next session
|
|
36
|
+
* load; older ones are left for the user to inspect and resume by hand. Bounds
|
|
37
|
+
* the "open your laptop, the loop picks back up" pattern to realistic gaps
|
|
38
|
+
* (overnight, a workday) without silently relaunching token-spending work
|
|
39
|
+
* against a session nobody touched in weeks.
|
|
40
|
+
*/
|
|
41
|
+
const AUTORESEARCH_RESUME_STALENESS_MS = 24 * 60 * 60 * 1000;
|
|
42
|
+
|
|
33
43
|
export const createAutoresearchExtension: ExtensionFactory = api => {
|
|
34
44
|
const runtimeStore = createRuntimeStore();
|
|
35
45
|
const dashboard = createDashboardController();
|
|
@@ -47,7 +57,7 @@ export const createAutoresearchExtension: ExtensionFactory = api => {
|
|
|
47
57
|
return { session, currentBranch };
|
|
48
58
|
};
|
|
49
59
|
|
|
50
|
-
const rehydrate = async (ctx: ExtensionContext): Promise<void> => {
|
|
60
|
+
const rehydrate = async (ctx: ExtensionContext, resumeEligible: boolean): Promise<void> => {
|
|
51
61
|
const runtime = getRuntime(ctx);
|
|
52
62
|
const control = reconstructControlState(ctx.sessionManager.getBranch());
|
|
53
63
|
runtime.goal = control.goal;
|
|
@@ -69,13 +79,15 @@ export const createAutoresearchExtension: ExtensionFactory = api => {
|
|
|
69
79
|
const onActiveBranch = session === null || session.branch === null || session.branch === currentBranch;
|
|
70
80
|
runtime.autoresearchMode = control.autoresearchMode && onActiveBranch;
|
|
71
81
|
|
|
82
|
+
let pendingRow: RunRow | null = null;
|
|
72
83
|
if (session && onActiveBranch) {
|
|
73
84
|
const storage = await openAutoresearchStorageIfExists(ctx.cwd);
|
|
74
85
|
if (storage) {
|
|
75
86
|
const loggedRuns = storage.listLoggedRuns(session.id);
|
|
76
87
|
runtime.state = buildExperimentState(session, loggedRuns);
|
|
77
88
|
runtime.goal = runtime.goal ?? session.goal;
|
|
78
|
-
|
|
89
|
+
pendingRow = storage.getPendingRun(session.id);
|
|
90
|
+
runtime.lastRunSummary = pendingRunSummaryFromRow(pendingRow);
|
|
79
91
|
} else {
|
|
80
92
|
runtime.state = createExperimentState();
|
|
81
93
|
runtime.lastRunSummary = null;
|
|
@@ -102,6 +114,41 @@ export const createAutoresearchExtension: ExtensionFactory = api => {
|
|
|
102
114
|
if (toolsChanged) {
|
|
103
115
|
await api.setActiveTools(nextActiveTools);
|
|
104
116
|
}
|
|
117
|
+
|
|
118
|
+
// Auto-rearm across a process restart (article gap: "the mode itself has to be
|
|
119
|
+
// manually re-armed with /autoresearch after a restart, since session_shutdown
|
|
120
|
+
// doesn't leave anything wired to auto-relaunch" — the "morning briefing"/
|
|
121
|
+
// nightly-loop pattern requires the NEXT process to pick a pending run back up
|
|
122
|
+
// on its own, not wait for the user to notice and retype /autoresearch).
|
|
123
|
+
// `autoResumeArmed` is deliberately NOT restored here (reset above, matching
|
|
124
|
+
// existing behavior) — that flag is a live in-process signal set by the
|
|
125
|
+
// experiment tools mid-run; it cannot be reconstructed from persisted state
|
|
126
|
+
// and re-arming it unconditionally on every resume would fire even when the
|
|
127
|
+
// prior process ended cleanly with nothing left to continue. The only
|
|
128
|
+
// cross-restart-durable signal is an actual pending (run-but-never-logged)
|
|
129
|
+
// run row, so that's the sole trigger here.
|
|
130
|
+
if (
|
|
131
|
+
resumeEligible &&
|
|
132
|
+
runtime.autoresearchMode &&
|
|
133
|
+
pendingRow?.completedAt !== undefined &&
|
|
134
|
+
pendingRow.completedAt !== null &&
|
|
135
|
+
Date.now() - pendingRow.completedAt <= AUTORESEARCH_RESUME_STALENESS_MS &&
|
|
136
|
+
!ctx.hasPendingMessages()
|
|
137
|
+
) {
|
|
138
|
+
const pendingRun = runtime.lastRunSummary;
|
|
139
|
+
runtime.lastAutoResumePendingRunNumber = pendingRow.id;
|
|
140
|
+
api.sendMessage(
|
|
141
|
+
{
|
|
142
|
+
customType: "autoresearch-resume",
|
|
143
|
+
content: prompt.render(resumeMessageTemplate, {
|
|
144
|
+
has_pending_run: Boolean(pendingRun),
|
|
145
|
+
}),
|
|
146
|
+
display: false,
|
|
147
|
+
attribution: "agent",
|
|
148
|
+
},
|
|
149
|
+
{ deliverAs: "nextTurn", triggerTurn: true },
|
|
150
|
+
);
|
|
151
|
+
}
|
|
105
152
|
};
|
|
106
153
|
|
|
107
154
|
const setMode = (
|
|
@@ -245,10 +292,17 @@ export const createAutoresearchExtension: ExtensionFactory = api => {
|
|
|
245
292
|
},
|
|
246
293
|
});
|
|
247
294
|
|
|
248
|
-
api.on("session_start", (_event, ctx) => rehydrate(ctx));
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
295
|
+
api.on("session_start", (_event, ctx) => rehydrate(ctx, true));
|
|
296
|
+
// "resume" = picked from the /resume session picker or `jeopi -r`, the exact
|
|
297
|
+
// "reopen your laptop, the loop should still be going" moment the article
|
|
298
|
+
// describes. "new"/"fork" start a fresh conversation off old history — no
|
|
299
|
+
// auto-continuation there, matching session_start's "don't fire on plain
|
|
300
|
+
// process launch into an unrelated session" restraint.
|
|
301
|
+
api.on("session_switch", (event, ctx) => rehydrate(ctx, event.reason === "resume"));
|
|
302
|
+
// Mid-conversation navigation (branch off an earlier message, move through the
|
|
303
|
+
// tree), not a process/session restart — never auto-fires a fresh turn here.
|
|
304
|
+
api.on("session_branch", (_event, ctx) => rehydrate(ctx, false));
|
|
305
|
+
api.on("session_tree", (_event, ctx) => rehydrate(ctx, false));
|
|
252
306
|
api.on("session_shutdown", (_event, ctx) => {
|
|
253
307
|
dashboard.clear(ctx);
|
|
254
308
|
runtimeStore.clear(getSessionKey(ctx));
|
|
@@ -416,13 +416,13 @@ export const SETTINGS_SCHEMA = {
|
|
|
416
416
|
},
|
|
417
417
|
"advisor.includeThinking": {
|
|
418
418
|
type: "boolean",
|
|
419
|
-
default:
|
|
419
|
+
default: false,
|
|
420
420
|
ui: {
|
|
421
421
|
tab: "model",
|
|
422
422
|
group: "Advisor",
|
|
423
423
|
label: "Advisor Sees Thinking",
|
|
424
424
|
description:
|
|
425
|
-
"Include the primary agent's thinking blocks in advisor transcript updates. Some provider classifiers (e.g. Anthropic's reasoning_extraction) refuse payloads that replay thinking as text; the advisor
|
|
425
|
+
"Include the primary agent's thinking blocks in advisor transcript updates. Off by default: a reviewer that sees the maker's own reasoning trail tends to anchor on conclusions the maker already committed to instead of bringing an independent angle (the advisor's own directive is to 'bring the angle they skipped, never re-run reasoning they already have'); the advisor still sees full messages, tool calls, results, and diffs. Some provider classifiers (e.g. Anthropic's reasoning_extraction) also refuse payloads that replay thinking as text if enabled; the advisor auto-disables this for the session after such a refusal.",
|
|
426
426
|
condition: "advisorEnabled",
|
|
427
427
|
},
|
|
428
428
|
},
|
|
@@ -3946,6 +3946,18 @@ export const SETTINGS_SCHEMA = {
|
|
|
3946
3946
|
},
|
|
3947
3947
|
},
|
|
3948
3948
|
|
|
3949
|
+
"goal.verifyCompletion": {
|
|
3950
|
+
type: "boolean",
|
|
3951
|
+
default: true,
|
|
3952
|
+
ui: {
|
|
3953
|
+
tab: "tasks",
|
|
3954
|
+
group: "Modes",
|
|
3955
|
+
label: "Verify Goal Completion",
|
|
3956
|
+
description:
|
|
3957
|
+
"Spawn an independent goal-verifier sub-agent to grade a completion claim against the objective and diff before accepting it",
|
|
3958
|
+
},
|
|
3959
|
+
},
|
|
3960
|
+
|
|
3949
3961
|
"title.refreshOnReplan": {
|
|
3950
3962
|
type: "boolean",
|
|
3951
3963
|
default: true,
|
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Deterministic evidence gate for goal completion — the code-level counterpart to
|
|
3
|
+
* the prompt-side completion audit (`prompts/goals/goal-continuation.md`).
|
|
4
|
+
*
|
|
5
|
+
* `assertSubstantiveCompletionEvidence` (completion-evidence.ts) only checks that
|
|
6
|
+
* the completion *claim text* is non-generic; it cannot see whether the work was
|
|
7
|
+
* actually verified. This gate closes that "gate theater" failure mode with
|
|
8
|
+
* runtime signals the session already observes: a goal turn that MUTATED project
|
|
9
|
+
* files can self-report complete only when a verification signal (test / build /
|
|
10
|
+
* typecheck / lint run) was observed AFTER the last mutation.
|
|
11
|
+
*
|
|
12
|
+
* Design mirrors gajae-code's `classifyDoneGate` + jeo-code's `applyEvidenceGate`:
|
|
13
|
+
* - Pure classification over recorded signals — no re-scan of the transcript.
|
|
14
|
+
* - Single-pushback latch: the first blocked `complete` bounces with a corrective
|
|
15
|
+
* message; a second `complete` with no new evidence passes (the escape hatch for
|
|
16
|
+
* docs/config-only changes where no verification command applies). Any new
|
|
17
|
+
* mutation recorded after a bounce re-arms the gate.
|
|
18
|
+
*/
|
|
19
|
+
|
|
20
|
+
/** Tool names whose successful execution counts as a project-file mutation. */
|
|
21
|
+
export const MUTATION_TOOL_NAMES: Record<string, true> = {
|
|
22
|
+
edit: true,
|
|
23
|
+
write: true,
|
|
24
|
+
ast_edit: true,
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Commands (or their output head) that count as a verification signal: a test,
|
|
29
|
+
* build, typecheck, or lint invocation. Matches command text and the first
|
|
30
|
+
* `VERIFY_OUTPUT_SCAN_LIMIT` chars of output — enough to catch a runner's banner
|
|
31
|
+
* without rescanning megabytes of logs.
|
|
32
|
+
*/
|
|
33
|
+
export const VERIFY_SIGNAL_RE = /\b(test|tests|tsc|tsgo|typecheck|lint|build|check|spec|pytest|vitest|jest|biome)\b/i;
|
|
34
|
+
|
|
35
|
+
/** Output prefix length scanned for verification signals. */
|
|
36
|
+
export const VERIFY_OUTPUT_SCAN_LIMIT = 2000;
|
|
37
|
+
|
|
38
|
+
/** True when a bash/eval command (or the head of its output) proves a verification ran. */
|
|
39
|
+
export function isVerificationSignal(command: string, output = ""): boolean {
|
|
40
|
+
return VERIFY_SIGNAL_RE.test(command) || VERIFY_SIGNAL_RE.test(output.slice(0, VERIFY_OUTPUT_SCAN_LIMIT));
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export type EvidenceGateState = "pass" | "unverified" | "stale-verification";
|
|
44
|
+
|
|
45
|
+
export interface EvidenceGateVerdict {
|
|
46
|
+
state: EvidenceGateState;
|
|
47
|
+
/** When true, `complete` should be bounced once with `message`. */
|
|
48
|
+
block: boolean;
|
|
49
|
+
/** Corrective message for the bounce; empty when `state === "pass"`. */
|
|
50
|
+
message: string;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Tracks mutation/verification evidence for the active goal. Sequence-ordered so
|
|
55
|
+
* "verified, then mutated again" is detected as stale evidence regardless of how
|
|
56
|
+
* many turns separate the two.
|
|
57
|
+
*/
|
|
58
|
+
export class GoalEvidenceTracker {
|
|
59
|
+
#seq = 0;
|
|
60
|
+
#lastMutationSeq = 0;
|
|
61
|
+
#lastVerificationSeq = 0;
|
|
62
|
+
/** Set when a `complete` was bounced; cleared by any new mutation. */
|
|
63
|
+
#bypassArmed = false;
|
|
64
|
+
|
|
65
|
+
/** Reset all evidence (new goal, or goal replaced). */
|
|
66
|
+
reset(): void {
|
|
67
|
+
this.#seq = 0;
|
|
68
|
+
this.#lastMutationSeq = 0;
|
|
69
|
+
this.#lastVerificationSeq = 0;
|
|
70
|
+
this.#bypassArmed = false;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/** Record a successful project-file mutation (edit/write/ast_edit). */
|
|
74
|
+
recordMutation(): void {
|
|
75
|
+
this.#lastMutationSeq = ++this.#seq;
|
|
76
|
+
// New mutation invalidates a previously granted escape hatch: the model
|
|
77
|
+
// must re-justify (or re-verify) the new change.
|
|
78
|
+
this.#bypassArmed = false;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/** Record a successful verification run (test/build/typecheck/lint). */
|
|
82
|
+
recordVerification(): void {
|
|
83
|
+
this.#lastVerificationSeq = ++this.#seq;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
get sawMutation(): boolean {
|
|
87
|
+
return this.#lastMutationSeq > 0;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
get sawVerification(): boolean {
|
|
91
|
+
return this.#lastVerificationSeq > 0;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/** True when the last mutation happened after the last verification. */
|
|
95
|
+
get verificationStale(): boolean {
|
|
96
|
+
return this.#lastMutationSeq > this.#lastVerificationSeq;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* Classify a `complete` attempt. First blocked attempt arms the bypass latch;
|
|
101
|
+
* the next attempt (with no new mutation in between) passes.
|
|
102
|
+
*/
|
|
103
|
+
classifyCompletion(): EvidenceGateVerdict {
|
|
104
|
+
if (!this.sawMutation) {
|
|
105
|
+
return { state: "pass", block: false, message: "" };
|
|
106
|
+
}
|
|
107
|
+
if (this.sawVerification && !this.verificationStale) {
|
|
108
|
+
return { state: "pass", block: false, message: "" };
|
|
109
|
+
}
|
|
110
|
+
if (this.#bypassArmed) {
|
|
111
|
+
// Second attempt without new evidence: the escape hatch for changes
|
|
112
|
+
// where no verification command applies (docs/config-only).
|
|
113
|
+
return { state: "pass", block: false, message: "" };
|
|
114
|
+
}
|
|
115
|
+
this.#bypassArmed = true;
|
|
116
|
+
if (!this.sawVerification) {
|
|
117
|
+
return {
|
|
118
|
+
state: "unverified",
|
|
119
|
+
block: true,
|
|
120
|
+
message:
|
|
121
|
+
"Completion blocked: this goal session modified project files but no verification signal " +
|
|
122
|
+
"(test/build/typecheck/lint run) was observed. Run the narrowest command that proves the " +
|
|
123
|
+
"change works, then call complete again. If verification is genuinely not applicable " +
|
|
124
|
+
"(docs/config-only change), call complete again and say why in the evidence.",
|
|
125
|
+
};
|
|
126
|
+
}
|
|
127
|
+
return {
|
|
128
|
+
state: "stale-verification",
|
|
129
|
+
block: true,
|
|
130
|
+
message:
|
|
131
|
+
"Completion blocked: files were modified AFTER the last successful verification run, so that " +
|
|
132
|
+
"evidence no longer reflects the current tree. Re-run the narrowest verification command " +
|
|
133
|
+
"against the latest changes, then call complete again. If the later edits are " +
|
|
134
|
+
"verification-irrelevant (docs/config-only), call complete again and say why in the evidence.",
|
|
135
|
+
};
|
|
136
|
+
}
|
|
137
|
+
}
|
package/src/goals/index.ts
CHANGED