jeopi-cli 16.2.30 → 16.4.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 +32 -0
- package/dist/cli.js +2824 -2736
- package/dist/types/config/settings-schema.d.ts +32 -2
- package/dist/types/goals/runtime.d.ts +16 -0
- package/dist/types/goals/verifier.d.ts +9 -0
- package/dist/types/memories/index.d.ts +9 -0
- package/dist/types/memory-backend/types.d.ts +11 -0
- package/dist/types/session/agent-session.d.ts +22 -7
- package/dist/types/slash-commands/helpers/skill-verify.d.ts +5 -0
- package/dist/types/task/model-tier-classifier.d.ts +8 -0
- package/dist/types/tools/builtin-names.d.ts +1 -1
- package/dist/types/tools/index.d.ts +1 -0
- package/dist/types/tools/learn.d.ts +4 -0
- package/dist/types/tools/path-write-lock.d.ts +10 -0
- package/dist/types/tools/vision-verify.d.ts +34 -0
- package/package.json +12 -12
- package/src/autolearn/controller.ts +21 -5
- package/src/autoresearch/index.ts +60 -6
- package/src/config/settings-schema.ts +38 -2
- package/src/edit/index.ts +2 -1
- package/src/goals/runtime.ts +51 -0
- package/src/goals/verifier.ts +100 -0
- package/src/internal-urls/docs-index.generated.txt +2 -2
- package/src/memories/index.ts +10 -2
- package/src/memory-backend/local-backend.ts +34 -0
- package/src/memory-backend/types.ts +11 -0
- package/src/prompts/agents/designer.md +1 -0
- package/src/prompts/agents/goal-verifier.md +51 -0
- package/src/prompts/tools/learn.md +2 -0
- package/src/prompts/tools/vision-verify-system.md +16 -0
- package/src/prompts/tools/vision-verify.md +20 -0
- package/src/sdk.ts +2 -0
- package/src/session/agent-session.ts +51 -8
- package/src/slash-commands/builtin-registry.ts +9 -0
- package/src/slash-commands/helpers/skill-verify.ts +171 -0
- package/src/task/agents.ts +2 -0
- package/src/task/index.ts +26 -1
- package/src/task/model-tier-classifier.ts +43 -0
- package/src/tools/builtin-names.ts +1 -0
- package/src/tools/index.ts +4 -0
- package/src/tools/learn.ts +16 -1
- package/src/tools/path-write-lock.ts +44 -0
- package/src/tools/vision-verify.ts +347 -0
- package/src/tools/write.ts +12 -9
package/src/memories/index.ts
CHANGED
|
@@ -1342,7 +1342,7 @@ export function getMemoryRoot(agentDir: string, cwd: string): string {
|
|
|
1342
1342
|
* consolidation artifacts (`MEMORY.md`, `memory_summary.md`, `skills/`) so a
|
|
1343
1343
|
* consolidation pass never clobbers manually captured lessons.
|
|
1344
1344
|
*/
|
|
1345
|
-
const LEARNED_LESSONS_FILE = "learned.md";
|
|
1345
|
+
export const LEARNED_LESSONS_FILE = "learned.md";
|
|
1346
1346
|
/** Newest-first cap on retained lessons, bounding file growth by entry count. */
|
|
1347
1347
|
const MAX_LEARNED_LESSONS = 100;
|
|
1348
1348
|
/** Per-field char caps so a single huge capture can't bloat learned.md. */
|
|
@@ -1414,7 +1414,15 @@ export async function saveLearnedLesson(
|
|
|
1414
1414
|
return { backend: "local", stored: 0, message: "Empty lesson; nothing stored." };
|
|
1415
1415
|
}
|
|
1416
1416
|
const context = input.context ? normalizeLearnedText(input.context, MAX_LEARNED_CONTEXT_CHARS) : "";
|
|
1417
|
-
const
|
|
1417
|
+
const contextSuffix = context ? ` _(context: ${context})_` : "";
|
|
1418
|
+
let line: string;
|
|
1419
|
+
if (input.verified === true) {
|
|
1420
|
+
const evidence = input.evidence ? normalizeLearnedText(input.evidence, MAX_LEARNED_CONTEXT_CHARS) : "";
|
|
1421
|
+
const evidenceSuffix = evidence ? ` _(evidence: ${evidence})_` : "";
|
|
1422
|
+
line = `- [VERIFIED] ${content}${evidenceSuffix}${contextSuffix}`;
|
|
1423
|
+
} else {
|
|
1424
|
+
line = `- [UNVERIFIED] ${content}${contextSuffix}`;
|
|
1425
|
+
}
|
|
1418
1426
|
const filePath = path.join(getMemoryRoot(agentDir, cwd), LEARNED_LESSONS_FILE);
|
|
1419
1427
|
|
|
1420
1428
|
// Serialize the read-modify-write per file: parallel `learn` calls (sibling
|
|
@@ -1,8 +1,12 @@
|
|
|
1
|
+
import * as path from "node:path";
|
|
2
|
+
import { isEnoent } from "jeopi-utils";
|
|
1
3
|
import {
|
|
2
4
|
buildMemoryToolDeveloperInstructions,
|
|
3
5
|
clearMemoryData,
|
|
4
6
|
clearMemoryToolDeveloperInstructionsCache,
|
|
5
7
|
enqueueMemoryConsolidation,
|
|
8
|
+
getMemoryRoot,
|
|
9
|
+
LEARNED_LESSONS_FILE,
|
|
6
10
|
saveLearnedLesson,
|
|
7
11
|
startMemoryStartupTask,
|
|
8
12
|
} from "../memories";
|
|
@@ -44,4 +48,34 @@ export const localBackend: MemoryBackend = {
|
|
|
44
48
|
"Local rollout-summary memory is active; lessons from the `learn` tool are saved to learned.md. Structured search is not available.",
|
|
45
49
|
};
|
|
46
50
|
},
|
|
51
|
+
async stats(agentDir, cwd) {
|
|
52
|
+
const filePath = path.join(getMemoryRoot(agentDir, cwd), LEARNED_LESSONS_FILE);
|
|
53
|
+
let raw = "";
|
|
54
|
+
try {
|
|
55
|
+
raw = await Bun.file(filePath).text();
|
|
56
|
+
} catch (err) {
|
|
57
|
+
if (!isEnoent(err)) throw err;
|
|
58
|
+
}
|
|
59
|
+
let total = 0;
|
|
60
|
+
let verified = 0;
|
|
61
|
+
for (const rawLine of raw.split("\n")) {
|
|
62
|
+
const line = rawLine.trim();
|
|
63
|
+
if (!line.startsWith("- ")) continue;
|
|
64
|
+
total++;
|
|
65
|
+
if (line.startsWith(VERIFIED_PREFIX)) verified++;
|
|
66
|
+
}
|
|
67
|
+
const unverified = total - verified;
|
|
68
|
+
const coverage = total > 0 ? `${((verified / total) * 100).toFixed(1)}%` : "N/A";
|
|
69
|
+
return [
|
|
70
|
+
"# Local Memory Stats",
|
|
71
|
+
"",
|
|
72
|
+
`- Lessons: ${total}`,
|
|
73
|
+
`- Verified: ${verified}`,
|
|
74
|
+
`- Unverified: ${unverified}`,
|
|
75
|
+
`- Verification coverage: ${coverage}`,
|
|
76
|
+
].join("\n");
|
|
77
|
+
},
|
|
47
78
|
};
|
|
79
|
+
|
|
80
|
+
/** Line prefix `saveLearnedLesson` writes for verified/unverified lessons in `learned.md`. */
|
|
81
|
+
const VERIFIED_PREFIX = "- [VERIFIED]";
|
|
@@ -60,6 +60,17 @@ export interface MemoryBackendSaveInput {
|
|
|
60
60
|
context?: string;
|
|
61
61
|
source?: string;
|
|
62
62
|
importance?: number;
|
|
63
|
+
/**
|
|
64
|
+
* Marks this lesson as independently re-checked against the repo (a
|
|
65
|
+
* command run, a file read, a test executed) rather than merely asserted
|
|
66
|
+
* from inference. Mirrors the "verify" stage of the fail → investigate →
|
|
67
|
+
* verify → distill → consult self-improvement progression: a lesson that
|
|
68
|
+
* skips verification is still worth capturing, but downstream consumers
|
|
69
|
+
* (e.g. `stats()`) should be able to tell the two apart.
|
|
70
|
+
*/
|
|
71
|
+
verified?: boolean;
|
|
72
|
+
/** The short check that was run to establish `verified` (a command, a file path, a test name). Ignored when `verified` is not `true`. */
|
|
73
|
+
evidence?: string;
|
|
63
74
|
}
|
|
64
75
|
|
|
65
76
|
export interface MemoryBackendSaveResult {
|
|
@@ -20,6 +20,7 @@ Treat the design system as the foundation — UI built without one collapses int
|
|
|
20
20
|
2. **No coherent system? Build the minimal one first.** Extract what exists, then define a palette, type scale, spacing scale (4px/8px base), radii/shadows/transitions, and primitive components — THEN implement the request against it.
|
|
21
21
|
3. **Compose with the system, never around it.** Colors → tokens/CSS variables, never hardcoded hex; spacing → scale values, never arbitrary px; type → scale steps; components → extend/compose existing primitives, not one-off div soup. Need something outside the system? Add the new token to the system first, then use it — never a one-off override.
|
|
22
22
|
4. **Verify before done.** Every color a token, every spacing on the scale, every component on the existing composition pattern, zero magic numbers — a designer would see consistency across old and new. Any "no" → not done.
|
|
23
|
+
5. **Visually verify the rendered result.** Code-level checks (tokens/spacing) don't catch layout/rendering bugs. After producing a UI change, capture it (`tab.screenshot()` for browser-rendered UI, or the existing screenshot path) and call `vision_verify` with the task's stated goal — pass `baseline` when checking for regression against a known-good prior state.
|
|
23
24
|
</design-system>
|
|
24
25
|
|
|
25
26
|
<procedure>
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: goal-verifier
|
|
3
|
+
description: "Read-only independent grader for goal-mode completion. Verdicts whether the actual diff satisfies the stated objective and evidence claim — sees only the objective, evidence, and repo state, never the maker's reasoning transcript."
|
|
4
|
+
tools: read, grep, glob, bash, ast_grep
|
|
5
|
+
spawns: explore
|
|
6
|
+
model: pi/slow
|
|
7
|
+
thinking-level: high
|
|
8
|
+
output:
|
|
9
|
+
properties:
|
|
10
|
+
verdict:
|
|
11
|
+
metadata:
|
|
12
|
+
description: Completion verdict for the goal under review
|
|
13
|
+
enum: [okay, iterate, reject]
|
|
14
|
+
justification:
|
|
15
|
+
metadata:
|
|
16
|
+
description: Evidence-backed reasoning for the verdict, citing inspected files/diff hunks/command output
|
|
17
|
+
type: string
|
|
18
|
+
summary:
|
|
19
|
+
metadata:
|
|
20
|
+
description: 1-3 sentence plain-text summary of what was actually verified
|
|
21
|
+
type: string
|
|
22
|
+
optionalProperties:
|
|
23
|
+
required_fixes:
|
|
24
|
+
metadata:
|
|
25
|
+
description: Concrete gaps that must be fixed before the goal can be marked complete (empty for okay)
|
|
26
|
+
elements:
|
|
27
|
+
type: string
|
|
28
|
+
---
|
|
29
|
+
|
|
30
|
+
Decide whether a goal-mode objective is genuinely satisfied by the repo's current state — you are the independent grader, not the agent that did the work.
|
|
31
|
+
|
|
32
|
+
<procedure>
|
|
33
|
+
1. Read the `<objective>` and `<evidence>` given to you below. Treat both as claims to verify, not facts.
|
|
34
|
+
2. Run `git status` and `git diff` (add `git diff --stat` first on a large diff) to see every file actually changed this session. Read the changed files for context beyond the diff hunks when the diff alone doesn't establish correctness.
|
|
35
|
+
3. If the evidence claims a verification command ran (test/build/lint/typecheck), re-run the narrowest form of that command yourself against the current tree — do not trust the claim without confirming it. Do not run a full/slow suite when a scoped one covers the changed files; do not run anything destructive.
|
|
36
|
+
4. Cross-check the diff against the objective: does every part of the objective have corresponding evidence in the diff, or a clear reason no code change was needed for that part? Flag objective clauses with no corresponding change.
|
|
37
|
+
5. Decide the verdict and record it with the structured output.
|
|
38
|
+
</procedure>
|
|
39
|
+
|
|
40
|
+
<constraints>
|
|
41
|
+
- Read-only: you NEVER edit, write, or revert files. Verification commands you run must not mutate the tree (no `--fix`, no auto-format writes, no `git commit`/`git checkout`).
|
|
42
|
+
- Do not invent problems; reject or iterate only with concrete, cited gaps between the objective and the diff.
|
|
43
|
+
- A verdict grounded in nothing is worthless: your justification MUST name the files/diff hunks/commands you actually inspected or ran, and their actual output.
|
|
44
|
+
- You have no access to the maker's reasoning, chat transcript, or thinking blocks — judge the objective against the artifact (diff + verification output) alone. If the evidence text asserts something the diff doesn't support, that is a gap, not a detail to take on faith.
|
|
45
|
+
</constraints>
|
|
46
|
+
|
|
47
|
+
<verdicts>
|
|
48
|
+
- `okay` — every part of the objective is satisfied by the diff, and claimed verification actually holds when re-checked.
|
|
49
|
+
- `iterate` — real progress exists but named gaps remain (missing piece of the objective, evidence claim that doesn't hold up, or verification that fails when re-run); list each in `required_fixes`.
|
|
50
|
+
- `reject` — the diff does not address the objective at all, or the evidence is fabricated/contradicted by re-running it.
|
|
51
|
+
</verdicts>
|
|
@@ -5,3 +5,5 @@ Use after solving something whose insight will pay off again: a non-obvious fix,
|
|
|
5
5
|
Provide the optional `skill` object when the lesson is a repeatable *procedure* worth codifying as a `SKILL.md` (not just a fact). Managed skills are written to an isolated directory (`~/.jeopi/agent/managed-skills`) and are surfaced like normal skills next session. They NEVER touch user-authored skills. Frontmatter is generated from `name` and `description`.
|
|
6
6
|
|
|
7
7
|
Capture sparingly and specifically. One strong, reusable lesson beats several vague ones.
|
|
8
|
+
|
|
9
|
+
Set `verified: true` when you independently re-checked the claim against the repo — you ran a command, read a file, or ran a test that directly confirmed it — and pass a short `evidence` string naming that check (e.g. "ran `bun test src/foo.test.ts`, all 12 pass" or "read `package.json:12`, confirms dependency pin"). Leave `verified` unset (the default) for a hypothesis or pattern you noticed but did not independently confirm.
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
You are a UI/artifact verification assistant.
|
|
2
|
+
|
|
3
|
+
Core behavior:
|
|
4
|
+
- Be evidence-first: judge only what is visibly present in the image(s).
|
|
5
|
+
- If something is unclear or partially occluded, treat it as unmet rather than guessing it is fine.
|
|
6
|
+
- NEVER fabricate details that are not visible.
|
|
7
|
+
- When a baseline (prior) screenshot is provided, compare the current screenshot against it and treat any visible divergence as a gap unless the goal explicitly calls for that change.
|
|
8
|
+
|
|
9
|
+
Output contract — respond with ONLY a single JSON object, no prose before or after it, no markdown code fence, matching exactly:
|
|
10
|
+
{"matches": boolean, "summary": string, "gaps": string[]}
|
|
11
|
+
|
|
12
|
+
- `matches`: true only if the current screenshot fully satisfies the stated goal (and, when a baseline is given, shows no unintended regression from it).
|
|
13
|
+
- `summary`: one to three sentences describing what you observed.
|
|
14
|
+
- `gaps`: specific, concrete differences between what is observed and what the goal (or baseline) requires. Empty array when `matches` is true.
|
|
15
|
+
|
|
16
|
+
Return raw JSON only — any other format is a failure to follow instructions.
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
Verifies a screenshot or UI artifact against a stated goal with a vision-capable model and returns a structured verdict.
|
|
2
|
+
|
|
3
|
+
<instruction>
|
|
4
|
+
- Use this after producing or modifying a UI to check the result against the task's stated goal
|
|
5
|
+
- Provide `screenshot` as a local image file path, `Image #N` attachment label, or `attachment://N` URI
|
|
6
|
+
- Write a specific `goal`: what the UI/artifact should look like or achieve
|
|
7
|
+
- Pass `baseline` (a path to a prior screenshot of the same view) to check for visual regression against a known-good state
|
|
8
|
+
- Use this tool over `inspect_image` when you need a pass/fail verdict with structured gaps, not free-text description
|
|
9
|
+
</instruction>
|
|
10
|
+
|
|
11
|
+
<output>
|
|
12
|
+
- Returns a structured verdict: `matches` (boolean), `summary`, and `gaps` (empty when `matches` is true)
|
|
13
|
+
- No image content blocks are returned in tool output
|
|
14
|
+
</output>
|
|
15
|
+
|
|
16
|
+
<critical>
|
|
17
|
+
- If image submission is blocked by settings, the tool will fail with an actionable error
|
|
18
|
+
- If configured model does not support image input, configure a vision-capable model role before retrying
|
|
19
|
+
- If the model's response cannot be parsed as the expected JSON shape, the tool fails with the raw response included — retry rather than trust an unstructured fallback
|
|
20
|
+
</critical>
|
package/src/sdk.ts
CHANGED
|
@@ -76,6 +76,7 @@ import {
|
|
|
76
76
|
setActiveSkills,
|
|
77
77
|
} from "./extensibility/skills";
|
|
78
78
|
import { type FileSlashCommand, loadSlashCommands as loadSlashCommandsInternal } from "./extensibility/slash-commands";
|
|
79
|
+
import { verifyGoalCompletion } from "./goals/verifier";
|
|
79
80
|
import type { HindsightSessionState } from "./hindsight/state";
|
|
80
81
|
import { LocalProtocolHandler, type LocalProtocolOptions } from "./internal-urls";
|
|
81
82
|
import { LSP_STARTUP_EVENT_CHANNEL, type LspStartupEvent } from "./lsp/startup-events";
|
|
@@ -2756,6 +2757,7 @@ export async function createAgentSession(options: CreateAgentSessionOptions = {}
|
|
|
2756
2757
|
providerSessionId: options.providerSessionId,
|
|
2757
2758
|
parentEvalSessionId: options.parentEvalSessionId,
|
|
2758
2759
|
advisorTools,
|
|
2760
|
+
verifyGoalCompletion: (objective, evidence) => verifyGoalCompletion(toolSession, objective, evidence),
|
|
2759
2761
|
titleSystemPrompt: options.titleSystemPrompt,
|
|
2760
2762
|
});
|
|
2761
2763
|
hasSession = true;
|
|
@@ -222,7 +222,7 @@ import { ExtensionToolWrapper } from "../extensibility/extensions/wrapper";
|
|
|
222
222
|
import type { HookCommandContext } from "../extensibility/hooks/types";
|
|
223
223
|
import type { Skill, SkillWarning } from "../extensibility/skills";
|
|
224
224
|
import { expandSlashCommand, type FileSlashCommand } from "../extensibility/slash-commands";
|
|
225
|
-
import { GoalRuntime } from "../goals/runtime";
|
|
225
|
+
import { type GoalCompletionVerdict, GoalRuntime } from "../goals/runtime";
|
|
226
226
|
import type { Goal, GoalModeState } from "../goals/state";
|
|
227
227
|
import type { HindsightSessionState } from "../hindsight/state";
|
|
228
228
|
import { type LocalProtocolOptions, resolveLocalUrlToPath } from "../internal-urls";
|
|
@@ -539,13 +539,19 @@ function refusalBackoffBudgetMs(): number {
|
|
|
539
539
|
}
|
|
540
540
|
/**
|
|
541
541
|
* Budget for callers on the user-visible `/quit` / `/exit` shutdown path that
|
|
542
|
-
* want to cap how long they wait for `MnemopiSessionState.dispose()`
|
|
543
|
-
*
|
|
544
|
-
*
|
|
545
|
-
*
|
|
546
|
-
*
|
|
542
|
+
* want to cap how long they wait for `MnemopiSessionState.dispose()` (and the
|
|
543
|
+
* Hindsight retain-queue flush, see the dispose() call site) to finish writing
|
|
544
|
+
* this session's memory before the process exits. Consolidate fires fresh LLM
|
|
545
|
+
* fact extractions, each a 1-3s round-trip; the retain-queue flush is a single
|
|
546
|
+
* HTTP POST. The prior 1500ms budget was tighter than a single extraction
|
|
547
|
+
* round-trip, so it lost the race on effectively every session with any
|
|
548
|
+
* memory-worthy content — "write before walking away" in name only. 8s still
|
|
549
|
+
* bounds worst-case UI unresponsiveness on /exit while giving a small
|
|
550
|
+
* consolidation batch (1-3 facts) a realistic chance to land before the
|
|
551
|
+
* process exits. Callers that keep the process/session host alive must omit
|
|
552
|
+
* it so dispose still awaits the full write pipeline unbounded.
|
|
547
553
|
*/
|
|
548
|
-
export const SHUTDOWN_CONSOLIDATE_BUDGET_MS =
|
|
554
|
+
export const SHUTDOWN_CONSOLIDATE_BUDGET_MS = 8_000;
|
|
549
555
|
|
|
550
556
|
export interface AgentSessionDisposeOptions {
|
|
551
557
|
mnemopiConsolidateTimeoutMs?: number;
|
|
@@ -728,6 +734,15 @@ export interface AgentSessionConfig {
|
|
|
728
734
|
rebuildSystemPrompt?: (toolNames: string[], tools: Map<string, AgentTool>) => Promise<{ systemPrompt: string[] }>;
|
|
729
735
|
/** Rebuild the SSH tool from current capability discovery results. */
|
|
730
736
|
reloadSshTool?: () => Promise<AgentTool | null>;
|
|
737
|
+
/**
|
|
738
|
+
* Independent-verifier bridge for goal-mode completion (see goals/verifier.ts).
|
|
739
|
+
* Spawns the bundled `goal-verifier` sub-agent to grade a completion claim against
|
|
740
|
+
* the objective and the actual repo diff — a fresh session/context, no exposure to
|
|
741
|
+
* this session's transcript. Wired from sdk.ts (needs the constructed `toolSession`).
|
|
742
|
+
* Undefined when `goal.verifyCompletion` is off or the caller has no tool session
|
|
743
|
+
* (e.g. minimal SDK embedders) — GoalRuntime skips independent grading in that case.
|
|
744
|
+
*/
|
|
745
|
+
verifyGoalCompletion?: (objective: string, evidence: string) => Promise<GoalCompletionVerdict>;
|
|
731
746
|
requestedToolNames?: ReadonlySet<string>;
|
|
732
747
|
/**
|
|
733
748
|
* Optional accessor for live MCP server instructions. Read by the session's
|
|
@@ -2210,6 +2225,14 @@ export class AgentSession {
|
|
|
2210
2225
|
{ deliverAs: message.deliverAs },
|
|
2211
2226
|
);
|
|
2212
2227
|
},
|
|
2228
|
+
verifyCompletion: config.verifyGoalCompletion
|
|
2229
|
+
? async (objective: string, evidence: string): Promise<GoalCompletionVerdict> => {
|
|
2230
|
+
if (!this.settings.get("goal.verifyCompletion")) {
|
|
2231
|
+
return { verdict: "okay", justification: "goal.verifyCompletion disabled", requiredFixes: [] };
|
|
2232
|
+
}
|
|
2233
|
+
return config.verifyGoalCompletion!(objective, evidence);
|
|
2234
|
+
}
|
|
2235
|
+
: undefined,
|
|
2213
2236
|
});
|
|
2214
2237
|
this.#cancelExitRecorder = postmortem.register(`agent-session:${this.sessionManager.getSessionId()}`, reason => {
|
|
2215
2238
|
this.#recordSessionExit(reason);
|
|
@@ -5618,7 +5641,27 @@ export class AgentSession {
|
|
|
5618
5641
|
// Reversed, the spliced batch survives just long enough to fail the
|
|
5619
5642
|
// identity check and get dropped with a `session vanished` warning.
|
|
5620
5643
|
const hindsightState = this.getHindsightSessionState();
|
|
5621
|
-
|
|
5644
|
+
// Bounded when a shutdown budget is set (interactive /quit /exit): the
|
|
5645
|
+
// underlying HindsightApi client makes plain `fetch()` calls with no
|
|
5646
|
+
// timeout/signal, so an unreachable or hung Hindsight server would
|
|
5647
|
+
// otherwise block process exit indefinitely. Callers that keep the
|
|
5648
|
+
// process/session host alive (no timeout passed) still await the flush
|
|
5649
|
+
// unbounded, matching the mnemopi dispose contract below.
|
|
5650
|
+
if (hindsightState) {
|
|
5651
|
+
const { mnemopiConsolidateTimeoutMs: timeoutMs } = options;
|
|
5652
|
+
if (timeoutMs !== undefined && timeoutMs > 0) {
|
|
5653
|
+
try {
|
|
5654
|
+
await withTimeout(hindsightState.flushRetainQueue(), timeoutMs, "Hindsight retain-queue flush");
|
|
5655
|
+
} catch (error) {
|
|
5656
|
+
logger.warn("Hindsight retain-queue flush exceeded shutdown budget; remaining items dropped", {
|
|
5657
|
+
timeoutMs,
|
|
5658
|
+
error: String(error),
|
|
5659
|
+
});
|
|
5660
|
+
}
|
|
5661
|
+
} else {
|
|
5662
|
+
await hindsightState.flushRetainQueue();
|
|
5663
|
+
}
|
|
5664
|
+
}
|
|
5622
5665
|
this.setHindsightSessionState(undefined);
|
|
5623
5666
|
hindsightState?.dispose();
|
|
5624
5667
|
const mnemopiState = setMnemopiSessionState(this, undefined);
|
|
@@ -43,6 +43,7 @@ import { createMarketplaceManager } from "./helpers/marketplace-manager";
|
|
|
43
43
|
import { handleMcpAcp } from "./helpers/mcp";
|
|
44
44
|
import { commandConsumed, errorMessage, parseSlashCommand, parseSubcommand, usage } from "./helpers/parse";
|
|
45
45
|
import { describeRedeemOutcome, type ResetUsageAccount, toResetUsageAccounts } from "./helpers/reset-usage";
|
|
46
|
+
import { handleSkillCommand } from "./helpers/skill-verify";
|
|
46
47
|
import { handleSshAcp } from "./helpers/ssh";
|
|
47
48
|
import { launchStatsDashboard, parseStatsDashboardArgs } from "./helpers/stats-dashboard";
|
|
48
49
|
import { handleTodoAcp } from "./helpers/todo";
|
|
@@ -1348,6 +1349,14 @@ const BUILTIN_SLASH_COMMAND_REGISTRY: ReadonlyArray<SlashCommandSpec> = [
|
|
|
1348
1349
|
await runtime.ctx.handleSSHCommand(command.text);
|
|
1349
1350
|
},
|
|
1350
1351
|
},
|
|
1352
|
+
{
|
|
1353
|
+
name: "verify-skill",
|
|
1354
|
+
description: "Run a skill's `## Eval Cases` and report pass/fail",
|
|
1355
|
+
acpDescription: "Verify a skill's eval cases",
|
|
1356
|
+
inlineHint: "<name>",
|
|
1357
|
+
allowArgs: true,
|
|
1358
|
+
handle: handleSkillCommand,
|
|
1359
|
+
},
|
|
1351
1360
|
{
|
|
1352
1361
|
name: "new",
|
|
1353
1362
|
priority: 96,
|
|
@@ -0,0 +1,171 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `/verify-skill <name>` — self-verification for a skill's `## Eval Cases`.
|
|
3
|
+
*
|
|
4
|
+
* Resolves a skill from the active session, extracts a bulleted `## Eval
|
|
5
|
+
* Cases` markdown section from its `SKILL.md` body (if present), and spawns
|
|
6
|
+
* one read-only `critic` subagent per case to verdict whether the claim still
|
|
7
|
+
* holds against the current repo state. Renders a pass/fail markdown report;
|
|
8
|
+
* never mutates the skill file.
|
|
9
|
+
*
|
|
10
|
+
* Named `verify-skill` (not `skill`) to avoid colliding with the pre-existing
|
|
11
|
+
* `/skill:<name>` direct-skill-invocation convention (both parse to the
|
|
12
|
+
* command-name token `skill` under `parseSlashCommand`'s earliest-whitespace-
|
|
13
|
+
* or-colon splitting rule — see slash-commands/helpers/parse.ts).
|
|
14
|
+
*/
|
|
15
|
+
import { MANAGED_SKILLS_PROVIDER_ID } from "../../autolearn/managed-skills";
|
|
16
|
+
import { runEvalAgent } from "../../eval/agent-bridge";
|
|
17
|
+
import { getBundledAgent } from "../../task/agents";
|
|
18
|
+
import * as taskDiscovery from "../../task/discovery";
|
|
19
|
+
import type { ToolSession } from "../../tools";
|
|
20
|
+
import type { ParsedSlashCommand, SlashCommandResult, SlashCommandRuntime } from "../types";
|
|
21
|
+
import { commandConsumed, usage } from "./parse";
|
|
22
|
+
|
|
23
|
+
const VERIFIER_AGENT_NAME = "critic";
|
|
24
|
+
|
|
25
|
+
interface EvalCaseVerdict {
|
|
26
|
+
claim: string;
|
|
27
|
+
passed: boolean;
|
|
28
|
+
reason: string;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/** Extract the bulleted list under a `## Eval Cases` heading, if present. */
|
|
32
|
+
export function extractEvalCases(skillBody: string): string[] {
|
|
33
|
+
const headingMatch = /^##\s+Eval Cases\s*$/m.exec(skillBody);
|
|
34
|
+
if (!headingMatch) return [];
|
|
35
|
+
const sectionStart = headingMatch.index + headingMatch[0].length;
|
|
36
|
+
const rest = skillBody.slice(sectionStart);
|
|
37
|
+
const nextHeadingMatch = /^#{1,6}\s+\S/m.exec(rest);
|
|
38
|
+
const sectionBody = nextHeadingMatch ? rest.slice(0, nextHeadingMatch.index) : rest;
|
|
39
|
+
const cases: string[] = [];
|
|
40
|
+
for (const line of sectionBody.split("\n")) {
|
|
41
|
+
const bulletMatch = /^\s*[-*+]\s+(.+)$/.exec(line);
|
|
42
|
+
if (bulletMatch?.[1]) cases.push(bulletMatch[1].trim());
|
|
43
|
+
}
|
|
44
|
+
return cases;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/** Verdict a single eval-case claim against the current repo state via the bundled `critic` agent. */
|
|
48
|
+
async function verifyEvalCase(session: ToolSession, claim: string): Promise<EvalCaseVerdict> {
|
|
49
|
+
const verifyPrompt =
|
|
50
|
+
`Verify this claim against the current repository state: ${claim}\n\n` +
|
|
51
|
+
"Report whether it still holds (yes/no) and why, citing files/commands.";
|
|
52
|
+
// Explicit schema, not the agent's own frontmatter default: runEvalAgent (the
|
|
53
|
+
// eval `agent()` bridge) only applies `outputSchema` when the caller supplies
|
|
54
|
+
// one — it never falls back to the agent's `output` frontmatter on its own
|
|
55
|
+
// (see goals/verifier.ts for the same pattern). Read the bundled definition's
|
|
56
|
+
// declared shape so this can't drift from critic.md.
|
|
57
|
+
const schema = getBundledAgent(VERIFIER_AGENT_NAME)?.output;
|
|
58
|
+
const args =
|
|
59
|
+
schema === undefined
|
|
60
|
+
? { prompt: verifyPrompt, agent: VERIFIER_AGENT_NAME }
|
|
61
|
+
: { prompt: verifyPrompt, agent: VERIFIER_AGENT_NAME, schema };
|
|
62
|
+
try {
|
|
63
|
+
const result = await runEvalAgent(args, { session });
|
|
64
|
+
let verdict: unknown;
|
|
65
|
+
try {
|
|
66
|
+
verdict = JSON.parse(result.text);
|
|
67
|
+
} catch {
|
|
68
|
+
// Freeform fallback: no parseable JSON, treat non-empty output as inconclusive-pass with raw text as reason.
|
|
69
|
+
return { claim, passed: true, reason: result.text.trim().slice(0, 300) || "No structured verdict returned." };
|
|
70
|
+
}
|
|
71
|
+
if (verdict && typeof verdict === "object") {
|
|
72
|
+
const record = verdict as Record<string, unknown>;
|
|
73
|
+
const rawVerdict = record.verdict;
|
|
74
|
+
const passed = rawVerdict === "okay";
|
|
75
|
+
const reason =
|
|
76
|
+
(typeof record.summary === "string" && record.summary) ||
|
|
77
|
+
(typeof record.justification === "string" && record.justification) ||
|
|
78
|
+
"No justification returned.";
|
|
79
|
+
return { claim, passed, reason: reason.slice(0, 400) };
|
|
80
|
+
}
|
|
81
|
+
return { claim, passed: false, reason: "Verifier returned an unparseable verdict." };
|
|
82
|
+
} catch (err) {
|
|
83
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
84
|
+
return { claim, passed: false, reason: `Verification subagent failed: ${message}` };
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
function renderReport(skillName: string, isManaged: boolean, verdicts: EvalCaseVerdict[]): string {
|
|
89
|
+
const lines: string[] = [`## Skill verification: \`${skillName}\``, ""];
|
|
90
|
+
const failures: EvalCaseVerdict[] = [];
|
|
91
|
+
for (const verdict of verdicts) {
|
|
92
|
+
lines.push(`- ${verdict.passed ? "✅ PASS" : "❌ FAIL"} — ${verdict.claim}`);
|
|
93
|
+
lines.push(` ${verdict.reason}`);
|
|
94
|
+
if (!verdict.passed) failures.push(verdict);
|
|
95
|
+
}
|
|
96
|
+
const passCount = verdicts.length - failures.length;
|
|
97
|
+
lines.push("", `${passCount}/${verdicts.length} eval cases passed.`);
|
|
98
|
+
if (failures.length > 0 && isManaged) {
|
|
99
|
+
lines.push(
|
|
100
|
+
"",
|
|
101
|
+
"This is a managed (auto-learn) skill. To record the failure mode, append a line like this under a `## Known Failure Modes` section (not applied automatically):",
|
|
102
|
+
"",
|
|
103
|
+
);
|
|
104
|
+
for (const failure of failures) {
|
|
105
|
+
lines.push(`- "${failure.claim}" — ${failure.reason}`);
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
return lines.join("\n");
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/** `/verify-skill <name>` handler — resolves a skill, runs its `## Eval Cases`, reports pass/fail. */
|
|
112
|
+
export async function handleSkillCommand(
|
|
113
|
+
command: ParsedSlashCommand,
|
|
114
|
+
runtime: SlashCommandRuntime,
|
|
115
|
+
): Promise<SlashCommandResult> {
|
|
116
|
+
const skillName = command.args.trim();
|
|
117
|
+
if (!skillName) {
|
|
118
|
+
return usage("Usage: /verify-skill <name>", runtime);
|
|
119
|
+
}
|
|
120
|
+
const skill = runtime.session.skills.find(candidate => candidate.name === skillName);
|
|
121
|
+
if (!skill) {
|
|
122
|
+
const available = runtime.session.skills.map(candidate => candidate.name).join(", ") || "none";
|
|
123
|
+
return usage(`Unknown skill "${skillName}". Available: ${available}`, runtime);
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
const body = await Bun.file(skill.filePath).text();
|
|
127
|
+
const evalCases = extractEvalCases(body);
|
|
128
|
+
if (evalCases.length === 0) {
|
|
129
|
+
await runtime.output(
|
|
130
|
+
`No eval cases defined for this skill — add an \`## Eval Cases\` section with bulleted verifiable claims to enable self-verification`,
|
|
131
|
+
);
|
|
132
|
+
return commandConsumed();
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
await runtime.output(`Verifying ${evalCases.length} eval case(s) for \`${skillName}\`…`);
|
|
136
|
+
|
|
137
|
+
// Discover once, upfront, to fail fast with a clear message if the critic
|
|
138
|
+
// agent is unavailable (disabled, or bundled agents failed to load) rather
|
|
139
|
+
// than surfacing N identical spawn errors.
|
|
140
|
+
const { agents } = await taskDiscovery.discoverAgents(runtime.cwd);
|
|
141
|
+
if (!taskDiscovery.getAgent(agents, VERIFIER_AGENT_NAME)) {
|
|
142
|
+
return usage(
|
|
143
|
+
`The "${VERIFIER_AGENT_NAME}" agent is unavailable in this session; cannot run eval cases.`,
|
|
144
|
+
runtime,
|
|
145
|
+
);
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
const session: ToolSession = {
|
|
149
|
+
cwd: runtime.cwd,
|
|
150
|
+
hasUI: false,
|
|
151
|
+
settings: runtime.settings,
|
|
152
|
+
skills: [...runtime.session.skills],
|
|
153
|
+
taskDepth: 0,
|
|
154
|
+
getSessionFile: () => runtime.session.sessionManager.getSessionFile() ?? null,
|
|
155
|
+
getSessionSpawns: () => "*",
|
|
156
|
+
getPlanModeState: () => runtime.session.getPlanModeState(),
|
|
157
|
+
getActiveModelString: () =>
|
|
158
|
+
runtime.session.model ? `${runtime.session.model.provider}/${runtime.session.model.id}` : undefined,
|
|
159
|
+
modelRegistry: runtime.session.modelRegistry,
|
|
160
|
+
authStorage: runtime.session.modelRegistry.authStorage,
|
|
161
|
+
getAgentId: () => runtime.session.getAgentId() ?? null,
|
|
162
|
+
};
|
|
163
|
+
const verdicts: EvalCaseVerdict[] = [];
|
|
164
|
+
for (const claim of evalCases) {
|
|
165
|
+
verdicts.push(await verifyEvalCase(session, claim));
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
const isManaged = skill._source?.provider === MANAGED_SKILLS_PROVIDER_ID;
|
|
169
|
+
await runtime.output(renderReport(skillName, isManaged, verdicts));
|
|
170
|
+
return commandConsumed();
|
|
171
|
+
}
|
package/src/task/agents.ts
CHANGED
|
@@ -12,6 +12,7 @@ import designerMd from "../prompts/agents/designer.md" with { type: "text" };
|
|
|
12
12
|
import exploreMd from "../prompts/agents/explore.md" with { type: "text" };
|
|
13
13
|
// Embed agent markdown files at build time
|
|
14
14
|
import agentFrontmatterTemplate from "../prompts/agents/frontmatter.md" with { type: "text" };
|
|
15
|
+
import goalVerifierMd from "../prompts/agents/goal-verifier.md" with { type: "text" };
|
|
15
16
|
import librarianMd from "../prompts/agents/librarian.md" with { type: "text" };
|
|
16
17
|
|
|
17
18
|
import planMd from "../prompts/agents/plan.md" with { type: "text" };
|
|
@@ -47,6 +48,7 @@ const EMBEDDED_AGENT_DEFS: EmbeddedAgentDef[] = [
|
|
|
47
48
|
{ fileName: "explore.md", template: exploreMd },
|
|
48
49
|
{ fileName: "plan.md", template: planMd },
|
|
49
50
|
{ fileName: "critic.md", template: criticMd },
|
|
51
|
+
{ fileName: "goal-verifier.md", template: goalVerifierMd },
|
|
50
52
|
{ fileName: "architect.md", template: architectMd },
|
|
51
53
|
{ fileName: "designer.md", template: designerMd },
|
|
52
54
|
{ fileName: "reviewer.md", template: reviewerMd },
|
package/src/task/index.ts
CHANGED
|
@@ -67,6 +67,7 @@ import {
|
|
|
67
67
|
prepareIsolationContext,
|
|
68
68
|
runIsolatedSubprocess,
|
|
69
69
|
} from "./isolation-runner";
|
|
70
|
+
import { classifyDefaultTaskModelTier } from "./model-tier-classifier";
|
|
70
71
|
import { generateTaskName } from "./name-generator";
|
|
71
72
|
import { AgentOutputManager } from "./output-manager";
|
|
72
73
|
import { mapWithConcurrencyLimit, normalizeConcurrencyLimit, Semaphore } from "./parallel";
|
|
@@ -1202,7 +1203,7 @@ export class TaskTool implements AgentTool<TaskToolSchemaInstance, TaskToolDetai
|
|
|
1202
1203
|
const agentModelOverrides = this.session.settings.get("task.agentModelOverrides");
|
|
1203
1204
|
const settingsModelOverride = agentModelOverrides[agentName];
|
|
1204
1205
|
const parentActiveModelPattern = this.session.getActiveModelString?.();
|
|
1205
|
-
|
|
1206
|
+
let modelOverride = resolveAgentModelPatterns({
|
|
1206
1207
|
settingsOverride: settingsModelOverride,
|
|
1207
1208
|
agentModel: effectiveAgent.model,
|
|
1208
1209
|
settings: this.session.settings,
|
|
@@ -1211,6 +1212,30 @@ export class TaskTool implements AgentTool<TaskToolSchemaInstance, TaskToolDetai
|
|
|
1211
1212
|
});
|
|
1212
1213
|
const thinkingLevelOverride = effectiveAgent.thinkingLevel;
|
|
1213
1214
|
|
|
1215
|
+
// Auto-tier the generic default `task` agent to the cheap `smol` model
|
|
1216
|
+
// when the assignment classifies as trivially simple and nothing else
|
|
1217
|
+
// (settings override) already pinned the model. Named specialists carry
|
|
1218
|
+
// their own `model:` frontmatter, so gating on the literal "task" agent
|
|
1219
|
+
// name naturally excludes them without inspecting frontmatter here.
|
|
1220
|
+
const autoModel = this.session.getActiveModel?.();
|
|
1221
|
+
const autoModelRegistry = this.session.modelRegistry;
|
|
1222
|
+
if (
|
|
1223
|
+
agentName === "task" &&
|
|
1224
|
+
settingsModelOverride === undefined &&
|
|
1225
|
+
this.session.settings.get("task.autoModelTier") &&
|
|
1226
|
+
autoModel &&
|
|
1227
|
+
autoModelRegistry
|
|
1228
|
+
) {
|
|
1229
|
+
const tier = await classifyDefaultTaskModelTier(assignment, {
|
|
1230
|
+
settings: this.session.settings,
|
|
1231
|
+
registry: autoModelRegistry,
|
|
1232
|
+
model: autoModel,
|
|
1233
|
+
sessionId: this.session.getSessionId?.() ?? undefined,
|
|
1234
|
+
signal,
|
|
1235
|
+
});
|
|
1236
|
+
if (tier === "smol") modelOverride = ["pi/smol"];
|
|
1237
|
+
}
|
|
1238
|
+
|
|
1214
1239
|
// Output schema priority: agent frontmatter > inherited parent session.
|
|
1215
1240
|
// The task call itself never carries a schema; workflows needing ad-hoc
|
|
1216
1241
|
// structured output go through eval agent(prompt, schema).
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Default `task`-agent spawn model-tier classifier.
|
|
3
|
+
*
|
|
4
|
+
* Reuses the auto-thinking difficulty classifier ({@link classifyDifficulty})
|
|
5
|
+
* to decide whether a `task()` spawn's assignment is trivially simple enough
|
|
6
|
+
* to route to the cheap `smol` model tier instead of the default `task`
|
|
7
|
+
* tier. Only ever suggests a downgrade — never blocks or upgrades — and
|
|
8
|
+
* fails open (returns `undefined`, meaning "don't override") on any
|
|
9
|
+
* classification error or timeout, mirroring `AgentSession`'s
|
|
10
|
+
* `#applyAutoThinkingLevel` fallback behavior.
|
|
11
|
+
*/
|
|
12
|
+
import { Effort } from "jeopi-ai";
|
|
13
|
+
import { logger } from "jeopi-utils";
|
|
14
|
+
import { type ClassifyDifficultyDeps, classifyDifficulty } from "../auto-thinking/classifier";
|
|
15
|
+
|
|
16
|
+
/** Timeout (ms) for the spawn-time model-tier classification before failing open. */
|
|
17
|
+
const MODEL_TIER_CLASSIFICATION_TIMEOUT_MS = 2000;
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Classify `assignment` and return `"smol"` when the work is trivially
|
|
21
|
+
* simple ({@link Effort.Minimal} or {@link Effort.Low}), or `undefined` when
|
|
22
|
+
* the caller should leave normal model resolution in place (not-simple
|
|
23
|
+
* enough, no classifier available, or the classification failed/timed out).
|
|
24
|
+
*/
|
|
25
|
+
export async function classifyDefaultTaskModelTier(
|
|
26
|
+
assignment: string,
|
|
27
|
+
deps: ClassifyDifficultyDeps,
|
|
28
|
+
): Promise<"smol" | undefined> {
|
|
29
|
+
const controller = new AbortController();
|
|
30
|
+
const timer = setTimeout(() => controller.abort(), MODEL_TIER_CLASSIFICATION_TIMEOUT_MS);
|
|
31
|
+
const signal = deps.signal ? AbortSignal.any([deps.signal, controller.signal]) : controller.signal;
|
|
32
|
+
try {
|
|
33
|
+
const effort = await classifyDifficulty(assignment, { ...deps, signal });
|
|
34
|
+
return effort === Effort.Minimal || effort === Effort.Low ? "smol" : undefined;
|
|
35
|
+
} catch (error) {
|
|
36
|
+
logger.debug("task: model-tier classification failed; leaving model resolution unchanged", {
|
|
37
|
+
error: error instanceof Error ? error.message : String(error),
|
|
38
|
+
});
|
|
39
|
+
return undefined;
|
|
40
|
+
} finally {
|
|
41
|
+
clearTimeout(timer);
|
|
42
|
+
}
|
|
43
|
+
}
|