jeopi-cli 16.2.22 → 16.2.24

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 (49) hide show
  1. package/CHANGELOG.md +31 -0
  2. package/dist/cli.js +2565 -2524
  3. package/dist/types/advisor/runtime.d.ts +9 -1
  4. package/dist/types/cli/update-cli.d.ts +37 -0
  5. package/dist/types/config/settings-schema.d.ts +12 -1
  6. package/dist/types/extensibility/plugins/loader.d.ts +4 -4
  7. package/dist/types/extensibility/plugins/trust.d.ts +30 -0
  8. package/dist/types/goals/completion-evidence.d.ts +21 -0
  9. package/dist/types/goals/runtime.d.ts +1 -1
  10. package/dist/types/goals/state.d.ts +3 -0
  11. package/dist/types/goals/tools/goal-tool.d.ts +2 -0
  12. package/dist/types/main.d.ts +20 -0
  13. package/dist/types/mnemopi/state.d.ts +1 -0
  14. package/dist/types/session/agent-session.d.ts +3 -1
  15. package/dist/types/task/critic-gate.d.ts +34 -0
  16. package/dist/types/tools/builtin-names.d.ts +1 -1
  17. package/dist/types/tools/index.d.ts +6 -1
  18. package/dist/types/tools/subagent.d.ts +70 -0
  19. package/dist/types/tools/subagent.test.d.ts +1 -0
  20. package/package.json +12 -12
  21. package/src/advisor/__tests__/advisor.test.ts +173 -0
  22. package/src/advisor/runtime.ts +66 -2
  23. package/src/cli/update-cli.ts +144 -0
  24. package/src/config/settings-schema.ts +14 -2
  25. package/src/extensibility/plugins/loader.ts +35 -9
  26. package/src/extensibility/plugins/trust.ts +133 -0
  27. package/src/goals/completion-evidence.ts +73 -0
  28. package/src/goals/runtime.ts +6 -1
  29. package/src/goals/state.ts +3 -0
  30. package/src/goals/tools/goal-tool.ts +16 -1
  31. package/src/internal-urls/docs-index.generated.txt +2 -2
  32. package/src/main.ts +56 -10
  33. package/src/memories/index.ts +21 -1
  34. package/src/mnemopi/state.ts +10 -2
  35. package/src/modes/interactive-mode.ts +21 -0
  36. package/src/prompts/memories/read-path.md +3 -1
  37. package/src/prompts/system/safety-kernel.md +17 -0
  38. package/src/prompts/system/tool-call-cycle-redirect.md +5 -0
  39. package/src/prompts/tools/goal.md +1 -1
  40. package/src/prompts/tools/subagent.md +29 -0
  41. package/src/session/agent-session.ts +68 -19
  42. package/src/slash-commands/builtin-registry.ts +18 -0
  43. package/src/system-prompt.ts +14 -2
  44. package/src/task/critic-gate.ts +46 -0
  45. package/src/task/index.ts +63 -2
  46. package/src/tools/builtin-names.ts +1 -0
  47. package/src/tools/index.ts +9 -1
  48. package/src/tools/subagent.test.ts +285 -0
  49. package/src/tools/subagent.ts +582 -0
@@ -47,14 +47,22 @@ export interface AdvisorRuntimeHost {
47
47
  beginAdvisorUpdate?(): void;
48
48
  /** Surface a non-recovering advisor failure to the host UI without adding model-visible context. */
49
49
  notifyFailure?(error: unknown): void;
50
+ /** Surface a one-time notice that thinking was stripped from advisor renders after a provider refusal. */
51
+ notifyThinkingStripped?(): void;
50
52
  }
