pi-goal-list-loop-audit 0.27.7 → 0.27.9
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.
|
@@ -104,6 +104,7 @@ function buildGoalAuditorPrompt(goal: Goal, completionSummary: string | null | u
|
|
|
104
104
|
"You are the independent completion auditor for pi-goal-list-loop-audit.",
|
|
105
105
|
"The executor claims the goal is complete. Your job is to decide whether the user's objective is actually satisfied.",
|
|
106
106
|
"Be skeptical and semantic. Do not approve from paperwork, intent, file count, word count, build success, or a plausible summary alone.",
|
|
107
|
+
"Chunk output near context-full: prefer focused, evidence-quote-first replies (one tool call at a time, raw output inline) over mega-replies that hit the output-token cap. The orchestrator's auto-continue fires on stop_reason=\"length\" but pre-empting by chunking is cheaper than recovering from the cap.",
|
|
107
108
|
"Use read/grep/find/ls/bash as needed to inspect real artifacts. Do not mutate files or run destructive commands.",
|
|
108
109
|
"If the work is only an alpha scaffold, generated template, shallow draft, proxy milestone, or lacks the user-facing value requested, disapprove.",
|
|
109
110
|
"If any explicit requirement is missing, weakly verified, contradicted, or not inspectable with the available evidence, disapprove.",
|
|
@@ -231,12 +231,19 @@ function goalLines(g: Goal, state: State, audit: AuditDisplayProgress | null | u
|
|
|
231
231
|
});
|
|
232
232
|
// v0.27.1: what survives the pause — the first question at a pause is
|
|
233
233
|
// "did I lose the work?". Answer it on the card.
|
|
234
|
+
// v0.27.9: when the goal has no telemetry yet (restored-in-fresh-session
|
|
235
|
+
// before the first turn), render "awaiting first turn" instead of "saved"
|
|
236
|
+
// — the latter was misleading because no work was ever "saved" before the
|
|
237
|
+
// session ended.
|
|
234
238
|
const spent: string[] = [];
|
|
235
239
|
const tokUsed = g.usage?.tokensUsed ?? 0;
|
|
236
|
-
if (tokUsed > 0) spent.push(`${fmtTokens(tokUsed)} tok spent`);
|
|
237
240
|
const audits = g.auditHistory?.length ?? 0;
|
|
241
|
+
if (tokUsed > 0) spent.push(`${fmtTokens(tokUsed)} tok spent`);
|
|
238
242
|
if (audits > 0) spent.push(`${audits} audit${audits === 1 ? "" : "s"}`);
|
|
239
|
-
const
|
|
243
|
+
const hasTelemetry = spent.length > 0;
|
|
244
|
+
const savedLine = hasTelemetry
|
|
245
|
+
? `saved — ${spent.join(" · ")} · resumes exactly here`
|
|
246
|
+
: `awaiting first turn — resumes exactly here`;
|
|
240
247
|
if (g.pauseSuggestedAction) {
|
|
241
248
|
lines.push(`├─ ${paint(theme, "dim", truncate(savedLine, budget))}`);
|
|
242
249
|
const wrapped = wrap(g.pauseSuggestedAction, budget, 3);
|
|
@@ -83,6 +83,18 @@ export interface Settings {
|
|
|
83
83
|
* Always wins over subagentModelStrategy — the managed override is written
|
|
84
84
|
* WITH this pin regardless of strategy. */
|
|
85
85
|
subagentModelOverrides?: Record<string, string>;
|
|
86
|
+
/** v0.27.9: per-tool overrides — allowlist (force tools visible despite
|
|
87
|
+
* an external modlist), hidden (force tools hidden even when allowed by
|
|
88
|
+
* the session), and per-tool config (Record<toolName, Record<key, value>>
|
|
89
|
+
* — extensible for tool-specific knobs like timeouts, formats, etc.). */
|
|
90
|
+
toolOverrides?: {
|
|
91
|
+
/** Tools that MUST be active even when an external allowlist hides them. */
|
|
92
|
+
allow?: string[];
|
|
93
|
+
/** Tools that MUST be hidden even when the session allows them. */
|
|
94
|
+
hide?: string[];
|
|
95
|
+
/** Per-tool configuration knobs (extensible). */
|
|
96
|
+
perToolConfig?: Record<string, Record<string, unknown>>;
|
|
97
|
+
};
|
|
86
98
|
}
|
|
87
99
|
|
|
88
100
|
export const DEFAULT_SETTINGS: Settings = {
|
|
@@ -146,6 +158,7 @@ export const SETTINGS_KEYS: Array<keyof Settings> = [
|
|
|
146
158
|
"stallShortWords",
|
|
147
159
|
"stallSimilarityThreshold",
|
|
148
160
|
"postaudit",
|
|
161
|
+
"toolOverrides",
|
|
149
162
|
];
|
|
150
163
|
|
|
151
164
|
/** Where each effective setting comes from (for the /glla display). */
|
package/extensions/loops/goal.ts
CHANGED
|
@@ -679,7 +679,7 @@ function archiveCurrentGoal(ctx: ExtensionContext, status: Status, stopReason?:
|
|
|
679
679
|
function fireReviewer(
|
|
680
680
|
ctx: ExtensionContext,
|
|
681
681
|
source: { kind: "goal" | "list"; goalId: string; objective: string; terminal: string },
|
|
682
|
-
opts: { manual?: boolean; mode?: "off" | "
|
|
682
|
+
opts: { manual?: boolean; mode?: "off" | "on" | "auto" | "aggressive" } = {},
|
|
683
683
|
): void {
|
|
684
684
|
try {
|
|
685
685
|
const settings = loadSettings(ctx.cwd);
|
|
@@ -3068,12 +3068,12 @@ async function cmdReview(args: string, ctx: ExtensionContext): Promise<void> {
|
|
|
3068
3068
|
const parts = args.trim().split(/\s+/).filter(Boolean);
|
|
3069
3069
|
const id = parts[0] ?? "";
|
|
3070
3070
|
const modeArg = parts[1];
|
|
3071
|
-
const validModes = ["off", "
|
|
3071
|
+
const validModes = ["off", "on", "auto", "aggressive"] as const;
|
|
3072
3072
|
const mode = (validModes as readonly string[]).includes(modeArg ?? "")
|
|
3073
3073
|
? (modeArg as typeof validModes[number])
|
|
3074
3074
|
: undefined;
|
|
3075
3075
|
if (modeArg && !mode) {
|
|
3076
|
-
ctx.ui.notify(`Unknown mode "${modeArg}" — use off |
|
|
3076
|
+
ctx.ui.notify(`Unknown mode "${modeArg}" — use off | on | auto | aggressive.`, "warning");
|
|
3077
3077
|
return;
|
|
3078
3078
|
}
|
|
3079
3079
|
if (!id) {
|
|
@@ -3101,6 +3101,98 @@ async function cmdReview(args: string, ctx: ExtensionContext): Promise<void> {
|
|
|
3101
3101
|
fireReviewer(ctx, { kind: "goal", goalId, objective, terminal: "goal-complete" }, { manual: true, mode });
|
|
3102
3102
|
}
|
|
3103
3103
|
|
|
3104
|
+
/** v0.27.9: /glla tooloverride <action> [args] — per-tool override menu.
|
|
3105
|
+
* Actions:
|
|
3106
|
+
* list show current allow/hide/perToolConfig
|
|
3107
|
+
* allow <tool> force <tool> visible despite modlist
|
|
3108
|
+
* hide <tool> force <tool> hidden despite session
|
|
3109
|
+
* unallow <tool> remove from allow list
|
|
3110
|
+
* unhide <tool> remove from hide list
|
|
3111
|
+
* set <tool> <key>=<value> write perToolConfig[tool][key]
|
|
3112
|
+
* unset <tool> <key> remove perToolConfig[tool][key]
|
|
3113
|
+
* Example: /glla tooloverride allow bash hide write_file set bash timeout=60 */
|
|
3114
|
+
async function cmdToolOverride(args: string, ctx: ExtensionContext): Promise<void> {
|
|
3115
|
+
const settings = loadSettings(ctx.cwd);
|
|
3116
|
+
const current = settings.toolOverrides ?? {};
|
|
3117
|
+
const parts = args.trim().split(/\s+/).filter(Boolean);
|
|
3118
|
+
const action = parts[0];
|
|
3119
|
+
if (!action || action === "list" || action === "show") {
|
|
3120
|
+
const allow = current.allow ?? [];
|
|
3121
|
+
const hide = current.hide ?? [];
|
|
3122
|
+
const cfg = current.perToolConfig ?? {};
|
|
3123
|
+
const out = `toolOverrides (project):\n allow: ${allow.length ? allow.join(", ") : "(none)"}\n hide: ${hide.length ? hide.join(", ") : "(none)"}\n perToolConfig: ${Object.keys(cfg).length ? JSON.stringify(cfg) : "(none)"}`;
|
|
3124
|
+
ctx.ui.notify(out, "info");
|
|
3125
|
+
return;
|
|
3126
|
+
}
|
|
3127
|
+
const apply = (patch: Partial<NonNullable<Settings["toolOverrides"]>>) => {
|
|
3128
|
+
saveSettings("project", ctx.cwd, { toolOverrides: { ...current, ...patch } });
|
|
3129
|
+
};
|
|
3130
|
+
if (action === "allow" || action === "hide" || action === "unallow" || action === "unhide") {
|
|
3131
|
+
const tool = parts[1];
|
|
3132
|
+
if (!tool) {
|
|
3133
|
+
ctx.ui.notify(`Usage: /glla tooloverride ${action} <tool>`, "warning");
|
|
3134
|
+
return;
|
|
3135
|
+
}
|
|
3136
|
+
if (action === "allow") {
|
|
3137
|
+
const allow = current.allow ?? [];
|
|
3138
|
+
if (!allow.includes(tool)) apply({ allow: [...allow, tool] });
|
|
3139
|
+
ctx.ui.notify(`toolOverrides.allow += ${tool}`, "info");
|
|
3140
|
+
} else if (action === "hide") {
|
|
3141
|
+
const hide = current.hide ?? [];
|
|
3142
|
+
if (!hide.includes(tool)) apply({ hide: [...hide, tool] });
|
|
3143
|
+
ctx.ui.notify(`toolOverrides.hide += ${tool}`, "info");
|
|
3144
|
+
} else if (action === "unallow") {
|
|
3145
|
+
apply({ allow: (current.allow ?? []).filter((t) => t !== tool) });
|
|
3146
|
+
ctx.ui.notify(`toolOverrides.allow -= ${tool}`, "info");
|
|
3147
|
+
} else {
|
|
3148
|
+
apply({ hide: (current.hide ?? []).filter((t) => t !== tool) });
|
|
3149
|
+
ctx.ui.notify(`toolOverrides.hide -= ${tool}`, "info");
|
|
3150
|
+
}
|
|
3151
|
+
return;
|
|
3152
|
+
}
|
|
3153
|
+
if (action === "set" || action === "unset") {
|
|
3154
|
+
const tool = parts[1];
|
|
3155
|
+
const kv = parts[2];
|
|
3156
|
+
if (!tool || !kv) {
|
|
3157
|
+
ctx.ui.notify(`Usage: /glla tooloverride ${action} <tool> <key>[=<value>]`, "warning");
|
|
3158
|
+
return;
|
|
3159
|
+
}
|
|
3160
|
+
const cfg = { ...(current.perToolConfig ?? {}) };
|
|
3161
|
+
const toolCfg = { ...(cfg[tool] ?? {}) };
|
|
3162
|
+
if (action === "set") {
|
|
3163
|
+
const eq = kv.indexOf("=");
|
|
3164
|
+
if (eq < 0) {
|
|
3165
|
+
ctx.ui.notify(`set needs key=value: got "${kv}"`, "warning");
|
|
3166
|
+
return;
|
|
3167
|
+
}
|
|
3168
|
+
const k = kv.slice(0, eq);
|
|
3169
|
+
const v: unknown = parseToolOverrideValue(kv.slice(eq + 1));
|
|
3170
|
+
toolCfg[k] = v;
|
|
3171
|
+
} else {
|
|
3172
|
+
delete toolCfg[kv];
|
|
3173
|
+
}
|
|
3174
|
+
cfg[tool] = toolCfg;
|
|
3175
|
+
apply({ perToolConfig: cfg });
|
|
3176
|
+
ctx.ui.notify(`toolOverrides.perToolConfig.${tool} ${action === "set" ? "set" : "unset"}`, "info");
|
|
3177
|
+
return;
|
|
3178
|
+
}
|
|
3179
|
+
ctx.ui.notify(`Unknown tooloverride action: ${action}. Use: list | allow | hide | unallow | unhide | set | unset.`, "warning");
|
|
3180
|
+
}
|
|
3181
|
+
|
|
3182
|
+
/** Parse a tool-override value: numbers, booleans, JSON objects/arrays, else string. */
|
|
3183
|
+
function parseToolOverrideValue(s: string): unknown {
|
|
3184
|
+
const trimmed = s.trim();
|
|
3185
|
+
if (trimmed === "true") return true;
|
|
3186
|
+
if (trimmed === "false") return false;
|
|
3187
|
+
if (trimmed === "null") return null;
|
|
3188
|
+
if (/^-?\d+$/.test(trimmed)) return Number(trimmed);
|
|
3189
|
+
if (/^-?\d+\.\d+$/.test(trimmed)) return Number(trimmed);
|
|
3190
|
+
if (trimmed.startsWith("{") || trimmed.startsWith("[")) {
|
|
3191
|
+
try { return JSON.parse(trimmed); } catch { /* fall through */ }
|
|
3192
|
+
}
|
|
3193
|
+
return trimmed;
|
|
3194
|
+
}
|
|
3195
|
+
|
|
3104
3196
|
/** v0.27.5: /glla reviewer | postaudit — the post-completion audit config menu
|
|
3105
3197
|
* (project-scoped). Reads the dual-write settings (postaudit wins over the
|
|
3106
3198
|
* legacy reviewer key), and writes back to whichever key was read first —
|
|
@@ -3129,8 +3221,8 @@ async function cmdReviewerSettings(ctx: ExtensionContext): Promise<void> {
|
|
|
3129
3221
|
try {
|
|
3130
3222
|
if (choice.startsWith("Enabled")) save({ enabled: !cfg.enabled });
|
|
3131
3223
|
else if (choice.startsWith("Mode")) {
|
|
3132
|
-
// v0.27.
|
|
3133
|
-
const order: Array<"off" | "
|
|
3224
|
+
// v0.27.9: 4-state cycle off → on → auto → aggressive → off
|
|
3225
|
+
const order: Array<"off" | "on" | "auto" | "aggressive"> = ["off", "on", "auto", "aggressive"];
|
|
3134
3226
|
const i = order.indexOf(cfg.mode as typeof order[number]);
|
|
3135
3227
|
const next = order[(i + 1) % order.length]!;
|
|
3136
3228
|
save({ mode: next });
|
|
@@ -3257,6 +3349,11 @@ async function cmdSettings(args: string, ctx: ExtensionContext): Promise<void> {
|
|
|
3257
3349
|
await cmdReviewerSettings(ctx);
|
|
3258
3350
|
return;
|
|
3259
3351
|
}
|
|
3352
|
+
// v0.27.9: per-tool overrides — sub-mode `tooloverride <action> <tool>`
|
|
3353
|
+
if (/^tooloverride\b/.test(trimmed)) {
|
|
3354
|
+
await cmdToolOverride(trimmed.slice("tooloverride".length).trim(), ctx);
|
|
3355
|
+
return;
|
|
3356
|
+
}
|
|
3260
3357
|
if (!trimmed) {
|
|
3261
3358
|
if (ctx.hasUI) {
|
|
3262
3359
|
await openSettingsUI(ctx);
|
|
@@ -3663,13 +3760,32 @@ export default function (pi: ExtensionAPI): void {
|
|
|
3663
3760
|
// v0.24.5 tool-visibility self-heal: surface the notify exactly once
|
|
3664
3761
|
// per session so the user learns about an external allowlist once and
|
|
3665
3762
|
// can fix their profile to silence it.
|
|
3763
|
+
// v0.27.9: also applies toolOverrides.allow / toolOverrides.hide from
|
|
3764
|
+
// .pi-glla/settings.json — the project's per-tool policy wins over the
|
|
3765
|
+
// external allowlist (allow) or over the session default (hide).
|
|
3666
3766
|
let toolHealNotified = false;
|
|
3667
3767
|
function ensureAgentToolsActive(pi: ExtensionAPI, ctx: ExtensionContext): void {
|
|
3668
3768
|
try {
|
|
3669
3769
|
const active = pi.getActiveTools();
|
|
3670
3770
|
const missing = missingGllaTools(active);
|
|
3671
|
-
|
|
3672
|
-
|
|
3771
|
+
const overrides = loadSettings(ctx.cwd).toolOverrides;
|
|
3772
|
+
let next = [...active, ...missing];
|
|
3773
|
+
let changed = missing.length > 0;
|
|
3774
|
+
// Apply per-tool allowlist — force tools visible despite an external modlist.
|
|
3775
|
+
if (overrides?.allow && overrides.allow.length > 0) {
|
|
3776
|
+
const toAdd = overrides.allow.filter((t) => !next.includes(t));
|
|
3777
|
+
if (toAdd.length > 0) {
|
|
3778
|
+
next = [...next, ...toAdd];
|
|
3779
|
+
changed = true;
|
|
3780
|
+
}
|
|
3781
|
+
}
|
|
3782
|
+
// Apply per-tool hide — force tools hidden even when the session allows them.
|
|
3783
|
+
if (overrides?.hide && overrides.hide.length > 0) {
|
|
3784
|
+
const before = next.length;
|
|
3785
|
+
next = next.filter((t) => !overrides.hide!.includes(t));
|
|
3786
|
+
if (next.length !== before) changed = true;
|
|
3787
|
+
}
|
|
3788
|
+
if (changed) pi.setActiveTools(next);
|
|
3673
3789
|
if (!toolHealNotified) {
|
|
3674
3790
|
toolHealNotified = true;
|
|
3675
3791
|
const list = missing.join(", ");
|
package/extensions/reviewer.ts
CHANGED
|
@@ -15,7 +15,7 @@
|
|
|
15
15
|
import * as fs from "node:fs";
|
|
16
16
|
import * as path from "node:path";
|
|
17
17
|
|
|
18
|
-
export type ReviewerMode = "off" | "
|
|
18
|
+
export type ReviewerMode = "off" | "on" | "auto" | "aggressive";
|
|
19
19
|
|
|
20
20
|
export interface ReviewerConfig {
|
|
21
21
|
enabled: boolean;
|
|
@@ -37,7 +37,7 @@ export interface ReviewerConfig {
|
|
|
37
37
|
|
|
38
38
|
export const DEFAULT_REVIEWER_CONFIG: ReviewerConfig = {
|
|
39
39
|
enabled: true,
|
|
40
|
-
mode: "
|
|
40
|
+
mode: "on",
|
|
41
41
|
fireOn: ["goal-complete", "list-complete"],
|
|
42
42
|
doNotFireOn: ["goal-aborted", "goal-paused"],
|
|
43
43
|
cascade: ["convert-findings-to-list", "queue-leftovers", "fire-audit-on-clean", "notify-and-idle"],
|
|
@@ -49,9 +49,15 @@ export const DEFAULT_REVIEWER_CONFIG: ReviewerConfig = {
|
|
|
49
49
|
maxReviewsPerDay: 20,
|
|
50
50
|
};
|
|
51
51
|
|
|
52
|
-
/** Merge a partial project-settings block over the defaults.
|
|
52
|
+
/** Merge a partial project-settings block over the defaults. v0.27.9:
|
|
53
|
+
* migrate legacy `"default"` / `"report"` mode values to `"on"` (the
|
|
54
|
+
* contract-compliant 4-mode set is now `off | on | auto | aggressive`). */
|
|
53
55
|
export function resolveReviewerConfig(block?: Partial<ReviewerConfig>): ReviewerConfig {
|
|
54
|
-
|
|
56
|
+
const merged = { ...DEFAULT_REVIEWER_CONFIG, ...(block ?? {}) };
|
|
57
|
+
if ((merged.mode as string) === "default" || (merged.mode as string) === "report") {
|
|
58
|
+
merged.mode = "on";
|
|
59
|
+
}
|
|
60
|
+
return merged;
|
|
55
61
|
}
|
|
56
62
|
|
|
57
63
|
export type FindingClass = "bug" | "refactor" | "architectural" | "strategic";
|
|
@@ -250,11 +256,10 @@ export function runReviewer(
|
|
|
250
256
|
let cascadeStep = "notify-and-idle";
|
|
251
257
|
const auto = config.mode === "auto" || config.mode === "aggressive";
|
|
252
258
|
const aggressive = config.mode === "aggressive";
|
|
253
|
-
const reportOnly = config.mode === "report";
|
|
254
259
|
|
|
255
260
|
// Cascade: findings → list items (leverage: fix-without-confirm).
|
|
256
261
|
const convertStep = source.kind === "goal" ? "convert-findings-to-list" : "queue-leftovers";
|
|
257
|
-
if (bugs.length > 0 && config.cascade.includes(convertStep)
|
|
262
|
+
if (bugs.length > 0 && config.cascade.includes(convertStep)) {
|
|
258
263
|
deps.enqueueListItems(bugs.map((f) => f.text));
|
|
259
264
|
enqueued = bugs.length;
|
|
260
265
|
cascadeStep = convertStep;
|
|
@@ -263,7 +268,7 @@ export function runReviewer(
|
|
|
263
268
|
// auto mode → /list items (the auto-loop rolls straight into them).
|
|
264
269
|
// aggressive mode → enqueue AND relaunch as the next active goal
|
|
265
270
|
// (skips both Confirm and the queue — the unattended rig never stops).
|
|
266
|
-
if (architectural.length > 0
|
|
271
|
+
if (architectural.length > 0) {
|
|
267
272
|
if (aggressive) {
|
|
268
273
|
deps.enqueueListItems(architectural.map((f) => f.text));
|
|
269
274
|
// v0.27.5 aggressive: also propose the FIRST architectural finding
|
|
@@ -293,7 +298,7 @@ export function runReviewer(
|
|
|
293
298
|
// auto mode enqueues the audit as a /list item (no Confirm — the
|
|
294
299
|
// cascade keeps rolling until the findings run dry).
|
|
295
300
|
// aggressive mode → relaunch the audit goal directly (no Confirm).
|
|
296
|
-
if (findings.length === 0 && config.cascade.includes("fire-audit-on-clean")
|
|
301
|
+
if (findings.length === 0 && config.cascade.includes("fire-audit-on-clean")) {
|
|
297
302
|
const auditObjective = `Post-completion regression scan after ${source.goalId} (${config.auditScope})`;
|
|
298
303
|
if (aggressive) {
|
|
299
304
|
deps.proposeGoal(auditObjective, "aggressive postaudit: clean completion — relaunching the regression scan as /goal");
|
|
@@ -309,7 +314,6 @@ export function runReviewer(
|
|
|
309
314
|
cascadeStep = "fire-audit-on-clean";
|
|
310
315
|
}
|
|
311
316
|
}
|
|
312
|
-
if (reportOnly) cascadeStep = "report-only";
|
|
313
317
|
|
|
314
318
|
const report: ReviewReport = {
|
|
315
319
|
goalId: source.goalId,
|
|
@@ -345,7 +349,7 @@ export function runReviewer(
|
|
|
345
349
|
export function reviewerMenuOptions(cfg: ReviewerConfig): string[] {
|
|
346
350
|
return [
|
|
347
351
|
`Enabled — ${cfg.enabled ? "ON" : "OFF"}`,
|
|
348
|
-
`Mode — ${cfg.mode} (off = silenced ·
|
|
352
|
+
`Mode — ${cfg.mode} (off = silenced · on = Confirm-gated cascade · auto = auto-loop, no Confirms · aggressive = auto + relaunch)`,
|
|
349
353
|
`Leverage mode — ${cfg.leverageMode} (bug/refactor findings)`,
|
|
350
354
|
`Fire on goal-complete — ${cfg.fireOn.includes("goal-complete") ? "ON" : "OFF"}`,
|
|
351
355
|
`Fire on list-complete — ${cfg.fireOn.includes("list-complete") ? "ON" : "OFF"}`,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "pi-goal-list-loop-audit",
|
|
3
|
-
"version": "0.27.
|
|
3
|
+
"version": "0.27.9",
|
|
4
4
|
"description": "Goal. Loop. Audit. Done. \u2014 a pi-coding-agent extension that supervises long-running work, with isolated auditor on each completion. Beat bamboozling by design: the auditor runs in a fresh session with no extensions, no skills, no editor \u2014 only the read tools needed to verify your goal.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"author": "dracon",
|