@yagni-app/code 0.3.0 → 0.3.2
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/dist/cli.js +12 -0
- package/dist/connectClaudeCode.d.ts +77 -0
- package/dist/connectClaudeCode.js +228 -0
- package/dist/connectCodex.d.ts +75 -0
- package/dist/connectCodex.js +201 -0
- package/dist/crashReport.d.ts +12 -0
- package/dist/crashReport.js +28 -1
- package/dist/extension/approvedPrefixes.d.ts +11 -0
- package/dist/extension/approvedPrefixes.js +30 -0
- package/dist/extension/askAdvisorTool.d.ts +18 -3
- package/dist/extension/askAdvisorTool.js +121 -15
- package/dist/extension/askYagniTool.d.ts +23 -0
- package/dist/extension/askYagniTool.js +42 -2
- package/dist/extension/branding.d.ts +11 -1
- package/dist/extension/branding.js +47 -7
- package/dist/extension/config.d.ts +12 -0
- package/dist/extension/config.js +2 -1
- package/dist/extension/crashReport.d.ts +18 -0
- package/dist/extension/crashReport.js +35 -2
- package/dist/extension/execPolicy.d.ts +17 -1
- package/dist/extension/execPolicy.js +227 -33
- package/dist/extension/flywheel.d.ts +44 -0
- package/dist/extension/flywheel.js +53 -0
- package/dist/extension/footer.d.ts +8 -1
- package/dist/extension/footer.js +33 -19
- package/dist/extension/guardian.d.ts +14 -4
- package/dist/extension/guardian.js +35 -11
- package/dist/extension/index.d.ts +20 -3
- package/dist/extension/index.js +92 -13
- package/dist/extension/mineBeat.d.ts +95 -0
- package/dist/extension/mineBeat.js +193 -0
- package/dist/extension/permission.d.ts +2 -1
- package/dist/extension/permission.js +75 -22
- package/dist/extension/pipeline/goCommand.js +6 -4
- package/dist/extension/pipeline/invocation.d.ts +24 -2
- package/dist/extension/pipeline/invocation.js +30 -2
- package/dist/extension/pipeline/personas.js +2 -2
- package/dist/extension/pipeline/resilience.d.ts +2 -1
- package/dist/extension/pipeline/resilience.js +21 -2
- package/dist/extension/pipeline/runRegistry.d.ts +9 -1
- package/dist/extension/pipeline/runRegistry.js +22 -1
- package/dist/extension/recordDecisionTool.d.ts +8 -0
- package/dist/extension/recordDecisionTool.js +24 -0
- package/dist/extension/subagents.d.ts +7 -1
- package/dist/extension/subagents.js +73 -5
- package/dist/extension/todos.d.ts +28 -1
- package/dist/extension/todos.js +76 -1
- package/dist/extension/ultra.d.ts +27 -0
- package/dist/extension/ultra.js +76 -0
- package/dist/login.d.ts +4 -2
- package/dist/login.js +19 -4
- package/dist/promptEnrichment.d.ts +1 -1
- package/dist/promptEnrichment.js +1 -1
- package/dist/token.d.ts +25 -0
- package/dist/token.js +45 -0
- package/package.json +3 -2
|
@@ -24,8 +24,22 @@
|
|
|
24
24
|
import { appendFileSync, mkdirSync, readFileSync } from "node:fs";
|
|
25
25
|
import { join } from "node:path";
|
|
26
26
|
import { codeStateHome } from "../stateHome.js";
|
|
27
|
-
/**
|
|
27
|
+
/** Default bound on simultaneously in-flight /go runs in one process (spec §3b). */
|
|
28
28
|
export const MAX_CONCURRENT_RUNS = 3;
|
|
29
|
+
/** Hard ceiling for the env override — a typo must not launch hundreds of runs. */
|
|
30
|
+
export const MAX_CONCURRENT_RUNS_CEILING = 32;
|
|
31
|
+
/**
|
|
32
|
+
* Resolve the in-flight /go cap from the environment. `YAGNI_MAX_CONCURRENT_RUNS`
|
|
33
|
+
* raises (or lowers) the default for fleet-scale operators; non-numeric or < 1
|
|
34
|
+
* falls back to the default, and anything above the ceiling clamps to it.
|
|
35
|
+
*/
|
|
36
|
+
export function resolveMaxConcurrentRuns(env = process.env) {
|
|
37
|
+
const raw = env.YAGNI_MAX_CONCURRENT_RUNS?.trim();
|
|
38
|
+
const parsed = raw ? Number.parseInt(raw, 10) : NaN;
|
|
39
|
+
if (!Number.isFinite(parsed) || parsed < 1)
|
|
40
|
+
return MAX_CONCURRENT_RUNS;
|
|
41
|
+
return Math.min(parsed, MAX_CONCURRENT_RUNS_CEILING);
|
|
42
|
+
}
|
|
29
43
|
/**
|
|
30
44
|
* A non-terminal row whose journal has been quiet this long is treated as
|
|
31
45
|
* INTERRUPTED (its process died) rather than still running elsewhere. Sits
|
|
@@ -106,6 +120,13 @@ const active = new Map();
|
|
|
106
120
|
export function _resetRunRegistryForTest() {
|
|
107
121
|
active.clear();
|
|
108
122
|
}
|
|
123
|
+
// NOTE on growth: the mirror is append-only and grows without bound on a
|
|
124
|
+
// long-lived install. In-place compaction was reviewed and REMOVED (PR #1698):
|
|
125
|
+
// a fold+rewrite without cross-process exclusion can permanently erase another
|
|
126
|
+
// process's terminal settle (nothing ever re-appends a final row), which would
|
|
127
|
+
// resurrect a finished run as "interrupted" and invite duplicate worktree
|
|
128
|
+
// adoption. Compaction needs an inter-process lock + unique temp files —
|
|
129
|
+
// tracked separately; until then, growth is the safe failure mode.
|
|
109
130
|
/** Fail-soft append of one full row to the mirror (self-heals a torn previous write). */
|
|
110
131
|
function appendRow(row) {
|
|
111
132
|
try {
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import type { ToolDefinition } from "@earendil-works/pi-coding-agent";
|
|
2
2
|
import { Type } from "typebox";
|
|
3
|
+
import { type FlywheelState } from "./flywheel.js";
|
|
3
4
|
/** Options for {@link makeRecordDecisionTool}. */
|
|
4
5
|
export interface MakeRecordDecisionToolOptions {
|
|
5
6
|
baseUrl: string;
|
|
@@ -7,6 +8,13 @@ export interface MakeRecordDecisionToolOptions {
|
|
|
7
8
|
fetchImpl?: typeof fetch;
|
|
8
9
|
/** Idempotency-key source (default: crypto.randomUUID); injected in tests. */
|
|
9
10
|
makeIdempotencyKey?: () => string;
|
|
11
|
+
/**
|
|
12
|
+
* Shared flywheel session state (Run 7). A record_decision that follows a
|
|
13
|
+
* surfaced no-position suggestion sends `dedupe: true` — a mid-run agent
|
|
14
|
+
* has no human to adjudicate a near-duplicate. A human `/decide` never
|
|
15
|
+
* rides this state.
|
|
16
|
+
*/
|
|
17
|
+
flywheel?: FlywheelState;
|
|
10
18
|
}
|
|
11
19
|
/** The durable fields of a recorded product-intent decision. */
|
|
12
20
|
export interface RecordDecisionParams {
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { randomUUID } from "node:crypto";
|
|
2
2
|
import { Type } from "typebox";
|
|
3
|
+
import { consumeFlywheelAttribution } from "./flywheel.js";
|
|
3
4
|
import { sendOrSpool } from "./spool.js";
|
|
4
5
|
/**
|
|
5
6
|
* POST a single decision to the token-scoped grounding endpoint and return its
|
|
@@ -67,12 +68,20 @@ export function makeRecordDecisionTool(opts) {
|
|
|
67
68
|
// never bank the same decision twice. Transport failures and 5xx are
|
|
68
69
|
// spooled durably instead of lost (R4 write half).
|
|
69
70
|
const idempotencyKey = (opts.makeIdempotencyKey ?? randomUUID)();
|
|
71
|
+
// Run 7 flywheel attribution: a record answering the QUESTION a
|
|
72
|
+
// surfaced no-position suggestion asked about asks the backend to
|
|
73
|
+
// dedupe against active decisions first (decisive, not advisory — no
|
|
74
|
+
// human is present). An unrelated record never inherits the flag.
|
|
75
|
+
const flywheelAttributed = opts.flywheel
|
|
76
|
+
? consumeFlywheelAttribution(opts.flywheel, params.question)
|
|
77
|
+
: false;
|
|
70
78
|
const outcome = await sendOrSpool(opts, "record_decision", "/api/yagni-code/decisions", {
|
|
71
79
|
question: params.question,
|
|
72
80
|
decision: params.decision,
|
|
73
81
|
rationale: params.rationale,
|
|
74
82
|
repo: params.repo,
|
|
75
83
|
workItemId: params.workItemId,
|
|
84
|
+
...(flywheelAttributed ? { dedupe: true } : {}),
|
|
76
85
|
}, idempotencyKey, signal);
|
|
77
86
|
if (outcome.kind === "rejected") {
|
|
78
87
|
throw new Error(outcome.message);
|
|
@@ -92,6 +101,21 @@ export function makeRecordDecisionTool(opts) {
|
|
|
92
101
|
};
|
|
93
102
|
}
|
|
94
103
|
const data = outcome.json;
|
|
104
|
+
if (data?.deduped) {
|
|
105
|
+
// The backend matched an existing active decision and inserted
|
|
106
|
+
// nothing; surface it so the agent leans on the recorded judgment.
|
|
107
|
+
const existing = data.existing;
|
|
108
|
+
const summary = existing?.decision ? ` ${existing.decision}` : "";
|
|
109
|
+
return {
|
|
110
|
+
content: [
|
|
111
|
+
{
|
|
112
|
+
type: "text",
|
|
113
|
+
text: `An equivalent decision is already recorded; nothing new was banked.${summary}`,
|
|
114
|
+
},
|
|
115
|
+
],
|
|
116
|
+
details: { id: existing?.id ?? null, spooled: false },
|
|
117
|
+
};
|
|
118
|
+
}
|
|
95
119
|
return {
|
|
96
120
|
content: [{ type: "text", text: "Recorded the decision in YAGNI." }],
|
|
97
121
|
details: { id: data?.id ?? null, spooled: false },
|
|
@@ -20,11 +20,13 @@
|
|
|
20
20
|
import { type ExtensionAPI, type ExtensionContext } from "@earendil-works/pi-coding-agent";
|
|
21
21
|
import { Type } from "typebox";
|
|
22
22
|
import { runStage } from "./pipeline/runner.js";
|
|
23
|
-
import type
|
|
23
|
+
import { type ModelTier, type PipelineStage } from "./pipeline/types.js";
|
|
24
24
|
import { renderSubagentCall, renderSubagentResult } from "./subagentRender.js";
|
|
25
25
|
export declare const SUBAGENT_TOOL_NAME = "subagent";
|
|
26
26
|
export declare const GENERAL_AGENT_NAME = "general";
|
|
27
27
|
export declare const MAX_PARALLEL_SUBAGENTS = 4;
|
|
28
|
+
/** Wider fan-out ceiling while the session is in ultra mode (/ultra). */
|
|
29
|
+
export declare const MAX_PARALLEL_SUBAGENTS_ULTRA = 8;
|
|
28
30
|
/**
|
|
29
31
|
* The default tool surface a subagent gets when its definition declares none:
|
|
30
32
|
* the full working set plus grounded answers, mirroring what a Claude Code
|
|
@@ -92,6 +94,8 @@ export interface MakeSubagentToolDeps {
|
|
|
92
94
|
runStageImpl?: typeof runStage;
|
|
93
95
|
discover?: (deps: DiscoverDeps) => SubagentDef[];
|
|
94
96
|
homeDir?: string;
|
|
97
|
+
/** Live ultra-mode probe (/ultra): widens the per-call fan-out ceiling. */
|
|
98
|
+
isUltra?: () => boolean;
|
|
95
99
|
}
|
|
96
100
|
export declare function makeSubagentTool(deps?: MakeSubagentToolDeps): {
|
|
97
101
|
name: string;
|
|
@@ -135,6 +139,8 @@ export declare function makeSubagentTool(deps?: MakeSubagentToolDeps): {
|
|
|
135
139
|
export interface RegisterSubagentsDeps {
|
|
136
140
|
discover?: (deps: DiscoverDeps) => SubagentDef[];
|
|
137
141
|
homeDir?: string;
|
|
142
|
+
/** Live ultra-mode probe (/ultra): widens the per-call fan-out ceiling. */
|
|
143
|
+
isUltra?: () => boolean;
|
|
138
144
|
}
|
|
139
145
|
/** Wire the subagent tool and the /agents listing command. */
|
|
140
146
|
export declare function registerSubagents(pi: ExtensionAPI, deps?: RegisterSubagentsDeps): void;
|
|
@@ -23,7 +23,9 @@ import { delimiter, join } from "node:path";
|
|
|
23
23
|
import { parseFrontmatter } from "@earendil-works/pi-coding-agent";
|
|
24
24
|
import { Type } from "typebox";
|
|
25
25
|
import { sanitizeCallerSegment } from "./config.js";
|
|
26
|
+
import { withResilience } from "./pipeline/resilience.js";
|
|
26
27
|
import { runStage } from "./pipeline/runner.js";
|
|
28
|
+
import { DEFAULT_RESILIENCE_POLICY } from "./pipeline/types.js";
|
|
27
29
|
import { applyChildEvent, finalizeTask, formatWorkingMessage, newTaskProgress, progressSummaryText, renderSubagentCall, renderSubagentResult, } from "./subagentRender.js";
|
|
28
30
|
/**
|
|
29
31
|
* YAG-471 attribution: the `x-yagni-caller` prefix for a subagent invocation.
|
|
@@ -34,6 +36,8 @@ const SUBAGENT_CALLER_PREFIX = "subagent:";
|
|
|
34
36
|
export const SUBAGENT_TOOL_NAME = "subagent";
|
|
35
37
|
export const GENERAL_AGENT_NAME = "general";
|
|
36
38
|
export const MAX_PARALLEL_SUBAGENTS = 4;
|
|
39
|
+
/** Wider fan-out ceiling while the session is in ultra mode (/ultra). */
|
|
40
|
+
export const MAX_PARALLEL_SUBAGENTS_ULTRA = 8;
|
|
37
41
|
/**
|
|
38
42
|
* The default tool surface a subagent gets when its definition declares none:
|
|
39
43
|
* the full working set plus grounded answers, mirroring what a Claude Code
|
|
@@ -68,6 +72,8 @@ const GENERAL_BODY = `You are a capable software-engineering subagent with a fre
|
|
|
68
72
|
|
|
69
73
|
You are grounded in how THIS company works: call ask_yagni before inferring a convention, an ownership rule, or anything organization-specific.
|
|
70
74
|
|
|
75
|
+
Never fabricate file paths, contents, or findings. If you cannot find something, say so.
|
|
76
|
+
|
|
71
77
|
Your final message is your report back to the driving agent, which has NOT seen what you read or did. Make it compressed and complete: what you did, what you found, exact file paths and key excerpts, and anything the driver must know before continuing.`;
|
|
72
78
|
const GENERAL_AGENT = {
|
|
73
79
|
name: GENERAL_AGENT_NAME,
|
|
@@ -83,6 +89,11 @@ not write code and you do not run commands; you read and report.
|
|
|
83
89
|
You are grounded in how THIS company works: call ask_yagni before inferring a
|
|
84
90
|
convention, an ownership rule, or anything organization-specific.
|
|
85
91
|
|
|
92
|
+
Never fabricate file paths, contents, or code. Every path you cite must be
|
|
93
|
+
one you actually read with a tool. If you cannot find something, say "not
|
|
94
|
+
found" — a plausible-sounding invention is worse than no answer because the
|
|
95
|
+
driving agent trusts your report.
|
|
96
|
+
|
|
86
97
|
Your final message is your report back to the driving agent, which has NOT
|
|
87
98
|
seen what you read. Make it compressed and complete: exact file paths, the
|
|
88
99
|
key excerpts, and a one-paragraph map of how the pieces relate. Say what you
|
|
@@ -108,6 +119,9 @@ your final message instead of guessing.
|
|
|
108
119
|
You are grounded in how THIS company works: call ask_yagni before inferring a
|
|
109
120
|
convention, an ownership rule, or anything organization-specific.
|
|
110
121
|
|
|
122
|
+
Never fabricate file paths or results. Report what you actually did and what
|
|
123
|
+
you actually found.
|
|
124
|
+
|
|
111
125
|
Your final message is your report back to the driving agent, which has NOT
|
|
112
126
|
seen what you did. List every file you touched, what changed in each, the
|
|
113
127
|
commands you ran with their outcomes, and anything you deliberately left
|
|
@@ -123,6 +137,53 @@ const IMPLEMENTER_AGENT = {
|
|
|
123
137
|
body: IMPLEMENTER_BODY,
|
|
124
138
|
source: "builtin",
|
|
125
139
|
};
|
|
140
|
+
const VERIFICATION_BODY = `You are an adversarial verifier. Another agent
|
|
141
|
+
produced work — a change, a plan, or a claim — and your job is to try to
|
|
142
|
+
BREAK it, not to summarize it. Default to skepticism: hunt for the concrete
|
|
143
|
+
failure scenario (the inputs, state, or sequence that makes it wrong). Bash
|
|
144
|
+
is read-only here (\`git diff\`, \`git log\`, \`git show\`); do NOT modify files
|
|
145
|
+
or run builds.
|
|
146
|
+
|
|
147
|
+
If your task names a lens (correctness, edge cases, codebase fit, security,
|
|
148
|
+
…), judge ONLY through that lens and leave the rest to your sibling
|
|
149
|
+
verifiers.
|
|
150
|
+
|
|
151
|
+
You are grounded in how THIS company works: call ask_yagni before inferring
|
|
152
|
+
a convention, an ownership rule, or anything organization-specific.
|
|
153
|
+
|
|
154
|
+
Never fabricate file paths or findings. If you could not verify something,
|
|
155
|
+
say exactly what you tried and why you could not.
|
|
156
|
+
|
|
157
|
+
Your final message is your verdict back to the driving agent, which has NOT
|
|
158
|
+
seen what you read. Format:
|
|
159
|
+
## Verdict
|
|
160
|
+
BROKEN or HOLDS, with one sentence why.
|
|
161
|
+
## Findings
|
|
162
|
+
Each real problem: file:line, the concrete failure scenario, severity. No
|
|
163
|
+
style nits.
|
|
164
|
+
## Not verified
|
|
165
|
+
What you could not check, and why.
|
|
166
|
+
|
|
167
|
+
A HOLDS after real digging is valuable; a rubber stamp is not. If you found
|
|
168
|
+
nothing, say exactly what you tried to break and how.`;
|
|
169
|
+
/** The diamond's reduce layer: refute-first review of completed work on the
|
|
170
|
+
* advanced tier (judgment is the whole job, so the premium is worth paying —
|
|
171
|
+
* the server clamps subagent children at advanced anyway). It has no
|
|
172
|
+
* edit/write tools; bash is included for `git diff`-style inspection and is
|
|
173
|
+
* restricted to read-only USE by the persona, the same prompt-level stance as
|
|
174
|
+
* /go's reviewer — not a technical guarantee. */
|
|
175
|
+
const VERIFICATION_AGENT = {
|
|
176
|
+
name: "verification",
|
|
177
|
+
description: "Adversarial verification of completed work: tries to refute a change, " +
|
|
178
|
+
"plan, or claim and reports concrete failure scenarios. Does not edit " +
|
|
179
|
+
"files. Fan out 2-3 with different lenses (correctness, edge cases, " +
|
|
180
|
+
"codebase fit) for anything significant and treat agreement as " +
|
|
181
|
+
"confirmation.",
|
|
182
|
+
model: "advanced",
|
|
183
|
+
tools: ["read", "grep", "find", "ls", "bash", "ask_yagni"],
|
|
184
|
+
body: VERIFICATION_BODY,
|
|
185
|
+
source: "builtin",
|
|
186
|
+
};
|
|
126
187
|
// Concrete tiers a subagent can actually run on. `balanced` is deliberately NOT
|
|
127
188
|
// a member here even though it is a member of `ModelTier`: a subagent needs
|
|
128
189
|
// ONE model for its whole run, and balanced is a session-level routing policy,
|
|
@@ -210,7 +271,7 @@ function loadAgentsFromDir(dir, source) {
|
|
|
210
271
|
export function discoverSubagents(deps) {
|
|
211
272
|
const home = deps.homeDir ?? homedir();
|
|
212
273
|
const layers = [
|
|
213
|
-
[GENERAL_AGENT, SEARCHER_AGENT, IMPLEMENTER_AGENT],
|
|
274
|
+
[GENERAL_AGENT, SEARCHER_AGENT, IMPLEMENTER_AGENT, VERIFICATION_AGENT],
|
|
214
275
|
...pluginAgentDirs(deps.env ?? process.env).map((dir) => loadAgentsFromDir(dir, "plugin")),
|
|
215
276
|
loadAgentsFromDir(join(home, ".claude", "agents"), "user-claude"),
|
|
216
277
|
loadAgentsFromDir(join(deps.cwd, ".pi", "agents"), "project-pi"),
|
|
@@ -261,11 +322,17 @@ const parameters = Type.Object({
|
|
|
261
322
|
task: Type.String(),
|
|
262
323
|
agent: Type.Optional(Type.String()),
|
|
263
324
|
}), {
|
|
264
|
-
description: `Run several independent tasks in parallel (max ${MAX_PARALLEL_SUBAGENTS}). Use INSTEAD of task.`,
|
|
325
|
+
description: `Run several independent tasks in parallel (max ${MAX_PARALLEL_SUBAGENTS}; ${MAX_PARALLEL_SUBAGENTS_ULTRA} in ultra mode). Use INSTEAD of task.`,
|
|
265
326
|
})),
|
|
266
327
|
});
|
|
267
328
|
export function makeSubagentTool(deps = {}) {
|
|
268
|
-
|
|
329
|
+
// The default runner rides the /go pipeline's resilience wrapper, so a chat
|
|
330
|
+
// subagent gets the same idle + wall-clock ceilings and transient-only retry
|
|
331
|
+
// as a /go stage child (previously a hung subagent hung the tool call until
|
|
332
|
+
// the user pressed Esc). The synthetic stage id is "implement", so the
|
|
333
|
+
// wrapper's write-gate already refuses to re-run a child that may have
|
|
334
|
+
// landed a partial edit.
|
|
335
|
+
const run = deps.runStageImpl ?? withResilience(runStage, DEFAULT_RESILIENCE_POLICY);
|
|
269
336
|
const discover = deps.discover ?? discoverSubagents;
|
|
270
337
|
return {
|
|
271
338
|
name: SUBAGENT_TOOL_NAME,
|
|
@@ -293,8 +360,9 @@ export function makeSubagentTool(deps = {}) {
|
|
|
293
360
|
if (requested.length === 0) {
|
|
294
361
|
return fail("Error: pass `task` (or a `tasks` array) describing what to do.");
|
|
295
362
|
}
|
|
296
|
-
|
|
297
|
-
|
|
363
|
+
const maxParallel = deps.isUltra?.() ? MAX_PARALLEL_SUBAGENTS_ULTRA : MAX_PARALLEL_SUBAGENTS;
|
|
364
|
+
if (requested.length > maxParallel) {
|
|
365
|
+
return fail(`Error: at most ${maxParallel} parallel tasks per call.`);
|
|
298
366
|
}
|
|
299
367
|
const cwd = ctx?.cwd ?? process.cwd();
|
|
300
368
|
const agents = discover({ cwd, homeDir: deps.homeDir });
|
|
@@ -17,6 +17,16 @@ import { Type } from "typebox";
|
|
|
17
17
|
export declare const TODO_TOOL_NAME = "todo_write";
|
|
18
18
|
export declare const MAX_TODOS = 50;
|
|
19
19
|
export declare const MAX_TODO_TEXT = 300;
|
|
20
|
+
/**
|
|
21
|
+
* Staleness-reminder throttle (both counters must trip): a reminder is
|
|
22
|
+
* eligible only after this many assistant turns since the last todo_write AND
|
|
23
|
+
* this many since the last reminder. The two-counter shape (staleness gate +
|
|
24
|
+
* anti-spam gate) mirrors what Claude Code ships for its own todo tool; the
|
|
25
|
+
* driver model routinely stops updating the board mid-grind (the frozen
|
|
26
|
+
* "Todos 0/8" report), and a bare description-level instruction does not
|
|
27
|
+
* survive a long run.
|
|
28
|
+
*/
|
|
29
|
+
export declare const TODO_REMINDER_TURNS = 10;
|
|
20
30
|
/**
|
|
21
31
|
* The desktop's structured state record rides its own widget key, like the
|
|
22
32
|
* `/go` run state: one JSON line the app parses and renders itself, never
|
|
@@ -61,6 +71,23 @@ export interface TodoTheme {
|
|
|
61
71
|
export declare function renderTodoWidget(todos: TodoItem[], theme: TodoTheme): string[];
|
|
62
72
|
/** The desktop state record: exactly one JSON line under TODO_STATE_KEY. */
|
|
63
73
|
export declare function todoStateLine(todos: TodoItem[]): string;
|
|
74
|
+
/**
|
|
75
|
+
* PURE: is a staleness reminder due? Only when the board has open work (an
|
|
76
|
+
* empty or fully-completed list never nags) and BOTH throttle counters have
|
|
77
|
+
* reached {@link TODO_REMINDER_TURNS}.
|
|
78
|
+
*/
|
|
79
|
+
export declare function shouldRemindTodos(input: {
|
|
80
|
+
todos: TodoItem[];
|
|
81
|
+
turnsSinceWrite: number;
|
|
82
|
+
turnsSinceReminder: number;
|
|
83
|
+
}): boolean;
|
|
84
|
+
/**
|
|
85
|
+
* PURE: the hedged reminder block appended to a tool result when the board has
|
|
86
|
+
* gone stale. Carries the CURRENT list so the model can reconcile without a
|
|
87
|
+
* read, and explicitly licenses ignoring it, so an accurate board costs one
|
|
88
|
+
* glance rather than a spurious todo_write.
|
|
89
|
+
*/
|
|
90
|
+
export declare function formatTodoReminder(todos: TodoItem[]): string;
|
|
64
91
|
/** Replay the branch: the last todo_write result is the canonical list. */
|
|
65
92
|
export declare function reconstructTodos(entries: unknown[]): TodoItem[];
|
|
66
93
|
type TodoParams = {
|
|
@@ -104,7 +131,7 @@ export declare function makeTodoTool(get: () => TodoItem[], set: (todos: TodoIte
|
|
|
104
131
|
isError?: undefined;
|
|
105
132
|
}>;
|
|
106
133
|
};
|
|
107
|
-
/** Wire the tool, the branch-replay events, and
|
|
134
|
+
/** Wire the tool, the branch-replay events, the staleness reminder, and /todos. */
|
|
108
135
|
export declare function registerTodos(pi: ExtensionAPI): void;
|
|
109
136
|
export {};
|
|
110
137
|
//# sourceMappingURL=todos.d.ts.map
|
package/dist/extension/todos.js
CHANGED
|
@@ -17,6 +17,16 @@ import { isDesktopSurface } from "./surface.js";
|
|
|
17
17
|
export const TODO_TOOL_NAME = "todo_write";
|
|
18
18
|
export const MAX_TODOS = 50;
|
|
19
19
|
export const MAX_TODO_TEXT = 300;
|
|
20
|
+
/**
|
|
21
|
+
* Staleness-reminder throttle (both counters must trip): a reminder is
|
|
22
|
+
* eligible only after this many assistant turns since the last todo_write AND
|
|
23
|
+
* this many since the last reminder. The two-counter shape (staleness gate +
|
|
24
|
+
* anti-spam gate) mirrors what Claude Code ships for its own todo tool; the
|
|
25
|
+
* driver model routinely stops updating the board mid-grind (the frozen
|
|
26
|
+
* "Todos 0/8" report), and a bare description-level instruction does not
|
|
27
|
+
* survive a long run.
|
|
28
|
+
*/
|
|
29
|
+
export const TODO_REMINDER_TURNS = 10;
|
|
20
30
|
const WIDGET_KEY = "yagni-todos";
|
|
21
31
|
/**
|
|
22
32
|
* The desktop's structured state record rides its own widget key, like the
|
|
@@ -101,6 +111,33 @@ export function renderTodoWidget(todos, theme) {
|
|
|
101
111
|
export function todoStateLine(todos) {
|
|
102
112
|
return JSON.stringify({ v: 1, todos });
|
|
103
113
|
}
|
|
114
|
+
/**
|
|
115
|
+
* PURE: is a staleness reminder due? Only when the board has open work (an
|
|
116
|
+
* empty or fully-completed list never nags) and BOTH throttle counters have
|
|
117
|
+
* reached {@link TODO_REMINDER_TURNS}.
|
|
118
|
+
*/
|
|
119
|
+
export function shouldRemindTodos(input) {
|
|
120
|
+
const { todos, turnsSinceWrite, turnsSinceReminder } = input;
|
|
121
|
+
if (todos.length === 0)
|
|
122
|
+
return false;
|
|
123
|
+
const { done, total } = todoSummary(todos);
|
|
124
|
+
if (done === total)
|
|
125
|
+
return false;
|
|
126
|
+
return turnsSinceWrite >= TODO_REMINDER_TURNS && turnsSinceReminder >= TODO_REMINDER_TURNS;
|
|
127
|
+
}
|
|
128
|
+
/**
|
|
129
|
+
* PURE: the hedged reminder block appended to a tool result when the board has
|
|
130
|
+
* gone stale. Carries the CURRENT list so the model can reconcile without a
|
|
131
|
+
* read, and explicitly licenses ignoring it, so an accurate board costs one
|
|
132
|
+
* glance rather than a spurious todo_write.
|
|
133
|
+
*/
|
|
134
|
+
export function formatTodoReminder(todos) {
|
|
135
|
+
return ("⟦YAGNI todos⟧ The todo_write checklist has not been updated for a while. " +
|
|
136
|
+
"If the work has moved on, bring it current now: mark finished steps completed, " +
|
|
137
|
+
"set the step you are on to in_progress, and add newly discovered steps. " +
|
|
138
|
+
"If the list is already accurate, ignore this.\n" +
|
|
139
|
+
formatTodoList(todos));
|
|
140
|
+
}
|
|
104
141
|
/** Replay the branch: the last todo_write result is the canonical list. */
|
|
105
142
|
export function reconstructTodos(entries) {
|
|
106
143
|
let todos = [];
|
|
@@ -186,9 +223,14 @@ export function makeTodoTool(get, set) {
|
|
|
186
223
|
},
|
|
187
224
|
};
|
|
188
225
|
}
|
|
189
|
-
/** Wire the tool, the branch-replay events, and
|
|
226
|
+
/** Wire the tool, the branch-replay events, the staleness reminder, and /todos. */
|
|
190
227
|
export function registerTodos(pi) {
|
|
191
228
|
let todos = [];
|
|
229
|
+
// Staleness-reminder counters (see TODO_REMINDER_TURNS). Session-local like
|
|
230
|
+
// the list cache itself; branch replay resets them so a resume/fork never
|
|
231
|
+
// opens with an instantly-due reminder.
|
|
232
|
+
let turnsSinceWrite = 0;
|
|
233
|
+
let turnsSinceReminder = 0;
|
|
192
234
|
const reconstruct = (ctx) => {
|
|
193
235
|
try {
|
|
194
236
|
todos = reconstructTodos(ctx.sessionManager.getBranch());
|
|
@@ -196,12 +238,45 @@ export function registerTodos(pi) {
|
|
|
196
238
|
catch {
|
|
197
239
|
todos = [];
|
|
198
240
|
}
|
|
241
|
+
turnsSinceWrite = 0;
|
|
242
|
+
turnsSinceReminder = 0;
|
|
199
243
|
paintWidget(ctx, todos);
|
|
200
244
|
};
|
|
201
245
|
pi.on("session_start", async (_event, ctx) => reconstruct(ctx));
|
|
202
246
|
pi.on("session_tree", async (_event, ctx) => reconstruct(ctx));
|
|
247
|
+
// Turn counting: one tick per finalized assistant message, the same "turn"
|
|
248
|
+
// the model experiences between opportunities to call todo_write.
|
|
249
|
+
pi.on("message_end", async (event) => {
|
|
250
|
+
if (event.message?.role === "assistant") {
|
|
251
|
+
turnsSinceWrite += 1;
|
|
252
|
+
turnsSinceReminder += 1;
|
|
253
|
+
}
|
|
254
|
+
});
|
|
255
|
+
// The reminder rides an existing tool result (the same result-modification
|
|
256
|
+
// seam ambient recall uses), so it reaches the model mid-run without
|
|
257
|
+
// spending a turn. Never appended to todo_write's own result, and fail-soft:
|
|
258
|
+
// a reminder must never break a tool call.
|
|
259
|
+
pi.on("tool_result", async (event) => {
|
|
260
|
+
try {
|
|
261
|
+
if (event.toolName === TODO_TOOL_NAME)
|
|
262
|
+
return;
|
|
263
|
+
if (!shouldRemindTodos({ todos, turnsSinceWrite, turnsSinceReminder }))
|
|
264
|
+
return;
|
|
265
|
+
turnsSinceReminder = 0;
|
|
266
|
+
return {
|
|
267
|
+
content: [
|
|
268
|
+
...event.content,
|
|
269
|
+
{ type: "text", text: `\n\n${formatTodoReminder(todos)}` },
|
|
270
|
+
],
|
|
271
|
+
};
|
|
272
|
+
}
|
|
273
|
+
catch {
|
|
274
|
+
return;
|
|
275
|
+
}
|
|
276
|
+
});
|
|
203
277
|
pi.registerTool(makeTodoTool(() => todos, (next) => {
|
|
204
278
|
todos = next;
|
|
279
|
+
turnsSinceWrite = 0;
|
|
205
280
|
}));
|
|
206
281
|
pi.registerCommand("todos", {
|
|
207
282
|
description: "Show the agent's current task list for this session.",
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Ultra mode (/ultra) — an explicit, session-scoped dial for aggressive
|
|
3
|
+
* multi-agent orchestration ("the diamond": fan out → reduce → synthesize).
|
|
4
|
+
*
|
|
5
|
+
* Off by default so the trial-default behavior is unchanged; toggling on swaps
|
|
6
|
+
* the driver's delegation paragraph for the diamond directive (branding.ts's
|
|
7
|
+
* YAGNI_IDENTITY_ULTRA) and widens the subagent tool's per-call fan-out
|
|
8
|
+
* ceiling (subagents.ts). The two halves take effect at different moments:
|
|
9
|
+
* the fan-out ceiling is probed live on every subagent call, but the identity
|
|
10
|
+
* is read in index.ts's before_agent_start handler, which pi fires only when
|
|
11
|
+
* a NEW user prompt is submitted — a toggle mid-run leaves the running task on
|
|
12
|
+
* its existing instructions until the next message (the handler notifies when
|
|
13
|
+
* that is the case). Ultra is a prompt + ceiling change only: it never touches
|
|
14
|
+
* the permission mode, the model tier, or the /go pipeline.
|
|
15
|
+
*/
|
|
16
|
+
import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
|
17
|
+
export interface UltraHolder {
|
|
18
|
+
get(): boolean;
|
|
19
|
+
set(on: boolean): void;
|
|
20
|
+
}
|
|
21
|
+
export declare function createUltraHolder(initial?: boolean): UltraHolder;
|
|
22
|
+
/**
|
|
23
|
+
* Wire the /ultra command onto a shared holder. No argument toggles; `on` /
|
|
24
|
+
* `off` set explicitly; `status` reports without changing anything.
|
|
25
|
+
*/
|
|
26
|
+
export declare function registerUltraCommand(pi: ExtensionAPI, holder: UltraHolder): void;
|
|
27
|
+
//# sourceMappingURL=ultra.d.ts.map
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Ultra mode (/ultra) — an explicit, session-scoped dial for aggressive
|
|
3
|
+
* multi-agent orchestration ("the diamond": fan out → reduce → synthesize).
|
|
4
|
+
*
|
|
5
|
+
* Off by default so the trial-default behavior is unchanged; toggling on swaps
|
|
6
|
+
* the driver's delegation paragraph for the diamond directive (branding.ts's
|
|
7
|
+
* YAGNI_IDENTITY_ULTRA) and widens the subagent tool's per-call fan-out
|
|
8
|
+
* ceiling (subagents.ts). The two halves take effect at different moments:
|
|
9
|
+
* the fan-out ceiling is probed live on every subagent call, but the identity
|
|
10
|
+
* is read in index.ts's before_agent_start handler, which pi fires only when
|
|
11
|
+
* a NEW user prompt is submitted — a toggle mid-run leaves the running task on
|
|
12
|
+
* its existing instructions until the next message (the handler notifies when
|
|
13
|
+
* that is the case). Ultra is a prompt + ceiling change only: it never touches
|
|
14
|
+
* the permission mode, the model tier, or the /go pipeline.
|
|
15
|
+
*/
|
|
16
|
+
export function createUltraHolder(initial = false) {
|
|
17
|
+
let on = initial;
|
|
18
|
+
return {
|
|
19
|
+
get: () => on,
|
|
20
|
+
set: (v) => {
|
|
21
|
+
on = v;
|
|
22
|
+
},
|
|
23
|
+
};
|
|
24
|
+
}
|
|
25
|
+
/** Status chip shown while ultra is on (same footer surface as the /mode chip). */
|
|
26
|
+
const ULTRA_STATUS = "◆ ultra";
|
|
27
|
+
const ULTRA_ON_COPY = "Ultra mode ON: meaningful work fans out to parallel subagents, adversarial " +
|
|
28
|
+
"verification agents try to break the result, then the agent synthesizes. " +
|
|
29
|
+
"Expect more subagent spend per task.";
|
|
30
|
+
const ULTRA_OFF_COPY = "Ultra mode OFF: back to delegate-when-useful.";
|
|
31
|
+
/**
|
|
32
|
+
* Appended when the toggle lands mid-run: the identity swap only applies when
|
|
33
|
+
* the next prompt is submitted (see the module docblock), so without this note
|
|
34
|
+
* the chip flips while the running task visibly keeps its old behavior — which
|
|
35
|
+
* reads as ultra mode being broken.
|
|
36
|
+
*/
|
|
37
|
+
const MID_RUN_NOTE = " The task currently running keeps its existing instructions; the change takes full effect on your next message.";
|
|
38
|
+
/**
|
|
39
|
+
* Wire the /ultra command onto a shared holder. No argument toggles; `on` /
|
|
40
|
+
* `off` set explicitly; `status` reports without changing anything.
|
|
41
|
+
*/
|
|
42
|
+
export function registerUltraCommand(pi, holder) {
|
|
43
|
+
pi.registerCommand("ultra", {
|
|
44
|
+
description: "Toggle ultra mode: aggressive fan-out/verify/synthesize orchestration. /ultra on | off | status.",
|
|
45
|
+
handler: async (args, ctx) => {
|
|
46
|
+
const notify = (m, t) => {
|
|
47
|
+
if (ctx.hasUI)
|
|
48
|
+
ctx.ui.notify(m, t);
|
|
49
|
+
};
|
|
50
|
+
const arg = args.trim().toLowerCase();
|
|
51
|
+
if (arg && arg !== "on" && arg !== "off" && arg !== "status") {
|
|
52
|
+
notify(`Unknown argument "${arg}". Use /ultra, /ultra on, /ultra off, or /ultra status.`, "warning");
|
|
53
|
+
return;
|
|
54
|
+
}
|
|
55
|
+
if (arg === "status") {
|
|
56
|
+
notify(holder.get() ? ULTRA_ON_COPY : ULTRA_OFF_COPY, "info");
|
|
57
|
+
return;
|
|
58
|
+
}
|
|
59
|
+
const next = arg === "on" ? true : arg === "off" ? false : !holder.get();
|
|
60
|
+
holder.set(next);
|
|
61
|
+
try {
|
|
62
|
+
if (ctx.hasUI)
|
|
63
|
+
ctx.ui.setStatus?.("yagni-ultra", next ? ULTRA_STATUS : undefined);
|
|
64
|
+
}
|
|
65
|
+
catch {
|
|
66
|
+
// The chip is chrome; never let it break /ultra.
|
|
67
|
+
}
|
|
68
|
+
// Guarded probe: test fakes (and any minimal harness ctx) may not carry
|
|
69
|
+
// isIdle, and its absence must read as idle, never as busy.
|
|
70
|
+
const midRun = typeof ctx.isIdle === "function" && !ctx.isIdle();
|
|
71
|
+
const copy = next ? ULTRA_ON_COPY : ULTRA_OFF_COPY;
|
|
72
|
+
notify(midRun ? copy + MID_RUN_NOTE : copy, "info");
|
|
73
|
+
},
|
|
74
|
+
});
|
|
75
|
+
}
|
|
76
|
+
//# sourceMappingURL=ultra.js.map
|
package/dist/login.d.ts
CHANGED
|
@@ -34,8 +34,10 @@ type OpenRunner = (cmd: string, args: string[]) => Promise<void>;
|
|
|
34
34
|
*
|
|
35
35
|
* macOS: open <url>
|
|
36
36
|
* Linux: xdg-open <url>
|
|
37
|
-
* Windows:
|
|
38
|
-
* without
|
|
37
|
+
* Windows: rundll32 url.dll,FileProtocolHandler <url> (opens the default
|
|
38
|
+
* browser without going through cmd.exe — `cmd /c start <url>`
|
|
39
|
+
* re-parses its arguments as a shell line, so a hostile URL with
|
|
40
|
+
* `&`/`^` metacharacters could execute commands)
|
|
39
41
|
*/
|
|
40
42
|
export declare function resolveOpenCommand(url: string, platform?: NodeJS.Platform): {
|
|
41
43
|
cmd: string;
|
package/dist/login.js
CHANGED
|
@@ -31,14 +31,17 @@ function isTimeoutError(err) {
|
|
|
31
31
|
*
|
|
32
32
|
* macOS: open <url>
|
|
33
33
|
* Linux: xdg-open <url>
|
|
34
|
-
* Windows:
|
|
35
|
-
* without
|
|
34
|
+
* Windows: rundll32 url.dll,FileProtocolHandler <url> (opens the default
|
|
35
|
+
* browser without going through cmd.exe — `cmd /c start <url>`
|
|
36
|
+
* re-parses its arguments as a shell line, so a hostile URL with
|
|
37
|
+
* `&`/`^` metacharacters could execute commands)
|
|
36
38
|
*/
|
|
37
39
|
export function resolveOpenCommand(url, platform = process.platform) {
|
|
38
40
|
if (platform === "darwin")
|
|
39
41
|
return { cmd: "open", args: [url] };
|
|
40
|
-
if (platform === "win32")
|
|
41
|
-
return { cmd: "
|
|
42
|
+
if (platform === "win32") {
|
|
43
|
+
return { cmd: "rundll32", args: ["url.dll,FileProtocolHandler", url] };
|
|
44
|
+
}
|
|
42
45
|
return { cmd: "xdg-open", args: [url] };
|
|
43
46
|
}
|
|
44
47
|
const spawnRunner = (cmd, args) => new Promise((resolve, reject) => {
|
|
@@ -50,6 +53,18 @@ const spawnRunner = (cmd, args) => new Promise((resolve, reject) => {
|
|
|
50
53
|
* URL is still printed for manual copy. `runner` is injectable for tests.
|
|
51
54
|
*/
|
|
52
55
|
export const realOpenUrl = (url, runner = spawnRunner) => {
|
|
56
|
+
// Only ever hand http(s) URLs to the OS opener — refuse file:, javascript:,
|
|
57
|
+
// or custom schemes a compromised server response could try to smuggle in.
|
|
58
|
+
let parsed;
|
|
59
|
+
try {
|
|
60
|
+
parsed = new URL(url);
|
|
61
|
+
}
|
|
62
|
+
catch {
|
|
63
|
+
return Promise.resolve();
|
|
64
|
+
}
|
|
65
|
+
if (parsed.protocol !== "http:" && parsed.protocol !== "https:") {
|
|
66
|
+
return Promise.resolve();
|
|
67
|
+
}
|
|
53
68
|
const { cmd, args } = resolveOpenCommand(url);
|
|
54
69
|
return runner(cmd, args).catch(() => {
|
|
55
70
|
// Silently fail — the user can still copy the URL manually.
|
|
@@ -36,5 +36,5 @@ export declare function promptEnrichmentDisabled(env: NodeJS.ProcessEnv): boolea
|
|
|
36
36
|
* model, and the load-bearing instructions (ask_yagni contract, delegation)
|
|
37
37
|
* live elsewhere in the prompt.
|
|
38
38
|
*/
|
|
39
|
-
export declare const ENGINEERING_PRACTICE_SECTION = "Engineering practice:\n\
|
|
39
|
+
export declare const ENGINEERING_PRACTICE_SECTION = "Engineering practice:\n\nAnswering vs acting: distinguish what the user is asking for before responding. When the user asks you to analyze, investigate, find a root cause, study how something works, explore an approach, or asks a strategic or advisory question (\"should we...\", \"what's your read on...\", \"go/no-go on...\"), answer in prose \u2014 do not start coding or editing files. When the user asks you to implement, fix, or change something, use your tools to make the actual edits and run the actual commands \u2014 do not answer with a description of what you would do, or with code for the user to apply themselves.\n\nConventions:\n- Never assume a library is available, however well known. Before using one, confirm the project already depends on it (its package manifest, or imports in neighboring files).\n- When editing, read the surrounding code and its imports first; match the file's existing style, naming, and patterns rather than introducing your own.\n- When creating a new file or component, study an existing sibling first and follow its structure.\n- Never write code that logs or exposes secrets, keys, or credentials.\n\nVerification:\n- Consider what the code you are changing is supposed to do (from its name, location, and callers) before you change it.\n- Verify changes with the project's own tests when possible. Never assume a test framework or command \u2014 check the README, package scripts, or neighboring tests for the real one.\n- After completing a task, run the project's lint and typecheck commands if you know them; if you cannot find them, ask the user and suggest recording them in AGENTS.md for next time.\n\nVersion control:\n- No unsolicited commits: commit only when the user asked for one or the task at hand clearly calls for it.\n\nGit safety:\n- You may be in a dirty git worktree. Never revert existing changes you did not make unless explicitly asked \u2014 these were made by the user.\n- If there are unrelated changes in files you are touching, read and work with them rather than reverting.\n- If changes appear in unrelated files, ignore them and do not revert.\n- Do not amend a commit unless explicitly asked.\n- If you notice unexpected changes you did not make while working, stop immediately and ask the user.\n- Never use destructive git commands (git reset --hard, git checkout --) unless the user explicitly requests or approves them.\n\nTodo discipline:\n- Track multi-step work with todo_write: keep exactly one item in_progress at a time, mark items completed the moment they are done, and add newly discovered steps as pending.\n- Do not batch-complete items or create single-step plans. Skip planning for trivially small work (~25% of tasks).\n\nMode awareness:\n- In auto mode, proactively run tests, lint, and typecheck after your changes.\n- In review mode, propose verification steps but wait for approval before running them.\n- In plan mode, explore and design only \u2014 the gate holds all writes.\n\nCommunication:\n- Answer directly, without preamble or postamble (\"Here is what I will do next...\", \"Based on the information provided...\"). Match the length of your answer to the question.\n- After making edits, report the outcome briefly; do not restate the diff or explain the code you just wrote unless asked.\n- Do not add code comments that narrate what you changed or why the change is correct; comments are for future readers of the code.\n- Reference code as file_path:line_number so the user can jump to it.\n- Before running a non-trivial command that changes state, say in one line what it does and why.\n- Never guess or fabricate URLs. Only use URLs the user provided or that appear in local files.\n- No emojis unless the user asks for them.";
|
|
40
40
|
//# sourceMappingURL=promptEnrichment.d.ts.map
|
package/dist/promptEnrichment.js
CHANGED
|
@@ -41,7 +41,7 @@ export function promptEnrichmentDisabled(env) {
|
|
|
41
41
|
*/
|
|
42
42
|
export const ENGINEERING_PRACTICE_SECTION = `Engineering practice:
|
|
43
43
|
|
|
44
|
-
|
|
44
|
+
Answering vs acting: distinguish what the user is asking for before responding. When the user asks you to analyze, investigate, find a root cause, study how something works, explore an approach, or asks a strategic or advisory question ("should we...", "what's your read on...", "go/no-go on..."), answer in prose — do not start coding or editing files. When the user asks you to implement, fix, or change something, use your tools to make the actual edits and run the actual commands — do not answer with a description of what you would do, or with code for the user to apply themselves.
|
|
45
45
|
|
|
46
46
|
Conventions:
|
|
47
47
|
- Never assume a library is available, however well known. Before using one, confirm the project already depends on it (its package manifest, or imports in neighboring files).
|