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.
- package/CHANGELOG.md +31 -0
- package/dist/cli.js +2565 -2524
- package/dist/types/advisor/runtime.d.ts +9 -1
- package/dist/types/cli/update-cli.d.ts +37 -0
- package/dist/types/config/settings-schema.d.ts +12 -1
- package/dist/types/extensibility/plugins/loader.d.ts +4 -4
- package/dist/types/extensibility/plugins/trust.d.ts +30 -0
- package/dist/types/goals/completion-evidence.d.ts +21 -0
- package/dist/types/goals/runtime.d.ts +1 -1
- package/dist/types/goals/state.d.ts +3 -0
- package/dist/types/goals/tools/goal-tool.d.ts +2 -0
- package/dist/types/main.d.ts +20 -0
- package/dist/types/mnemopi/state.d.ts +1 -0
- package/dist/types/session/agent-session.d.ts +3 -1
- package/dist/types/task/critic-gate.d.ts +34 -0
- package/dist/types/tools/builtin-names.d.ts +1 -1
- package/dist/types/tools/index.d.ts +6 -1
- package/dist/types/tools/subagent.d.ts +70 -0
- package/dist/types/tools/subagent.test.d.ts +1 -0
- package/package.json +12 -12
- package/src/advisor/__tests__/advisor.test.ts +173 -0
- package/src/advisor/runtime.ts +66 -2
- package/src/cli/update-cli.ts +144 -0
- package/src/config/settings-schema.ts +14 -2
- package/src/extensibility/plugins/loader.ts +35 -9
- package/src/extensibility/plugins/trust.ts +133 -0
- package/src/goals/completion-evidence.ts +73 -0
- package/src/goals/runtime.ts +6 -1
- package/src/goals/state.ts +3 -0
- package/src/goals/tools/goal-tool.ts +16 -1
- package/src/internal-urls/docs-index.generated.txt +2 -2
- package/src/main.ts +56 -10
- package/src/memories/index.ts +21 -1
- package/src/mnemopi/state.ts +10 -2
- package/src/modes/interactive-mode.ts +21 -0
- package/src/prompts/memories/read-path.md +3 -1
- package/src/prompts/system/safety-kernel.md +17 -0
- package/src/prompts/system/tool-call-cycle-redirect.md +5 -0
- package/src/prompts/tools/goal.md +1 -1
- package/src/prompts/tools/subagent.md +29 -0
- package/src/session/agent-session.ts +68 -19
- package/src/slash-commands/builtin-registry.ts +18 -0
- package/src/system-prompt.ts +14 -2
- package/src/task/critic-gate.ts +46 -0
- package/src/task/index.ts +63 -2
- package/src/tools/builtin-names.ts +1 -0
- package/src/tools/index.ts +9 -1
- package/src/tools/subagent.test.ts +285 -0
- 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";
|
package/dist/types/main.d.ts
CHANGED
|
@@ -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.
|
|
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.
|
|
60
|
-
"jeopi-stats": "16.2.
|
|
61
|
-
"jeopi-agent-core": "16.2.
|
|
62
|
-
"jeopi-ai": "16.2.
|
|
63
|
-
"jeopi-catalog": "16.2.
|
|
64
|
-
"jeopi-mnemopi": "16.2.
|
|
65
|
-
"jeopi-natives": "16.2.
|
|
66
|
-
"jeopi-tui": "16.2.
|
|
67
|
-
"jeopi-utils": "16.2.
|
|
68
|
-
"jeopi-wire": "16.2.
|
|
69
|
-
"jeopi-snapcompact": "16.2.
|
|
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>();
|