@yagni-app/code-staging 0.0.0 → 0.1.0-staging.1004.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (147) hide show
  1. package/LICENSE.md +55 -0
  2. package/README.md +166 -0
  3. package/dist/branding.d.ts +25 -0
  4. package/dist/branding.js +27 -0
  5. package/dist/claudeCompat.d.ts +78 -0
  6. package/dist/claudeCompat.js +198 -0
  7. package/dist/cli.d.ts +55 -0
  8. package/dist/cli.js +326 -0
  9. package/dist/config.d.ts +19 -0
  10. package/dist/config.js +28 -0
  11. package/dist/credentials.d.ts +41 -0
  12. package/dist/credentials.js +75 -0
  13. package/dist/distribution.d.ts +15 -0
  14. package/dist/distribution.js +45 -0
  15. package/dist/doctor.d.ts +94 -0
  16. package/dist/doctor.js +343 -0
  17. package/dist/extension/advisor.d.ts +84 -0
  18. package/dist/extension/advisor.js +101 -0
  19. package/dist/extension/askAdvisorTool.d.ts +83 -0
  20. package/dist/extension/askAdvisorTool.js +188 -0
  21. package/dist/extension/askYagniTool.d.ts +39 -0
  22. package/dist/extension/askYagniTool.js +63 -0
  23. package/dist/extension/bless.d.ts +45 -0
  24. package/dist/extension/bless.js +74 -0
  25. package/dist/extension/branding.d.ts +48 -0
  26. package/dist/extension/branding.js +119 -0
  27. package/dist/extension/chipEditor.d.ts +117 -0
  28. package/dist/extension/chipEditor.js +369 -0
  29. package/dist/extension/config.d.ts +112 -0
  30. package/dist/extension/config.js +108 -0
  31. package/dist/extension/costHud.d.ts +73 -0
  32. package/dist/extension/costHud.js +123 -0
  33. package/dist/extension/decisionCapture.d.ts +52 -0
  34. package/dist/extension/decisionCapture.js +66 -0
  35. package/dist/extension/decisions.d.ts +83 -0
  36. package/dist/extension/decisions.js +200 -0
  37. package/dist/extension/diagnostics.d.ts +41 -0
  38. package/dist/extension/diagnostics.js +110 -0
  39. package/dist/extension/index.d.ts +116 -0
  40. package/dist/extension/index.js +388 -0
  41. package/dist/extension/initDone.d.ts +28 -0
  42. package/dist/extension/initDone.js +66 -0
  43. package/dist/extension/initPass.d.ts +152 -0
  44. package/dist/extension/initPass.js +394 -0
  45. package/dist/extension/nextWorkTool.d.ts +51 -0
  46. package/dist/extension/nextWorkTool.js +80 -0
  47. package/dist/extension/permission.d.ts +91 -0
  48. package/dist/extension/permission.js +236 -0
  49. package/dist/extension/pipeline/activity.d.ts +37 -0
  50. package/dist/extension/pipeline/activity.js +151 -0
  51. package/dist/extension/pipeline/activityFeed.d.ts +59 -0
  52. package/dist/extension/pipeline/activityFeed.js +175 -0
  53. package/dist/extension/pipeline/budget.d.ts +48 -0
  54. package/dist/extension/pipeline/budget.js +68 -0
  55. package/dist/extension/pipeline/checkpoint.d.ts +31 -0
  56. package/dist/extension/pipeline/checkpoint.js +176 -0
  57. package/dist/extension/pipeline/eval.d.ts +168 -0
  58. package/dist/extension/pipeline/eval.js +182 -0
  59. package/dist/extension/pipeline/events.d.ts +56 -0
  60. package/dist/extension/pipeline/events.js +147 -0
  61. package/dist/extension/pipeline/findings.d.ts +42 -0
  62. package/dist/extension/pipeline/findings.js +144 -0
  63. package/dist/extension/pipeline/finish.d.ts +128 -0
  64. package/dist/extension/pipeline/finish.js +307 -0
  65. package/dist/extension/pipeline/goCommand.d.ts +128 -0
  66. package/dist/extension/pipeline/goCommand.js +972 -0
  67. package/dist/extension/pipeline/goCompareCommand.d.ts +48 -0
  68. package/dist/extension/pipeline/goCompareCommand.js +184 -0
  69. package/dist/extension/pipeline/goFlags.d.ts +40 -0
  70. package/dist/extension/pipeline/goFlags.js +46 -0
  71. package/dist/extension/pipeline/goStatusCommands.d.ts +59 -0
  72. package/dist/extension/pipeline/goStatusCommands.js +188 -0
  73. package/dist/extension/pipeline/invocation.d.ts +45 -0
  74. package/dist/extension/pipeline/invocation.js +64 -0
  75. package/dist/extension/pipeline/orchestrator.d.ts +131 -0
  76. package/dist/extension/pipeline/orchestrator.js +636 -0
  77. package/dist/extension/pipeline/personas.d.ts +44 -0
  78. package/dist/extension/pipeline/personas.js +248 -0
  79. package/dist/extension/pipeline/resilience.d.ts +85 -0
  80. package/dist/extension/pipeline/resilience.js +166 -0
  81. package/dist/extension/pipeline/resume.d.ts +18 -0
  82. package/dist/extension/pipeline/resume.js +106 -0
  83. package/dist/extension/pipeline/runRegistry.d.ts +112 -0
  84. package/dist/extension/pipeline/runRegistry.js +202 -0
  85. package/dist/extension/pipeline/runSession.d.ts +152 -0
  86. package/dist/extension/pipeline/runSession.js +167 -0
  87. package/dist/extension/pipeline/runState.d.ts +158 -0
  88. package/dist/extension/pipeline/runState.js +264 -0
  89. package/dist/extension/pipeline/runner.d.ts +79 -0
  90. package/dist/extension/pipeline/runner.js +298 -0
  91. package/dist/extension/pipeline/scrubSecrets.d.ts +15 -0
  92. package/dist/extension/pipeline/scrubSecrets.js +32 -0
  93. package/dist/extension/pipeline/stages.d.ts +51 -0
  94. package/dist/extension/pipeline/stages.js +113 -0
  95. package/dist/extension/pipeline/ticketResolution.d.ts +34 -0
  96. package/dist/extension/pipeline/ticketResolution.js +75 -0
  97. package/dist/extension/pipeline/types.d.ts +436 -0
  98. package/dist/extension/pipeline/types.js +48 -0
  99. package/dist/extension/pipeline/verify.d.ts +277 -0
  100. package/dist/extension/pipeline/verify.js +758 -0
  101. package/dist/extension/pipeline/workspace.d.ts +32 -0
  102. package/dist/extension/pipeline/workspace.js +73 -0
  103. package/dist/extension/pipeline/worktree.d.ts +107 -0
  104. package/dist/extension/pipeline/worktree.js +200 -0
  105. package/dist/extension/provider.d.ts +11 -0
  106. package/dist/extension/provider.js +26 -0
  107. package/dist/extension/recall.d.ts +93 -0
  108. package/dist/extension/recall.js +187 -0
  109. package/dist/extension/recordContextTool.d.ts +38 -0
  110. package/dist/extension/recordContextTool.js +85 -0
  111. package/dist/extension/recordDecisionTool.d.ts +52 -0
  112. package/dist/extension/recordDecisionTool.js +102 -0
  113. package/dist/extension/repoDocs.d.ts +81 -0
  114. package/dist/extension/repoDocs.js +260 -0
  115. package/dist/extension/resilientFetch.d.ts +60 -0
  116. package/dist/extension/resilientFetch.js +133 -0
  117. package/dist/extension/reviewTool.d.ts +34 -0
  118. package/dist/extension/reviewTool.js +81 -0
  119. package/dist/extension/spool.d.ts +92 -0
  120. package/dist/extension/spool.js +266 -0
  121. package/dist/extension/stateHome.d.ts +2 -0
  122. package/dist/extension/stateHome.js +6 -0
  123. package/dist/extension/subagents.d.ts +135 -0
  124. package/dist/extension/subagents.js +281 -0
  125. package/dist/extension/surface.d.ts +10 -0
  126. package/dist/extension/surface.js +12 -0
  127. package/dist/extension/todos.d.ts +110 -0
  128. package/dist/extension/todos.js +217 -0
  129. package/dist/extension/tokenProvider.d.ts +93 -0
  130. package/dist/extension/tokenProvider.js +234 -0
  131. package/dist/launch.d.ts +76 -0
  132. package/dist/launch.js +111 -0
  133. package/dist/login.d.ts +45 -0
  134. package/dist/login.js +142 -0
  135. package/dist/logout.d.ts +14 -0
  136. package/dist/logout.js +34 -0
  137. package/dist/paths.d.ts +31 -0
  138. package/dist/paths.js +87 -0
  139. package/dist/piPackage.d.ts +33 -0
  140. package/dist/piPackage.js +71 -0
  141. package/dist/profiles.d.ts +80 -0
  142. package/dist/profiles.js +222 -0
  143. package/dist/refresh.d.ts +70 -0
  144. package/dist/refresh.js +117 -0
  145. package/dist/upgrade.d.ts +97 -0
  146. package/dist/upgrade.js +284 -0
  147. package/package.json +39 -3
