pi-anchor 0.1.1 → 0.2.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/dist/{chunk-QKBUATPO.js → chunk-T62PHIQI.js} +8 -5
- package/dist/extension.cjs +67 -6
- package/dist/extension.js +60 -2
- package/dist/index.cjs +8 -5
- package/dist/index.d.cts +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.js +1 -1
- package/package.json +1 -1
- package/src/context_injector.ts +14 -5
- package/src/extension.ts +64 -2
|
@@ -931,16 +931,16 @@ function makeSafeTaskAnnotation(filePath, anchor) {
|
|
|
931
931
|
}
|
|
932
932
|
return "\n\n" + formatter(`\u2316 anchor context: #${anchor.id} ${anchor.title} (${anchor.priority.toUpperCase()})`);
|
|
933
933
|
}
|
|
934
|
-
function renderColdStartAnchorsContext(store, cwdOrNow, nowArg) {
|
|
934
|
+
function renderColdStartAnchorsContext(store, cwdOrNow, nowArg, preflightFailures) {
|
|
935
935
|
let cwd;
|
|
936
936
|
let now = Date.now();
|
|
937
937
|
if (typeof cwdOrNow === "number") {
|
|
938
938
|
now = cwdOrNow;
|
|
939
939
|
} else if (typeof cwdOrNow === "string") {
|
|
940
940
|
cwd = cwdOrNow;
|
|
941
|
-
|
|
942
|
-
|
|
943
|
-
|
|
941
|
+
}
|
|
942
|
+
if (typeof nowArg === "number") {
|
|
943
|
+
now = nowArg;
|
|
944
944
|
}
|
|
945
945
|
sweepStore(store, now);
|
|
946
946
|
const activeAnchors = store.list({ status: "active", cwd });
|
|
@@ -968,7 +968,9 @@ function renderColdStartAnchorsContext(store, cwdOrNow, nowArg) {
|
|
|
968
968
|
}
|
|
969
969
|
const ageDays = Math.floor((now - a.createdAt) / (24 * 60 * 60 * 1e3));
|
|
970
970
|
const ageHint = ageDays > 0 ? ` (${ageDays}d old)` : "";
|
|
971
|
-
|
|
971
|
+
const failureSnippet = preflightFailures?.get(a.id);
|
|
972
|
+
const verifyHint = failureSnippet ? ` [\u26A1 \u7269\u7406\u9A8C\u8BC1\u5931\u8D25: ${failureSnippet.replace(/\s+/g, " ").slice(-100)}]` : a.verifyCommand ? ` [verify: ${a.verifyCommand}]` : "";
|
|
973
|
+
return `\u2022 #${a.id} ${projHint}${targetHint} [${a.priority.toUpperCase()}] ${a.title}${verifyHint}${ageHint}${tagHint}${fileHint}`;
|
|
972
974
|
});
|
|
973
975
|
return [
|
|
974
976
|
`<active-anchors count="${activeAnchors.length}">`,
|
|
@@ -977,6 +979,7 @@ function renderColdStartAnchorsContext(store, cwdOrNow, nowArg) {
|
|
|
977
979
|
``,
|
|
978
980
|
`CONVERSATIONAL PROTOCOL (HUMAN PARTNER TONE):`,
|
|
979
981
|
`- If the user opens the session with a casual greeting or continuation ("hi", "\u5728\u5417", "\u7EE7\u7EED", "\u65E9\u4E0A\u597D"), naturally prioritize items tagged [\u4ECA\u65E5\u805A\u7126\xB7Due Today] in a friendly assistant tone (e.g. "\u55E8\uFF01\u4ECA\u5929\u89C4\u5212\u7684\u3010...\u3011\u73B0\u5728\u5904\u7406\u8FD8\u662F\u770B\u522B\u7684\uFF1F").`,
|
|
982
|
+
`- If an anchor has [\u26A1 \u7269\u7406\u9A8C\u8BC1\u5931\u8D25], its verifyCommand failed on session start. Target the failing assertion directly.`,
|
|
980
983
|
`- If the user issues a direct, concrete new instruction (e.g. "\u5E2E\u6211\u67E5\u4E0B\u8FD9\u4E2A\u62A5\u9519"), DO NOT interrupt their flow. Focus directly on their instruction.`,
|
|
981
984
|
`- When a commitment is satisfied during work, automatically call anchor(action="settle", id=...) to clear it.`,
|
|
982
985
|
`</active-anchors>`
|
package/dist/extension.cjs
CHANGED
|
@@ -965,16 +965,16 @@ function makeSafeTaskAnnotation(filePath, anchor) {
|
|
|
965
965
|
}
|
|
966
966
|
return "\n\n" + formatter(`\u2316 anchor context: #${anchor.id} ${anchor.title} (${anchor.priority.toUpperCase()})`);
|
|
967
967
|
}
|
|
968
|
-
function renderColdStartAnchorsContext(store, cwdOrNow, nowArg) {
|
|
968
|
+
function renderColdStartAnchorsContext(store, cwdOrNow, nowArg, preflightFailures) {
|
|
969
969
|
let cwd;
|
|
970
970
|
let now = Date.now();
|
|
971
971
|
if (typeof cwdOrNow === "number") {
|
|
972
972
|
now = cwdOrNow;
|
|
973
973
|
} else if (typeof cwdOrNow === "string") {
|
|
974
974
|
cwd = cwdOrNow;
|
|
975
|
-
|
|
976
|
-
|
|
977
|
-
|
|
975
|
+
}
|
|
976
|
+
if (typeof nowArg === "number") {
|
|
977
|
+
now = nowArg;
|
|
978
978
|
}
|
|
979
979
|
sweepStore(store, now);
|
|
980
980
|
const activeAnchors = store.list({ status: "active", cwd });
|
|
@@ -1002,7 +1002,9 @@ function renderColdStartAnchorsContext(store, cwdOrNow, nowArg) {
|
|
|
1002
1002
|
}
|
|
1003
1003
|
const ageDays = Math.floor((now - a.createdAt) / (24 * 60 * 60 * 1e3));
|
|
1004
1004
|
const ageHint = ageDays > 0 ? ` (${ageDays}d old)` : "";
|
|
1005
|
-
|
|
1005
|
+
const failureSnippet = preflightFailures?.get(a.id);
|
|
1006
|
+
const verifyHint = failureSnippet ? ` [\u26A1 \u7269\u7406\u9A8C\u8BC1\u5931\u8D25: ${failureSnippet.replace(/\s+/g, " ").slice(-100)}]` : a.verifyCommand ? ` [verify: ${a.verifyCommand}]` : "";
|
|
1007
|
+
return `\u2022 #${a.id} ${projHint}${targetHint} [${a.priority.toUpperCase()}] ${a.title}${verifyHint}${ageHint}${tagHint}${fileHint}`;
|
|
1006
1008
|
});
|
|
1007
1009
|
return [
|
|
1008
1010
|
`<active-anchors count="${activeAnchors.length}">`,
|
|
@@ -1011,6 +1013,7 @@ function renderColdStartAnchorsContext(store, cwdOrNow, nowArg) {
|
|
|
1011
1013
|
``,
|
|
1012
1014
|
`CONVERSATIONAL PROTOCOL (HUMAN PARTNER TONE):`,
|
|
1013
1015
|
`- If the user opens the session with a casual greeting or continuation ("hi", "\u5728\u5417", "\u7EE7\u7EED", "\u65E9\u4E0A\u597D"), naturally prioritize items tagged [\u4ECA\u65E5\u805A\u7126\xB7Due Today] in a friendly assistant tone (e.g. "\u55E8\uFF01\u4ECA\u5929\u89C4\u5212\u7684\u3010...\u3011\u73B0\u5728\u5904\u7406\u8FD8\u662F\u770B\u522B\u7684\uFF1F").`,
|
|
1016
|
+
`- If an anchor has [\u26A1 \u7269\u7406\u9A8C\u8BC1\u5931\u8D25], its verifyCommand failed on session start. Target the failing assertion directly.`,
|
|
1014
1017
|
`- If the user issues a direct, concrete new instruction (e.g. "\u5E2E\u6211\u67E5\u4E0B\u8FD9\u4E2A\u62A5\u9519"), DO NOT interrupt their flow. Focus directly on their instruction.`,
|
|
1015
1018
|
`- When a commitment is satisfied during work, automatically call anchor(action="settle", id=...) to clear it.`,
|
|
1016
1019
|
`</active-anchors>`
|
|
@@ -1277,18 +1280,52 @@ async function openAnchorDashboard(ctx, store) {
|
|
|
1277
1280
|
|
|
1278
1281
|
// src/extension.ts
|
|
1279
1282
|
var import_node_child_process2 = require("child_process");
|
|
1283
|
+
var import_node_fs3 = __toESM(require("fs"), 1);
|
|
1284
|
+
var import_node_path5 = __toESM(require("path"), 1);
|
|
1280
1285
|
var MUTATION_TOOLS2 = /* @__PURE__ */ new Set(["edit", "write", "patch", "apply_diff", "create_file", "modify"]);
|
|
1281
1286
|
function extension_default(pi) {
|
|
1282
1287
|
const store = new AnchorStore();
|
|
1283
1288
|
const observer = new SessionTouchObserver();
|
|
1284
1289
|
const annotatedThisSession = /* @__PURE__ */ new Set();
|
|
1290
|
+
const preflightFailures = /* @__PURE__ */ new Map();
|
|
1285
1291
|
pi.on("session_start", async (event, ctx) => {
|
|
1286
1292
|
observer.clear();
|
|
1287
1293
|
annotatedThisSession.clear();
|
|
1294
|
+
preflightFailures.clear();
|
|
1288
1295
|
const sweep = sweepStore(store);
|
|
1289
1296
|
if (sweep.transitionedToSleeping.length > 0) {
|
|
1290
1297
|
ctx.ui.notify(`Anchor: ${sweep.transitionedToSleeping.length} \u4E2A\u975E\u6D3B\u8DC3\u4EFB\u52A1\u5DF2\u8FDB\u5165\u4F11\u7720`, "info");
|
|
1291
1298
|
}
|
|
1299
|
+
const activeAnchors = store.list({ cwd: ctx.cwd }).filter((a) => a.status === "active");
|
|
1300
|
+
for (const a of activeAnchors) {
|
|
1301
|
+
if (a.verifyCommand) {
|
|
1302
|
+
try {
|
|
1303
|
+
const res = runPhysicalVerification(a, ctx.cwd);
|
|
1304
|
+
if (res.success) {
|
|
1305
|
+
store.settle(a.id, {
|
|
1306
|
+
settledBy: "verification-test",
|
|
1307
|
+
summary: `Physical pre-flight test passed: ${a.verifyCommand}`
|
|
1308
|
+
});
|
|
1309
|
+
ctx.ui.notify(`\u2693 \u7269\u7406\u9A8C\u8BC1\u901A\u8FC7\uFF01\u951A\u70B9 #${a.id} ("${a.title}") \u5DF2\u81EA\u52A8\u7ED3\u6848\u5F52\u6863\u3002`, "info");
|
|
1310
|
+
} else {
|
|
1311
|
+
preflightFailures.set(a.id, res.output.slice(-250));
|
|
1312
|
+
}
|
|
1313
|
+
} catch (_) {
|
|
1314
|
+
}
|
|
1315
|
+
}
|
|
1316
|
+
}
|
|
1317
|
+
try {
|
|
1318
|
+
const home = process.env.HOME || process.env.USERPROFILE || "";
|
|
1319
|
+
const pendingPath = import_node_path5.default.join(home, ".anchor", "pending_exit.json");
|
|
1320
|
+
if (import_node_fs3.default.existsSync(pendingPath)) {
|
|
1321
|
+
const pending = JSON.parse(import_node_fs3.default.readFileSync(pendingPath, "utf-8"));
|
|
1322
|
+
if (pending.cwd === ctx.cwd && Date.now() - pending.timestamp < 24 * 60 * 60 * 1e3) {
|
|
1323
|
+
ctx.ui.notify(`\u2316 [Anchor \u9000\u51FA\u5B88\u62A4] \u68C0\u6D4B\u5230\u4E0A\u6B21\u5F02\u5E38\u9000\u51FA\u4E14\u6709 ${pending.modifiedCount} \u4E2A\u672A\u63D0\u4EA4\u6539\u52A8\uFF0C\u53EF\u7528 anchor(action="pin") \u56FA\u5316\u4EFB\u52A1\u3002`, "info");
|
|
1324
|
+
}
|
|
1325
|
+
import_node_fs3.default.unlinkSync(pendingPath);
|
|
1326
|
+
}
|
|
1327
|
+
} catch (_) {
|
|
1328
|
+
}
|
|
1292
1329
|
updateAnchorStatusBar(ctx, store);
|
|
1293
1330
|
if (event.reason !== "resume") {
|
|
1294
1331
|
updateStartupBanner(ctx, store);
|
|
@@ -1304,7 +1341,7 @@ function extension_default(pi) {
|
|
|
1304
1341
|
const messageTurns = entries.filter((e) => e.type === "message");
|
|
1305
1342
|
const isColdStart = messageTurns.length <= 1;
|
|
1306
1343
|
if (isColdStart) {
|
|
1307
|
-
const contextSnippet = renderColdStartAnchorsContext(store, ctx.cwd);
|
|
1344
|
+
const contextSnippet = renderColdStartAnchorsContext(store, ctx.cwd, void 0, preflightFailures);
|
|
1308
1345
|
if (contextSnippet) {
|
|
1309
1346
|
return {
|
|
1310
1347
|
systemPrompt: `${event.systemPrompt}
|
|
@@ -1603,4 +1640,28 @@ Mark as completed and archive?`
|
|
|
1603
1640
|
updateAnchorStatusBar(ctx, store);
|
|
1604
1641
|
}
|
|
1605
1642
|
});
|
|
1643
|
+
pi.on("session_shutdown", async (event, ctx) => {
|
|
1644
|
+
if (event.reason === "quit") {
|
|
1645
|
+
try {
|
|
1646
|
+
const gitStatus = (0, import_node_child_process2.execSync)("git status --porcelain", {
|
|
1647
|
+
cwd: ctx.cwd,
|
|
1648
|
+
encoding: "utf-8",
|
|
1649
|
+
timeout: 2e3,
|
|
1650
|
+
stdio: ["ignore", "pipe", "ignore"]
|
|
1651
|
+
}).trim();
|
|
1652
|
+
const activeAnchors = store.list({ cwd: ctx.cwd }).filter((a) => a.status === "active");
|
|
1653
|
+
if (gitStatus && activeAnchors.length === 0) {
|
|
1654
|
+
const home = process.env.HOME || process.env.USERPROFILE || "";
|
|
1655
|
+
const pendingPath = import_node_path5.default.join(home, ".anchor", "pending_exit.json");
|
|
1656
|
+
const data = {
|
|
1657
|
+
cwd: ctx.cwd,
|
|
1658
|
+
timestamp: Date.now(),
|
|
1659
|
+
modifiedCount: gitStatus.split("\n").filter(Boolean).length
|
|
1660
|
+
};
|
|
1661
|
+
import_node_fs3.default.writeFileSync(pendingPath, JSON.stringify(data, null, 2), "utf-8");
|
|
1662
|
+
}
|
|
1663
|
+
} catch (_) {
|
|
1664
|
+
}
|
|
1665
|
+
}
|
|
1666
|
+
});
|
|
1606
1667
|
}
|
package/dist/extension.js
CHANGED
|
@@ -11,23 +11,57 @@ import {
|
|
|
11
11
|
sweepStore,
|
|
12
12
|
updateAnchorStatusBar,
|
|
13
13
|
updateStartupBanner
|
|
14
|
-
} from "./chunk-
|
|
14
|
+
} from "./chunk-T62PHIQI.js";
|
|
15
15
|
|
|
16
16
|
// src/extension.ts
|
|
17
17
|
import { Type } from "@sinclair/typebox";
|
|
18
18
|
import { execSync } from "child_process";
|
|
19
|
+
import fs from "fs";
|
|
20
|
+
import path from "path";
|
|
19
21
|
var MUTATION_TOOLS = /* @__PURE__ */ new Set(["edit", "write", "patch", "apply_diff", "create_file", "modify"]);
|
|
20
22
|
function extension_default(pi) {
|
|
21
23
|
const store = new AnchorStore();
|
|
22
24
|
const observer = new SessionTouchObserver();
|
|
23
25
|
const annotatedThisSession = /* @__PURE__ */ new Set();
|
|
26
|
+
const preflightFailures = /* @__PURE__ */ new Map();
|
|
24
27
|
pi.on("session_start", async (event, ctx) => {
|
|
25
28
|
observer.clear();
|
|
26
29
|
annotatedThisSession.clear();
|
|
30
|
+
preflightFailures.clear();
|
|
27
31
|
const sweep = sweepStore(store);
|
|
28
32
|
if (sweep.transitionedToSleeping.length > 0) {
|
|
29
33
|
ctx.ui.notify(`Anchor: ${sweep.transitionedToSleeping.length} \u4E2A\u975E\u6D3B\u8DC3\u4EFB\u52A1\u5DF2\u8FDB\u5165\u4F11\u7720`, "info");
|
|
30
34
|
}
|
|
35
|
+
const activeAnchors = store.list({ cwd: ctx.cwd }).filter((a) => a.status === "active");
|
|
36
|
+
for (const a of activeAnchors) {
|
|
37
|
+
if (a.verifyCommand) {
|
|
38
|
+
try {
|
|
39
|
+
const res = runPhysicalVerification(a, ctx.cwd);
|
|
40
|
+
if (res.success) {
|
|
41
|
+
store.settle(a.id, {
|
|
42
|
+
settledBy: "verification-test",
|
|
43
|
+
summary: `Physical pre-flight test passed: ${a.verifyCommand}`
|
|
44
|
+
});
|
|
45
|
+
ctx.ui.notify(`\u2693 \u7269\u7406\u9A8C\u8BC1\u901A\u8FC7\uFF01\u951A\u70B9 #${a.id} ("${a.title}") \u5DF2\u81EA\u52A8\u7ED3\u6848\u5F52\u6863\u3002`, "info");
|
|
46
|
+
} else {
|
|
47
|
+
preflightFailures.set(a.id, res.output.slice(-250));
|
|
48
|
+
}
|
|
49
|
+
} catch (_) {
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
try {
|
|
54
|
+
const home = process.env.HOME || process.env.USERPROFILE || "";
|
|
55
|
+
const pendingPath = path.join(home, ".anchor", "pending_exit.json");
|
|
56
|
+
if (fs.existsSync(pendingPath)) {
|
|
57
|
+
const pending = JSON.parse(fs.readFileSync(pendingPath, "utf-8"));
|
|
58
|
+
if (pending.cwd === ctx.cwd && Date.now() - pending.timestamp < 24 * 60 * 60 * 1e3) {
|
|
59
|
+
ctx.ui.notify(`\u2316 [Anchor \u9000\u51FA\u5B88\u62A4] \u68C0\u6D4B\u5230\u4E0A\u6B21\u5F02\u5E38\u9000\u51FA\u4E14\u6709 ${pending.modifiedCount} \u4E2A\u672A\u63D0\u4EA4\u6539\u52A8\uFF0C\u53EF\u7528 anchor(action="pin") \u56FA\u5316\u4EFB\u52A1\u3002`, "info");
|
|
60
|
+
}
|
|
61
|
+
fs.unlinkSync(pendingPath);
|
|
62
|
+
}
|
|
63
|
+
} catch (_) {
|
|
64
|
+
}
|
|
31
65
|
updateAnchorStatusBar(ctx, store);
|
|
32
66
|
if (event.reason !== "resume") {
|
|
33
67
|
updateStartupBanner(ctx, store);
|
|
@@ -43,7 +77,7 @@ function extension_default(pi) {
|
|
|
43
77
|
const messageTurns = entries.filter((e) => e.type === "message");
|
|
44
78
|
const isColdStart = messageTurns.length <= 1;
|
|
45
79
|
if (isColdStart) {
|
|
46
|
-
const contextSnippet = renderColdStartAnchorsContext(store, ctx.cwd);
|
|
80
|
+
const contextSnippet = renderColdStartAnchorsContext(store, ctx.cwd, void 0, preflightFailures);
|
|
47
81
|
if (contextSnippet) {
|
|
48
82
|
return {
|
|
49
83
|
systemPrompt: `${event.systemPrompt}
|
|
@@ -342,6 +376,30 @@ Mark as completed and archive?`
|
|
|
342
376
|
updateAnchorStatusBar(ctx, store);
|
|
343
377
|
}
|
|
344
378
|
});
|
|
379
|
+
pi.on("session_shutdown", async (event, ctx) => {
|
|
380
|
+
if (event.reason === "quit") {
|
|
381
|
+
try {
|
|
382
|
+
const gitStatus = execSync("git status --porcelain", {
|
|
383
|
+
cwd: ctx.cwd,
|
|
384
|
+
encoding: "utf-8",
|
|
385
|
+
timeout: 2e3,
|
|
386
|
+
stdio: ["ignore", "pipe", "ignore"]
|
|
387
|
+
}).trim();
|
|
388
|
+
const activeAnchors = store.list({ cwd: ctx.cwd }).filter((a) => a.status === "active");
|
|
389
|
+
if (gitStatus && activeAnchors.length === 0) {
|
|
390
|
+
const home = process.env.HOME || process.env.USERPROFILE || "";
|
|
391
|
+
const pendingPath = path.join(home, ".anchor", "pending_exit.json");
|
|
392
|
+
const data = {
|
|
393
|
+
cwd: ctx.cwd,
|
|
394
|
+
timestamp: Date.now(),
|
|
395
|
+
modifiedCount: gitStatus.split("\n").filter(Boolean).length
|
|
396
|
+
};
|
|
397
|
+
fs.writeFileSync(pendingPath, JSON.stringify(data, null, 2), "utf-8");
|
|
398
|
+
}
|
|
399
|
+
} catch (_) {
|
|
400
|
+
}
|
|
401
|
+
}
|
|
402
|
+
});
|
|
345
403
|
}
|
|
346
404
|
export {
|
|
347
405
|
extension_default as default
|
package/dist/index.cjs
CHANGED
|
@@ -1286,16 +1286,16 @@ function makeSafeTaskAnnotation(filePath, anchor) {
|
|
|
1286
1286
|
}
|
|
1287
1287
|
return "\n\n" + formatter(`\u2316 anchor context: #${anchor.id} ${anchor.title} (${anchor.priority.toUpperCase()})`);
|
|
1288
1288
|
}
|
|
1289
|
-
function renderColdStartAnchorsContext(store, cwdOrNow, nowArg) {
|
|
1289
|
+
function renderColdStartAnchorsContext(store, cwdOrNow, nowArg, preflightFailures) {
|
|
1290
1290
|
let cwd;
|
|
1291
1291
|
let now = Date.now();
|
|
1292
1292
|
if (typeof cwdOrNow === "number") {
|
|
1293
1293
|
now = cwdOrNow;
|
|
1294
1294
|
} else if (typeof cwdOrNow === "string") {
|
|
1295
1295
|
cwd = cwdOrNow;
|
|
1296
|
-
|
|
1297
|
-
|
|
1298
|
-
|
|
1296
|
+
}
|
|
1297
|
+
if (typeof nowArg === "number") {
|
|
1298
|
+
now = nowArg;
|
|
1299
1299
|
}
|
|
1300
1300
|
sweepStore(store, now);
|
|
1301
1301
|
const activeAnchors = store.list({ status: "active", cwd });
|
|
@@ -1323,7 +1323,9 @@ function renderColdStartAnchorsContext(store, cwdOrNow, nowArg) {
|
|
|
1323
1323
|
}
|
|
1324
1324
|
const ageDays = Math.floor((now - a.createdAt) / (24 * 60 * 60 * 1e3));
|
|
1325
1325
|
const ageHint = ageDays > 0 ? ` (${ageDays}d old)` : "";
|
|
1326
|
-
|
|
1326
|
+
const failureSnippet = preflightFailures?.get(a.id);
|
|
1327
|
+
const verifyHint = failureSnippet ? ` [\u26A1 \u7269\u7406\u9A8C\u8BC1\u5931\u8D25: ${failureSnippet.replace(/\s+/g, " ").slice(-100)}]` : a.verifyCommand ? ` [verify: ${a.verifyCommand}]` : "";
|
|
1328
|
+
return `\u2022 #${a.id} ${projHint}${targetHint} [${a.priority.toUpperCase()}] ${a.title}${verifyHint}${ageHint}${tagHint}${fileHint}`;
|
|
1327
1329
|
});
|
|
1328
1330
|
return [
|
|
1329
1331
|
`<active-anchors count="${activeAnchors.length}">`,
|
|
@@ -1332,6 +1334,7 @@ function renderColdStartAnchorsContext(store, cwdOrNow, nowArg) {
|
|
|
1332
1334
|
``,
|
|
1333
1335
|
`CONVERSATIONAL PROTOCOL (HUMAN PARTNER TONE):`,
|
|
1334
1336
|
`- If the user opens the session with a casual greeting or continuation ("hi", "\u5728\u5417", "\u7EE7\u7EED", "\u65E9\u4E0A\u597D"), naturally prioritize items tagged [\u4ECA\u65E5\u805A\u7126\xB7Due Today] in a friendly assistant tone (e.g. "\u55E8\uFF01\u4ECA\u5929\u89C4\u5212\u7684\u3010...\u3011\u73B0\u5728\u5904\u7406\u8FD8\u662F\u770B\u522B\u7684\uFF1F").`,
|
|
1337
|
+
`- If an anchor has [\u26A1 \u7269\u7406\u9A8C\u8BC1\u5931\u8D25], its verifyCommand failed on session start. Target the failing assertion directly.`,
|
|
1335
1338
|
`- If the user issues a direct, concrete new instruction (e.g. "\u5E2E\u6211\u67E5\u4E0B\u8FD9\u4E2A\u62A5\u9519"), DO NOT interrupt their flow. Focus directly on their instruction.`,
|
|
1336
1339
|
`- When a commitment is satisfied during work, automatically call anchor(action="settle", id=...) to clear it.`,
|
|
1337
1340
|
`</active-anchors>`
|
package/dist/index.d.cts
CHANGED
|
@@ -397,7 +397,7 @@ declare function makeSafeTaskAnnotation(filePath: string, anchor: Anchor): strin
|
|
|
397
397
|
* Only active anchors for the current project context are injected.
|
|
398
398
|
* From turn 2 onwards, this returns empty string (0 tokens).
|
|
399
399
|
*/
|
|
400
|
-
declare function renderColdStartAnchorsContext(store: AnchorStore, cwdOrNow?: string | number, nowArg?: number): string;
|
|
400
|
+
declare function renderColdStartAnchorsContext(store: AnchorStore, cwdOrNow?: string | number, nowArg?: number, preflightFailures?: Map<string, string>): string;
|
|
401
401
|
/** Legacy alias for backwards compatibility */
|
|
402
402
|
declare const renderActiveAnchorsContext: typeof renderColdStartAnchorsContext;
|
|
403
403
|
|
package/dist/index.d.ts
CHANGED
|
@@ -397,7 +397,7 @@ declare function makeSafeTaskAnnotation(filePath: string, anchor: Anchor): strin
|
|
|
397
397
|
* Only active anchors for the current project context are injected.
|
|
398
398
|
* From turn 2 onwards, this returns empty string (0 tokens).
|
|
399
399
|
*/
|
|
400
|
-
declare function renderColdStartAnchorsContext(store: AnchorStore, cwdOrNow?: string | number, nowArg?: number): string;
|
|
400
|
+
declare function renderColdStartAnchorsContext(store: AnchorStore, cwdOrNow?: string | number, nowArg?: number, preflightFailures?: Map<string, string>): string;
|
|
401
401
|
/** Legacy alias for backwards compatibility */
|
|
402
402
|
declare const renderActiveAnchorsContext: typeof renderColdStartAnchorsContext;
|
|
403
403
|
|
package/dist/index.js
CHANGED
package/package.json
CHANGED
package/src/context_injector.ts
CHANGED
|
@@ -89,7 +89,8 @@ export function makeSafeTaskAnnotation(filePath: string, anchor: Anchor): string
|
|
|
89
89
|
export function renderColdStartAnchorsContext(
|
|
90
90
|
store: AnchorStore,
|
|
91
91
|
cwdOrNow?: string | number,
|
|
92
|
-
nowArg?: number
|
|
92
|
+
nowArg?: number,
|
|
93
|
+
preflightFailures?: Map<string, string>
|
|
93
94
|
): string {
|
|
94
95
|
let cwd: string | undefined;
|
|
95
96
|
let now = Date.now();
|
|
@@ -98,9 +99,9 @@ export function renderColdStartAnchorsContext(
|
|
|
98
99
|
now = cwdOrNow;
|
|
99
100
|
} else if (typeof cwdOrNow === 'string') {
|
|
100
101
|
cwd = cwdOrNow;
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
102
|
+
}
|
|
103
|
+
if (typeof nowArg === 'number') {
|
|
104
|
+
now = nowArg;
|
|
104
105
|
}
|
|
105
106
|
|
|
106
107
|
// 1. Run opportunistic sweep to ensure expired anchors don't leak into context
|
|
@@ -137,7 +138,14 @@ export function renderColdStartAnchorsContext(
|
|
|
137
138
|
const ageDays = Math.floor((now - a.createdAt) / (24 * 60 * 60 * 1000));
|
|
138
139
|
const ageHint = ageDays > 0 ? ` (${ageDays}d old)` : '';
|
|
139
140
|
|
|
140
|
-
|
|
141
|
+
const failureSnippet = preflightFailures?.get(a.id);
|
|
142
|
+
const verifyHint = failureSnippet
|
|
143
|
+
? ` [⚡ 物理验证失败: ${failureSnippet.replace(/\s+/g, ' ').slice(-100)}]`
|
|
144
|
+
: a.verifyCommand
|
|
145
|
+
? ` [verify: ${a.verifyCommand}]`
|
|
146
|
+
: '';
|
|
147
|
+
|
|
148
|
+
return `• #${a.id} ${projHint}${targetHint} [${a.priority.toUpperCase()}] ${a.title}${verifyHint}${ageHint}${tagHint}${fileHint}`;
|
|
141
149
|
});
|
|
142
150
|
|
|
143
151
|
return [
|
|
@@ -147,6 +155,7 @@ export function renderColdStartAnchorsContext(
|
|
|
147
155
|
``,
|
|
148
156
|
`CONVERSATIONAL PROTOCOL (HUMAN PARTNER TONE):`,
|
|
149
157
|
`- If the user opens the session with a casual greeting or continuation ("hi", "在吗", "继续", "早上好"), naturally prioritize items tagged [今日聚焦·Due Today] in a friendly assistant tone (e.g. "嗨!今天规划的【...】现在处理还是看别的?").`,
|
|
158
|
+
`- If an anchor has [⚡ 物理验证失败], its verifyCommand failed on session start. Target the failing assertion directly.`,
|
|
150
159
|
`- If the user issues a direct, concrete new instruction (e.g. "帮我查下这个报错"), DO NOT interrupt their flow. Focus directly on their instruction.`,
|
|
151
160
|
`- When a commitment is satisfied during work, automatically call anchor(action="settle", id=...) to clear it.`,
|
|
152
161
|
`</active-anchors>`
|
package/src/extension.ts
CHANGED
|
@@ -8,6 +8,7 @@ import { normalizePath, findMatchedAnchors } from './matcher.ts';
|
|
|
8
8
|
import { sweepStore } from './decay.ts';
|
|
9
9
|
import { updateAnchorStatusBar, openAnchorDashboard, updateStartupBanner } from './tui.ts';
|
|
10
10
|
import { execSync } from 'node:child_process';
|
|
11
|
+
import fs from 'node:fs';
|
|
11
12
|
import path from 'node:path';
|
|
12
13
|
|
|
13
14
|
const MUTATION_TOOLS = new Set(['edit', 'write', 'patch', 'apply_diff', 'create_file', 'modify']);
|
|
@@ -17,17 +18,52 @@ export default function (pi: ExtensionAPI) {
|
|
|
17
18
|
const store = new AnchorStore();
|
|
18
19
|
const observer = new SessionTouchObserver();
|
|
19
20
|
const annotatedThisSession = new Set<string>();
|
|
21
|
+
const preflightFailures = new Map<string, string>();
|
|
20
22
|
|
|
21
|
-
// 1. Session start: sweep stale tasks,
|
|
23
|
+
// 1. Session start: sweep stale tasks, run physical pre-flight tests, update TUI status bar
|
|
22
24
|
pi.on('session_start', async (event: any, ctx: ExtensionContext) => {
|
|
23
25
|
observer.clear();
|
|
24
26
|
annotatedThisSession.clear();
|
|
27
|
+
preflightFailures.clear();
|
|
28
|
+
|
|
25
29
|
const sweep = sweepStore(store);
|
|
26
30
|
|
|
27
31
|
if (sweep.transitionedToSleeping.length > 0) {
|
|
28
32
|
ctx.ui.notify(`Anchor: ${sweep.transitionedToSleeping.length} 个非活跃任务已进入休眠`, 'info');
|
|
29
33
|
}
|
|
30
34
|
|
|
35
|
+
// Physical Pre-flight Verification: auto-settle passing tasks or record failing output
|
|
36
|
+
const activeAnchors = store.list({ cwd: ctx.cwd }).filter(a => a.status === 'active');
|
|
37
|
+
for (const a of activeAnchors) {
|
|
38
|
+
if (a.verifyCommand) {
|
|
39
|
+
try {
|
|
40
|
+
const res = runPhysicalVerification(a, ctx.cwd);
|
|
41
|
+
if (res.success) {
|
|
42
|
+
store.settle(a.id, {
|
|
43
|
+
settledBy: 'verification-test',
|
|
44
|
+
summary: `Physical pre-flight test passed: ${a.verifyCommand}`
|
|
45
|
+
});
|
|
46
|
+
ctx.ui.notify(`⚓ 物理验证通过!锚点 #${a.id} ("${a.title}") 已自动结案归档。`, 'info');
|
|
47
|
+
} else {
|
|
48
|
+
preflightFailures.set(a.id, res.output.slice(-250));
|
|
49
|
+
}
|
|
50
|
+
} catch (_) {}
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
// Exit Guard notification check
|
|
55
|
+
try {
|
|
56
|
+
const home = process.env.HOME || process.env.USERPROFILE || '';
|
|
57
|
+
const pendingPath = path.join(home, '.anchor', 'pending_exit.json');
|
|
58
|
+
if (fs.existsSync(pendingPath)) {
|
|
59
|
+
const pending = JSON.parse(fs.readFileSync(pendingPath, 'utf-8'));
|
|
60
|
+
if (pending.cwd === ctx.cwd && Date.now() - pending.timestamp < 24 * 60 * 60 * 1000) {
|
|
61
|
+
ctx.ui.notify(`⌖ [Anchor 退出守护] 检测到上次异常退出且有 ${pending.modifiedCount} 个未提交改动,可用 anchor(action="pin") 固化任务。`, 'info');
|
|
62
|
+
}
|
|
63
|
+
fs.unlinkSync(pendingPath);
|
|
64
|
+
}
|
|
65
|
+
} catch (_) {}
|
|
66
|
+
|
|
31
67
|
updateAnchorStatusBar(ctx, store);
|
|
32
68
|
|
|
33
69
|
// Only render startup banner on initial startup or new session, not on mid-session resume
|
|
@@ -50,7 +86,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
50
86
|
const isColdStart = messageTurns.length <= 1;
|
|
51
87
|
|
|
52
88
|
if (isColdStart) {
|
|
53
|
-
const contextSnippet = renderColdStartAnchorsContext(store, ctx.cwd);
|
|
89
|
+
const contextSnippet = renderColdStartAnchorsContext(store, ctx.cwd, undefined, preflightFailures);
|
|
54
90
|
if (contextSnippet) {
|
|
55
91
|
return {
|
|
56
92
|
systemPrompt: `${event.systemPrompt}\n\n${contextSnippet}`
|
|
@@ -398,4 +434,30 @@ export default function (pi: ExtensionAPI) {
|
|
|
398
434
|
updateAnchorStatusBar(ctx, store);
|
|
399
435
|
}
|
|
400
436
|
});
|
|
437
|
+
|
|
438
|
+
// 7. Exit Guard: detect uncommitted changes on abrupt session shutdown
|
|
439
|
+
pi.on('session_shutdown', async (event: any, ctx: ExtensionContext) => {
|
|
440
|
+
if (event.reason === 'quit') {
|
|
441
|
+
try {
|
|
442
|
+
const gitStatus = execSync('git status --porcelain', {
|
|
443
|
+
cwd: ctx.cwd,
|
|
444
|
+
encoding: 'utf-8',
|
|
445
|
+
timeout: 2000,
|
|
446
|
+
stdio: ['ignore', 'pipe', 'ignore']
|
|
447
|
+
}).trim();
|
|
448
|
+
|
|
449
|
+
const activeAnchors = store.list({ cwd: ctx.cwd }).filter(a => a.status === 'active');
|
|
450
|
+
if (gitStatus && activeAnchors.length === 0) {
|
|
451
|
+
const home = process.env.HOME || process.env.USERPROFILE || '';
|
|
452
|
+
const pendingPath = path.join(home, '.anchor', 'pending_exit.json');
|
|
453
|
+
const data = {
|
|
454
|
+
cwd: ctx.cwd,
|
|
455
|
+
timestamp: Date.now(),
|
|
456
|
+
modifiedCount: gitStatus.split('\n').filter(Boolean).length
|
|
457
|
+
};
|
|
458
|
+
fs.writeFileSync(pendingPath, JSON.stringify(data, null, 2), 'utf-8');
|
|
459
|
+
}
|
|
460
|
+
} catch (_) {}
|
|
461
|
+
}
|
|
462
|
+
});
|
|
401
463
|
}
|