jeopi-cli 16.2.23 → 16.2.25
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 +33 -0
- package/dist/cli.js +3011 -2970
- 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/slash-commands/types.d.ts +2 -0
- 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/session/session-context.ts +8 -2
- package/src/slash-commands/builtin-registry.ts +28 -0
- package/src/slash-commands/types.ts +2 -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 +305 -0
- package/src/tools/subagent.ts +591 -0
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Plugin trust gate — trust-on-first-use consent for third-party plugin code.
|
|
3
|
+
*
|
|
4
|
+
* `custom-tools`/`hooks`/`commands`/`extensions` loaders all execute arbitrary
|
|
5
|
+
* TypeScript via native `import()` with full `exec`/`pi` API access. User-authored
|
|
6
|
+
* tools under `.jeopi/tools/` etc. are trusted by construction (the user wrote or
|
|
7
|
+
* explicitly configured them). Plugin-installed code is different: it arrives via
|
|
8
|
+
* `bun install <name>` from a marketplace/npm registry, i.e. third-party code the
|
|
9
|
+
* user did not author. This module gates that path specifically.
|
|
10
|
+
*
|
|
11
|
+
* Mirrors the `report-tool-issue.ts` auto-QA consent design: a process-global cache,
|
|
12
|
+
* a disk-backed decision store, a single host-registered handler for the interactive
|
|
13
|
+
* popup, and default-deny when no handler is registered (headless/CI/non-interactive
|
|
14
|
+
* runs never silently execute unreviewed third-party code).
|
|
15
|
+
*
|
|
16
|
+
* Trust is keyed by `"<name>@<version>"` — any version bump (including a
|
|
17
|
+
* marketplace auto-update) invalidates the previous grant and re-prompts, the same
|
|
18
|
+
* supply-chain assumption package managers use for pinned versions.
|
|
19
|
+
*/
|
|
20
|
+
import { $flag, getPluginTrustStorePath, isEnoent, logger } from "jeopi-utils";
|
|
21
|
+
|
|
22
|
+
export type PluginTrustDecision = "granted" | "denied";
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Resolver for "do you trust this plugin?" consent.
|
|
26
|
+
*
|
|
27
|
+
* Return values mirror {@link import("../../tools/report-tool-issue").AutoQaConsentHandler}:
|
|
28
|
+
* - `true` — trusted; load for this run and persist.
|
|
29
|
+
* - `false` — untrusted; skip for this run and persist.
|
|
30
|
+
* - `null` — dismissed without a decision; skip this run only, re-prompt next time.
|
|
31
|
+
*/
|
|
32
|
+
export type PluginTrustHandler = (plugin: { name: string; version: string }) => Promise<boolean | null>;
|
|
33
|
+
|
|
34
|
+
interface TrustStoreShape {
|
|
35
|
+
[trustKey: string]: PluginTrustDecision;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
let trustHandler: PluginTrustHandler | null = null;
|
|
39
|
+
/** Process-global cache so a grant/denial in the parent applies to subagents immediately. */
|
|
40
|
+
const cachedDecisions = new Map<string, PluginTrustDecision>();
|
|
41
|
+
/** Single-flight in-flight prompts, keyed by trust key, so concurrent loaders don't stack popups. */
|
|
42
|
+
const inFlightPrompts = new Map<string, Promise<boolean>>();
|
|
43
|
+
let diskStoreCache: TrustStoreShape | null = null;
|
|
44
|
+
|
|
45
|
+
export function pluginTrustKey(plugin: { name: string; version: string }): string {
|
|
46
|
+
return `${plugin.name}@${plugin.version}`;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/** Register the interactive trust-prompt handler. Passing `null` clears it (teardown). */
|
|
50
|
+
export function setPluginTrustHandler(handler: PluginTrustHandler | null): void {
|
|
51
|
+
trustHandler = handler;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/** Test-only: reset all in-memory trust state. Never call from production code. */
|
|
55
|
+
export function __resetPluginTrustForTests(): void {
|
|
56
|
+
trustHandler = null;
|
|
57
|
+
cachedDecisions.clear();
|
|
58
|
+
inFlightPrompts.clear();
|
|
59
|
+
diskStoreCache = null;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
async function readTrustStore(): Promise<TrustStoreShape> {
|
|
63
|
+
if (diskStoreCache) return diskStoreCache;
|
|
64
|
+
try {
|
|
65
|
+
diskStoreCache = (await Bun.file(getPluginTrustStorePath()).json()) as TrustStoreShape;
|
|
66
|
+
} catch (err) {
|
|
67
|
+
if (!isEnoent(err)) {
|
|
68
|
+
logger.debug("plugin trust store read failed", { error: String(err) });
|
|
69
|
+
}
|
|
70
|
+
diskStoreCache = {};
|
|
71
|
+
}
|
|
72
|
+
return diskStoreCache;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
async function persistTrustDecision(trustKey: string, decision: PluginTrustDecision): Promise<void> {
|
|
76
|
+
const store = await readTrustStore();
|
|
77
|
+
store[trustKey] = decision;
|
|
78
|
+
diskStoreCache = store;
|
|
79
|
+
try {
|
|
80
|
+
await Bun.write(getPluginTrustStorePath(), JSON.stringify(store, null, "\t"));
|
|
81
|
+
} catch (err) {
|
|
82
|
+
logger.warn("plugin trust store write failed", { error: String(err) });
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* Resolve whether `plugin` is trusted to load. Never throws — handler errors
|
|
88
|
+
* degrade to "denied for this call" without caching, so a later invocation
|
|
89
|
+
* (e.g. after the user fixes their terminal) can re-prompt.
|
|
90
|
+
*/
|
|
91
|
+
export async function resolvePluginTrust(plugin: { name: string; version: string }): Promise<boolean> {
|
|
92
|
+
// Escape hatch for headless/CI/scripted runs where no consent handler is ever
|
|
93
|
+
// registered (the default-deny branch below would otherwise silently disable
|
|
94
|
+
// every installed plugin). Explicit opt-in only — mirrors PI_AUTO_QA_PUSH.
|
|
95
|
+
if ($flag("PI_TRUST_ALL_PLUGINS", false)) return true;
|
|
96
|
+
|
|
97
|
+
const trustKey = pluginTrustKey(plugin);
|
|
98
|
+
const cached = cachedDecisions.get(trustKey);
|
|
99
|
+
if (cached !== undefined) return cached === "granted";
|
|
100
|
+
|
|
101
|
+
const store = await readTrustStore();
|
|
102
|
+
const persisted = store[trustKey];
|
|
103
|
+
if (persisted !== undefined) {
|
|
104
|
+
cachedDecisions.set(trustKey, persisted);
|
|
105
|
+
return persisted === "granted";
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
if (!trustHandler) return false;
|
|
109
|
+
const inFlight = inFlightPrompts.get(trustKey);
|
|
110
|
+
if (inFlight) return inFlight;
|
|
111
|
+
|
|
112
|
+
const handler = trustHandler;
|
|
113
|
+
const prompt = (async () => {
|
|
114
|
+
try {
|
|
115
|
+
const granted = await handler(plugin);
|
|
116
|
+
if (granted === null) {
|
|
117
|
+
// Dismissed (ESC/click-away): skip this call only, don't cache or persist.
|
|
118
|
+
return false;
|
|
119
|
+
}
|
|
120
|
+
const decision: PluginTrustDecision = granted ? "granted" : "denied";
|
|
121
|
+
cachedDecisions.set(trustKey, decision);
|
|
122
|
+
await persistTrustDecision(trustKey, decision);
|
|
123
|
+
return granted;
|
|
124
|
+
} catch (err) {
|
|
125
|
+
logger.warn("plugin trust handler threw", { error: String(err) });
|
|
126
|
+
return false;
|
|
127
|
+
} finally {
|
|
128
|
+
inFlightPrompts.delete(trustKey);
|
|
129
|
+
}
|
|
130
|
+
})();
|
|
131
|
+
inFlightPrompts.set(trustKey, prompt);
|
|
132
|
+
return prompt;
|
|
133
|
+
}
|
|
@@ -0,0 +1,73 @@
|
|
|
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
|
+
|
|
15
|
+
/** Below this length, evidence can't meaningfully describe what was verified. */
|
|
16
|
+
export const MIN_COMPLETION_EVIDENCE_LENGTH = 15;
|
|
17
|
+
|
|
18
|
+
/** Rubber-stamp phrases that carry no verification content, normalized (trimmed,
|
|
19
|
+
* lowercased, trailing punctuation stripped). Mirrors the intent of
|
|
20
|
+
* `advisor/emission-guard.ts`'s `SUPPRESSED_NORMALIZED_PHRASES`. */
|
|
21
|
+
const TRIVIAL_COMPLETION_EVIDENCE_PHRASES = new Set([
|
|
22
|
+
"done",
|
|
23
|
+
"complete",
|
|
24
|
+
"completed",
|
|
25
|
+
"finished",
|
|
26
|
+
"ok",
|
|
27
|
+
"okay",
|
|
28
|
+
"yes",
|
|
29
|
+
"verified",
|
|
30
|
+
"all done",
|
|
31
|
+
"n/a",
|
|
32
|
+
"na",
|
|
33
|
+
"none",
|
|
34
|
+
"good",
|
|
35
|
+
"looks good",
|
|
36
|
+
"lgtm",
|
|
37
|
+
"task complete",
|
|
38
|
+
"task done",
|
|
39
|
+
"goal complete",
|
|
40
|
+
"goal achieved",
|
|
41
|
+
"it works",
|
|
42
|
+
"it's done",
|
|
43
|
+
]);
|
|
44
|
+
|
|
45
|
+
function normalizeCompletionEvidence(text: string): string {
|
|
46
|
+
return text
|
|
47
|
+
.trim()
|
|
48
|
+
.toLowerCase()
|
|
49
|
+
.replace(/[.!]+$/, "");
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Validate that `evidence` is a substantive, non-generic completion claim.
|
|
54
|
+
* Returns the trimmed evidence on success; throws a plain `Error` describing
|
|
55
|
+
* what's missing otherwise (callers map this to their own error type).
|
|
56
|
+
*/
|
|
57
|
+
export function assertSubstantiveCompletionEvidence(evidence: string | undefined): string {
|
|
58
|
+
const trimmed = evidence?.trim();
|
|
59
|
+
if (!trimmed) {
|
|
60
|
+
throw new Error(
|
|
61
|
+
"evidence is required when op=complete: state which deliverables you verified and how (tests run, output inspected, files diffed, etc.)",
|
|
62
|
+
);
|
|
63
|
+
}
|
|
64
|
+
if (
|
|
65
|
+
trimmed.length < MIN_COMPLETION_EVIDENCE_LENGTH ||
|
|
66
|
+
TRIVIAL_COMPLETION_EVIDENCE_PHRASES.has(normalizeCompletionEvidence(trimmed))
|
|
67
|
+
) {
|
|
68
|
+
throw new Error(
|
|
69
|
+
"evidence is too generic to count as verification — describe the specific deliverables checked and how, not a status word",
|
|
70
|
+
);
|
|
71
|
+
}
|
|
72
|
+
return trimmed;
|
|
73
|
+
}
|
package/src/goals/runtime.ts
CHANGED
|
@@ -2,6 +2,7 @@ import { escapeXmlText, prompt, Snowflake } from "jeopi-utils";
|
|
|
2
2
|
import goalBudgetLimitPrompt from "../prompts/goals/goal-budget-limit.md" with { type: "text" };
|
|
3
3
|
import goalContinuationPrompt from "../prompts/goals/goal-continuation.md" with { type: "text" };
|
|
4
4
|
import goalModeActivePrompt from "../prompts/goals/goal-mode-active.md" with { type: "text" };
|
|
5
|
+
import { assertSubstantiveCompletionEvidence } from "./completion-evidence";
|
|
5
6
|
import type { Goal, GoalBudgetSteering, GoalModeState, GoalRuntimeEvent, GoalTokenUsage } from "./state";
|
|
6
7
|
|
|
7
8
|
export interface GoalRuntimeHost {
|
|
@@ -470,7 +471,10 @@ export class GoalRuntime {
|
|
|
470
471
|
});
|
|
471
472
|
}
|
|
472
473
|
|
|
473
|
-
async completeGoalFromTool(): Promise<Goal> {
|
|
474
|
+
async completeGoalFromTool(evidence: string): Promise<Goal> {
|
|
475
|
+
// Validated here too (not just in GoalTool), so any direct GoalRuntime caller
|
|
476
|
+
// (SDK embedders bypassing the tool schema) gets the same guardrail.
|
|
477
|
+
const verifiedEvidence = assertSubstantiveCompletionEvidence(evidence);
|
|
474
478
|
return await this.#withAccounting(async () => {
|
|
475
479
|
await this.#flushUsageLocked("suppressed");
|
|
476
480
|
const state = this.#getStateClone();
|
|
@@ -485,6 +489,7 @@ export class GoalRuntime {
|
|
|
485
489
|
}
|
|
486
490
|
state.enabled = false;
|
|
487
491
|
state.goal.status = "complete";
|
|
492
|
+
state.goal.completionEvidence = verifiedEvidence;
|
|
488
493
|
state.goal.updatedAt = this.#now();
|
|
489
494
|
state.mode = "exiting";
|
|
490
495
|
state.reason = "completed";
|
package/src/goals/state.ts
CHANGED
|
@@ -11,6 +11,9 @@ export interface Goal {
|
|
|
11
11
|
timeUsedSeconds: number;
|
|
12
12
|
createdAt: number;
|
|
13
13
|
updatedAt: number;
|
|
14
|
+
/** Set only when status transitions to "complete"; the caller's verification claim
|
|
15
|
+
* (see `goals/completion-evidence.ts`). Absent for goals that never completed. */
|
|
16
|
+
completionEvidence?: string;
|
|
14
17
|
}
|
|
15
18
|
|
|
16
19
|
export interface GoalModeState {
|
|
@@ -11,6 +11,7 @@ import type { ToolSession } from "../../tools";
|
|
|
11
11
|
import { formatErrorDetail, TRUNCATE_LENGTHS } from "../../tools/render-utils";
|
|
12
12
|
import { ToolError } from "../../tools/tool-errors";
|
|
13
13
|
import { framedBlock, renderStatusLine, truncateToWidth } from "../../tui";
|
|
14
|
+
import { assertSubstantiveCompletionEvidence } from "../completion-evidence";
|
|
14
15
|
import { completionBudgetReport, remainingTokens } from "../runtime";
|
|
15
16
|
import type { Goal, GoalStatus, GoalToolDetails } from "../state";
|
|
16
17
|
|
|
@@ -18,6 +19,9 @@ const goalSchema = type({
|
|
|
18
19
|
op: type("'create' | 'get' | 'complete' | 'resume' | 'drop'").describe("goal operation"),
|
|
19
20
|
"objective?": type("string").describe("goal objective"),
|
|
20
21
|
"token_budget?": type("number.integer").describe("token budget"),
|
|
22
|
+
"evidence?": type("string").describe(
|
|
23
|
+
"required when op=complete: what you verified and how (tests run, output inspected, files diffed, etc.)",
|
|
24
|
+
),
|
|
21
25
|
});
|
|
22
26
|
|
|
23
27
|
export type GoalToolInput = typeof goalSchema.infer;
|
|
@@ -55,6 +59,14 @@ function validateCreateParams(params: GoalToolInput): { objective: string; token
|
|
|
55
59
|
return { objective, tokenBudget };
|
|
56
60
|
}
|
|
57
61
|
|
|
62
|
+
function validateCompleteParams(params: GoalToolInput): string {
|
|
63
|
+
try {
|
|
64
|
+
return assertSubstantiveCompletionEvidence(params.evidence);
|
|
65
|
+
} catch (err) {
|
|
66
|
+
throw new ToolError(err instanceof Error ? err.message : String(err));
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
58
70
|
export class GoalTool implements AgentTool<typeof goalSchema, GoalToolDetails> {
|
|
59
71
|
readonly name = "goal";
|
|
60
72
|
readonly label = "Goal";
|
|
@@ -94,7 +106,7 @@ export class GoalTool implements AgentTool<typeof goalSchema, GoalToolDetails> {
|
|
|
94
106
|
const dropped = await runtime.dropGoal();
|
|
95
107
|
response = buildGoalToolResponse(dropped ?? null);
|
|
96
108
|
} else {
|
|
97
|
-
const completed = await runtime.completeGoalFromTool();
|
|
109
|
+
const completed = await runtime.completeGoalFromTool(validateCompleteParams(params));
|
|
98
110
|
response = buildGoalToolResponse(completed, { includeCompletionReport: true });
|
|
99
111
|
}
|
|
100
112
|
let text: string;
|
|
@@ -106,6 +118,9 @@ export class GoalTool implements AgentTool<typeof goalSchema, GoalToolDetails> {
|
|
|
106
118
|
if (response.remainingTokens !== null) {
|
|
107
119
|
text += `\nRemaining tokens: ${response.remainingTokens}`;
|
|
108
120
|
}
|
|
121
|
+
if (response.goal.completionEvidence) {
|
|
122
|
+
text += `\nEvidence: ${response.goal.completionEvidence}`;
|
|
123
|
+
}
|
|
109
124
|
if (response.completionBudgetReport) {
|
|
110
125
|
text += `\n\n${response.completionBudgetReport}`;
|
|
111
126
|
}
|