@@ -0,0 +1,187 @@
1
+ /**
2
+ * Ambient judgment recall (spec §4, M1 — degraded-tolerant).
3
+ *
4
+ * After the `read` tool returns, fetch the compact recorded judgment for that
5
+ * path (`GET /recall`) and append a `⟦YAGNI recall⟧ …` block to the tool result
6
+ * so the agent sees the decisions/corrections already banked for this file —
7
+ * zero extra turns, no model call.
8
+ *
9
+ * pi-recall-surface finding (verified against pi 0.80.2
10
+ * `dist/core/extensions/types.d.ts`): the interactive `tool_result` event is
11
+ * documented "Fired after a tool executes. Can modify result." and its handler
12
+ * result `ToolResultEventResult` exposes `content?: (TextContent | ImageContent)[]`.
13
+ * So RESULT MODIFICATION is supported: we return the original content plus the
14
+ * recall block. (The documented `before_agent_start` brief-injection fallback is
15
+ * therefore NOT needed here.)
16
+ *
17
+ * Everything is fail-soft and bounded:
18
+ * - `AbortSignal.timeout` + a hard deadline race cap the fetch at 1500ms, so
19
+ * recall NEVER blocks a read beyond the timeout — even if the shared
20
+ * authed-fetch seam attempts a 401 refresh underneath.
21
+ * - per-path session cache (a path is fetched at most once per session).
22
+ * - skipped entirely when the corpus is thin (<3 active decisions) or eval mode.
23
+ * - any failure degrades to no injection.
24
+ */
25
+ import { isAbsolute, relative } from "node:path";
26
+ import { composeAbortSignal } from "./pipeline/resilience.js";
27
+ /** Recall must never block a read beyond this (spec §4). */
28
+ export const RECALL_TIMEOUT_MS = 1500;
29
+ /** Below this many active decisions, recall is skipped entirely (spec: <3). */
30
+ export const RECALL_MIN_DECISIONS = 3;
31
+ /** Client-side caps mirror the backend (compact by construction). */
32
+ export const RECALL_MAX_DECISIONS = 5;
33
+ export const RECALL_MAX_CORRECTIONS = 3;
34
+ function isRecallDecision(v) {
35
+ const d = v;
36
+ return !!d && typeof d.id === "string" && typeof d.question === "string" && typeof d.decision === "string";
37
+ }
38
+ function isRecallCorrection(v) {
39
+ const c = v;
40
+ return !!c && typeof c.summary === "string";
41
+ }
42
+ /** Defensive parse of a /recall response into a {@link RecallResult}, or null. */
43
+ export function normalizeRecall(data) {
44
+ if (!data || typeof data !== "object")
45
+ return null;
46
+ const d = data;
47
+ const decisions = Array.isArray(d.decisions)
48
+ ? d.decisions.filter(isRecallDecision).slice(0, RECALL_MAX_DECISIONS)
49
+ : [];
50
+ const corrections = Array.isArray(d.corrections)
51
+ ? d.corrections.filter(isRecallCorrection).slice(0, RECALL_MAX_CORRECTIONS)
52
+ : [];
53
+ return { decisions, corrections, thin: d.thin === true };
54
+ }
55
+ /**
56
+ * Pure: derive the repo-relative path the backend expects from a read tool's
57
+ * `path` argument and the session cwd. Rebases an absolute path INSIDE the tree
58
+ * to a relative one (an outside path stays absolute), and strips a leading `./`.
59
+ */
60
+ export function toRepoRelativePath(readPath, cwd) {
61
+ let p = readPath.trim();
62
+ if (isAbsolute(p)) {
63
+ const rel = relative(cwd, p);
64
+ if (rel && !rel.startsWith("..") && !isAbsolute(rel))
65
+ p = rel;
66
+ }
67
+ return p.replace(/^\.\//, "");
68
+ }
69
+ /**
70
+ * Pure: render the compact `⟦YAGNI recall⟧` block, or null when there is
71
+ * nothing to inject (missing result, thin corpus, or empty). No em-dashes.
72
+ */
73
+ export function formatRecallBlock(result) {
74
+ if (!result || result.thin)
75
+ return null;
76
+ const parts = [];
77
+ for (const d of result.decisions)
78
+ parts.push(`- ${d.question} => ${d.decision}`);
79
+ for (const c of result.corrections)
80
+ parts.push(`- correction: ${c.summary}`);
81
+ if (parts.length === 0)
82
+ return null;
83
+ return `⟦YAGNI recall⟧ recorded judgment for this path:\n${parts.join("\n")}`;
84
+ }
85
+ /**
86
+ * Run `work` bounded by a hard `timeoutMs` deadline: resolves the work's value
87
+ * on settle, or `null` when the deadline fires first (the deadline signal is
88
+ * aborted so the outstanding fetch tears down). Guarantees a return within
89
+ * `timeoutMs` regardless of what the fetch impl does underneath.
90
+ */
91
+ function withDeadline(timeoutMs, work) {
92
+ const controller = new AbortController();
93
+ return new Promise((resolve) => {
94
+ const timer = setTimeout(() => {
95
+ controller.abort();
96
+ resolve(null);
97
+ }, timeoutMs);
98
+ timer.unref?.();
99
+ work(controller.signal).then((v) => {
100
+ clearTimeout(timer);
101
+ resolve(v);
102
+ }, () => {
103
+ clearTimeout(timer);
104
+ resolve(null);
105
+ });
106
+ });
107
+ }
108
+ /**
109
+ * GET /recall for a repo-relative path, bounded at {@link RECALL_TIMEOUT_MS}.
110
+ * Uses the shared authed fetch (inherits the 401 refresh-retry), but the hard
111
+ * deadline still caps the total wait. Returns null on any non-2xx, timeout, or
112
+ * error (fail-soft).
113
+ */
114
+ export async function fetchRecall(opts, path, signal) {
115
+ const timeoutMs = opts.timeoutMs ?? RECALL_TIMEOUT_MS;
116
+ const fetchImpl = opts.fetchImpl ?? fetch;
117
+ const query = new URLSearchParams({ path });
118
+ if (opts.repo)
119
+ query.set("repo", opts.repo);
120
+ const url = `${opts.baseUrl}/api/yagni-code/recall?${query.toString()}`;
121
+ return withDeadline(timeoutMs, async (deadlineSignal) => {
122
+ const composed = composeAbortSignal(signal, deadlineSignal);
123
+ try {
124
+ const res = await fetchImpl(url, {
125
+ method: "GET",
126
+ headers: { authorization: `Bearer ${opts.getToken() ?? ""}` },
127
+ signal: composed,
128
+ });
129
+ if (!res.ok)
130
+ return null;
131
+ return normalizeRecall((await res.json()));
132
+ }
133
+ catch {
134
+ return null;
135
+ }
136
+ });
137
+ }
138
+ /**
139
+ * Wire the `tool_result` handler that appends recall to a `read` result. No-op
140
+ * (nothing registered) when the corpus is thin (<3 decisions) or in eval mode,
141
+ * so there is zero overhead in those cases.
142
+ */
143
+ export function registerAmbientRecall(pi, deps) {
144
+ if (deps.evalMode)
145
+ return;
146
+ if ((deps.decisionsCount ?? 0) < RECALL_MIN_DECISIONS)
147
+ return;
148
+ const doFetch = deps.fetchRecallImpl ?? fetchRecall;
149
+ // Per-path session cache: a path is fetched at most once. Only real results
150
+ // (success, including a thin/empty payload) are cached; a transient failure
151
+ // (null) is NOT cached, so a later read of the same path can retry.
152
+ const cache = new Map();
153
+ const clientOpts = {
154
+ baseUrl: deps.baseUrl,
155
+ getToken: deps.getToken,
156
+ fetchImpl: deps.fetchImpl,
157
+ timeoutMs: deps.timeoutMs,
158
+ repo: deps.repo,
159
+ };
160
+ pi.on("tool_result", async (event, ctx) => {
161
+ try {
162
+ if (event.toolName !== "read" || event.isError)
163
+ return;
164
+ const rawPath = event.input.path;
165
+ if (typeof rawPath !== "string" || rawPath.trim().length === 0)
166
+ return;
167
+ const relPath = toRepoRelativePath(rawPath, ctx.cwd);
168
+ let result;
169
+ if (cache.has(relPath)) {
170
+ result = cache.get(relPath) ?? null;
171
+ }
172
+ else {
173
+ result = await doFetch(clientOpts, relPath, ctx.signal);
174
+ if (result !== null)
175
+ cache.set(relPath, result);
176
+ }
177
+ const block = formatRecallBlock(result);
178
+ if (!block)
179
+ return;
180
+ return { content: [...event.content, { type: "text", text: `\n\n${block}` }] };
181
+ }
182
+ catch {
183
+ return; // fail-soft: recall never breaks a read
184
+ }
185
+ });
186
+ }
187
+ //# sourceMappingURL=recall.js.map
@@ -0,0 +1,38 @@
1
+ import type { ToolDefinition } from "@earendil-works/pi-coding-agent";
2
+ import { Type } from "typebox";
3
+ /** Options for {@link makeRecordEngineeringContextTool}. */
4
+ export interface MakeRecordContextToolOptions {
5
+ baseUrl: string;
6
+ getToken: () => string | undefined;
7
+ fetchImpl?: typeof fetch;
8
+ /** Idempotency-key source (default: crypto.randomUUID); injected in tests. */
9
+ makeIdempotencyKey?: () => string;
10
+ /**
11
+ * How to resolve the agent's pushed head SHA when a PR is being handed off.
12
+ * Injectable so the unit test is deterministic; defaults to `git rev-parse
13
+ * HEAD` in `ctx.cwd` (best-effort — resolves to undefined on any failure, so
14
+ * the server simply records no head SHA). Mirrors {@link reviewTool}'s
15
+ * `getDiff` exec seam since pi's ExtensionContext exposes no exec.
16
+ */
17
+ resolveHeadSha?: (cwd: string | undefined, signal?: AbortSignal) => Promise<string | undefined>;
18
+ }
19
+ declare const parameters: Type.TObject<{
20
+ ticket: Type.TString;
21
+ body: Type.TString;
22
+ prUrl: Type.TOptional<Type.TString>;
23
+ repo: Type.TOptional<Type.TString>;
24
+ prNumber: Type.TOptional<Type.TNumber>;
25
+ }>;
26
+ /**
27
+ * Build the `record_engineering_context` tool.
28
+ *
29
+ * Writes the engineering rationale ("why it was done", the tradeoffs/decisions)
30
+ * back onto the originating YAGNI work-item, closing the loop so the ticket
31
+ * carries the decision context. Reversible internal write.
32
+ */
33
+ export declare function makeRecordEngineeringContextTool(opts: MakeRecordContextToolOptions): ToolDefinition<typeof parameters, {
34
+ workItemId: string | null;
35
+ spooled?: boolean;
36
+ }>;
37
+ export {};
38
+ //# sourceMappingURL=recordContextTool.d.ts.map
@@ -0,0 +1,85 @@
1
+ import { execFile } from "node:child_process";
2
+ import { randomUUID } from "node:crypto";
3
+ import { Type } from "typebox";
4
+ import { sendOrSpool } from "./spool.js";
5
+ const parameters = Type.Object({
6
+ ticket: Type.String(),
7
+ body: Type.String(),
8
+ prUrl: Type.Optional(Type.String()),
9
+ repo: Type.Optional(Type.String()),
10
+ prNumber: Type.Optional(Type.Number()),
11
+ });
12
+ /** Best-effort `git rev-parse HEAD` in `cwd`; resolves to undefined on failure. */
13
+ function defaultResolveHeadSha(cwd, signal) {
14
+ return new Promise((resolve) => {
15
+ execFile("git", ["rev-parse", "HEAD"], { cwd, signal, maxBuffer: 1024 * 1024 }, (err, stdout) => resolve(err ? undefined : stdout.trim() || undefined));
16
+ });
17
+ }
18
+ /**
19
+ * Build the `record_engineering_context` tool.
20
+ *
21
+ * Writes the engineering rationale ("why it was done", the tradeoffs/decisions)
22
+ * back onto the originating YAGNI work-item, closing the loop so the ticket
23
+ * carries the decision context. Reversible internal write.
24
+ */
25
+ export function makeRecordEngineeringContextTool(opts) {
26
+ return {
27
+ name: "record_engineering_context",
28
+ label: "Record engineering context",
29
+ description: "Record the engineering rationale for a change back onto its work-item/ticket so " +
30
+ "the team has the decision context: WHY it was done, the tradeoffs, and what was " +
31
+ "considered — not just what changed. Pass the ticket identifier (e.g. YAG-42), the " +
32
+ "rationale `body`, and optionally the `prUrl`. Use after finishing work tied to a ticket.",
33
+ promptSnippet: "record_engineering_context: write WHY a change was made back onto its ticket.",
34
+ promptGuidelines: [
35
+ "After finishing work tied to a ticket, record the rationale (the decision and tradeoffs), not just a restatement of the diff.",
36
+ "Pass the ticket identifier exactly as the user/work-item gives it (e.g. YAG-42).",
37
+ "Keep it the durable 'why' a teammate would want months later, concise and specific.",
38
+ "If you opened a PR for this work, pass `repo` (owner/name) and `prNumber` so the change can be matched and learned from after merge.",
39
+ ],
40
+ parameters,
41
+ async execute(_toolCallId, params, signal, onUpdate, ctx) {
42
+ onUpdate?.({
43
+ content: [{ type: "text", text: `Recording context on ${params.ticket}…` }],
44
+ details: { workItemId: null },
45
+ });
46
+ const headSha = await (opts.resolveHeadSha ?? defaultResolveHeadSha)(ctx?.cwd, signal);
47
+ // Every logical write carries a generated idempotencyKey (the backend
48
+ // dedups on it), so transport failures and 5xx are durably spooled and
49
+ // replayed later instead of lost or double-recorded (R4 write half).
50
+ const idempotencyKey = (opts.makeIdempotencyKey ?? randomUUID)();
51
+ const outcome = await sendOrSpool(opts, "record_engineering_context", "/api/yagni-code/record", {
52
+ ticket: params.ticket,
53
+ body: params.body,
54
+ prUrl: params.prUrl,
55
+ repo: params.repo,
56
+ prNumber: params.prNumber,
57
+ headSha,
58
+ cwd: ctx?.cwd,
59
+ }, idempotencyKey, signal);
60
+ if (outcome.kind === "rejected") {
61
+ throw new Error(outcome.message);
62
+ }
63
+ if (outcome.kind === "spooled") {
64
+ // Honest: not recorded YET, but durably saved and replayed with the
65
+ // same idempotencyKey at the next session start or successful write.
66
+ return {
67
+ content: [
68
+ {
69
+ type: "text",
70
+ text: `Could not reach YAGNI (${outcome.reason}). ` +
71
+ `Saved the context for ${params.ticket} locally; it will sync automatically once the backend is reachable.`,
72
+ },
73
+ ],
74
+ details: { workItemId: null, spooled: true },
75
+ };
76
+ }
77
+ const data = outcome.json;
78
+ return {
79
+ content: [{ type: "text", text: `Recorded engineering context on ${params.ticket}.` }],
80
+ details: { workItemId: data?.workItemId ?? null, spooled: false },
81
+ };
82
+ },
83
+ };
84
+ }
85
+ //# sourceMappingURL=recordContextTool.js.map
@@ -0,0 +1,52 @@
1
+ import type { ToolDefinition } from "@earendil-works/pi-coding-agent";
2
+ import { Type } from "typebox";
3
+ /** Options for {@link makeRecordDecisionTool}. */
4
+ export interface MakeRecordDecisionToolOptions {
5
+ baseUrl: string;
6
+ getToken: () => string | undefined;
7
+ fetchImpl?: typeof fetch;
8
+ /** Idempotency-key source (default: crypto.randomUUID); injected in tests. */
9
+ makeIdempotencyKey?: () => string;
10
+ }
11
+ /** The durable fields of a recorded product-intent decision. */
12
+ export interface RecordDecisionParams {
13
+ question: string;
14
+ decision: string;
15
+ rationale?: string;
16
+ repo?: string;
17
+ workItemId?: string;
18
+ }
19
+ export interface RecordDecisionResult {
20
+ id: string | null;
21
+ spooled?: boolean;
22
+ reason?: string;
23
+ }
24
+ /**
25
+ * POST a single decision to the token-scoped grounding endpoint and return its
26
+ * new id. Extracted so the CLI init pass (repo intake) can seed the corpus
27
+ * through the SAME `/api/yagni-code/decisions` write the tool uses — no new
28
+ * backend transport. Throws a friendly error on any non-2xx; callers that must
29
+ * stay fail-soft (the init pass) wrap the call in try/catch.
30
+ */
31
+ export declare function recordDecision(opts: MakeRecordDecisionToolOptions, params: RecordDecisionParams, signal?: AbortSignal): Promise<RecordDecisionResult>;
32
+ declare const parameters: Type.TObject<{
33
+ question: Type.TString;
34
+ decision: Type.TString;
35
+ rationale: Type.TOptional<Type.TString>;
36
+ repo: Type.TOptional<Type.TString>;
37
+ workItemId: Type.TOptional<Type.TString>;
38
+ }>;
39
+ /**
40
+ * Build the `record_decision` tool.
41
+ *
42
+ * Banks a product-intent decision ("what should this do / what does the user
43
+ * want here") into the YAGNI app so the SAME question is answered by `ask_yagni`
44
+ * next time instead of interrupting a human. This is the capture half of the
45
+ * judgment loop; `ask_yagni` is the read-back half. Reversible internal write.
46
+ */
47
+ export declare function makeRecordDecisionTool(opts: MakeRecordDecisionToolOptions): ToolDefinition<typeof parameters, {
48
+ id: string | null;
49
+ spooled?: boolean;
50
+ }>;
51
+ export {};
52
+ //# sourceMappingURL=recordDecisionTool.d.ts.map
@@ -0,0 +1,102 @@
1
+ import { randomUUID } from "node:crypto";
2
+ import { Type } from "typebox";
3
+ import { sendOrSpool } from "./spool.js";
4
+ /**
5
+ * POST a single decision to the token-scoped grounding endpoint and return its
6
+ * new id. Extracted so the CLI init pass (repo intake) can seed the corpus
7
+ * through the SAME `/api/yagni-code/decisions` write the tool uses — no new
8
+ * backend transport. Throws a friendly error on any non-2xx; callers that must
9
+ * stay fail-soft (the init pass) wrap the call in try/catch.
10
+ */
11
+ export async function recordDecision(opts, params, signal) {
12
+ const idempotencyKey = (opts.makeIdempotencyKey ?? randomUUID)();
13
+ const outcome = await sendOrSpool(opts, "record_decision", "/api/yagni-code/decisions", {
14
+ question: params.question,
15
+ decision: params.decision,
16
+ rationale: params.rationale,
17
+ repo: params.repo,
18
+ workItemId: params.workItemId,
19
+ }, idempotencyKey, signal);
20
+ if (outcome.kind === "rejected") {
21
+ throw new Error(outcome.message);
22
+ }
23
+ if (outcome.kind === "spooled") {
24
+ return { id: null, spooled: true, reason: outcome.reason };
25
+ }
26
+ const data = outcome.json;
27
+ return { id: data?.id ?? null, spooled: false };
28
+ }
29
+ const parameters = Type.Object({
30
+ question: Type.String(),
31
+ decision: Type.String(),
32
+ rationale: Type.Optional(Type.String()),
33
+ repo: Type.Optional(Type.String()),
34
+ workItemId: Type.Optional(Type.String()),
35
+ });
36
+ /**
37
+ * Build the `record_decision` tool.
38
+ *
39
+ * Banks a product-intent decision ("what should this do / what does the user
40
+ * want here") into the YAGNI app so the SAME question is answered by `ask_yagni`
41
+ * next time instead of interrupting a human. This is the capture half of the
42
+ * judgment loop; `ask_yagni` is the read-back half. Reversible internal write.
43
+ */
44
+ export function makeRecordDecisionTool(opts) {
45
+ return {
46
+ name: "record_decision",
47
+ label: "Record decision",
48
+ description: "Record a product-intent decision ('what should this do', 'what does the user want here') so " +
49
+ "YAGNI can answer the same question next time instead of interrupting a human. Pass the " +
50
+ "`question` that was decided, the `decision` (the answer/direction taken), and optionally the " +
51
+ "`rationale` (why). Use right after a product-intent call is resolved — especially one you had " +
52
+ "to ask the user about.",
53
+ promptSnippet: "record_decision: bank a 'what should this do' decision so YAGNI answers it next time.",
54
+ promptGuidelines: [
55
+ "Before asking the USER a product-intent question, call ask_yagni first — the decision may already be recorded.",
56
+ "When a product-intent decision IS resolved (especially one you had to ask the user about), call record_decision so the next similar question is answered without interrupting a human.",
57
+ "Record the durable decision and its 'why', not a restatement of the code change.",
58
+ ],
59
+ parameters,
60
+ async execute(_toolCallId, params, signal, onUpdate, _ctx) {
61
+ onUpdate?.({
62
+ content: [{ type: "text", text: "Recording decision in YAGNI…" }],
63
+ details: { id: null },
64
+ });
65
+ // Every logical write carries a generated idempotencyKey; the backend
66
+ // dedups on it, so a spool replay (or an authed-fetch 401 retry) can
67
+ // never bank the same decision twice. Transport failures and 5xx are
68
+ // spooled durably instead of lost (R4 write half).
69
+ const idempotencyKey = (opts.makeIdempotencyKey ?? randomUUID)();
70
+ const outcome = await sendOrSpool(opts, "record_decision", "/api/yagni-code/decisions", {
71
+ question: params.question,
72
+ decision: params.decision,
73
+ rationale: params.rationale,
74
+ repo: params.repo,
75
+ workItemId: params.workItemId,
76
+ }, idempotencyKey, signal);
77
+ if (outcome.kind === "rejected") {
78
+ throw new Error(outcome.message);
79
+ }
80
+ if (outcome.kind === "spooled") {
81
+ // Honest: not recorded YET, but durably saved and replayed with the
82
+ // same idempotencyKey at the next session start or successful write.
83
+ return {
84
+ content: [
85
+ {
86
+ type: "text",
87
+ text: `Could not reach YAGNI (${outcome.reason}). ` +
88
+ "Saved the decision locally; it will sync automatically once the backend is reachable.",
89
+ },
90
+ ],
91
+ details: { id: null, spooled: true },
92
+ };
93
+ }
94
+ const data = outcome.json;
95
+ return {
96
+ content: [{ type: "text", text: "Recorded the decision in YAGNI." }],
97
+ details: { id: data?.id ?? null, spooled: false },
98
+ };
99
+ },
100
+ };
101
+ }
102
+ //# sourceMappingURL=recordDecisionTool.js.map
@@ -0,0 +1,81 @@
1
+ /**
2
+ * Repo-doc grounding collector for ask_yagni (the retrieval coverage fix from
3
+ * the 2026-07 grounding-gate eval): coding agents ask codebase/architecture
4
+ * questions — ADRs, schema, conventions — that the workspace corpus (Vision,
5
+ * Library, decisions, corrections) structurally cannot answer, because that
6
+ * knowledge lives in the repo. This module gathers the few most relevant repo
7
+ * docs from the CALLER'S working tree so the backend can fold them into the
8
+ * numbered source catalog.
9
+ *
10
+ * Reading from the checkout (not a server-side index) is load-bearing: the docs
11
+ * are always exactly as old as the checkout being edited, so an agent working
12
+ * on an old base sees that era's conventions, never today's.
13
+ *
14
+ * BASELINE, not working tree (echo-chamber guard): docs are enumerated and read
15
+ * at git HEAD, never from the live working tree. A /go run edits the tree
16
+ * without committing until FINISH, so mid-run ask_yagni calls from the review
17
+ * and fix stages would otherwise retrieve the candidate's OWN in-flight edits
18
+ * to a doc and have the backend present that self-authored text as
19
+ * source-of-truth context. HEAD is the pre-run baseline in both the /go
20
+ * pipeline and the eval driver (which commits the exported tree before the
21
+ * agent starts). A non-git directory falls back to the working-tree walk —
22
+ * there is no baseline to prefer.
23
+ *
24
+ * Pure-ish and bounded: a small fixed set of root docs plus a capped doc list,
25
+ * lexical scoring against the question (no model call, no network), hard caps
26
+ * on file count, bytes read, and excerpt size. All fs/git access sits behind
27
+ * injectable seams so tests never touch a real repo.
28
+ */
29
+ /** One repo doc offered to the backend as grounding. `path` is repo-relative. */
30
+ export interface RepoDocSnippet {
31
+ path: string;
32
+ excerpt: string;
33
+ }
34
+ /** Injectable fs seam (default: node:fs sync calls). */
35
+ export interface RepoDocsFs {
36
+ readFileSync: (p: string) => string;
37
+ readdirSync: (p: string) => Array<{
38
+ name: string;
39
+ isDirectory(): boolean;
40
+ isFile(): boolean;
41
+ }>;
42
+ statSize: (p: string) => number;
43
+ }
44
+ /**
45
+ * Injectable git seam. `listDocsAtHead` returns every tracked path at HEAD (or
46
+ * null when cwd is not a usable git tree — the fs fallback then applies);
47
+ * `readAtHead` returns one file's baseline content (null when unreadable).
48
+ */
49
+ export interface RepoDocsGit {
50
+ listDocsAtHead: (cwd: string) => string[] | null;
51
+ readAtHead: (cwd: string, rel: string) => string | null;
52
+ }
53
+ export interface CollectRepoDocsOptions {
54
+ /** Max docs returned (default 3). */
55
+ maxDocs?: number;
56
+ /** Max chars per doc excerpt (default 2000). */
57
+ maxExcerptChars?: number;
58
+ /** Cap on files scanned during the docs/ walk (default 400). */
59
+ maxFiles?: number;
60
+ /** Skip files larger than this many bytes (default 512 KiB). */
61
+ maxFileBytes?: number;
62
+ fsImpl?: RepoDocsFs;
63
+ /** Git seam; `null` disables git entirely (forces the working-tree fallback). */
64
+ gitImpl?: RepoDocsGit | null;
65
+ }
66
+ /** Lowercased, deduped, stopword-free query tokens (length >= 3). */
67
+ export declare function tokenizeQuery(query: string): string[];
68
+ /**
69
+ * Build a bounded excerpt: the lines around each token match, merged into
70
+ * windows, joined with ellipses. A path-only hit (no body match) takes the head
71
+ * of the file instead — the title/intro is the most general summary.
72
+ */
73
+ export declare function buildExcerpt(body: string, tokens: string[], maxChars: number): string;
74
+ /**
75
+ * Collect the repo docs most relevant to `query` from the working tree at
76
+ * `cwd`. Returns [] on any repo without matching docs (or without docs at all)
77
+ * — the caller then sends nothing and the backend behaves exactly as before.
78
+ * Never throws: grounding must not be able to break the ask itself.
79
+ */
80
+ export declare function collectRepoDocs(cwd: string, query: string, opts?: CollectRepoDocsOptions): RepoDocSnippet[];
81
+ //# sourceMappingURL=repoDocs.d.ts.map