pi-anchor 0.2.4 → 0.3.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/README.md +22 -18
- package/README_zh.md +22 -18
- package/assets/hero-en.svg +39 -60
- package/assets/hero-zh.svg +39 -60
- package/bin/anchor.js +38 -51
- package/dist/{chunk-G72UHPBN.js → chunk-Q7IILJFA.js} +165 -43
- package/dist/extension.cjs +188 -54
- package/dist/extension.d.cts +1 -1
- package/dist/extension.d.ts +1 -1
- package/dist/extension.js +34 -16
- package/dist/index.cjs +169 -43
- package/dist/index.d.cts +63 -36
- package/dist/index.d.ts +63 -36
- package/dist/index.js +9 -1
- package/dist/{store-BNKjzhS_.d.cts → store-t2_TDhJn.d.cts} +2 -1
- package/dist/{store-BNKjzhS_.d.ts → store-t2_TDhJn.d.ts} +2 -1
- package/package.json +2 -2
- package/src/extension.ts +43 -19
- package/src/matcher.ts +51 -0
- package/src/settlement.ts +72 -6
- package/src/store.ts +25 -9
- package/src/tui.ts +77 -29
- package/src/types.ts +7 -7
package/dist/index.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { a as AnchorStatus, b as Anchor, A as AnchorStore, T as TouchMatchResult, S as SettlementProposal } from './store-
|
|
2
|
-
export { c as AnchorDecayPolicy, d as AnchorDurability, e as AnchorEvidence, f as AnchorPriority, g as AnchorStoreState, D as DEFAULT_DECAY_POLICY, h as DURABLE_DECAY_POLICY, E as EPHEMERAL_DECAY_POLICY, i as acquireSyncLock, j as atomicRenameWithRetry, k as detectDurability, l as detectRecurrence, m as detectTargetDate, n as getDefaultStorageDir, o as getEphemeralDecayPolicy, p as getTodayDateString } from './store-
|
|
1
|
+
import { a as AnchorStatus, b as Anchor, A as AnchorStore, T as TouchMatchResult, S as SettlementProposal } from './store-t2_TDhJn.js';
|
|
2
|
+
export { c as AnchorDecayPolicy, d as AnchorDurability, e as AnchorEvidence, f as AnchorPriority, g as AnchorStoreState, D as DEFAULT_DECAY_POLICY, h as DURABLE_DECAY_POLICY, E as EPHEMERAL_DECAY_POLICY, i as acquireSyncLock, j as atomicRenameWithRetry, k as detectDurability, l as detectRecurrence, m as detectTargetDate, n as getDefaultStorageDir, o as getEphemeralDecayPolicy, p as getTodayDateString } from './store-t2_TDhJn.js';
|
|
3
3
|
import { ExtensionContext } from '@earendil-works/pi-coding-agent';
|
|
4
4
|
|
|
5
5
|
interface DecayEvaluation {
|
|
@@ -35,6 +35,16 @@ declare function normalizePath(filePath: string, baseCwd?: string): string;
|
|
|
35
35
|
* Compare two workspace CWDs safely (case-insensitive on Windows, supports subdirectory scoping)
|
|
36
36
|
*/
|
|
37
37
|
declare function isSameOrSubPath(anchorCwd?: string, targetCwd?: string): boolean;
|
|
38
|
+
/**
|
|
39
|
+
* Extract explicit file or directory paths mentioned in natural language title/description
|
|
40
|
+
* (e.g. "D:/College/优化/EXTERNAL_ARCHITECTURE_INSPIRATIONS.md", "src/auth/jwt.ts")
|
|
41
|
+
*/
|
|
42
|
+
declare function extractPathsFromText(text?: string): string[];
|
|
43
|
+
/**
|
|
44
|
+
* Determine whether an anchor specifically targets a given workspace directory (`targetCwd`),
|
|
45
|
+
* checking `anchor.cwd`, `anchor.project`, and `anchor.files`.
|
|
46
|
+
*/
|
|
47
|
+
declare function isAnchorTargetingWorkspace(anchor: Anchor, targetCwd?: string): boolean;
|
|
38
48
|
/**
|
|
39
49
|
* Match touched files against an anchor's registered physical patterns
|
|
40
50
|
* Only anchors with explicit files/globs/prefixes are matched against file touches.
|
|
@@ -47,6 +57,35 @@ declare function matchAnchorAgainstTouchedFiles(anchor: Anchor, touchedFiles: st
|
|
|
47
57
|
*/
|
|
48
58
|
declare function findMatchedAnchors(anchors: Anchor[], touchedFiles: string[]): TouchMatchResult[];
|
|
49
59
|
|
|
60
|
+
declare class SessionTouchObserver {
|
|
61
|
+
private touchedFiles;
|
|
62
|
+
private modifiedFiles;
|
|
63
|
+
private inspectedFiles;
|
|
64
|
+
private committed;
|
|
65
|
+
private commitMessages;
|
|
66
|
+
clear(): void;
|
|
67
|
+
/**
|
|
68
|
+
* Observe and record a tool call invocation, separating inspection from mutation
|
|
69
|
+
*/
|
|
70
|
+
recordToolCall(toolName: string, input: Record<string, unknown>): void;
|
|
71
|
+
/**
|
|
72
|
+
* Add a file path manually (e.g. from git status diff)
|
|
73
|
+
*/
|
|
74
|
+
addTouchedFile(filePath: string, isModified?: boolean): void;
|
|
75
|
+
getModifiedFiles(): string[];
|
|
76
|
+
getInspectedFiles(): string[];
|
|
77
|
+
getTouchedFiles(): string[];
|
|
78
|
+
hasCommitted(): boolean;
|
|
79
|
+
getCommitMessages(): string[];
|
|
80
|
+
/**
|
|
81
|
+
* Check if any commit message explicitly references or resolves an anchor
|
|
82
|
+
*/
|
|
83
|
+
matchesCommit(anchor: Anchor): {
|
|
84
|
+
matched: boolean;
|
|
85
|
+
message?: string;
|
|
86
|
+
};
|
|
87
|
+
}
|
|
88
|
+
|
|
50
89
|
/**
|
|
51
90
|
* Execute physical verification test command for an anchor
|
|
52
91
|
*/
|
|
@@ -55,9 +94,18 @@ declare function runPhysicalVerification(anchor: Anchor, cwd: string): {
|
|
|
55
94
|
output: string;
|
|
56
95
|
};
|
|
57
96
|
/**
|
|
58
|
-
*
|
|
97
|
+
* Real-time autonomous settlement during active session:
|
|
98
|
+
* Triggered immediately when a non-error shell command runs `git commit` or a matching `verifyCommand`.
|
|
99
|
+
* Works for BOTH file-bound and file-less (`files: []`) anchors!
|
|
59
100
|
*/
|
|
60
|
-
declare function
|
|
101
|
+
declare function tryRealtimeAutoSettle(store: AnchorStore, observer: SessionTouchObserver, cwd?: string, executedCommand?: string): Array<{
|
|
102
|
+
anchor: Anchor;
|
|
103
|
+
reason: string;
|
|
104
|
+
}>;
|
|
105
|
+
/**
|
|
106
|
+
* Generate settlement candidates based on files touched or commits made during the session
|
|
107
|
+
*/
|
|
108
|
+
declare function generateSettlementProposals(store: AnchorStore, touchedFiles: string[], cwd?: string, observer?: SessionTouchObserver): SettlementProposal[];
|
|
61
109
|
/**
|
|
62
110
|
* Format settlement proposal into terminal-ready card
|
|
63
111
|
*/
|
|
@@ -82,6 +130,14 @@ declare function classifyAnchor(anchor: Anchor, now?: number): AnchorQuadrant;
|
|
|
82
130
|
* Group active anchors by cognitive quadrant
|
|
83
131
|
*/
|
|
84
132
|
declare function groupAnchorsByQuadrant(anchors: Anchor[], now?: number): GroupedAnchors;
|
|
133
|
+
/**
|
|
134
|
+
* Sort unfinished anchors into a single compact, priority-driven engineering list:
|
|
135
|
+
* 1. Active before Sleeping (quiet)
|
|
136
|
+
* 2. Matching current workspace before other workspaces
|
|
137
|
+
* 3. Due Today / Overdue before open-ended
|
|
138
|
+
* 4. Priority (P0 -> P1 -> P2)
|
|
139
|
+
*/
|
|
140
|
+
declare function sortAnchorsForDisplay(anchors: Anchor[], cwd?: string, now?: number): Anchor[];
|
|
85
141
|
/**
|
|
86
142
|
* Strip ANSI escape codes from string
|
|
87
143
|
*/
|
|
@@ -134,7 +190,7 @@ declare function formatRelativeTime(timestamp: number, now?: number): string;
|
|
|
134
190
|
*/
|
|
135
191
|
declare function formatRemainingTtl(anchor: Anchor, now?: number): string | undefined;
|
|
136
192
|
/**
|
|
137
|
-
* Format origin
|
|
193
|
+
* Format origin workspace / project label concisely
|
|
138
194
|
*/
|
|
139
195
|
declare function formatOrigin(anchor: Anchor | string): string;
|
|
140
196
|
/**
|
|
@@ -148,39 +204,10 @@ declare function updateAnchorStatusBar(ctx: ExtensionContext, store: AnchorStore
|
|
|
148
204
|
*/
|
|
149
205
|
declare function updateStartupBanner(ctx: ExtensionContext, store: AnchorStore): void;
|
|
150
206
|
/**
|
|
151
|
-
* Clean,
|
|
207
|
+
* Clean, flat priority-sorted engineering checklist (includes both active and quiet/sleeping tasks so nothing is ever lost)
|
|
152
208
|
*/
|
|
153
209
|
declare function openAnchorDashboard(ctx: ExtensionContext, store: AnchorStore): Promise<void>;
|
|
154
210
|
|
|
155
|
-
declare class SessionTouchObserver {
|
|
156
|
-
private touchedFiles;
|
|
157
|
-
private modifiedFiles;
|
|
158
|
-
private inspectedFiles;
|
|
159
|
-
private committed;
|
|
160
|
-
private commitMessages;
|
|
161
|
-
clear(): void;
|
|
162
|
-
/**
|
|
163
|
-
* Observe and record a tool call invocation, separating inspection from mutation
|
|
164
|
-
*/
|
|
165
|
-
recordToolCall(toolName: string, input: Record<string, unknown>): void;
|
|
166
|
-
/**
|
|
167
|
-
* Add a file path manually (e.g. from git status diff)
|
|
168
|
-
*/
|
|
169
|
-
addTouchedFile(filePath: string, isModified?: boolean): void;
|
|
170
|
-
getModifiedFiles(): string[];
|
|
171
|
-
getInspectedFiles(): string[];
|
|
172
|
-
getTouchedFiles(): string[];
|
|
173
|
-
hasCommitted(): boolean;
|
|
174
|
-
getCommitMessages(): string[];
|
|
175
|
-
/**
|
|
176
|
-
* Check if any commit message explicitly references or resolves an anchor
|
|
177
|
-
*/
|
|
178
|
-
matchesCommit(anchor: Anchor): {
|
|
179
|
-
matched: boolean;
|
|
180
|
-
message?: string;
|
|
181
|
-
};
|
|
182
|
-
}
|
|
183
|
-
|
|
184
211
|
/**
|
|
185
212
|
* Universal multi-language comment generator.
|
|
186
213
|
* Maps file extension to safe comment syntax.
|
|
@@ -245,4 +272,4 @@ declare class AnchorProtocol {
|
|
|
245
272
|
};
|
|
246
273
|
}
|
|
247
274
|
|
|
248
|
-
export { type AgentAdapter, Anchor, AnchorProtocol, type AnchorQuadrant, AnchorStatus, AnchorStore, COMMENT_FORMATS, type DecayEvaluation, type GroupedAnchors, SessionTouchObserver, SettlementProposal, type SweepResult, TouchMatchResult, classifyAnchor, evaluateAnchorDecay, findMatchedAnchors, formatCreationTime, formatOrigin, formatRelativeTime, formatRemainingTtl, formatSettlementCard, formatTargetDate, generateSettlementProposals, getDisplayWidth, globToRegExp, groupAnchorsByQuadrant, hasActiveUI, isSameOrSubPath, makeSafeTaskAnnotation, matchAnchorAgainstTouchedFiles, normalizePath, openAnchorDashboard, padToWidth, renderActiveAnchorsContext, renderColdStartAnchorsContext, runPhysicalVerification, stripAnsi, sweepStore, truncateToWidth, updateAnchorStatusBar, updateStartupBanner };
|
|
275
|
+
export { type AgentAdapter, Anchor, AnchorProtocol, type AnchorQuadrant, AnchorStatus, AnchorStore, COMMENT_FORMATS, type DecayEvaluation, type GroupedAnchors, SessionTouchObserver, SettlementProposal, type SweepResult, TouchMatchResult, classifyAnchor, evaluateAnchorDecay, extractPathsFromText, findMatchedAnchors, formatCreationTime, formatOrigin, formatRelativeTime, formatRemainingTtl, formatSettlementCard, formatTargetDate, generateSettlementProposals, getDisplayWidth, globToRegExp, groupAnchorsByQuadrant, hasActiveUI, isAnchorTargetingWorkspace, isSameOrSubPath, makeSafeTaskAnnotation, matchAnchorAgainstTouchedFiles, normalizePath, openAnchorDashboard, padToWidth, renderActiveAnchorsContext, renderColdStartAnchorsContext, runPhysicalVerification, sortAnchorsForDisplay, stripAnsi, sweepStore, truncateToWidth, tryRealtimeAutoSettle, updateAnchorStatusBar, updateStartupBanner };
|
package/dist/index.js
CHANGED
|
@@ -12,6 +12,7 @@ import {
|
|
|
12
12
|
detectRecurrence,
|
|
13
13
|
detectTargetDate,
|
|
14
14
|
evaluateAnchorDecay,
|
|
15
|
+
extractPathsFromText,
|
|
15
16
|
findMatchedAnchors,
|
|
16
17
|
formatCreationTime,
|
|
17
18
|
formatOrigin,
|
|
@@ -27,6 +28,7 @@ import {
|
|
|
27
28
|
globToRegExp,
|
|
28
29
|
groupAnchorsByQuadrant,
|
|
29
30
|
hasActiveUI,
|
|
31
|
+
isAnchorTargetingWorkspace,
|
|
30
32
|
isSameOrSubPath,
|
|
31
33
|
makeSafeTaskAnnotation,
|
|
32
34
|
matchAnchorAgainstTouchedFiles,
|
|
@@ -36,12 +38,14 @@ import {
|
|
|
36
38
|
renderActiveAnchorsContext,
|
|
37
39
|
renderColdStartAnchorsContext,
|
|
38
40
|
runPhysicalVerification,
|
|
41
|
+
sortAnchorsForDisplay,
|
|
39
42
|
stripAnsi,
|
|
40
43
|
sweepStore,
|
|
41
44
|
truncateToWidth,
|
|
45
|
+
tryRealtimeAutoSettle,
|
|
42
46
|
updateAnchorStatusBar,
|
|
43
47
|
updateStartupBanner
|
|
44
|
-
} from "./chunk-
|
|
48
|
+
} from "./chunk-Q7IILJFA.js";
|
|
45
49
|
|
|
46
50
|
// src/protocol.ts
|
|
47
51
|
var AnchorProtocol = class {
|
|
@@ -116,6 +120,7 @@ export {
|
|
|
116
120
|
detectRecurrence,
|
|
117
121
|
detectTargetDate,
|
|
118
122
|
evaluateAnchorDecay,
|
|
123
|
+
extractPathsFromText,
|
|
119
124
|
findMatchedAnchors,
|
|
120
125
|
formatCreationTime,
|
|
121
126
|
formatOrigin,
|
|
@@ -131,6 +136,7 @@ export {
|
|
|
131
136
|
globToRegExp,
|
|
132
137
|
groupAnchorsByQuadrant,
|
|
133
138
|
hasActiveUI,
|
|
139
|
+
isAnchorTargetingWorkspace,
|
|
134
140
|
isSameOrSubPath,
|
|
135
141
|
makeSafeTaskAnnotation,
|
|
136
142
|
matchAnchorAgainstTouchedFiles,
|
|
@@ -140,9 +146,11 @@ export {
|
|
|
140
146
|
renderActiveAnchorsContext,
|
|
141
147
|
renderColdStartAnchorsContext,
|
|
142
148
|
runPhysicalVerification,
|
|
149
|
+
sortAnchorsForDisplay,
|
|
143
150
|
stripAnsi,
|
|
144
151
|
sweepStore,
|
|
145
152
|
truncateToWidth,
|
|
153
|
+
tryRealtimeAutoSettle,
|
|
146
154
|
updateAnchorStatusBar,
|
|
147
155
|
updateStartupBanner
|
|
148
156
|
};
|
|
@@ -87,7 +87,8 @@ declare const DEFAULT_DECAY_POLICY: AnchorDecayPolicy;
|
|
|
87
87
|
*/
|
|
88
88
|
declare function detectDurability(title: string): AnchorDurability;
|
|
89
89
|
/**
|
|
90
|
-
* Get custom ephemeral decay policy based on temporal span
|
|
90
|
+
* Get custom ephemeral decay policy based on temporal span.
|
|
91
|
+
* Note: graveyardDays is always 9999 so unfinished tasks are NEVER silently deleted.
|
|
91
92
|
*/
|
|
92
93
|
declare function getEphemeralDecayPolicy(title: string): AnchorDecayPolicy;
|
|
93
94
|
/**
|
|
@@ -87,7 +87,8 @@ declare const DEFAULT_DECAY_POLICY: AnchorDecayPolicy;
|
|
|
87
87
|
*/
|
|
88
88
|
declare function detectDurability(title: string): AnchorDurability;
|
|
89
89
|
/**
|
|
90
|
-
* Get custom ephemeral decay policy based on temporal span
|
|
90
|
+
* Get custom ephemeral decay policy based on temporal span.
|
|
91
|
+
* Note: graveyardDays is always 9999 so unfinished tasks are NEVER silently deleted.
|
|
91
92
|
*/
|
|
92
93
|
declare function getEphemeralDecayPolicy(title: string): AnchorDecayPolicy;
|
|
93
94
|
/**
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "pi-anchor",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"description": "
|
|
3
|
+
"version": "0.3.0",
|
|
4
|
+
"description": "Compact, autonomous, persistent task memory for Pi — pin tasks across sessions (/pin), auto-wake on project/file touch, and auto-settle in real time on git commit with 0 TODO.md clutter.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "dist/index.js",
|
|
7
7
|
"module": "dist/index.js",
|
package/src/extension.ts
CHANGED
|
@@ -3,10 +3,10 @@ import { Type } from '@sinclair/typebox';
|
|
|
3
3
|
import { AnchorStore, detectTargetDate } from './store.ts';
|
|
4
4
|
import { SessionTouchObserver } from './observer.ts';
|
|
5
5
|
import { renderColdStartAnchorsContext, renderActiveAnchorsContext, makeSafeTaskAnnotation } from './context_injector.ts';
|
|
6
|
-
import { generateSettlementProposals, runPhysicalVerification } from './settlement.ts';
|
|
7
|
-
import { normalizePath, isSameOrSubPath, findMatchedAnchors } from './matcher.ts';
|
|
6
|
+
import { generateSettlementProposals, runPhysicalVerification, tryRealtimeAutoSettle } from './settlement.ts';
|
|
7
|
+
import { normalizePath, isSameOrSubPath, findMatchedAnchors, isAnchorTargetingWorkspace } from './matcher.ts';
|
|
8
8
|
import { sweepStore } from './decay.ts';
|
|
9
|
-
import { updateAnchorStatusBar, openAnchorDashboard, updateStartupBanner, truncateToWidth, hasActiveUI,
|
|
9
|
+
import { updateAnchorStatusBar, openAnchorDashboard, updateStartupBanner, truncateToWidth, hasActiveUI, sortAnchorsForDisplay } from './tui.ts';
|
|
10
10
|
import { execSync } from 'node:child_process';
|
|
11
11
|
import fs from 'node:fs';
|
|
12
12
|
import path from 'node:path';
|
|
@@ -63,19 +63,15 @@ export function parseAnchorCommandInput(raw: string, now: number = Date.now()):
|
|
|
63
63
|
*/
|
|
64
64
|
export function resolveAnchorTarget(store: AnchorStore, idOrIndex: string, cwd?: string): string | undefined {
|
|
65
65
|
const clean = idOrIndex.trim().replace(/^#/, '');
|
|
66
|
-
// 1. Check 1-based sequential index from the
|
|
66
|
+
// 1. Check 1-based sequential index from the sorted display list first when user passes a plain number
|
|
67
67
|
if (/^\d+$/.test(clean)) {
|
|
68
68
|
const seqIndex = parseInt(clean, 10);
|
|
69
|
-
const
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
...groups.backlog
|
|
76
|
-
];
|
|
77
|
-
if (seqIndex >= 1 && seqIndex <= sortedActive.length) {
|
|
78
|
-
return sortedActive[seqIndex - 1].id;
|
|
69
|
+
const visible = store
|
|
70
|
+
.list({ cwd, all: true })
|
|
71
|
+
.filter(a => a.status === 'active' || a.status === 'sleeping');
|
|
72
|
+
const sorted = sortAnchorsForDisplay(visible, cwd);
|
|
73
|
+
if (seqIndex >= 1 && seqIndex <= sorted.length) {
|
|
74
|
+
return sorted[seqIndex - 1].id;
|
|
79
75
|
}
|
|
80
76
|
}
|
|
81
77
|
// 2. Fallback to direct ID lookup in store
|
|
@@ -98,8 +94,16 @@ export default function (pi: ExtensionAPI) {
|
|
|
98
94
|
|
|
99
95
|
const sweep = sweepStore(store);
|
|
100
96
|
|
|
101
|
-
if
|
|
102
|
-
|
|
97
|
+
// Smart Workspace Wakeup: if a sleeping anchor specifically targets the workspace we just opened, wake it automatically!
|
|
98
|
+
const sleepingForWorkspace = store
|
|
99
|
+
.list({ status: 'sleeping', all: true })
|
|
100
|
+
.filter(a => (a.files.length > 0 || (a.project && a.project !== 'global') || a.cwd) && isAnchorTargetingWorkspace(a, ctx.cwd));
|
|
101
|
+
for (const sa of sleepingForWorkspace) {
|
|
102
|
+
store.touch(sa.id);
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
if (sweep.transitionedToSleeping.length > 0 && sleepingForWorkspace.length === 0) {
|
|
106
|
+
ctx.ui.notify(`⌖ Anchor: ${sweep.transitionedToSleeping.length} inactive task(s) moved to quiet mode (/anchor to view)`, 'info');
|
|
103
107
|
}
|
|
104
108
|
|
|
105
109
|
// Physical Pre-flight Verification: auto-settle passing tasks or record failing output (scoped strictly to current cwd)
|
|
@@ -174,11 +178,31 @@ export default function (pi: ExtensionAPI) {
|
|
|
174
178
|
}
|
|
175
179
|
});
|
|
176
180
|
|
|
177
|
-
// 4. JIT Path-Triggered Context Alert
|
|
181
|
+
// 4. JIT Path-Triggered Context Alert & Real-Time Autonomous Commit/Test Settlement
|
|
178
182
|
pi.on('tool_result', async (event: any, ctx: ExtensionContext) => {
|
|
183
|
+
const lowerTool = String(event.toolName || '').toLowerCase();
|
|
184
|
+
|
|
179
185
|
// Only verified, non-error tool executions are recorded to prevent false-positive settlement
|
|
180
186
|
if (!event.isError) {
|
|
181
187
|
observer.recordToolCall(event.toolName, event.input || {});
|
|
188
|
+
|
|
189
|
+
// Real-time autonomous settlement on git commit or passing verifyCommand!
|
|
190
|
+
if (lowerTool === 'bash' || lowerTool === 'powershell') {
|
|
191
|
+
const executedCmd = String((event.input as any)?.command || '');
|
|
192
|
+
if (executedCmd.includes('git commit') || executedCmd.length > 0) {
|
|
193
|
+
const settledNow = tryRealtimeAutoSettle(store, observer, ctx.cwd, executedCmd);
|
|
194
|
+
if (settledNow.length > 0) {
|
|
195
|
+
updateAnchorStatusBar(ctx, store);
|
|
196
|
+
if (hasActiveUI(ctx)) {
|
|
197
|
+
for (const item of settledNow) {
|
|
198
|
+
try {
|
|
199
|
+
ctx.ui.notify(`⌖ Auto-settled #${item.anchor.id} ("${item.anchor.title}") via ${item.reason}`, 'info');
|
|
200
|
+
} catch {}
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
}
|
|
182
206
|
}
|
|
183
207
|
|
|
184
208
|
const pathInput = (event.input as any)?.path;
|
|
@@ -259,9 +283,9 @@ export default function (pi: ExtensionAPI) {
|
|
|
259
283
|
}
|
|
260
284
|
|
|
261
285
|
const modified = observer.getModifiedFiles();
|
|
262
|
-
if (modified.length === 0) return;
|
|
286
|
+
if (modified.length === 0 && !observer.hasCommitted()) return;
|
|
263
287
|
|
|
264
|
-
const proposals = generateSettlementProposals(store, modified, ctx.cwd);
|
|
288
|
+
const proposals = generateSettlementProposals(store, modified, ctx.cwd, observer);
|
|
265
289
|
if (proposals.length === 0) return;
|
|
266
290
|
|
|
267
291
|
// Check proposals for automated verification or one-tap settlement
|
package/src/matcher.ts
CHANGED
|
@@ -51,6 +51,57 @@ export function isSameOrSubPath(anchorCwd?: string, targetCwd?: string): boolean
|
|
|
51
51
|
return cmpA === cmpT || cmpT.startsWith(cmpA + '/') || cmpA.startsWith(cmpT + '/');
|
|
52
52
|
}
|
|
53
53
|
|
|
54
|
+
/**
|
|
55
|
+
* Extract explicit file or directory paths mentioned in natural language title/description
|
|
56
|
+
* (e.g. "D:/College/优化/EXTERNAL_ARCHITECTURE_INSPIRATIONS.md", "src/auth/jwt.ts")
|
|
57
|
+
*/
|
|
58
|
+
export function extractPathsFromText(text?: string): string[] {
|
|
59
|
+
if (!text) return [];
|
|
60
|
+
const results = new Set<string>();
|
|
61
|
+
// Match absolute Windows/POSIX paths or relative project paths with extensions/slashes
|
|
62
|
+
const regex = /(?:[a-zA-Z]:[\\/][^\s"'`,。;!?、,;)>\]]+|(?:\b(?:src|lib|dist|tests|test|docs|scripts|packages|extensions|bin|assets)[\\/][^\s"'`,。;!?、,;)>\]]+)|(?:\b[\w.-]+\.(?:ts|tsx|js|jsx|mjs|cjs|py|rs|go|java|c|cpp|h|cs|md|json|yaml|yml|toml|html|css|sql|sh)\b))/g;
|
|
63
|
+
let match: RegExpExecArray | null;
|
|
64
|
+
while ((match = regex.exec(text)) !== null) {
|
|
65
|
+
const cleaned = normalizePath(match[0].replace(/[.:;,'")\]}>]+$/, ''));
|
|
66
|
+
if (cleaned && cleaned.length >= 3) {
|
|
67
|
+
results.add(cleaned);
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
return Array.from(results);
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* Determine whether an anchor specifically targets a given workspace directory (`targetCwd`),
|
|
75
|
+
* checking `anchor.cwd`, `anchor.project`, and `anchor.files`.
|
|
76
|
+
*/
|
|
77
|
+
export function isAnchorTargetingWorkspace(anchor: Anchor, targetCwd?: string): boolean {
|
|
78
|
+
if (!targetCwd) return true;
|
|
79
|
+
const normTarget = targetCwd.trim().replace(/\\/g, '/').replace(/\/+$/, '');
|
|
80
|
+
const targetBase = path.basename(normTarget).toLowerCase();
|
|
81
|
+
|
|
82
|
+
// 1. Check explicit project path or project folder name
|
|
83
|
+
if (anchor.project && anchor.project !== 'global') {
|
|
84
|
+
const normProj = anchor.project.trim().replace(/\\/g, '/').replace(/\/+$/, '');
|
|
85
|
+
if (isSameOrSubPath(normProj, normTarget)) return true;
|
|
86
|
+
const projBase = path.basename(normProj).toLowerCase();
|
|
87
|
+
if (projBase && projBase === targetBase) return true;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
// 2. Check associated file paths
|
|
91
|
+
if (anchor.files && anchor.files.length > 0) {
|
|
92
|
+
for (const f of anchor.files) {
|
|
93
|
+
if (isSameOrSubPath(f, normTarget)) return true;
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
// 3. Check creation cwd (only if files are relative or empty, or if no conflicting absolute project path was given)
|
|
98
|
+
if (anchor.cwd && isSameOrSubPath(anchor.cwd, normTarget)) {
|
|
99
|
+
return true;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
return false;
|
|
103
|
+
}
|
|
104
|
+
|
|
54
105
|
/**
|
|
55
106
|
* Match touched files against an anchor's registered physical patterns
|
|
56
107
|
* Only anchors with explicit files/globs/prefixes are matched against file touches.
|
package/src/settlement.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import type { AnchorStore } from './store.ts';
|
|
2
|
-
import { findMatchedAnchors } from './matcher.ts';
|
|
2
|
+
import { findMatchedAnchors, isAnchorTargetingWorkspace } from './matcher.ts';
|
|
3
|
+
import type { SessionTouchObserver } from './observer.ts';
|
|
3
4
|
import type { Anchor, SettlementProposal } from './types.ts';
|
|
4
5
|
import { execSync } from 'node:child_process';
|
|
5
6
|
|
|
@@ -27,31 +28,96 @@ export function runPhysicalVerification(anchor: Anchor, cwd: string): { success:
|
|
|
27
28
|
}
|
|
28
29
|
|
|
29
30
|
/**
|
|
30
|
-
*
|
|
31
|
+
* Real-time autonomous settlement during active session:
|
|
32
|
+
* Triggered immediately when a non-error shell command runs `git commit` or a matching `verifyCommand`.
|
|
33
|
+
* Works for BOTH file-bound and file-less (`files: []`) anchors!
|
|
34
|
+
*/
|
|
35
|
+
export function tryRealtimeAutoSettle(
|
|
36
|
+
store: AnchorStore,
|
|
37
|
+
observer: SessionTouchObserver,
|
|
38
|
+
cwd?: string,
|
|
39
|
+
executedCommand?: string
|
|
40
|
+
): Array<{ anchor: Anchor; reason: string }> {
|
|
41
|
+
const candidates = store.list({ cwd }).filter(a => a.status === 'active' || a.status === 'sleeping');
|
|
42
|
+
if (candidates.length === 0) return [];
|
|
43
|
+
|
|
44
|
+
const settledList: Array<{ anchor: Anchor; reason: string }> = [];
|
|
45
|
+
const cmdTrimmed = (executedCommand || '').trim();
|
|
46
|
+
|
|
47
|
+
for (const a of candidates) {
|
|
48
|
+
// 1. Check real-time Git Commit semantic match (works even when a.files is empty!)
|
|
49
|
+
if (observer.hasCommitted()) {
|
|
50
|
+
const commitMatch = observer.matchesCommit(a);
|
|
51
|
+
if (commitMatch.matched && (!cwd || isAnchorTargetingWorkspace(a, cwd))) {
|
|
52
|
+
const settled = store.settle(a.id, {
|
|
53
|
+
settledBy: 'verification-test',
|
|
54
|
+
summary: `Auto-settled via Git commit: ${commitMatch.message}`,
|
|
55
|
+
touchedFiles: observer.getModifiedFiles()
|
|
56
|
+
});
|
|
57
|
+
settledList.push({ anchor: settled, reason: `git commit ("${commitMatch.message}")` });
|
|
58
|
+
continue;
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
// 2. Check if the executed shell command directly ran and passed the anchor's verifyCommand
|
|
63
|
+
if (a.verifyCommand && cmdTrimmed && cmdTrimmed.includes(a.verifyCommand.trim())) {
|
|
64
|
+
const settled = store.settle(a.id, {
|
|
65
|
+
settledBy: 'verification-test',
|
|
66
|
+
summary: `Auto-settled via passing command: ${a.verifyCommand}`,
|
|
67
|
+
touchedFiles: observer.getModifiedFiles()
|
|
68
|
+
});
|
|
69
|
+
settledList.push({ anchor: settled, reason: `verifyCommand (${a.verifyCommand})` });
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
return settledList;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* Generate settlement candidates based on files touched or commits made during the session
|
|
31
78
|
*/
|
|
32
79
|
export function generateSettlementProposals(
|
|
33
80
|
store: AnchorStore,
|
|
34
81
|
touchedFiles: string[],
|
|
35
|
-
cwd?: string
|
|
82
|
+
cwd?: string,
|
|
83
|
+
observer?: SessionTouchObserver
|
|
36
84
|
): SettlementProposal[] {
|
|
37
|
-
// Only match active/sleeping anchors belonging to current workspace
|
|
38
85
|
const activeAndSleeping = store.list({ cwd }).filter(a => a.status === 'active' || a.status === 'sleeping');
|
|
39
86
|
const matches = findMatchedAnchors(activeAndSleeping, touchedFiles);
|
|
87
|
+
const seenIds = new Set<string>();
|
|
40
88
|
|
|
41
89
|
const proposals: SettlementProposal[] = [];
|
|
42
90
|
|
|
43
91
|
for (const match of matches) {
|
|
92
|
+
seenIds.add(match.anchor.id);
|
|
44
93
|
const fileList = match.matchedFiles.slice(0, 3).join(', ');
|
|
45
|
-
const moreSuffix = match.matchedFiles.length > 3 ? `
|
|
94
|
+
const moreSuffix = match.matchedFiles.length > 3 ? ` (+${match.matchedFiles.length - 3} more)` : '';
|
|
46
95
|
|
|
47
96
|
proposals.push({
|
|
48
97
|
anchor: match.anchor,
|
|
49
98
|
matchedFiles: match.matchedFiles,
|
|
50
|
-
reason:
|
|
99
|
+
reason: `Modified ${fileList}${moreSuffix}`,
|
|
51
100
|
recommendedAction: match.score >= 0.85 ? 'settle' : 'defer'
|
|
52
101
|
});
|
|
53
102
|
}
|
|
54
103
|
|
|
104
|
+
// Also include file-less (or un-matched) anchors if a git commit message matched them
|
|
105
|
+
if (observer && observer.hasCommitted()) {
|
|
106
|
+
for (const a of activeAndSleeping) {
|
|
107
|
+
if (seenIds.has(a.id)) continue;
|
|
108
|
+
const cm = observer.matchesCommit(a);
|
|
109
|
+
if (cm.matched) {
|
|
110
|
+
seenIds.add(a.id);
|
|
111
|
+
proposals.push({
|
|
112
|
+
anchor: a,
|
|
113
|
+
matchedFiles: touchedFiles.slice(0, 3),
|
|
114
|
+
reason: `Matched git commit: "${cm.message}"`,
|
|
115
|
+
recommendedAction: 'settle'
|
|
116
|
+
});
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
|
|
55
121
|
return proposals;
|
|
56
122
|
}
|
|
57
123
|
|
package/src/store.ts
CHANGED
|
@@ -11,7 +11,7 @@ import type {
|
|
|
11
11
|
AnchorStoreState
|
|
12
12
|
} from './types.ts';
|
|
13
13
|
import { DEFAULT_DECAY_POLICY, DURABLE_DECAY_POLICY, EPHEMERAL_DECAY_POLICY } from './types.ts';
|
|
14
|
-
import { normalizePath, isSameOrSubPath } from './matcher.ts';
|
|
14
|
+
import { normalizePath, isSameOrSubPath, extractPathsFromText, isAnchorTargetingWorkspace } from './matcher.ts';
|
|
15
15
|
|
|
16
16
|
/**
|
|
17
17
|
* Detect whether a task is a short-term ephemeral reminder or a long-term architectural vision
|
|
@@ -31,15 +31,16 @@ export function detectDurability(title: string): AnchorDurability {
|
|
|
31
31
|
}
|
|
32
32
|
|
|
33
33
|
/**
|
|
34
|
-
* Get custom ephemeral decay policy based on temporal span
|
|
34
|
+
* Get custom ephemeral decay policy based on temporal span.
|
|
35
|
+
* Note: graveyardDays is always 9999 so unfinished tasks are NEVER silently deleted.
|
|
35
36
|
*/
|
|
36
37
|
export function getEphemeralDecayPolicy(title: string): AnchorDecayPolicy {
|
|
37
38
|
const lower = title.toLowerCase();
|
|
38
39
|
if (lower.includes('大后天')) {
|
|
39
|
-
return { activeDays:
|
|
40
|
+
return { activeDays: 7, sleepDays: 30, graveyardDays: 9999 };
|
|
40
41
|
}
|
|
41
42
|
if (lower.includes('后天')) {
|
|
42
|
-
return { activeDays:
|
|
43
|
+
return { activeDays: 7, sleepDays: 30, graveyardDays: 9999 };
|
|
43
44
|
}
|
|
44
45
|
return { ...EPHEMERAL_DECAY_POLICY };
|
|
45
46
|
}
|
|
@@ -415,7 +416,22 @@ export class AnchorStore {
|
|
|
415
416
|
const now = Date.now();
|
|
416
417
|
const id = this.generateId(state);
|
|
417
418
|
|
|
418
|
-
const
|
|
419
|
+
const explicitFiles = (input.files || []).map(f => normalizePath(f)).filter(Boolean);
|
|
420
|
+
const inferredFiles = explicitFiles.length > 0
|
|
421
|
+
? explicitFiles
|
|
422
|
+
: extractPathsFromText(`${input.title} ${input.description || ''}`);
|
|
423
|
+
|
|
424
|
+
let cwd = input.cwd ? normalizePath(input.cwd) : '';
|
|
425
|
+
// If project is an explicit absolute path (e.g. "D:/College/优化"), align cwd to it so workspace matching is never split
|
|
426
|
+
if (input.project && (/^[a-zA-Z]:[\\/]/.test(input.project.trim()) || input.project.trim().startsWith('/'))) {
|
|
427
|
+
cwd = normalizePath(input.project.trim());
|
|
428
|
+
} else if (inferredFiles.length > 0 && (/^[a-zA-Z]:\//.test(inferredFiles[0]) || inferredFiles[0].startsWith('/'))) {
|
|
429
|
+
const firstDir = path.dirname(inferredFiles[0]);
|
|
430
|
+
if (firstDir && firstDir !== '.') {
|
|
431
|
+
cwd = normalizePath(firstDir);
|
|
432
|
+
}
|
|
433
|
+
}
|
|
434
|
+
|
|
419
435
|
const projectName = input.project
|
|
420
436
|
? input.project.trim()
|
|
421
437
|
: (cwd ? path.basename(cwd) : 'global');
|
|
@@ -442,7 +458,7 @@ export class AnchorStore {
|
|
|
442
458
|
createdAt: now,
|
|
443
459
|
updatedAt: now,
|
|
444
460
|
lastTouchedAt: now,
|
|
445
|
-
files:
|
|
461
|
+
files: inferredFiles,
|
|
446
462
|
tags: (input.tags || []).map(t => t.trim()).filter(Boolean),
|
|
447
463
|
verifyCommand: input.verifyCommand?.trim() || undefined,
|
|
448
464
|
decay: input.decay ? { ...input.decay } : { ...decayPolicy }
|
|
@@ -497,10 +513,10 @@ export class AnchorStore {
|
|
|
497
513
|
if (filter?.priority && a.priority !== filter.priority) return false;
|
|
498
514
|
|
|
499
515
|
// Project context filtering:
|
|
500
|
-
// If an anchor has specific code files attached,
|
|
501
|
-
// If an anchor has NO code files attached (
|
|
516
|
+
// If an anchor has specific code files attached, match against cwd, project path/name, or file paths.
|
|
517
|
+
// If an anchor has NO code files attached (general tasks, memos), it is universal and visible everywhere!
|
|
502
518
|
if (!filter?.all && targetCwd) {
|
|
503
|
-
if (a.files.length > 0 && a.cwd && !
|
|
519
|
+
if (a.files.length > 0 && (a.cwd || a.project) && !isAnchorTargetingWorkspace(a, targetCwd)) {
|
|
504
520
|
return false;
|
|
505
521
|
}
|
|
506
522
|
}
|