@andares/pi-toolkits 0.10.0 → 0.11.0

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.
@@ -0,0 +1,284 @@
1
+ /**
2
+ * init feature — types shared by detect / plan / apply / audit.
3
+ *
4
+ * The split is intentional: `Facts` is what the disk says (no judgement),
5
+ * `Plan` is what the command intends to do about it (deterministic actions for
6
+ * code, tasks for the model), and `Finding` is what the audit can prove.
7
+ */
8
+ import type { ProjectLanguage } from "./spec.js";
9
+
10
+ export type Severity = "error" | "warning" | "info";
11
+
12
+ /** One audit result. `code` is stable so reports stay machine-readable. */
13
+ export interface Finding {
14
+ readonly code: string;
15
+ readonly severity: Severity;
16
+ readonly message: string;
17
+ readonly path?: string;
18
+ readonly line?: number;
19
+ }
20
+
21
+ /* -------------------------------------------------------------------------- */
22
+ /* Facts */
23
+ /* -------------------------------------------------------------------------- */
24
+
25
+ export interface FileInfo {
26
+ /** Path relative to the project root, POSIX separators. */
27
+ readonly rel: string;
28
+ readonly bytes: number;
29
+ readonly lines: number;
30
+ readonly mtimeMs: number;
31
+ }
32
+
33
+ export interface Heading {
34
+ readonly level: number;
35
+ readonly text: string;
36
+ readonly line: number;
37
+ }
38
+
39
+ export interface DocInfo extends FileInfo {
40
+ readonly text: string;
41
+ readonly headings: readonly Heading[];
42
+ }
43
+
44
+ export interface RootInfo {
45
+ readonly label: string;
46
+ /** Path as registered (relative to the project root when inside it). */
47
+ readonly path: string;
48
+ readonly absolute: string;
49
+ readonly role?: string;
50
+ readonly agentsMd: "none" | "scope-card" | "independent" | "unknown";
51
+ readonly source: "state" | "add-dir";
52
+ readonly exists: boolean;
53
+ readonly lastSeen?: string;
54
+ }
55
+
56
+ export interface FrameworkState {
57
+ version: number;
58
+ tool: string;
59
+ created: string;
60
+ updated: string;
61
+ profile: "core" | "full";
62
+ modules: string[];
63
+ lang: ProjectLanguage;
64
+ budgets?: {
65
+ agentsMdSoftChars?: number;
66
+ agentsMdHardChars?: number;
67
+ memoryFileChars?: number;
68
+ };
69
+ roots: RootInfo[];
70
+ managed: Array<{ path: string; sha256: string; template: string }>;
71
+ migrations: Array<{ at: string; to: string; notes?: string }>;
72
+ }
73
+
74
+ export interface RefArea {
75
+ files: number;
76
+ hits: number;
77
+ }
78
+
79
+ /** Bare `memory/` / `implementations/` references that a move would break. */
80
+ export interface RefScan {
81
+ /** Text files inspected. */
82
+ files: number;
83
+ /** Bare occurrences (already excludes `.agents/…` and `my-memory/`-style). */
84
+ hits: number;
85
+ byArea: Record<string, RefArea>;
86
+ examples: Array<{ path: string; line: number; text: string }>;
87
+ /** Files that contain at least one bare occurrence. */
88
+ paths: string[];
89
+ }
90
+
91
+ export interface ProjectFacts {
92
+ manifests: string[];
93
+ lockfiles: string[];
94
+ license?: string;
95
+ ci: string[];
96
+ scripts: Record<string, string>;
97
+ pkgManager?: string;
98
+ }
99
+
100
+ export interface ContextFiles {
101
+ agents?: DocInfo;
102
+ claude?: DocInfo;
103
+ override?: DocInfo;
104
+ piAgents?: DocInfo;
105
+ }
106
+
107
+ export interface Facts {
108
+ root: string;
109
+ isGit: boolean;
110
+ gitClean?: boolean;
111
+ gitDirtyCount?: number;
112
+ /** Present when the state file exists and parsed. */
113
+ state?: FrameworkState;
114
+ stateError?: string;
115
+ context: ContextFiles;
116
+ /** Framework-relevant directories that exist (relative paths). */
117
+ dirs: Record<string, boolean>;
118
+ /** Legacy locations that the framework wants under `.agents/`. */
119
+ deprecated: { memory: boolean; implementations: boolean; prompts: boolean };
120
+ /** File counts for the three legacy locations (all file types). */
121
+ deprecatedFiles: { memory: number; implementations: number; prompts: number };
122
+ /** `todos/` is retired; content becomes specs. */
123
+ retiredTodos: boolean;
124
+ /** Zero-byte files sitting at the root (crash artefacts, placeholders). */
125
+ rootEmptyFiles: string[];
126
+ /** Memory docs found at the canonical location, sorted by name. */
127
+ memory: DocInfo[];
128
+ /** Extra memory docs found in a legacy `memory/` directory. */
129
+ legacyMemory: DocInfo[];
130
+ specs: DocInfo[];
131
+ implementations: FileInfo[];
132
+ todos: DocInfo[];
133
+ legacyDocs: DocInfo[];
134
+ nestedAgents: FileInfo[];
135
+ /** Memory-module ids that already have a file (design, terms, review, …). */
136
+ modulesPresent: string[];
137
+ refs: RefScan;
138
+ project: ProjectFacts;
139
+ language: ProjectLanguage;
140
+ roots: RootInfo[];
141
+ warnings: string[];
142
+ }
143
+
144
+ /* -------------------------------------------------------------------------- */
145
+ /* Plan */
146
+ /* -------------------------------------------------------------------------- */
147
+
148
+ export type PlanMode =
149
+ | "fresh"
150
+ | "adopt"
151
+ | "reconcile"
152
+ | "upgrade"
153
+ | "audit-only"
154
+ | "unsupported";
155
+
156
+ export interface SnapshotAction {
157
+ readonly kind: "snapshot";
158
+ readonly paths: string[];
159
+ readonly files: number;
160
+ }
161
+
162
+ export interface MoveAction {
163
+ readonly kind: "move-dir";
164
+ readonly from: string;
165
+ readonly to: string;
166
+ readonly files: number;
167
+ /** untracked files ride along with a plain rename instead of `git mv`. */
168
+ readonly trackedOnly: boolean;
169
+ }
170
+
171
+ export interface MoveFileAction {
172
+ readonly kind: "move-file";
173
+ readonly from: string;
174
+ readonly to: string;
175
+ }
176
+
177
+ export interface RewriteAction {
178
+ readonly kind: "rewrite-refs";
179
+ /** Applied in order; first match wins (longest prefixes first). */
180
+ readonly pairs: Array<{ from: string; to: string }>;
181
+ readonly files: number;
182
+ readonly hits: number;
183
+ }
184
+
185
+ export interface CreateFileAction {
186
+ readonly kind: "create-file";
187
+ readonly path: string;
188
+ /** Template name in `templates/` or "inline" with `content`. */
189
+ readonly template: string;
190
+ readonly tokens: Record<string, string>;
191
+ readonly note: string;
192
+ }
193
+
194
+ export interface CreateDirsAction {
195
+ readonly kind: "create-dirs";
196
+ readonly paths: string[];
197
+ }
198
+
199
+ export interface WriteStateAction {
200
+ readonly kind: "write-state";
201
+ readonly state: FrameworkState;
202
+ }
203
+
204
+ export type Action =
205
+ | SnapshotAction
206
+ | MoveAction
207
+ | MoveFileAction
208
+ | RewriteAction
209
+ | CreateDirsAction
210
+ | CreateFileAction
211
+ | WriteStateAction;
212
+
213
+ export type TaskKind =
214
+ | "survey"
215
+ | "edit-agents"
216
+ | "merge-plan"
217
+ | "merge-legacy"
218
+ | "triage-todos"
219
+ | "fill-file"
220
+ | "root-cards"
221
+ | "budget-debt"
222
+ | "coverage";
223
+
224
+ export interface Task {
225
+ readonly id: string;
226
+ readonly kind: TaskKind;
227
+ readonly detail: string;
228
+ readonly targets?: string[];
229
+ readonly acceptance: string[];
230
+ }
231
+
232
+ export interface Plan {
233
+ readonly mode: PlanMode;
234
+ readonly root: string;
235
+ readonly language: ProjectLanguage;
236
+ readonly frameworkVersion: number;
237
+ readonly toolVersion: string;
238
+ readonly createdAt: string;
239
+ readonly actions: Action[];
240
+ readonly tasks: Task[];
241
+ /** Paths this run writes for the model and the operator. */
242
+ readonly artifacts: { plan?: string; coverage?: string; rewrite?: string; snapshot?: string };
243
+ /** Human summary lines shown in the confirmation panel. */
244
+ readonly summary: string[];
245
+ readonly warnings: string[];
246
+ readonly facts: Facts;
247
+ }
248
+
249
+ /* -------------------------------------------------------------------------- */
250
+ /* Apply */
251
+ /* -------------------------------------------------------------------------- */
252
+
253
+ export interface ApplyResult {
254
+ readonly snapshot?: { dir: string; files: number };
255
+ readonly moved: Array<{ from: string; to: string; files: number }>;
256
+ readonly rewrites?: { files: number; hits: number; report: string };
257
+ readonly created: string[];
258
+ readonly skipped: Array<{ path: string; reason: string }>;
259
+ readonly statePath?: string;
260
+ /** Machine-readable work order for the model stage. */
261
+ readonly planPath?: string;
262
+ readonly summary: string[];
263
+ }
264
+
265
+ /* -------------------------------------------------------------------------- */
266
+ /* Coverage */
267
+ /* -------------------------------------------------------------------------- */
268
+
269
+ export type CoverageStatus = "hit" | "mapped" | "open";
270
+
271
+ export interface CoverageRow {
272
+ readonly source: string;
273
+ readonly heading: string;
274
+ readonly status: CoverageStatus;
275
+ readonly destination?: string;
276
+ readonly reason?: string;
277
+ }
278
+
279
+ export interface CoverageReport {
280
+ readonly rows: CoverageRow[];
281
+ readonly open: number;
282
+ readonly mapped: number;
283
+ readonly hit: number;
284
+ }
@@ -0,0 +1,160 @@
1
+ /**
2
+ * init feature — the plan preview and confirmation.
3
+ *
4
+ * Migration moves files, so the operator must see what will happen before it
5
+ * happens. Three paths:
6
+ *
7
+ * - **TUI**: a bounded preview panel (summary, warnings, task ids) with one
8
+ * keystroke answers — `y`/Enter applies, `d` writes nothing (dry run),
9
+ * Esc cancels.
10
+ * - **dialog-capable non-TUI** (RPC): the host's selector.
11
+ * - **no UI** (print/json): fail closed — report only, never write.
12
+ *
13
+ * The preview is bounded on purpose: a plan can list hundreds of tasks, and a
14
+ * panel that scrolls off the screen is worse than a summary plus the full plan
15
+ * written to `.agents/framework/plans/` on the next step.
16
+ */
17
+ import type { ExtensionContext } from "@earendil-works/pi-coding-agent";
18
+ import { type Component, matchesKey, Text } from "@earendil-works/pi-tui";
19
+ import type { Plan } from "./types.js";
20
+
21
+ export type PlanDecision = "proceed" | "dry-run" | "cancel";
22
+
23
+ const MAX_PREVIEW_LINES = 16;
24
+
25
+ function previewLines(plan: Plan): string[] {
26
+ const lines: string[] = [`/ask-init — ${plan.mode}`, ""];
27
+ lines.push(...plan.summary);
28
+ if (plan.warnings.length > 0) {
29
+ lines.push("", "warnings:");
30
+ for (const warning of plan.warnings.slice(0, 4)) lines.push(` ! ${warning}`);
31
+ if (plan.warnings.length > 4) lines.push(` … ${plan.warnings.length - 4} more`);
32
+ }
33
+ if (plan.tasks.length > 0) {
34
+ lines.push("", `model tasks: ${plan.tasks.map((task) => task.id).join(", ")}`);
35
+ }
36
+ if (lines.length > MAX_PREVIEW_LINES) {
37
+ lines.splice(MAX_PREVIEW_LINES);
38
+ lines.push("…");
39
+ }
40
+ return lines;
41
+ }
42
+
43
+ class PlanPanel implements Component {
44
+ constructor(
45
+ private readonly body: Text,
46
+ private readonly footer: Text,
47
+ private readonly done: (decision: PlanDecision) => void,
48
+ ) {}
49
+
50
+ handleInput(data: string): void {
51
+ if (matchesKey(data, "escape") || matchesKey(data, "ctrl+c")) {
52
+ this.done("cancel");
53
+ return;
54
+ }
55
+ if (matchesKey(data, "enter") || data === "y" || data === "Y") {
56
+ this.done("proceed");
57
+ return;
58
+ }
59
+ if (data === "d" || data === "D") {
60
+ this.done("dry-run");
61
+ return;
62
+ }
63
+ }
64
+
65
+ render(width: number): string[] {
66
+ return [...this.body.render(width), ...this.footer.render(width)];
67
+ }
68
+
69
+ invalidate(): void {
70
+ this.body.invalidate();
71
+ this.footer.invalidate();
72
+ }
73
+ }
74
+
75
+ /** Ask the operator what to do with this plan. */
76
+ export async function confirmPlan(
77
+ ctx: ExtensionContext,
78
+ plan: Plan,
79
+ ): Promise<PlanDecision> {
80
+ if (!ctx.hasUI) return "dry-run";
81
+
82
+ if (ctx.mode !== "tui") {
83
+ const choice = await ctx.ui.select(
84
+ `/ask-init ${plan.mode}: ${plan.summary[0] ?? ""}`,
85
+ ["Proceed", "Report only (no writes)", "Cancel"],
86
+ );
87
+ if (choice === "Proceed") return "proceed";
88
+ if (choice === "Report only (no writes)") return "dry-run";
89
+ return "cancel";
90
+ }
91
+
92
+ return ctx.ui.custom<PlanDecision>((_tui, _theme, _keybindings, done) => {
93
+ const body = new Text(previewLines(plan).join("\n"));
94
+ const footer = new Text(
95
+ "[Enter/y] apply [d] report only, write nothing [Esc] cancel",
96
+ );
97
+ return new PlanPanel(body, footer, done);
98
+ });
99
+ }
100
+
101
+ /* -------------------------------------------------------------------------- */
102
+ /* Read-only report panel */
103
+ /* -------------------------------------------------------------------------- */
104
+
105
+ const MAX_REPORT_LINES = 22;
106
+
107
+ class ReportPanel implements Component {
108
+ constructor(
109
+ private readonly body: Text,
110
+ private readonly footer: Text,
111
+ private readonly done: () => void,
112
+ ) {}
113
+
114
+ handleInput(data: string): void {
115
+ if (
116
+ matchesKey(data, "escape") ||
117
+ matchesKey(data, "enter") ||
118
+ matchesKey(data, "ctrl+c") ||
119
+ data === "q"
120
+ ) {
121
+ this.done();
122
+ }
123
+ }
124
+
125
+ render(width: number): string[] {
126
+ return [...this.body.render(width), ...this.footer.render(width)];
127
+ }
128
+
129
+ invalidate(): void {
130
+ this.body.invalidate();
131
+ this.footer.invalidate();
132
+ }
133
+ }
134
+
135
+ /**
136
+ * Show a report (plan or audit) without writing anything. In TUI it opens a
137
+ * bounded panel; elsewhere the first lines go through `notify`, which is the
138
+ * only output channel print/json modes have.
139
+ */
140
+ export async function showReport(
141
+ ctx: ExtensionContext,
142
+ title: string,
143
+ text: string,
144
+ options: { path?: string } = {},
145
+ ): Promise<void> {
146
+ const lines = text.split("\n");
147
+ if (!ctx.hasUI || ctx.mode !== "tui") {
148
+ const head = lines.filter((line) => line.trim().length > 0).slice(0, 6).join("\n");
149
+ ctx.ui.notify(`${title}\n${head}${options.path ? `\n→ ${options.path}` : ""}`, "info");
150
+ return;
151
+ }
152
+ const clipped = lines.slice(0, MAX_REPORT_LINES);
153
+ if (lines.length > MAX_REPORT_LINES) clipped.push(`… ${lines.length - MAX_REPORT_LINES} more lines`);
154
+ if (options.path) clipped.push("", `full report: ${options.path}`);
155
+ await ctx.ui.custom<void>((_tui, _theme, _keybindings, done) => {
156
+ const body = new Text(clipped.join("\n"));
157
+ const footer = new Text("[Enter/Esc] close");
158
+ return new ReportPanel(body, footer, () => done(undefined));
159
+ });
160
+ }
package/src/index.ts CHANGED
@@ -12,6 +12,7 @@
12
12
  * - compact — dedicated compact model for /compact + auto-compact summaries
13
13
  * - autostart — replay project-declared slash commands at session start (/add-dir persistence)
14
14
  * - permission — session-scoped boundary permission checks (/ask-perm)
15
+ * - init — agent memory framework install / adoption (/ask-init)
15
16
  *
16
17
  * `ask` and `permission` are independent gates on one bus: each answers its own
17
18
  * question and the strictest answer wins, so they compose rather than replace
@@ -24,6 +25,7 @@ import { registerAsk } from "./features/ask/index.js";
24
25
  import { registerAutostart } from "./features/autostart/index.js";
25
26
  import { registerCompact } from "./features/compact/index.js";
26
27
  import { registerFavorites } from "./features/favorites/index.js";
28
+ import { registerInit } from "./features/init/index.js";
27
29
  import { registerPermission } from "./features/permission/index.js";
28
30
  import { registerReview } from "./features/review/index.js";
29
31
  import { registerStash } from "./features/stash/index.js";
@@ -41,6 +43,7 @@ export default function piToolkits(pi: ExtensionAPI): void {
41
43
  registerAutostart(pi);
42
44
  registerCompact(pi);
43
45
  registerFavorites(pi);
46
+ registerInit(pi);
44
47
  registerReview(pi);
45
48
  registerStash(pi);
46
49