Run prettier on .pi folder

This commit is contained in:
Claudio Ortolina
2026-05-04 21:21:10 +01:00
parent 286d6fecf3
commit 7bf89d9a1d
8 changed files with 272 additions and 191 deletions
+13 -5
View File
@@ -17,7 +17,7 @@ function loadConfig(cwd: string): Config {
function patternToRegex(pattern: string): RegExp {
let escaped = pattern
.replace(/[.+^${}()|[\]\\]/g, "\\$&") // Escape regex specials except *
.replace(/\*/g, ".*"); // * → .*
.replace(/\*/g, ".*"); // * → .*
return new RegExp(escaped, "i");
}
@@ -35,7 +35,7 @@ export default function (pi: ExtensionAPI) {
// Proactively tell the agent which paths are off-limits so it doesn't try in the first place
pi.on("before_agent_start", async (event, _ctx) => {
if (config.blocked_paths.length === 0) return;
const pathList = config.blocked_paths.map((p) => ` - \`${p}\``).join('\n');
const pathList = config.blocked_paths.map((p) => ` - \`${p}\``).join("\n");
return {
systemPrompt:
event.systemPrompt +
@@ -63,7 +63,8 @@ export default function (pi: ExtensionAPI) {
`🚫 ACCESS DENIED by sensitive-file-guard: paths matching "${match}" are permanently protected. ` +
`DO NOT retry with other tools (bash, find, ls, grep, etc.). ` +
`Tell the user the file is protected and STOP.`;
if (ctx.hasUI) ctx.ui.notify(`Blocked sensitive path: ${path}`, "warning");
if (ctx.hasUI)
ctx.ui.notify(`Blocked sensitive path: ${path}`, "warning");
return { block: true, reason };
}
return;
@@ -79,14 +80,21 @@ export default function (pi: ExtensionAPI) {
// Rough substring match: does the pattern text (with glob stripped)
// appear in the command?
const literal = p.replace(/[*?]/g, "");
return literal.length > 0 && command.toLowerCase().includes(literal.toLowerCase());
return (
literal.length > 0 &&
command.toLowerCase().includes(literal.toLowerCase())
);
});
if (pathHit) {
const reason =
`🚫 ACCESS DENIED by sensitive-file-guard: commands targeting "${pathHit}" are permanently blocked. ` +
`DO NOT retry with alternative commands. Tell the user the path is protected and STOP.`;
if (ctx.hasUI) ctx.ui.notify(`Blocked sensitive path in bash: ${pathHit}`, "warning");
if (ctx.hasUI)
ctx.ui.notify(
`Blocked sensitive path in bash: ${pathHit}`,
"warning",
);
return { block: true, reason };
}
+116 -64
View File
@@ -23,11 +23,7 @@ import {
DEFAULT_MAX_LINES,
} from "@mariozechner/pi-coding-agent";
import { BorderedLoader } from "@mariozechner/pi-coding-agent";
import {
matchesKey,
Key,
truncateToWidth,
} from "@mariozechner/pi-tui";
import { matchesKey, Key, truncateToWidth } from "@mariozechner/pi-tui";
import { Type } from "typebox";
// ── Types ───────────────────────────────────────────────────────────────────
@@ -275,17 +271,22 @@ async function fetchErrors(
token: string,
signal?: AbortSignal,
): Promise<ErrorListResponse> {
return fetchApi(url, token, (data) => {
const obj = data as Record<string, unknown>;
if (!obj.errors || !Array.isArray(obj.errors)) {
throw new Error(
`Unexpected API response: 'errors' field is missing or not an array. Got: ${
obj.errors === null ? "null" : typeof obj.errors
}.`,
);
}
return data as ErrorListResponse;
}, signal);
return fetchApi(
url,
token,
(data) => {
const obj = data as Record<string, unknown>;
if (!obj.errors || !Array.isArray(obj.errors)) {
throw new Error(
`Unexpected API response: 'errors' field is missing or not an array. Got: ${
obj.errors === null ? "null" : typeof obj.errors
}.`,
);
}
return data as ErrorListResponse;
},
signal,
);
}
async function fetchError(
@@ -293,17 +294,22 @@ async function fetchError(
token: string,
signal?: AbortSignal,
): Promise<ErrorDetailResponse> {
return fetchApi(url, token, (data) => {
const obj = data as Record<string, unknown>;
if (!obj.error || typeof obj.error !== "object") {
throw new Error(
`Unexpected API response: 'error' field is missing or not an object. Got: ${
obj.error === null ? "null" : typeof obj.error
}.`,
);
}
return data as ErrorDetailResponse;
}, signal);
return fetchApi(
url,
token,
(data) => {
const obj = data as Record<string, unknown>;
if (!obj.error || typeof obj.error !== "object") {
throw new Error(
`Unexpected API response: 'error' field is missing or not an object. Got: ${
obj.error === null ? "null" : typeof obj.error
}.`,
);
}
return data as ErrorDetailResponse;
},
signal,
);
}
// ── TUI Theme ───────────────────────────────────────────────────────────────
@@ -378,7 +384,10 @@ class ErrorBrowser {
private readonly baseUrl: string;
private readonly token: string;
private readonly requestRender: () => void;
private readonly notify: (msg: string, level: "error" | "info" | "warning") => void;
private readonly notify: (
msg: string,
level: "error" | "info" | "warning",
) => void;
constructor(
errors: ErrorListItem[],
@@ -411,7 +420,10 @@ class ErrorBrowser {
if (this.errors.length === 0) {
this.cursorIndex = -1;
} else {
this.cursorIndex = Math.max(0, Math.min(this.cursorIndex, this.errors.length - 1));
this.cursorIndex = Math.max(
0,
Math.min(this.cursorIndex, this.errors.length - 1),
);
}
}
@@ -423,7 +435,10 @@ class ErrorBrowser {
} else if (this.cursorIndex >= this.scrollOffset + maxEntries) {
this.scrollOffset = this.cursorIndex - maxEntries + 1;
}
this.scrollOffset = Math.max(0, Math.min(this.scrollOffset, this.errors.length - 1));
this.scrollOffset = Math.max(
0,
Math.min(this.scrollOffset, this.errors.length - 1),
);
}
// ── HTTP helpers ───────────────────────────────────────────────────────
@@ -471,7 +486,11 @@ class ErrorBrowser {
this.invalidate();
this.requestRender();
const url = buildUrl(this.baseUrl, "/api/v1/errors", this.buildQueryParams());
const url = buildUrl(
this.baseUrl,
"/api/v1/errors",
this.buildQueryParams(),
);
const controller = new AbortController();
this.currentAbortController = controller;
@@ -508,7 +527,11 @@ class ErrorBrowser {
this.invalidate();
this.requestRender();
const url = buildUrl(this.baseUrl, "/api/v1/errors", this.buildQueryParams());
const url = buildUrl(
this.baseUrl,
"/api/v1/errors",
this.buildQueryParams(),
);
const controller = new AbortController();
this.currentAbortController = controller;
@@ -638,7 +661,10 @@ class ErrorBrowser {
const pageSize = Math.floor(this.visibleHeight / this.errorEntryLines);
this.cursorIndex -= pageSize;
moved = true;
} else if (matchesKey(data, Key.pageDown) || matchesKey(data, Key.ctrl("f"))) {
} else if (
matchesKey(data, Key.pageDown) ||
matchesKey(data, Key.ctrl("f"))
) {
const pageSize = Math.floor(this.visibleHeight / this.errorEntryLines);
this.cursorIndex += pageSize;
moved = true;
@@ -689,7 +715,10 @@ class ErrorBrowser {
} else if (matchesKey(data, Key.pageUp)) {
this.detailScrollOffset -= this.visibleHeight;
moved = true;
} else if (matchesKey(data, Key.pageDown) || matchesKey(data, Key.ctrl("f"))) {
} else if (
matchesKey(data, Key.pageDown) ||
matchesKey(data, Key.ctrl("f"))
) {
this.detailScrollOffset += this.visibleHeight;
moved = true;
} else if (matchesKey(data, Key.ctrl("b"))) {
@@ -699,13 +728,19 @@ class ErrorBrowser {
this.detailScrollOffset = 0;
moved = true;
} else if (matchesKey(data, Key.end) || matchesKey(data, "G")) {
this.detailScrollOffset = Math.max(0, this.buildDetailLines().length - 1);
this.detailScrollOffset = Math.max(
0,
this.buildDetailLines().length - 1,
);
moved = true;
}
if (moved) {
const maxOffset = Math.max(0, this.buildDetailLines().length - 1);
this.detailScrollOffset = Math.max(0, Math.min(this.detailScrollOffset, maxOffset));
this.detailScrollOffset = Math.max(
0,
Math.min(this.detailScrollOffset, maxOffset),
);
this.invalidate();
}
@@ -746,7 +781,9 @@ class ErrorBrowser {
for (let i = 1; i < viewportRows - 1; i++) {
if (i === mid) {
result.push(truncateToWidth(theme.fg("muted", " Loading…"), width, ""));
result.push(
truncateToWidth(theme.fg("muted", " Loading…"), width, ""),
);
} else {
result.push("");
}
@@ -809,12 +846,24 @@ class ErrorBrowser {
else if (error.status === "resolved") statusColor = "success";
const mutedLabel = error.muted ? " [MUTED]" : "";
const statusBadge = theme.fg(statusColor, `[${error.status.toUpperCase()}]`);
const statusBadge = theme.fg(
statusColor,
`[${error.status.toUpperCase()}]`,
);
// Line 1: cursor + status badge + kind + truncated reason
const reasonText = truncateReason(error.reason, Math.max(30, width - 25));
const reasonText = truncateReason(
error.reason,
Math.max(30, width - 25),
);
const line1 = `${cursor} ${statusBadge}${mutedLabel} ${error.kind}: ${reasonText}`;
result.push(truncateToWidth(isCursor ? theme.fg("accent", line1) : line1, width, ""));
result.push(
truncateToWidth(
isCursor ? theme.fg("accent", line1) : line1,
width,
"",
),
);
// Line 2: source info
const sourceInfo = error.source_function
@@ -857,9 +906,7 @@ class ErrorBrowser {
truncateToWidth(
theme.fg(
"dim",
hasMore
? ` l load more q quit`
: ` — end of results — q quit`,
hasMore ? ` l load more q quit` : ` — end of results — q quit`,
),
width,
"",
@@ -972,13 +1019,13 @@ export default function prodErrorsExtension(pi: ExtensionAPI) {
parameters: Type.Object({
status: Type.Optional(
Type.Union([Type.Literal("resolved"), Type.Literal("unresolved")], {
description:
"Filter by error status: 'resolved' or 'unresolved'.",
description: "Filter by error status: 'resolved' or 'unresolved'.",
}),
),
muted: Type.Optional(
Type.Boolean({
description: "Filter by muted state. true = only muted, false = only unmuted.",
description:
"Filter by muted state. true = only muted, false = only unmuted.",
}),
),
search: Type.Optional(
@@ -1000,13 +1047,7 @@ export default function prodErrorsExtension(pi: ExtensionAPI) {
}),
),
}),
async execute(
_toolCallId,
params,
signal,
_onUpdate,
_ctx,
) {
async execute(_toolCallId, params, signal, _onUpdate, _ctx) {
// Early abort check
if (signal?.aborted) {
return { content: [{ type: "text", text: "Cancelled" }] };
@@ -1043,7 +1084,11 @@ export default function prodErrorsExtension(pi: ExtensionAPI) {
if (params.muted !== undefined && params.muted !== null) {
queryParams.muted = String(params.muted);
}
if (params.search !== undefined && params.search !== null && params.search !== "") {
if (
params.search !== undefined &&
params.search !== null &&
params.search !== ""
) {
queryParams.search = params.search;
}
if (params.limit !== undefined && params.limit !== null) {
@@ -1075,8 +1120,18 @@ export default function prodErrorsExtension(pi: ExtensionAPI) {
// Handle empty results
if (data.errors.length === 0) {
return {
content: [{ type: "text", text: "No errors found matching the given filters." }],
details: { total: data.total, count: 0, offset: data.offset, limit: data.limit },
content: [
{
type: "text",
text: "No errors found matching the given filters.",
},
],
details: {
total: data.total,
count: 0,
offset: data.offset,
limit: data.limit,
},
};
}
@@ -1126,13 +1181,7 @@ export default function prodErrorsExtension(pi: ExtensionAPI) {
"The error ID (integer) to fetch full details for. Get this from fetch_production_errors.",
}),
}),
async execute(
_toolCallId,
params,
signal,
_onUpdate,
_ctx,
) {
async execute(_toolCallId, params, signal, _onUpdate, _ctx) {
// Early abort check
if (signal?.aborted) {
return { content: [{ type: "text", text: "Cancelled" }] };
@@ -1256,7 +1305,10 @@ export default function prodErrorsExtension(pi: ExtensionAPI) {
);
if (errorsData === null) {
ctx.ui.notify("Cancelled or fetch failed — check logs for details", "info");
ctx.ui.notify(
"Cancelled or fetch failed — check logs for details",
"info",
);
return;
}
+55 -38
View File
@@ -21,11 +21,7 @@ import {
DEFAULT_MAX_LINES,
} from "@mariozechner/pi-coding-agent";
import { Type } from "typebox";
import {
matchesKey,
Key,
truncateToWidth,
} from "@mariozechner/pi-tui";
import { matchesKey, Key, truncateToWidth } from "@mariozechner/pi-tui";
// ── Types ───────────────────────────────────────────────────────────────────
@@ -80,7 +76,10 @@ class LogViewer {
this.visualMode = false;
this.visualAnchor = 0;
this.scrollOffset = 0;
this.visibleHeight = Math.min(lines.length, Math.max(10, process.stdout.rows - chromeHeight));
this.visibleHeight = Math.min(
lines.length,
Math.max(10, process.stdout.rows - chromeHeight),
);
this.clampCursor();
this.clampViewport();
this.invalidate();
@@ -101,7 +100,10 @@ class LogViewer {
if (this.lines.length === 0) {
this.cursorIndex = -1;
} else {
this.cursorIndex = Math.max(0, Math.min(this.cursorIndex, this.lines.length - 1));
this.cursorIndex = Math.max(
0,
Math.min(this.cursorIndex, this.lines.length - 1),
);
}
}
@@ -129,7 +131,10 @@ class LogViewer {
}
// Escape or q in normal mode closes the viewer
if (!this.visualMode && (matchesKey(data, Key.escape) || matchesKey(data, "q"))) {
if (
!this.visualMode &&
(matchesKey(data, Key.escape) || matchesKey(data, "q"))
) {
this.onClose?.();
return;
}
@@ -156,7 +161,10 @@ class LogViewer {
const start = Math.min(this.visualAnchor, this.cursorIndex);
const end = Math.max(this.visualAnchor, this.cursorIndex);
// this.lines[0] = newest, so reverse the slice for oldest-first
const text = this.lines.slice(start, end + 1).reverse().join("\n");
const text = this.lines
.slice(start, end + 1)
.reverse()
.join("\n");
this.onCopy?.(text);
}
return;
@@ -184,7 +192,10 @@ class LogViewer {
} else if (matchesKey(data, Key.pageUp)) {
this.cursorIndex -= this.visibleHeight;
moved = true;
} else if (matchesKey(data, Key.pageDown) || matchesKey(data, Key.ctrl("f"))) {
} else if (
matchesKey(data, Key.pageDown) ||
matchesKey(data, Key.ctrl("f"))
) {
this.cursorIndex += this.visibleHeight;
moved = true;
} else if (matchesKey(data, Key.ctrl("b"))) {
@@ -237,9 +248,8 @@ class LogViewer {
const basePrefix = ` ${numStr}`;
const isCursor = absIdx === this.cursorIndex;
const isSelected = this.visualMode
&& absIdx >= selStart
&& absIdx <= selEnd;
const isSelected =
this.visualMode && absIdx >= selStart && absIdx <= selEnd;
if (isCursor) {
// Cursor takes priority over selection highlight
@@ -349,13 +359,7 @@ export default function prodLogsExtension(pi: ExtensionAPI) {
}),
),
}),
async execute(
_toolCallId,
params,
signal,
_onUpdate,
_ctx,
) {
async execute(_toolCallId, params, signal, _onUpdate, _ctx) {
// Early abort check
if (signal?.aborted) {
return { content: [{ type: "text", text: "Cancelled" }] };
@@ -498,7 +502,10 @@ export default function prodLogsExtension(pi: ExtensionAPI) {
);
if (logLines === null) {
ctx.ui.notify("Cancelled or fetch failed — check logs for details", "info");
ctx.ui.notify(
"Cancelled or fetch failed — check logs for details",
"info",
);
return;
}
@@ -519,7 +526,10 @@ export default function prodLogsExtension(pi: ExtensionAPI) {
let loading = false;
const visibleLogLines = () =>
Math.min(logLinesMut.length, Math.max(10, process.stdout.rows - chromeHeight));
Math.min(
logLinesMut.length,
Math.max(10, process.stdout.rows - chromeHeight),
);
const viewer = new LogViewer(logLinesMut, visibleLogLines());
@@ -553,9 +563,10 @@ export default function prodLogsExtension(pi: ExtensionAPI) {
return {
render(width: number) {
const pct = viewer.total > 0
? Math.round((viewer.offset / viewer.total) * 100)
: 0;
const pct =
viewer.total > 0
? Math.round((viewer.offset / viewer.total) * 100)
: 0;
const from = viewer.offset + 1;
const to = Math.min(
viewer.offset + visibleLogLines(),
@@ -565,20 +576,28 @@ export default function prodLogsExtension(pi: ExtensionAPI) {
const result: string[] = [];
const border = theme.fg("accent", "─".repeat(width));
result.push(border);
result.push(truncateToWidth(
theme.fg("accent", theme.bold(" Production Logs")),
width,
"",
));
result.push(
truncateToWidth(
theme.fg("accent", theme.bold(" Production Logs")),
width,
"",
),
);
const countText = loading
? " Refreshing..."
: ` ${logLinesMut.length} lines`;
result.push(truncateToWidth(theme.fg("muted", countText), width, ""));
result.push(truncateToWidth(theme.fg("dim", "─".repeat(40)), width, ""));
result.push(
truncateToWidth(theme.fg("muted", countText), width, ""),
);
result.push(
truncateToWidth(theme.fg("dim", "─".repeat(40)), width, ""),
);
const infoText = ` Lines ${from}-${to} of ${logLinesMut.length} (${pct}%)`;
result.push(truncateToWidth(theme.fg("dim", infoText), width, ""));
result.push(
truncateToWidth(theme.fg("dim", infoText), width, ""),
);
// Log lines
const logRendered = viewer.render(width, theme);
@@ -590,11 +609,9 @@ export default function prodLogsExtension(pi: ExtensionAPI) {
: viewer.visualMode
? " VISUAL: j/k extend · y copy · Esc cancel · Enter copy line"
: " ↑↓/jk scroll · PgUp/PgDn page · Home/End jump · v visual · Enter copy line · r refresh · Esc close";
result.push(truncateToWidth(
theme.fg("dim", helpLine),
width,
"",
));
result.push(
truncateToWidth(theme.fg("dim", helpLine), width, ""),
);
result.push(border);
return result;
+66 -55
View File
@@ -33,7 +33,10 @@ const S3_REGION = "nbg1";
function humanSize(bytes: number): string {
if (bytes === 0) return "0 B";
const units = ["B", "KB", "MB", "GB", "TB"];
const i = Math.min(Math.floor(Math.log(bytes) / Math.log(1024)), units.length - 1);
const i = Math.min(
Math.floor(Math.log(bytes) / Math.log(1024)),
units.length - 1,
);
return `${(bytes / 1024 ** i).toFixed(1)} ${units[i]}`;
}
@@ -152,7 +155,10 @@ export default function s3BrowserExtension(pi: ExtensionAPI) {
);
if (objects === null) {
ctx.ui.notify("Cancelled or fetch failed — check logs for details", "info");
ctx.ui.notify(
"Cancelled or fetch failed — check logs for details",
"info",
);
return;
}
@@ -174,66 +180,71 @@ export default function s3BrowserExtension(pi: ExtensionAPI) {
// Summary header line
const summary = `${objects.length} files · ${humanSize(totalSize)} total`;
const selectedKey = await ctx.ui.custom<string | null>((tui, theme, _kb, done) => {
const container = new Container();
const selectedKey = await ctx.ui.custom<string | null>(
(tui, theme, _kb, done) => {
const container = new Container();
// Top border
container.addChild(
new DynamicBorder((s: string) => theme.fg("accent", s)),
);
// Top border
container.addChild(
new DynamicBorder((s: string) => theme.fg("accent", s)),
);
// Header
container.addChild(
new Text(theme.fg("accent", theme.bold("S3 Backups — ffmusiclibrary/prod/")), 1, 0),
);
container.addChild(
new Text(theme.fg("muted", summary), 1, 0),
);
// Header
container.addChild(
new Text(
theme.fg(
"accent",
theme.bold("S3 Backups — ffmusiclibrary/prod/"),
),
1,
0,
),
);
container.addChild(new Text(theme.fg("muted", summary), 1, 0));
// Spacer before list
container.addChild(
new Text(theme.fg("dim", "─".repeat(40)), 1, 0),
);
// Spacer before list
container.addChild(new Text(theme.fg("dim", "─".repeat(40)), 1, 0));
// SelectList
const selectList = new SelectList(items, Math.min(items.length, 20), {
selectedPrefix: (t: string) => theme.fg("accent", t),
selectedText: (t: string) => theme.fg("accent", t),
description: (t: string) => theme.fg("muted", t),
scrollInfo: (t: string) => theme.fg("dim", t),
noMatch: (t: string) => theme.fg("warning", t),
});
selectList.onSelect = (item) => done(item.value);
selectList.onCancel = () => done(null);
container.addChild(selectList);
// SelectList
const selectList = new SelectList(items, Math.min(items.length, 20), {
selectedPrefix: (t: string) => theme.fg("accent", t),
selectedText: (t: string) => theme.fg("accent", t),
description: (t: string) => theme.fg("muted", t),
scrollInfo: (t: string) => theme.fg("dim", t),
noMatch: (t: string) => theme.fg("warning", t),
});
selectList.onSelect = (item) => done(item.value);
selectList.onCancel = () => done(null);
container.addChild(selectList);
// Footer hint
container.addChild(
new Text(
theme.fg("dim", "↑↓ navigate · enter select · esc close"),
1,
0,
),
);
// Footer hint
container.addChild(
new Text(
theme.fg("dim", "↑↓ navigate · enter select · esc close"),
1,
0,
),
);
// Bottom border
container.addChild(
new DynamicBorder((s: string) => theme.fg("accent", s)),
);
// Bottom border
container.addChild(
new DynamicBorder((s: string) => theme.fg("accent", s)),
);
return {
render(width: number) {
return container.render(width);
},
invalidate() {
container.invalidate();
},
handleInput(data: string) {
selectList.handleInput(data);
tui.requestRender();
},
};
});
return {
render(width: number) {
return container.render(width);
},
invalidate() {
container.invalidate();
},
handleInput(data: string) {
selectList.handleInput(data);
tui.requestRender();
},
};
},
);
if (selectedKey) {
const obj = objects.find((o) => o.key === selectedKey);
+10 -9
View File
@@ -1,6 +1,7 @@
---
description: Create a new backlog task
---
Let's create a new backlog task via a predefined process. **IT'S CRITICAL YOU FOLLOW THIS PROCESS**.
To start, ask me for:
@@ -21,30 +22,30 @@ For a plan to be valid, it has to cover these requirements:
1. **Objective alignment** — The plan clearly states how it achieves the stated objective of the issue, with a direct mapping between the problem and the proposed solution.
2. **Simplicity and alternatives considered** — The plan identifies the simplest viable approach. It documents alternatives that were evaluated, explains why they were rejected or deferred, and
justifies why the chosen approach is the right trade-off for the objective.
justifies why the chosen approach is the right trade-off for the objective.
3. **Completeness and sequencing** — The plan covers every implementation step in a logical order. No gaps exist between "current state" and "done state." Dependencies between steps are
explicit so the plan can be executed sequentially without backtracking.
explicit so the plan can be executed sequentially without backtracking.
4. **Verifiability** — Each implementation step includes concrete verification instructions (tests to run, manual checks to perform, queries to validate) that prove the step was completed
correctly before moving on.
correctly before moving on.
5. **Architecture impact analysis** — The plan identifies all architectural touchpoints affected by the change (schemas, contexts, PubSub topics, supervision tree, routes, external APIs, UI
components) and describes how each is impacted, including any migration or deprecation path.
components) and describes how each is impacted, including any migration or deprecation path.
6. **Performance profile** — The plan explains the performance characteristics of the chosen approach: expected runtime complexity, database query patterns (including N+1 risks), memory
footprint, and any latency or throughput implications under realistic load.
footprint, and any latency or throughput implications under realistic load.
7. **Benchmarking requirements** — The plan identifies whether one-off or ongoing benchmarks are needed to validate or monitor the performance profile, and if so, specifies what to measure, how
to measure it, and what thresholds define acceptable performance.
to measure it, and what thresholds define acceptable performance.
8. **Cost profile** — If the implementation consumes paid resources (API calls, compute, storage, third-party services), the plan includes a cost estimate or model so the financial impact is
understood before implementation begins.
understood before implementation begins.
9. **Production infrastructure steps** — Any manual changes required in production (environment variables, service provisioning, database migrations with special handling, DNS changes, firewall
rules) are documented in a dedicated "Production Changes" section, separate from the implementation steps, with rollout and rollback instructions.
rules) are documented in a dedicated "Production Changes" section, separate from the implementation steps, with rollout and rollback instructions.
10. **Documentation updates** — The plan enumerates which project documentation files must be created or updated as part of the implementation (e.g., `docs/architecture.md`,
`docs/project-conventions.md`, README, API docs), with a summary of what changes each file needs.
`docs/project-conventions.md`, README, API docs), with a summary of what changes each file needs.
At the end, do not offer to start implementation. Instead, offer to start a new session so that I can review the plan via `/review-task-plan`.
+9 -9
View File
@@ -10,30 +10,30 @@ For a plan to be valid, it has to cover these requirements:
1. **Objective alignment** — The plan clearly states how it achieves the stated objective of the issue, with a direct mapping between the problem and the proposed solution.
2. **Simplicity and alternatives considered** — The plan identifies the simplest viable approach. It documents alternatives that were evaluated, explains why they were rejected or deferred, and
justifies why the chosen approach is the right trade-off for the objective.
justifies why the chosen approach is the right trade-off for the objective.
3. **Completeness and sequencing** — The plan covers every implementation step in a logical order. No gaps exist between "current state" and "done state." Dependencies between steps are
explicit so the plan can be executed sequentially without backtracking.
explicit so the plan can be executed sequentially without backtracking.
4. **Verifiability** — Each implementation step includes concrete verification instructions (tests to run, manual checks to perform, queries to validate) that prove the step was completed
correctly before moving on.
correctly before moving on.
5. **Architecture impact analysis** — The plan identifies all architectural touchpoints affected by the change (schemas, contexts, PubSub topics, supervision tree, routes, external APIs, UI
components) and describes how each is impacted, including any migration or deprecation path.
components) and describes how each is impacted, including any migration or deprecation path.
6. **Performance profile** — The plan explains the performance characteristics of the chosen approach: expected runtime complexity, database query patterns (including N+1 risks), memory
footprint, and any latency or throughput implications under realistic load.
footprint, and any latency or throughput implications under realistic load.
7. **Benchmarking requirements** — The plan identifies whether one-off or ongoing benchmarks are needed to validate or monitor the performance profile, and if so, specifies what to measure, how
to measure it, and what thresholds define acceptable performance.
to measure it, and what thresholds define acceptable performance.
8. **Cost profile** — If the implementation consumes paid resources (API calls, compute, storage, third-party services), the plan includes a cost estimate or model so the financial impact is
understood before implementation begins.
understood before implementation begins.
9. **Production infrastructure steps** — Any manual changes required in production (environment variables, service provisioning, database migrations with special handling, DNS changes, firewall
rules) are documented in a dedicated "Production Changes" section, separate from the implementation steps, with rollout and rollback instructions.
rules) are documented in a dedicated "Production Changes" section, separate from the implementation steps, with rollout and rollback instructions.
10. **Documentation updates** — The plan enumerates which project documentation files must be created or updated as part of the implementation (e.g., `docs/architecture.md`,
`docs/project-conventions.md`, README, API docs), with a summary of what changes each file needs.
`docs/project-conventions.md`, README, API docs), with a summary of what changes each file needs.
At the end, do not offer to start implementation. Instead, offer to start a new session so that I can review the plan via `/review-task-plan`.
+1 -5
View File
@@ -13,9 +13,5 @@
".gnupg/",
"~/.pi/agent/sessions/"
],
"blocked_commands": [
"printenv",
"\\benv\\b",
"\\bset\\b"
]
"blocked_commands": ["printenv", "\\benv\\b", "\\bset\\b"]
}
+2 -6
View File
@@ -1,8 +1,4 @@
{
"skills": [
"../.claude/skills"
],
"packages": [
"npm:pi-mcp-adapter"
]
"skills": ["../.claude/skills"],
"packages": ["npm:pi-mcp-adapter"]
}