pi-anchor 0.1.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.
- package/LICENSE +21 -0
- package/README.md +273 -0
- package/README_zh.md +272 -0
- package/bin/anchor.js +163 -0
- package/dist/chunk-NNYUD5O7.js +1295 -0
- package/dist/extension.cjs +1560 -0
- package/dist/extension.d.cts +5 -0
- package/dist/extension.d.ts +5 -0
- package/dist/extension.js +312 -0
- package/dist/index.cjs +1433 -0
- package/dist/index.d.cts +448 -0
- package/dist/index.d.ts +448 -0
- package/dist/index.js +144 -0
- package/package.json +64 -0
- package/src/context_injector.ts +157 -0
- package/src/decay.ts +123 -0
- package/src/extension.ts +363 -0
- package/src/index.ts +9 -0
- package/src/matcher.ts +130 -0
- package/src/observer.ts +141 -0
- package/src/pi-types.d.ts +42 -0
- package/src/protocol.ts +95 -0
- package/src/settlement.ts +72 -0
- package/src/store.ts +682 -0
- package/src/tui.ts +386 -0
- package/src/types.ts +104 -0
|
@@ -0,0 +1,312 @@
|
|
|
1
|
+
import {
|
|
2
|
+
AnchorStore,
|
|
3
|
+
SessionTouchObserver,
|
|
4
|
+
findMatchedAnchors,
|
|
5
|
+
generateSettlementProposals,
|
|
6
|
+
makeSafeTaskAnnotation,
|
|
7
|
+
normalizePath,
|
|
8
|
+
openAnchorDashboard,
|
|
9
|
+
renderColdStartAnchorsContext,
|
|
10
|
+
runPhysicalVerification,
|
|
11
|
+
sweepStore,
|
|
12
|
+
updateAnchorStatusBar,
|
|
13
|
+
updateStartupBanner
|
|
14
|
+
} from "./chunk-NNYUD5O7.js";
|
|
15
|
+
|
|
16
|
+
// src/extension.ts
|
|
17
|
+
import { Type } from "@sinclair/typebox";
|
|
18
|
+
import { execSync } from "child_process";
|
|
19
|
+
var MUTATION_TOOLS = /* @__PURE__ */ new Set(["edit", "write", "patch", "apply_diff", "create_file", "modify"]);
|
|
20
|
+
function extension_default(pi) {
|
|
21
|
+
const store = new AnchorStore();
|
|
22
|
+
const observer = new SessionTouchObserver();
|
|
23
|
+
const annotatedThisSession = /* @__PURE__ */ new Set();
|
|
24
|
+
pi.on("session_start", async (event, ctx) => {
|
|
25
|
+
observer.clear();
|
|
26
|
+
annotatedThisSession.clear();
|
|
27
|
+
const sweep = sweepStore(store);
|
|
28
|
+
if (sweep.transitionedToSleeping.length > 0) {
|
|
29
|
+
ctx.ui.notify(`Anchor: ${sweep.transitionedToSleeping.length} \u4E2A\u975E\u6D3B\u8DC3\u4EFB\u52A1\u5DF2\u8FDB\u5165\u4F11\u7720`, "info");
|
|
30
|
+
}
|
|
31
|
+
updateAnchorStatusBar(ctx, store);
|
|
32
|
+
if (event.reason !== "resume") {
|
|
33
|
+
updateStartupBanner(ctx, store);
|
|
34
|
+
}
|
|
35
|
+
});
|
|
36
|
+
pi.on("agent_start", async (_event, ctx) => {
|
|
37
|
+
if (ctx.hasUI && ctx.ui) {
|
|
38
|
+
ctx.ui.setWidget("anchor-startup", void 0);
|
|
39
|
+
}
|
|
40
|
+
});
|
|
41
|
+
pi.on("before_agent_start", async (event, ctx) => {
|
|
42
|
+
const entries = ctx.sessionManager?.getEntries() || [];
|
|
43
|
+
const messageTurns = entries.filter((e) => e.type === "message");
|
|
44
|
+
const isColdStart = messageTurns.length <= 1;
|
|
45
|
+
if (isColdStart) {
|
|
46
|
+
const contextSnippet = renderColdStartAnchorsContext(store, ctx.cwd);
|
|
47
|
+
if (contextSnippet) {
|
|
48
|
+
return {
|
|
49
|
+
systemPrompt: `${event.systemPrompt}
|
|
50
|
+
|
|
51
|
+
${contextSnippet}`
|
|
52
|
+
};
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
});
|
|
56
|
+
pi.on("tool_result", async (event, ctx) => {
|
|
57
|
+
if (!event.isError) {
|
|
58
|
+
observer.recordToolCall(event.toolName, event.input || {});
|
|
59
|
+
}
|
|
60
|
+
const pathInput = event.input?.path;
|
|
61
|
+
if (typeof pathInput !== "string") return;
|
|
62
|
+
const touchedPath = normalizePath(pathInput);
|
|
63
|
+
const anchors = store.list({ cwd: ctx.cwd }).filter((a) => a.status === "active" || a.status === "sleeping");
|
|
64
|
+
const matches = findMatchedAnchors(anchors, [touchedPath]);
|
|
65
|
+
if (matches.length > 0) {
|
|
66
|
+
const isMutation = MUTATION_TOOLS.has((event.toolName || "").toLowerCase());
|
|
67
|
+
if (isMutation && !event.isError) {
|
|
68
|
+
for (const m of matches) {
|
|
69
|
+
store.touch(m.anchor.id);
|
|
70
|
+
}
|
|
71
|
+
updateAnchorStatusBar(ctx, store);
|
|
72
|
+
}
|
|
73
|
+
const a = matches[0].anchor;
|
|
74
|
+
if (!annotatedThisSession.has(a.id)) {
|
|
75
|
+
annotatedThisSession.add(a.id);
|
|
76
|
+
const alert = makeSafeTaskAnnotation(touchedPath, a);
|
|
77
|
+
if (alert) {
|
|
78
|
+
const contents = [...event.content || []];
|
|
79
|
+
for (let i = contents.length - 1; i >= 0; i--) {
|
|
80
|
+
const item = contents[i];
|
|
81
|
+
if (item && item.type === "text") {
|
|
82
|
+
contents[i] = { ...item, text: item.text + alert };
|
|
83
|
+
return { content: contents };
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
});
|
|
90
|
+
pi.on("session_shutdown", async (_event, ctx) => {
|
|
91
|
+
try {
|
|
92
|
+
const gitStatus = execSync("git status --porcelain", {
|
|
93
|
+
cwd: ctx.cwd,
|
|
94
|
+
encoding: "utf-8",
|
|
95
|
+
timeout: 3e3,
|
|
96
|
+
stdio: ["ignore", "pipe", "ignore"]
|
|
97
|
+
});
|
|
98
|
+
const changed = gitStatus.split("\n").map((l) => l.slice(3).trim()).filter(Boolean);
|
|
99
|
+
for (const f of changed) {
|
|
100
|
+
observer.addTouchedFile(f);
|
|
101
|
+
}
|
|
102
|
+
} catch {
|
|
103
|
+
}
|
|
104
|
+
const modified = observer.getModifiedFiles();
|
|
105
|
+
if (modified.length === 0) return;
|
|
106
|
+
const proposals = generateSettlementProposals(store, modified, ctx.cwd);
|
|
107
|
+
if (proposals.length === 0) return;
|
|
108
|
+
for (const prop of proposals) {
|
|
109
|
+
const a = prop.anchor;
|
|
110
|
+
const commitMatch = observer.matchesCommit(a);
|
|
111
|
+
if (commitMatch.matched) {
|
|
112
|
+
store.settle(a.id, {
|
|
113
|
+
settledBy: "verification-test",
|
|
114
|
+
summary: `Auto-settled via Git commit: ${commitMatch.message}`,
|
|
115
|
+
touchedFiles: prop.matchedFiles
|
|
116
|
+
});
|
|
117
|
+
ctx.ui.notify(`\u2316 Auto-settled #${a.id} via Git commit (run /anchor undo to revert)`, "info");
|
|
118
|
+
continue;
|
|
119
|
+
}
|
|
120
|
+
if (a.verifyCommand) {
|
|
121
|
+
const verifyRes = runPhysicalVerification(a, ctx.cwd);
|
|
122
|
+
if (verifyRes.success) {
|
|
123
|
+
store.settle(a.id, {
|
|
124
|
+
settledBy: "verification-test",
|
|
125
|
+
summary: `Automated test passed: ${a.verifyCommand}`,
|
|
126
|
+
touchedFiles: prop.matchedFiles
|
|
127
|
+
});
|
|
128
|
+
ctx.ui.notify(`\u2316 Test passed (${a.verifyCommand}), auto-settled #${a.id}`, "info");
|
|
129
|
+
continue;
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
if (!ctx.hasUI || !ctx.ui) continue;
|
|
133
|
+
const ok = await ctx.ui.confirm(
|
|
134
|
+
"\u2316 Settle Anchor Task",
|
|
135
|
+
`Task #${a.id} [${a.title}] touched files (${prop.matchedFiles.slice(0, 2).join(", ")}).
|
|
136
|
+
Mark as completed and archive?`
|
|
137
|
+
);
|
|
138
|
+
if (ok) {
|
|
139
|
+
store.settle(a.id, {
|
|
140
|
+
settledBy: "one-tap-settlement",
|
|
141
|
+
touchedFiles: prop.matchedFiles
|
|
142
|
+
});
|
|
143
|
+
ctx.ui.notify(`\u2316 Settled #${a.id}: "${a.title}"`, "info");
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
});
|
|
147
|
+
pi.registerTool({
|
|
148
|
+
name: "anchor",
|
|
149
|
+
label: "Anchor (Cross-session Task Protocol)",
|
|
150
|
+
description: "Manage cross-session persistent tasks that survive terminal restarts and auto-evict upon code changes or settlement. Use when the user asks to retain, pin, remember, or track a multi-session goal across sessions, or when an ongoing commitment must not be forgotten. Actions: pin (create new cross-session anchor), list (view active and sleeping anchors), settle (close and archive a completed anchor), touch (refresh activity), sweep (run decay cleanup). Stored in ~/.anchor/ with zero project repository pollution.",
|
|
151
|
+
promptSnippet: "Anchor cross-session task contracts that survive terminal restarts and auto-evict",
|
|
152
|
+
promptGuidelines: [
|
|
153
|
+
'Use `anchor` when the user asks to retain a goal across sessions or record a reminder for later/tonight/tomorrow (e.g. "\u665A\u4E0A\u6E05\u7406\u5783\u573E", "\u660E\u5929\u4F18\u5316X", "\u4FDD\u7559\u4EFB\u52A1\u76F4\u5230\u5B8C\u6210").',
|
|
154
|
+
"BOUNDARY WITH TODO: `todo` is strictly for intra-session active work breakdown (step 1, step 2, step 3 right now). For future reminders or cross-session goals, ONLY use `anchor`. NEVER duplicate a cross-session reminder into both `todo` and `anchor`.",
|
|
155
|
+
"DO NOT over-engineer or assume automated scheduled tasks unless the user explicitly requests Windows Task Scheduler or cron.",
|
|
156
|
+
"Tasks are automatically scoped to the current project context without cluttering the project git repository.",
|
|
157
|
+
'When code for an anchor is completed and verified, call `anchor` with action "settle" to archive it and free context.',
|
|
158
|
+
"Active anchors are automatically injected into future sessions in an ultra-compact block."
|
|
159
|
+
],
|
|
160
|
+
parameters: Type.Object({
|
|
161
|
+
action: Type.Union([
|
|
162
|
+
Type.Literal("pin"),
|
|
163
|
+
Type.Literal("list"),
|
|
164
|
+
Type.Literal("settle"),
|
|
165
|
+
Type.Literal("touch"),
|
|
166
|
+
Type.Literal("sweep")
|
|
167
|
+
]),
|
|
168
|
+
title: Type.Optional(Type.String({ description: "Short imperative task title (for pin)" })),
|
|
169
|
+
description: Type.Optional(Type.String({ description: "Detailed context, acceptance criteria, or technical notes" })),
|
|
170
|
+
priority: Type.Optional(Type.Union([Type.Literal("p0"), Type.Literal("p1"), Type.Literal("p2")])),
|
|
171
|
+
project: Type.Optional(Type.String({ description: "Target project name or workspace (defaults to current directory if omitted)" })),
|
|
172
|
+
targetDate: Type.Optional(Type.String({ description: "Expected completion date (e.g. YYYY-MM-DD, today, tomorrow)" })),
|
|
173
|
+
files: Type.Optional(Type.Array(Type.String(), { description: "Associated file paths or directory prefixes" })),
|
|
174
|
+
tags: Type.Optional(Type.Array(Type.String(), { description: "Domain tags" })),
|
|
175
|
+
verifyCommand: Type.Optional(Type.String({ description: "Optional shell command for automated physical verification" })),
|
|
176
|
+
id: Type.Optional(Type.String({ description: "Anchor ID, e.g. anc-1 (for settle or touch)" }))
|
|
177
|
+
}),
|
|
178
|
+
async execute(_toolCallId, params, _signal, _onUpdate, ctx) {
|
|
179
|
+
if (!ctx) return { content: [{ type: "text", text: "Error: context required" }], isError: true };
|
|
180
|
+
if (params.action === "pin") {
|
|
181
|
+
if (!params.title) {
|
|
182
|
+
return { content: [{ type: "text", text: "Error: title is required for pin action" }], isError: true };
|
|
183
|
+
}
|
|
184
|
+
const anc = store.create({
|
|
185
|
+
title: params.title,
|
|
186
|
+
description: params.description,
|
|
187
|
+
priority: params.priority || "p1",
|
|
188
|
+
project: params.project,
|
|
189
|
+
targetDate: params.targetDate,
|
|
190
|
+
cwd: ctx.cwd,
|
|
191
|
+
files: params.files || [],
|
|
192
|
+
tags: params.tags || [],
|
|
193
|
+
verifyCommand: params.verifyCommand
|
|
194
|
+
});
|
|
195
|
+
updateAnchorStatusBar(ctx, store);
|
|
196
|
+
return {
|
|
197
|
+
content: [{
|
|
198
|
+
type: "text",
|
|
199
|
+
text: `Successfully anchored task #${anc.id}: "${anc.title}" [${anc.project}]. Stored in ~/.anchor/.`
|
|
200
|
+
}],
|
|
201
|
+
isError: false
|
|
202
|
+
};
|
|
203
|
+
}
|
|
204
|
+
if (params.action === "list") {
|
|
205
|
+
const active = store.list({ status: "active", cwd: ctx.cwd });
|
|
206
|
+
const sleeping = store.list({ status: "sleeping", cwd: ctx.cwd });
|
|
207
|
+
return {
|
|
208
|
+
content: [{
|
|
209
|
+
type: "text",
|
|
210
|
+
text: JSON.stringify({ active, sleeping }, null, 2)
|
|
211
|
+
}],
|
|
212
|
+
isError: false
|
|
213
|
+
};
|
|
214
|
+
}
|
|
215
|
+
if (params.action === "settle") {
|
|
216
|
+
if (!params.id) {
|
|
217
|
+
return { content: [{ type: "text", text: "Error: id is required for settle action" }], isError: true };
|
|
218
|
+
}
|
|
219
|
+
try {
|
|
220
|
+
const settled = store.settle(params.id, { settledBy: "verification-test" });
|
|
221
|
+
updateAnchorStatusBar(ctx, store);
|
|
222
|
+
return {
|
|
223
|
+
content: [{
|
|
224
|
+
type: "text",
|
|
225
|
+
text: `Anchor #${settled.id} successfully settled and evicted from active context.`
|
|
226
|
+
}],
|
|
227
|
+
isError: false
|
|
228
|
+
};
|
|
229
|
+
} catch (err) {
|
|
230
|
+
return { content: [{ type: "text", text: err.message }], isError: true };
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
if (params.action === "touch") {
|
|
234
|
+
if (!params.id) {
|
|
235
|
+
return { content: [{ type: "text", text: "Error: id is required for touch action" }], isError: true };
|
|
236
|
+
}
|
|
237
|
+
try {
|
|
238
|
+
const touched = store.touch(params.id);
|
|
239
|
+
updateAnchorStatusBar(ctx, store);
|
|
240
|
+
return {
|
|
241
|
+
content: [{
|
|
242
|
+
type: "text",
|
|
243
|
+
text: `Anchor #${touched.id} touched and decay timer refreshed.`
|
|
244
|
+
}],
|
|
245
|
+
isError: false
|
|
246
|
+
};
|
|
247
|
+
} catch (err) {
|
|
248
|
+
return { content: [{ type: "text", text: err.message }], isError: true };
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
if (params.action === "sweep") {
|
|
252
|
+
const res = sweepStore(store);
|
|
253
|
+
updateAnchorStatusBar(ctx, store);
|
|
254
|
+
return {
|
|
255
|
+
content: [{
|
|
256
|
+
type: "text",
|
|
257
|
+
text: `Sweep complete: ${res.transitionedToSleeping.length} sleeping, ${res.evictedToGraveyard.length} evicted to graveyard.`
|
|
258
|
+
}],
|
|
259
|
+
isError: false
|
|
260
|
+
};
|
|
261
|
+
}
|
|
262
|
+
return { content: [{ type: "text", text: "Unknown action" }], isError: true };
|
|
263
|
+
}
|
|
264
|
+
});
|
|
265
|
+
pi.registerCommand("anchor", {
|
|
266
|
+
description: "Inspect and settle active anchors",
|
|
267
|
+
handler: async (args, ctx) => {
|
|
268
|
+
const input = (args || "").trim();
|
|
269
|
+
if (!input || input === "list" || input === "ls") {
|
|
270
|
+
await openAnchorDashboard(ctx, store);
|
|
271
|
+
return;
|
|
272
|
+
}
|
|
273
|
+
if (input === "undo") {
|
|
274
|
+
try {
|
|
275
|
+
const restored = store.undoSettle();
|
|
276
|
+
ctx.ui.notify(`\u2316 Restored #${restored.id}: "${restored.title}"`, "info");
|
|
277
|
+
updateAnchorStatusBar(ctx, store);
|
|
278
|
+
} catch (err) {
|
|
279
|
+
ctx.ui.notify(`Revert failed: ${err.message}`, "error");
|
|
280
|
+
}
|
|
281
|
+
return;
|
|
282
|
+
}
|
|
283
|
+
const targetId = input.startsWith("#") ? input.slice(1) : input;
|
|
284
|
+
const item = store.get(targetId);
|
|
285
|
+
if (item) {
|
|
286
|
+
store.settle(targetId, { settledBy: "manual-command" });
|
|
287
|
+
ctx.ui.notify(`\u2316 Settled: "${item.title}"`, "info");
|
|
288
|
+
updateAnchorStatusBar(ctx, store);
|
|
289
|
+
return;
|
|
290
|
+
}
|
|
291
|
+
const anc = store.create({ title: input, cwd: ctx.cwd });
|
|
292
|
+
ctx.ui.notify(`\u2316 Pinned #${anc.id}: "${anc.title}"`, "info");
|
|
293
|
+
updateAnchorStatusBar(ctx, store);
|
|
294
|
+
}
|
|
295
|
+
});
|
|
296
|
+
pi.registerCommand("pin", {
|
|
297
|
+
description: "Quickly pin a task across sessions",
|
|
298
|
+
handler: async (args, ctx) => {
|
|
299
|
+
const title = (args || "").trim();
|
|
300
|
+
if (!title) {
|
|
301
|
+
await openAnchorDashboard(ctx, store);
|
|
302
|
+
return;
|
|
303
|
+
}
|
|
304
|
+
const anc = store.create({ title, cwd: ctx.cwd });
|
|
305
|
+
ctx.ui.notify(`\u2316 Pinned #${anc.id}: "${anc.title}"`, "info");
|
|
306
|
+
updateAnchorStatusBar(ctx, store);
|
|
307
|
+
}
|
|
308
|
+
});
|
|
309
|
+
}
|
|
310
|
+
export {
|
|
311
|
+
extension_default as default
|
|
312
|
+
};
|