53
+ /** Construction-time tuning for {@link AdvisorRuntime}. */
54
+ export interface AdvisorRuntimeOptions {
55
+ /** Render the primary's thinking blocks into advisor deltas (default true). */
56
+ includeThinking?: boolean;
57
+ }
58
+ export declare function isRefusalError(error: unknown): boolean;
51
59
  export declare class AdvisorRuntime {
52
60
  #private;
53
61
  private readonly agent;
54
62
  private readonly host;
55
63
  private readonly retryDelayMs;
56
64
  disposed: boolean;
57
- constructor(agent: AdvisorAgent, host: AdvisorRuntimeHost, retryDelayMs?: number);
65
+ constructor(agent: AdvisorAgent, host: AdvisorRuntimeHost, retryDelayMs?: number, options?: AdvisorRuntimeOptions);
58
66
  get backlog(): number;
59
67
  onTurnEnd(messages?: AgentMessage[]): void;
60
68
  waitForCatchup(maxMs: number, threshold: number, signal?: AbortSignal): Promise<void>;
@@ -64,6 +64,43 @@ export declare function getLatestRelease(): Promise<ReleaseInfo>;
64
64
  * registries resolve to `undefined` so startup never blocks on the check.
65
65
  */
66
66
  export declare function fetchNewerPublishedVersion(currentVersion: string): Promise<string | undefined>;
67
+ /** Minimal published GitHub release shape used by the update-notes and registry-lag helpers. */
68
+ export interface GitHubReleaseSummary {
69
+ version: string;
70
+ body: string;
71
+ }
72
+ /**
73
+ * Parse a GitHub releases API response into {@link GitHubReleaseSummary}
74
+ * entries, keeping only published (non-draft, non-prerelease) releases whose
75
+ * tag matches the `vX.Y.Z` convention. Split out from {@link fetchGitHubReleases}
76
+ * so the response-shape handling is independently testable without a network
77
+ * call.
78
+ */
79
+ export declare function parseGitHubReleases(data: unknown): GitHubReleaseSummary[];
80
+ /**
81
+ * Collect release-note bodies for versions in `(currentVersion, targetVersion]`,
82
+ * newest first. Backs the "what changed" block `jeopi update` prints under the
83
+ * "New version available" line, so a user several versions behind sees every
84
+ * intervening release, not just the newest body.
85
+ */
86
+ export declare function selectReleaseNotesInRange(releases: readonly GitHubReleaseSummary[], currentVersion: string, targetVersion: string): GitHubReleaseSummary[];
87
+ /**
88
+ * Render selected release notes as plain indented text, capped at `maxLines`.
89
+ * HTML comments (release template scaffolding) are stripped; a truncated
90
+ * rendering ends with a pointer at the releases page.
91
+ */
92
+ export declare function formatReleaseNotes(notes: readonly GitHubReleaseSummary[], maxLines?: number): string;
93
+ /**
94
+ * Detect a published GitHub release the npm registry has not served yet.
95
+ *
96
+ * When the release pipeline dies between the GitHub Release and the npm
97
+ * publish job (or npm credentials are missing so the publish silently
98
+ * no-ops), the npm `latest` this updater trusts stays behind while GitHub
99
+ * advances — and `jeopi update` would report "Already up to date" with no
100
+ * hint that a newer release exists. Returns the newest GitHub version
101
+ * strictly above `npmVersion`, else `undefined`.
102
+ */
103
+ export declare function detectRegistryLag(npmVersion: string, releases: readonly GitHubReleaseSummary[]): string | undefined;
67
104
  interface BunInstallCachePruneResult {
68
105
  scannedPackages: number;
69
106
  removedEntries: number;
@@ -187,6 +187,17 @@ export declare const SETTINGS_SCHEMA: {
187
187
  readonly condition: "advisorEnabled";
188
188
  };
189
189
  };
190
+ readonly "advisor.includeThinking": {
191
+ readonly type: "boolean";
192
+ readonly default: true;
193
+ readonly ui: {
194
+ readonly tab: "model";
195
+ readonly group: "Advisor";
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 also auto-disables this for the session after such a refusal.";
198
+ readonly condition: "advisorEnabled";
199
+ };
200
+ };
190
201
  readonly "advisor.syncBacklog": {
191
202
  readonly type: "enum";
192
203
  readonly values: readonly ["off", "1", "3", "5"];
@@ -1602,7 +1613,7 @@ export declare const SETTINGS_SCHEMA: {
1602
1613
  readonly tab: "interaction";
1603
1614
  readonly group: "Startup & Updates";
1604
1615
  readonly label: "Check for Updates";
1605
- readonly description: "Check for jeopi updates on startup";
1616
+ readonly description: "Check for jeopi updates on startup and periodically during long sessions";
1606
1617
  };
1607
1618
  };
1608
1619
  readonly "marketplace.autoUpdate": {
@@ -37,19 +37,19 @@ export declare function resolvePluginHookPaths(plugin: InstalledPlugin): string[
37
37
  export declare function resolvePluginCommandPaths(plugin: InstalledPlugin): string[];
38
38
  export declare function resolvePluginExtensionPaths(plugin: InstalledPlugin): string[];
39
39
  /**
40
- * Get all tool paths from all enabled plugins.
40
+ * Get all tool paths from all enabled *and trusted* plugins.
41
41
  */
42
42
  export declare function getAllPluginToolPaths(cwd: string): Promise<string[]>;
43
43
  /**
44
- * Get all hook paths from all enabled plugins.
44
+ * Get all hook paths from all enabled *and trusted* plugins.
45
45
  */
46
46
  export declare function getAllPluginHookPaths(cwd: string): Promise<string[]>;
47
47
  /**
48
- * Get all command paths from all enabled plugins.
48
+ * Get all command paths from all enabled *and trusted* plugins.
49
49
  */
50
50
  export declare function getAllPluginCommandPaths(cwd: string): Promise<string[]>;
51
51
  /**
52
- * Get all extension module paths from all enabled plugins.
52
+ * Get all extension module paths from all enabled *and trusted* plugins.
53
53
  */
54
54
  export declare function getAllPluginExtensionPaths(cwd: string): Promise<string[]>;
55
55
  /**
@@ -0,0 +1,30 @@
1
+ export type PluginTrustDecision = "granted" | "denied";
2
+ /**
3
+ * Resolver for "do you trust this plugin?" consent.
4
+ *
5
+ * Return values mirror {@link import("../../tools/report-tool-issue").AutoQaConsentHandler}:
6
+ * - `true` — trusted; load for this run and persist.
7
+ * - `false` — untrusted; skip for this run and persist.
8
+ * - `null` — dismissed without a decision; skip this run only, re-prompt next time.
9
+ */
10
+ export type PluginTrustHandler = (plugin: {
11
+ name: string;
12
+ version: string;
13
+ }) => Promise<boolean | null>;
14
+ export declare function pluginTrustKey(plugin: {
15
+ name: string;
16
+ version: string;
17
+ }): string;
18
+ /** Register the interactive trust-prompt handler. Passing `null` clears it (teardown). */
19
+ export declare function setPluginTrustHandler(handler: PluginTrustHandler | null): void;
20
+ /** Test-only: reset all in-memory trust state. Never call from production code. */
21
+ export declare function __resetPluginTrustForTests(): void;
22
+ /**
23
+ * Resolve whether `plugin` is trusted to load. Never throws — handler errors
24
+ * degrade to "denied for this call" without caching, so a later invocation
25
+ * (e.g. after the user fixes their terminal) can re-prompt.
26
+ */
27
+ export declare function resolvePluginTrust(plugin: {
28
+ name: string;
29
+ version: string;
30
+ }): Promise<boolean>;
@@ -0,0 +1,21 @@
1
+ /**
2
+ * Validation for the `evidence` argument the `complete` goal-tool op requires.
3
+ *
4
+ * Code-level counterpart to the prompt rule ("call `complete` only when the goal is
5
+ * actually done and verified" — `prompts/tools/goal.md`, `goal-continuation.md`).
6
+ * Before this, "call complete" was pure self-attestation: a model under budget
7
+ * pressure or scope-shrink temptation could close a goal with zero code-level
8
+ * friction. This does not (cannot) verify the claim is *true* — that still requires
9
+ * the model to have actually checked the repo state — but it forces the model to
10
+ * commit to a specific, non-trivial claim that a human reviewing the transcript can
11
+ * hold it to, and it rejects the exact rubber-stamp phrases the prompt already
12
+ * tells the model never to use as a reason to complete.
13
+ */
14
+ /** Below this length, evidence can't meaningfully describe what was verified. */
15
+ export declare const MIN_COMPLETION_EVIDENCE_LENGTH = 15;
16
+ /**
17
+ * Validate that `evidence` is a substantive, non-generic completion claim.
18
+ * Returns the trimmed evidence on success; throws a plain `Error` describing
19
+ * what's missing otherwise (callers map this to their own error type).
20
+ */
21
+ export declare function assertSubstantiveCompletionEvidence(evidence: string | undefined): string;
@@ -63,7 +63,7 @@ export declare class GoalRuntime {
63
63
  resumeGoal(): Promise<GoalModeState>;
64
64
  pauseGoal(): Promise<GoalModeState | undefined>;
65
65
  dropGoal(): Promise<Goal | undefined>;
66
- completeGoalFromTool(): Promise<Goal>;
66
+ completeGoalFromTool(evidence: string): Promise<Goal>;
67
67
  buildActivePrompt(): string | undefined;
68
68
  buildContinuationPrompt(): string | undefined;
69
69
  }
@@ -9,6 +9,9 @@ export interface Goal {
9
9
  timeUsedSeconds: number;
10
10
  createdAt: number;
11
11
  updatedAt: number;
12
+ /** Set only when status transitions to "complete"; the caller's verification claim
13
+ * (see `goals/completion-evidence.ts`). Absent for goals that never completed. */
14
+ completionEvidence?: string;
12
15
  }
13
16
  export interface GoalModeState {
14
17
  enabled: boolean;
@@ -8,6 +8,7 @@ declare const goalSchema: import("arktype/internal/variants/object.ts").ObjectTy
8
8
  op: "complete" | "create" | "drop" | "get" | "resume";
9
9
  objective?: string | undefined;
10
10
  token_budget?: number | undefined;
11
+ evidence?: string | undefined;
11
12
  }, {}>;
12
13
  export type GoalToolInput = typeof goalSchema.infer;
13
14
  export interface GoalToolResponse {
@@ -27,6 +28,7 @@ export declare class GoalTool implements AgentTool<typeof goalSchema, GoalToolDe
27
28
  op: "complete" | "create" | "drop" | "get" | "resume";
28
29
  objective?: string | undefined;
29
30
  token_budget?: number | undefined;
31
+ evidence?: string | undefined;
30
32
  }, {}>;
31
33
  readonly strict = true;
32
34
  readonly intent: "omit";
@@ -12,6 +12,26 @@ import { type SessionInfo } from "./session/session-listing";
12
12
  import { SessionManager } from "./session/session-manager";
13
13
  type RunAcpMode = (createSession: AcpSessionFactory) => Promise<never>;
14
14
  export declare function writeStartupNotice(parsedArgs: Pick<Args, "mode">, text: string): void;
15
+ /**
16
+ * How often a long-running interactive session rechecks the registry after the
17
+ * initial startup check. A session started before a release ships never
18
+ * re-queries on its own otherwise — `checkForNewVersion` only ever ran once at
19
+ * launch, so the "Update Available" banner stayed invisible until the user
20
+ * restarted, even hours into a session with a newer version already published.
21
+ */
22
+ export declare const VERSION_RECHECK_INTERVAL_MS: number;
23
+ /**
24
+ * Show the update banner exactly once per newer version — `lastNotified` is
25
+ * the version already surfaced (or the running version, before any notice),
26
+ * so a recheck that still resolves to the same newer version does not
27
+ * re-post the banner every interval tick. Returns the version to remember as
28
+ * `lastNotified` for the next call.
29
+ *
30
+ * Re-checks `startup.checkUpdate` at call time (not just inside
31
+ * `checkForNewVersion`): `/settings` can disable update checks while an
32
+ * in-flight fetch's promise was still pending.
33
+ */
34
+ export declare function notifyIfNewerVersion(mode: Pick<InteractiveMode, "showNewVersionNotification">, newVersion: string | undefined, lastNotified: string): string;
15
35
  export interface InteractiveModeNotify {
16
36
  kind: "warn" | "error" | "info";
17
37
  message: string;
@@ -127,4 +127,5 @@ export declare class MnemopiSessionState {
127
127
  }
128
128
  export declare function getMnemopiScopedDbPaths(config: MnemopiBackendConfig): readonly string[];
129
129
  export declare function getMnemopiScopedBanks(config: MnemopiBackendConfig): readonly string[];
130
+ export declare function formatRecallBlock(results: RecallResult[]): string;
130
131
  export {};
@@ -42,7 +42,7 @@ import { type IrcMessage } from "../irc/bus";
42
42
  import { type MnemopiSessionState } from "../mnemopi/state";
43
43
  import type { PlanModeState } from "../plan-mode/state";
44
44
  import { type SecretObfuscator } from "../secrets/obfuscator";
45
- import type { CriticGateState } from "../task/critic-gate";
45
+ import type { ApprovedPlanHash, CriticGateState } from "../task/critic-gate";
46
46
  import { type ConfiguredThinkingLevel } from "../thinking";
47
47
  import { type DiscoverableTool, type DiscoverableToolSearchIndex } from "../tool-discovery/tool-index";
48
48
  import type { CheckpointState, CompletedRewindState } from "../tools/checkpoint";
@@ -710,6 +710,8 @@ export declare class AgentSession {
710
710
  setPlanModeState(state: PlanModeState | undefined): void;
711
711
  getCriticGateState(): CriticGateState | undefined;
712
712
  setCriticGateState(state: CriticGateState | undefined): void;
713
+ getApprovedPlanHash(): ApprovedPlanHash | undefined;
714
+ setApprovedPlanHash(hash: ApprovedPlanHash | undefined): void;
713
715
  getGoalModeState(): GoalModeState | undefined;
714
716
  setGoalModeState(state: GoalModeState | undefined): void;
715
717
  get goalRuntime(): GoalRuntime;
@@ -22,6 +22,15 @@
22
22
  * input, even re-submitting to `critic` is refused — the agent must stop and
23
23
  * report the surviving gaps instead of burning tokens on convergence-free
24
24
  * re-planning.
25
+ *
26
+ * `okay` clearing the gate is not the whole story: nothing previously stopped
27
+ * the plan file from being edited *after* the critic approved it and *before*
28
+ * a non-read-only agent executes it (a TOCTOU window — jeo-code closes the
29
+ * same gap with a `sha256(planContent) === consensus_hash` pin in
30
+ * `team.ts`/`approve.ts`). {@link ApprovedPlanHash} / {@link hashPlanContent} /
31
+ * {@link planIntegrityMismatchMessage} port that pin: the caller records the
32
+ * approved plan's hash when a verdict resolves `okay`, and re-checks it
33
+ * immediately before spawning a non-read-only agent.
25
34
  */
26
35
  /** Agent name whose structured verdicts drive the gate. */
27
36
  export declare const CRITIC_AGENT_NAME = "critic";
@@ -74,3 +83,28 @@ export declare function evaluateCriticGateSpawn(state: CriticGateState | undefin
74
83
  * check that before invoking this.
75
84
  */
76
85
  export declare function criticGateWriteMessage(state: CriticGateState): string;
86
+ /** Canonical `local://` path for the plan the `/jeo` workflow gates on (see `prompts/agents/jeo.md`). */
87
+ export declare const GATED_PLAN_LOCAL_URL = "local://jeo-plan.md";
88
+ /** Hash of the plan content a critic verdict of `okay` approved, so a later
89
+ * spawn gate can detect the plan changing out from under an already-`okay`
90
+ * verdict. Held by the parent session alongside (but independent of)
91
+ * {@link CriticGateState} — it survives the gate clearing on `okay`, which is
92
+ * exactly when it starts mattering. */
93
+ export interface ApprovedPlanHash {
94
+ /** sha256 hex digest of the plan content at the moment of the `okay` verdict. */
95
+ hash: string;
96
+ /** Agent id of the critic run that approved this content. */
97
+ agentId: string;
98
+ /** Epoch ms when the verdict was recorded. */
99
+ at: number;
100
+ }
101
+ /** Stable hash of plan content for {@link ApprovedPlanHash} pinning. Not a
102
+ * cryptographic integrity boundary (the model can read its own hash target) —
103
+ * it exists to catch accidental or incidental drift between critic approval
104
+ * and execution, the same threat model as jeo-code's `consensus_hash`. */
105
+ export declare function hashPlanContent(content: string): string;
106
+ /**
107
+ * Error text for a non-read-only spawn attempted when the plan content no
108
+ * longer matches what the critic approved (edited after `okay`, or deleted).
109
+ */
110
+ export declare function planIntegrityMismatchMessage(approved: ApprovedPlanHash): string;
@@ -1,4 +1,4 @@
1
- export declare const BUILTIN_TOOL_NAMES: readonly ["read", "bash", "edit", "ast_grep", "ast_edit", "ask", "debug", "eval", "ssh", "github", "glob", "grep", "lsp", "inspect_image", "browser", "checkpoint", "rewind", "task", "job", "irc", "todo", "web_search", "search_tool_bm25", "write", "memory_edit", "retain", "recall", "reflect", "learn", "manage_skill"];
1
+ export declare const BUILTIN_TOOL_NAMES: readonly ["read", "bash", "edit", "ast_grep", "ast_edit", "ask", "debug", "eval", "ssh", "github", "glob", "grep", "lsp", "inspect_image", "browser", "checkpoint", "rewind", "task", "job", "irc", "subagent", "todo", "web_search", "search_tool_bm25", "write", "memory_edit", "retain", "recall", "reflect", "learn", "manage_skill"];
2
2
  export type BuiltinToolName = (typeof BUILTIN_TOOL_NAMES)[number];
3
3
  /** Return the canonical tool name for current and legacy built-in tool IDs. */
4
4
  export declare function normalizeToolName(name: string): string;
@@ -19,7 +19,7 @@ import type { ClientBridge } from "../session/client-bridge";
19
19
  import type { CustomMessage } from "../session/messages";
20
20
  import type { UsageStatistics } from "../session/session-entries";
21
21
  import type { ToolChoiceQueue } from "../session/tool-choice-queue";
22
- import type { CriticGateState } from "../task/critic-gate";
22
+ import type { ApprovedPlanHash, CriticGateState } from "../task/critic-gate";
23
23
  import type { AgentOutputManager } from "../task/output-manager";
24
24
  import type { DiscoverableTool, DiscoverableToolSearchIndex } from "../tool-discovery/tool-index";
25
25
  import type { EventBus } from "../utils/event-bus";
@@ -61,6 +61,7 @@ export * from "./resolve";
61
61
  export * from "./review";
62
62
  export * from "./search-tool-bm25";
63
63
  export * from "./ssh";
64
+ export * from "./subagent";
64
65
  export * from "./todo";
65
66
  export * from "./tts";
66
67
  export * from "./write";
@@ -229,6 +230,10 @@ export interface ToolSession {
229
230
  getCriticGateState?: () => CriticGateState | undefined;
230
231
  /** Replace the critic gate state (undefined clears the gate). */
231
232
  setCriticGateState?: (state: CriticGateState | undefined) => void;
233
+ /** Plan content hash a critic verdict of `okay` approved (see `task/critic-gate.ts`); survives the gate clearing on `okay` so a later spawn can detect post-approval plan drift. */
234
+ getApprovedPlanHash?: () => ApprovedPlanHash | undefined;
235
+ /** Replace the approved-plan hash (undefined clears it). */
236
+ setApprovedPlanHash?: (hash: ApprovedPlanHash | undefined) => void;
232
237
  /** Goal mode state (if active or paused) */
233
238
  getGoalModeState?: () => GoalModeState | undefined;
234
239
  /** Goal runtime for the active agent session. */
@@ -0,0 +1,70 @@
1
+ import type { AgentTool, AgentToolContext, AgentToolResult, AgentToolUpdateCallback } from "jeopi-agent-core";
2
+ import type { ToolSession } from "./index";
3
+ import { isIrcEnabled } from "./irc";
4
+ /** Same audience as `irc`: a subagent always has peers; a top-level session has peers only if it can still spawn via `task`. */
5
+ export declare const isSubagentToolEnabled: typeof isIrcEnabled;
6
+ declare const subagentSchema: import("arktype/internal/variants/object.ts").ObjectType<{
7
+ action: "await" | "cancel" | "inspect" | "list" | "pause" | "resume" | "steer";
8
+ id?: string | undefined;
9
+ ids?: string[] | undefined;
10
+ limit?: number | undefined;
11
+ verbosity?: "full" | "preview" | "receipt" | undefined;
12
+ message?: string | undefined;
13
+ pause?: boolean | undefined;
14
+ timeout_ms?: number | undefined;
15
+ }, {}>;
16
+ type SubagentParams = typeof subagentSchema.infer;
17
+ export type SubagentStatus = "queued" | "running" | "paused" | "completed" | "failed" | "cancelled" | "not_found";
18
+ export interface SubagentSnapshot {
19
+ id: string;
20
+ status: SubagentStatus;
21
+ /** True when the underlying agent session is still resident (idle/parked) and addressable via a follow-up `resume`/`steer`. */
22
+ resumable: boolean;
23
+ label: string;
24
+ durationMs: number;
25
+ displayName?: string;
26
+ activity?: string;
27
+ resultText?: string;
28
+ errorText?: string;
29
+ }
30
+ export interface SubagentToolDetails {
31
+ action: SubagentParams["action"];
32
+ subagents: SubagentSnapshot[];
33
+ /** Present when `limit` truncated the result set. */
34
+ truncated?: number;
35
+ cancelled?: {
36
+ id: string;
37
+ status: "cancelled" | "not_found" | "already_completed";
38
+ }[];
39
+ receipt?: {
40
+ to: string;
41
+ outcome: "injected" | "woken" | "revived" | "failed";
42
+ error?: string;
43
+ };
44
+ }
45
+ export declare class SubagentTool implements AgentTool<typeof subagentSchema, SubagentToolDetails> {
46
+ #private;
47
+ private readonly session;
48
+ readonly name = "subagent";
49
+ readonly approval: "read";
50
+ readonly label = "Subagent";
51
+ readonly summary = "List, inspect, await, cancel, pause, resume, and steer background task subagents";
52
+ readonly description: string;
53
+ readonly parameters: import("arktype/internal/variants/object.ts").ObjectType<{
54
+ action: "await" | "cancel" | "inspect" | "list" | "pause" | "resume" | "steer";
55
+ id?: string | undefined;
56
+ ids?: string[] | undefined;
57
+ limit?: number | undefined;
58
+ verbosity?: "full" | "preview" | "receipt" | undefined;
59
+ message?: string | undefined;
60
+ pause?: boolean | undefined;
61
+ timeout_ms?: number | undefined;
62
+ }, {}>;
63
+ readonly strict = true;
64
+ readonly interruptible = true;
65
+ readonly loadMode = "discoverable";
66
+ constructor(session: ToolSession);
67
+ static createIf(session: ToolSession): SubagentTool | null;
68
+ execute(_toolCallId: string, params: SubagentParams, signal?: AbortSignal, onUpdate?: AgentToolUpdateCallback<SubagentToolDetails>, _context?: AgentToolContext): Promise<AgentToolResult<SubagentToolDetails>>;
69
+ }
70
+ export {};
@@ -0,0 +1 @@
1
+ export {};
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "type": "module",
3
3
  "name": "jeopi-cli",
4
- "version": "16.2.22",
4
+ "version": "16.2.24",
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",
@@ -56,17 +56,17 @@
56
56
  "@agentclientprotocol/sdk": "0.25.0",
57
57
  "@babel/parser": "^7.29.7",
58
58
  "@mozilla/readability": "^0.6.0",
59
- "jeopi-hashline": "16.2.22",
60
- "jeopi-stats": "16.2.22",
61
- "jeopi-agent-core": "16.2.22",
62
- "jeopi-ai": "16.2.22",
63
- "jeopi-catalog": "16.2.22",
64
- "jeopi-mnemopi": "16.2.22",
65
- "jeopi-natives": "16.2.22",
66
- "jeopi-tui": "16.2.22",
67
- "jeopi-utils": "16.2.22",
68
- "jeopi-wire": "16.2.22",
69
- "jeopi-snapcompact": "16.2.22",
59
+ "jeopi-hashline": "16.2.24",
60
+ "jeopi-stats": "16.2.24",
61
+ "jeopi-agent-core": "16.2.24",
62
+ "jeopi-ai": "16.2.24",
63
+ "jeopi-catalog": "16.2.24",
64
+ "jeopi-mnemopi": "16.2.24",
65
+ "jeopi-natives": "16.2.24",
66
+ "jeopi-tui": "16.2.24",
67
+ "jeopi-utils": "16.2.24",
68
+ "jeopi-wire": "16.2.24",
69
+ "jeopi-snapcompact": "16.2.24",
70
70
  "@opentelemetry/api": "^1.9.1",
71
71
  "@opentelemetry/context-async-hooks": "^2.7.1",
72
72
  "@opentelemetry/exporter-trace-otlp-proto": "^0.218.0",
@@ -26,6 +26,7 @@ import {
26
26
  isAdvisorInterruptImmuneTurnActive,
27
27
  isAdvisorTranscriptName,
28
28
  isInterruptingSeverity,
29
+ isRefusalError,
29
30
  resolveAdvisorDeliveryChannel,
30
31
  type WatchdogConfigDoc,
31
32
  } from "..";
@@ -1312,6 +1313,178 @@ describe("advisor", () => {
1312
1313
  expect(state.messages).toHaveLength(2);
1313
1314
  });
1314
1315
 
1316
+ it("strips thinking and retries once when the provider refuses the payload (reasoning_extraction)", async () => {
1317
+ const promptInputs: string[] = [];
1318
+ const strippedNotices: number[] = [];
1319
+ let refuseNext = true;
1320
+ const agent: AdvisorAgent = {
1321
+ prompt: async input => {
1322
+ promptInputs.push(input);
1323
+ if (refuseNext) {
1324
+ refuseNext = false;
1325
+ throw new Error(
1326
+ "Refusal (reasoning_extraction): This request was blocked as it seems to violate Anthropic's Terms of Service restrictions on reverse engineering or duplicating model outputs.",
1327
+ );
1328
+ }
1329
+ },
1330
+ abort: () => {},
1331
+ reset: () => {},
1332
+ state: { messages: [] },
1333
+ };
1334
+ const thinking = "secret chain of thought the classifier flags";
1335
+ const messages: AgentMessage[] = [
1336
+ { role: "user", content: "do the thing", timestamp: 1 } as AgentMessage,
1337
+ {
1338
+ role: "assistant",
1339
+ content: [
1340
+ { type: "thinking", thinking },
1341
+ { type: "text", text: "done" },
1342
+ ],
1343
+ timestamp: 2,
1344
+ } as AgentMessage,
1345
+ ];
1346
+ const host: AdvisorRuntimeHost = {
1347
+ snapshotMessages: () => messages,
1348
+ enqueueAdvice: () => {},
1349
+ notifyThinkingStripped: () => strippedNotices.push(1),
1350
+ };
1351
+ const runtime = new AdvisorRuntime(agent, host, 0);
1352
+
1353
+ runtime.onTurnEnd(messages);
1354
+ await Bun.sleep(0);
1355
+ await Bun.sleep(0);
1356
+
1357
+ // First attempt carried thinking; the refusal triggered exactly one
1358
+ // stripped re-render instead of a blind identical retry.
1359
+ expect(promptInputs).toHaveLength(2);
1360
+ expect(promptInputs[0]).toContain("_thinking:_");
1361
+ expect(promptInputs[0]).toContain(thinking);
1362
+ expect(promptInputs[1]).not.toContain("_thinking:_");
1363
+ expect(promptInputs[1]).not.toContain(thinking);
1364
+ expect(promptInputs[1]).toContain("done");
1365
+ expect(strippedNotices).toHaveLength(1);
1366
+ expect(runtime.backlog).toBe(0);
1367
+
1368
+ // Thinking stays latched off for subsequent turns.
1369
+ messages.push({
1370
+ role: "assistant",
1371
+ content: [
1372
+ { type: "thinking", thinking: "later thoughts" },
1373
+ { type: "text", text: "next" },
1374
+ ],
1375
+ timestamp: 3,
1376
+ } as AgentMessage);
1377
+ runtime.onTurnEnd(messages);
1378
+ await Bun.sleep(0);
1379
+ expect(promptInputs).toHaveLength(3);
1380
+ expect(promptInputs[2]).not.toContain("later thoughts");
1381
+ expect(strippedNotices).toHaveLength(1);
1382
+ });
1383
+
1384
+ it("skips the degrade path when thinking is disabled at construction and falls through to the watchdog", async () => {
1385
+ const promptInputs: string[] = [];
1386
+ const strippedNotices: number[] = [];
1387
+ const failures: unknown[] = [];
1388
+ const agent: AdvisorAgent = {
1389
+ prompt: async input => {
1390
+ promptInputs.push(input);
1391
+ throw new Error("Refusal (reasoning_extraction): blocked");
1392
+ },
1393
+ abort: () => {},
1394
+ reset: () => {},
1395
+ state: { messages: [] },
1396
+ };
1397
+ const messages: AgentMessage[] = [
1398
+ { role: "user", content: "start", timestamp: 1 } as AgentMessage,
1399
+ {
1400
+ role: "assistant",
1401
+ content: [
1402
+ { type: "thinking", thinking: "hidden reasoning" },
1403
+ { type: "text", text: "visible" },
1404
+ ],
1405
+ timestamp: 2,
1406
+ } as AgentMessage,
1407
+ ];
1408
+ const host: AdvisorRuntimeHost = {
1409
+ snapshotMessages: () => messages,
1410
+ enqueueAdvice: () => {},
1411
+ notifyFailure: error => failures.push(error),
1412
+ notifyThinkingStripped: () => strippedNotices.push(1),
1413
+ };
1414
+ const runtime = new AdvisorRuntime(agent, host, 0, { includeThinking: false });
1415
+
1416
+ runtime.onTurnEnd(messages);
1417
+ await Bun.sleep(0);
1418
+ await Bun.sleep(0);
1419
+ await Bun.sleep(0);
1420
+
1421
+ // Thinking never rendered, so there is nothing to strip: the refusal is
1422
+ // handled by the normal 3-strike watchdog, not an infinite degrade loop.
1423
+ expect(promptInputs).toHaveLength(3);
1424
+ for (const input of promptInputs) {
1425
+ expect(input).not.toContain("hidden reasoning");
1426
+ }
1427
+ expect(strippedNotices).toHaveLength(0);
1428
+ expect(failures).toHaveLength(1);
1429
+ expect(runtime.backlog).toBe(0);
1430
+ });
1431
+
1432
+ it("trips the watchdog when refusals persist after the thinking strip", async () => {
1433
+ const promptInputs: string[] = [];
1434
+ const strippedNotices: number[] = [];
1435
+ const failures: unknown[] = [];
1436
+ const agent: AdvisorAgent = {
1437
+ prompt: async input => {
1438
+ promptInputs.push(input);
1439
+ throw new Error("Refusal (reasoning_extraction): still blocked");
1440
+ },
1441
+ abort: () => {},
1442
+ reset: () => {},
1443
+ state: { messages: [] },
1444
+ };
1445
+ const messages: AgentMessage[] = [
1446
+ { role: "user", content: "start", timestamp: 1 } as AgentMessage,
1447
+ {
1448
+ role: "assistant",
1449
+ content: [
1450
+ { type: "thinking", thinking: "flagged reasoning" },
1451
+ { type: "text", text: "visible" },
1452
+ ],
1453
+ timestamp: 2,
1454
+ } as AgentMessage,
1455
+ ];
1456
+ const host: AdvisorRuntimeHost = {
1457
+ snapshotMessages: () => messages,
1458
+ enqueueAdvice: () => {},
1459
+ notifyFailure: error => failures.push(error),
1460
+ notifyThinkingStripped: () => strippedNotices.push(1),
1461
+ };
1462
+ const runtime = new AdvisorRuntime(agent, host, 0);
1463
+
1464
+ runtime.onTurnEnd(messages);
1465
+ for (let i = 0; i < 6; i++) await Bun.sleep(0);
1466
+
1467
+ // 1 refused attempt with thinking + 3 stripped attempts → watchdog drop.
1468
+ // The strip fires exactly once; there is no auto-recovery loop.
1469
+ expect(promptInputs).toHaveLength(4);
1470
+ expect(promptInputs[0]).toContain("_thinking:_");
1471
+ expect(promptInputs[1]).not.toContain("_thinking:_");
1472
+ expect(strippedNotices).toHaveLength(1);
1473
+ expect(failures).toHaveLength(1);
1474
+ expect(runtime.backlog).toBe(0);
1475
+ });
1476
+
1477
+ it("classifies pi-ai refusal message shapes without matching ordinary errors", () => {
1478
+ expect(isRefusalError(new Error("Refusal (reasoning_extraction): blocked by ToS"))).toBe(true);
1479
+ expect(isRefusalError(new Error("Refusal (no details provided)"))).toBe(true);
1480
+ expect(isRefusalError(new Error("Refusal: something"))).toBe(true);
1481
+ expect(isRefusalError(new Error("Refusal"))).toBe(true);
1482
+ expect(isRefusalError("Error: Refusal (reasoning_extraction): blocked")).toBe(true);
1483
+ expect(isRefusalError(new Error("404 No endpoints available"))).toBe(false);
1484
+ expect(isRefusalError(new Error("Refusals are handled elsewhere"))).toBe(false);
1485
+ expect(isRefusalError(new Error("stream timeout"))).toBe(false);
1486
+ });
1487
+
1315
1488
  it("drops the in-flight batch when a reset aborts the advisor prompt", async () => {
1316
1489
  const promptInputs: string[] = [];
1317
1490
  const { promise: firstPromptStarted, resolve: startFirstPrompt } = Promise.withResolvers<void>();