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
package/src/tui.ts
ADDED
|
@@ -0,0 +1,386 @@
|
|
|
1
|
+
import type { ExtensionContext } from '@earendil-works/pi-coding-agent';
|
|
2
|
+
import { AnchorStore, getTodayDateString } from './store.ts';
|
|
3
|
+
import type { Anchor } from './types.ts';
|
|
4
|
+
import path from 'node:path';
|
|
5
|
+
|
|
6
|
+
export type AnchorQuadrant = 'Today' | 'Upcoming' | 'Habits' | 'Backlog';
|
|
7
|
+
|
|
8
|
+
export interface GroupedAnchors {
|
|
9
|
+
today: Anchor[];
|
|
10
|
+
upcoming: Anchor[];
|
|
11
|
+
habits: Anchor[];
|
|
12
|
+
backlog: Anchor[];
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Classify anchor into one of four cognitive quadrants:
|
|
17
|
+
* - 'Today': Due today or overdue
|
|
18
|
+
* - 'Upcoming': Due in the future (tomorrow, in 2d, specific dates)
|
|
19
|
+
* - 'Habits': Daily recurring habits
|
|
20
|
+
* - 'Backlog': Open-ended long-term vision
|
|
21
|
+
*/
|
|
22
|
+
export function classifyAnchor(anchor: Anchor, now: number = Date.now()): AnchorQuadrant {
|
|
23
|
+
if (anchor.recurrence === 'daily') {
|
|
24
|
+
return 'Habits';
|
|
25
|
+
}
|
|
26
|
+
if (!anchor.targetDate) {
|
|
27
|
+
return 'Backlog';
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
const todayStr = getTodayDateString(now);
|
|
31
|
+
if (anchor.targetDate <= todayStr) {
|
|
32
|
+
return 'Today';
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
return 'Upcoming';
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Group active anchors by cognitive quadrant
|
|
40
|
+
*/
|
|
41
|
+
export function groupAnchorsByQuadrant(anchors: Anchor[], now: number = Date.now()): GroupedAnchors {
|
|
42
|
+
const groups: GroupedAnchors = {
|
|
43
|
+
today: [],
|
|
44
|
+
upcoming: [],
|
|
45
|
+
habits: [],
|
|
46
|
+
backlog: []
|
|
47
|
+
};
|
|
48
|
+
|
|
49
|
+
for (const a of anchors) {
|
|
50
|
+
const q = classifyAnchor(a, now);
|
|
51
|
+
if (q === 'Today') groups.today.push(a);
|
|
52
|
+
else if (q === 'Upcoming') groups.upcoming.push(a);
|
|
53
|
+
else if (q === 'Habits') groups.habits.push(a);
|
|
54
|
+
else groups.backlog.push(a);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
return groups;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* Strip ANSI escape codes from string
|
|
62
|
+
*/
|
|
63
|
+
export function stripAnsi(str: string): string {
|
|
64
|
+
return str.replace(/\x1b\[[0-9;]*[a-zA-Z]/g, '');
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* Calculate display width in terminal columns, accounting for:
|
|
69
|
+
* - ANSI escape codes (width 0)
|
|
70
|
+
* - CJK ideographs & fullwidth forms (width 2)
|
|
71
|
+
* - Japanese Kana (width 2)
|
|
72
|
+
* - Korean Hangul syllables & Jamo (width 2)
|
|
73
|
+
* - Emojis & Pictographs (width 2)
|
|
74
|
+
* - Zero-width characters & joiners (width 0)
|
|
75
|
+
* - Standard ASCII (width 1)
|
|
76
|
+
*/
|
|
77
|
+
export function getDisplayWidth(str: string): number {
|
|
78
|
+
const clean = stripAnsi(str);
|
|
79
|
+
let width = 0;
|
|
80
|
+
|
|
81
|
+
for (const char of clean) {
|
|
82
|
+
const code = char.codePointAt(0) || 0;
|
|
83
|
+
|
|
84
|
+
// Zero-width characters (ZWJ, variation selectors, combining marks)
|
|
85
|
+
if (
|
|
86
|
+
code === 0x200d ||
|
|
87
|
+
code === 0xfe0f ||
|
|
88
|
+
code === 0xfe0e ||
|
|
89
|
+
(code >= 0x0300 && code <= 0x036f) ||
|
|
90
|
+
(code >= 0x200b && code <= 0x200f)
|
|
91
|
+
) {
|
|
92
|
+
continue;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
// Double-width characters: CJK, Hangul, Kana, Emojis
|
|
96
|
+
if (
|
|
97
|
+
(code >= 0x4e00 && code <= 0x9fff) ||
|
|
98
|
+
(code >= 0x3400 && code <= 0x4dbf) ||
|
|
99
|
+
(code >= 0x20000 && code <= 0x2a6df) ||
|
|
100
|
+
(code >= 0x2a700 && code <= 0x2b73f) ||
|
|
101
|
+
(code >= 0xff01 && code <= 0xff60) ||
|
|
102
|
+
(code >= 0x3000 && code <= 0x303f) ||
|
|
103
|
+
(code >= 0xac00 && code <= 0xd7af) ||
|
|
104
|
+
(code >= 0x1100 && code <= 0x11ff) ||
|
|
105
|
+
(code >= 0x3130 && code <= 0x318f) ||
|
|
106
|
+
(code >= 0x3040 && code <= 0x309f) ||
|
|
107
|
+
(code >= 0x30a0 && code <= 0x30ff) ||
|
|
108
|
+
(code >= 0x1f300 && code <= 0x1f9ff) ||
|
|
109
|
+
(code >= 0x1f600 && code <= 0x1f64f) ||
|
|
110
|
+
(code >= 0x1f680 && code <= 0x1f6ff) ||
|
|
111
|
+
(code >= 0x2600 && code <= 0x27bf) ||
|
|
112
|
+
(code >= 0x1fa70 && code <= 0x1faff)
|
|
113
|
+
) {
|
|
114
|
+
width += 2;
|
|
115
|
+
} else {
|
|
116
|
+
width += 1;
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
return width;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
/**
|
|
124
|
+
* Truncate string to target terminal visual width with ellipsis
|
|
125
|
+
*/
|
|
126
|
+
export function truncateToWidth(str: string, maxWidth: number, ellipsis = '…'): string {
|
|
127
|
+
const current = getDisplayWidth(str);
|
|
128
|
+
if (current <= maxWidth) return str;
|
|
129
|
+
|
|
130
|
+
const ellipsisWidth = getDisplayWidth(ellipsis);
|
|
131
|
+
const target = maxWidth - ellipsisWidth;
|
|
132
|
+
if (target <= 0) return ellipsis.slice(0, maxWidth);
|
|
133
|
+
|
|
134
|
+
let accumulated = '';
|
|
135
|
+
let accumWidth = 0;
|
|
136
|
+
|
|
137
|
+
for (const char of str) {
|
|
138
|
+
const charWidth = getDisplayWidth(char);
|
|
139
|
+
if (accumWidth + charWidth > target) {
|
|
140
|
+
break;
|
|
141
|
+
}
|
|
142
|
+
accumulated += char;
|
|
143
|
+
accumWidth += charWidth;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
return accumulated + ellipsis;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
/**
|
|
150
|
+
* Pad and/or truncate string to exact visual column width, eliminating column tearing
|
|
151
|
+
*/
|
|
152
|
+
export function padToWidth(str: string, targetWidth: number): string {
|
|
153
|
+
const truncated = truncateToWidth(str, targetWidth);
|
|
154
|
+
const current = getDisplayWidth(truncated);
|
|
155
|
+
if (current >= targetWidth) return truncated;
|
|
156
|
+
return truncated + ' '.repeat(targetWidth - current);
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
/**
|
|
160
|
+
* Format expected completion date in human-intuitive terms:
|
|
161
|
+
* - 'Daily' for daily recurring habits
|
|
162
|
+
* - 'Today' if targetDate is today
|
|
163
|
+
* - 'Tomorrow' if targetDate is tomorrow
|
|
164
|
+
* - 'In 2d' / 'In 3d' for near-term dates
|
|
165
|
+
* - 'MM-DD' for future dates
|
|
166
|
+
* - 'Overdue' if targetDate is past
|
|
167
|
+
* - 'Someday' for open-ended architecture/long-term vision
|
|
168
|
+
*/
|
|
169
|
+
export function formatTargetDate(anchor: Anchor, now: number = Date.now()): string {
|
|
170
|
+
if (anchor.recurrence === 'daily') {
|
|
171
|
+
return 'Daily';
|
|
172
|
+
}
|
|
173
|
+
if (!anchor.targetDate) {
|
|
174
|
+
return 'Someday';
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
const todayStr = getTodayDateString(now);
|
|
178
|
+
if (anchor.targetDate === todayStr) {
|
|
179
|
+
return 'Today';
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
const MS_DAY = 24 * 60 * 60 * 1000;
|
|
183
|
+
const tomorrowStr = getTodayDateString(now + MS_DAY);
|
|
184
|
+
if (anchor.targetDate === tomorrowStr) {
|
|
185
|
+
return 'Tomorrow';
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
const in2dStr = getTodayDateString(now + 2 * MS_DAY);
|
|
189
|
+
if (anchor.targetDate === in2dStr) {
|
|
190
|
+
return 'In 2d';
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
const in3dStr = getTodayDateString(now + 3 * MS_DAY);
|
|
194
|
+
if (anchor.targetDate === in3dStr) {
|
|
195
|
+
return 'In 3d';
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
if (anchor.targetDate < todayStr) {
|
|
199
|
+
return 'Overdue';
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
const parts = anchor.targetDate.split('-');
|
|
203
|
+
if (parts.length === 3) {
|
|
204
|
+
return `${parts[1]}-${parts[2]}`;
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
return anchor.targetDate;
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
/**
|
|
211
|
+
* Format creation timestamp as concise date/time:
|
|
212
|
+
* - 'Today HH:MM' if created today
|
|
213
|
+
* - 'Yesterday HH:MM' if created yesterday
|
|
214
|
+
* - 'MM-DD HH:MM' if created this year
|
|
215
|
+
* - 'YYYY-MM-DD' if older
|
|
216
|
+
*/
|
|
217
|
+
export function formatCreationTime(timestamp: number, now: number = Date.now()): string {
|
|
218
|
+
const d = new Date(timestamp);
|
|
219
|
+
const nowD = new Date(now);
|
|
220
|
+
|
|
221
|
+
const pad = (n: number) => String(n).padStart(2, '0');
|
|
222
|
+
const timeStr = `${pad(d.getHours())}:${pad(d.getMinutes())}`;
|
|
223
|
+
|
|
224
|
+
const todayStr = getTodayDateString(now);
|
|
225
|
+
const createdDayStr = getTodayDateString(timestamp);
|
|
226
|
+
|
|
227
|
+
if (createdDayStr === todayStr) {
|
|
228
|
+
return `Today ${timeStr}`;
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
const yesterdayStr = getTodayDateString(now - 24 * 60 * 60 * 1000);
|
|
232
|
+
if (createdDayStr === yesterdayStr) {
|
|
233
|
+
return `Yesterday ${timeStr}`;
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
const mmdd = `${pad(d.getMonth() + 1)}-${pad(d.getDate())}`;
|
|
237
|
+
if (d.getFullYear() === nowD.getFullYear()) {
|
|
238
|
+
return `${mmdd} ${timeStr}`;
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
return `${d.getFullYear()}-${mmdd}`;
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
/**
|
|
245
|
+
* Backward compatibility alias
|
|
246
|
+
*/
|
|
247
|
+
export function formatRelativeTime(timestamp: number, now: number = Date.now()): string {
|
|
248
|
+
return formatCreationTime(timestamp, now);
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
/**
|
|
252
|
+
* Backward compatibility alias (now returns target date representation)
|
|
253
|
+
*/
|
|
254
|
+
export function formatRemainingTtl(anchor: Anchor, now: number = Date.now()): string | undefined {
|
|
255
|
+
return formatTargetDate(anchor, now);
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
/**
|
|
259
|
+
* Format origin creation workspace folder (provenance: where the task was born/created)
|
|
260
|
+
*/
|
|
261
|
+
export function formatOrigin(anchor: Anchor | string): string {
|
|
262
|
+
if (typeof anchor === 'object') {
|
|
263
|
+
if (!anchor.cwd) return anchor.project || 'global';
|
|
264
|
+
return path.basename(anchor.cwd) || 'global';
|
|
265
|
+
}
|
|
266
|
+
if (!anchor) return 'global';
|
|
267
|
+
return path.basename(anchor) || 'global';
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
/**
|
|
271
|
+
* Update footer status bar capsule: `⌖ 1`
|
|
272
|
+
*/
|
|
273
|
+
export function updateAnchorStatusBar(ctx: ExtensionContext, store: AnchorStore): void {
|
|
274
|
+
if (!ctx.hasUI || !ctx.ui) return;
|
|
275
|
+
|
|
276
|
+
const active = store.list({ status: 'active', cwd: ctx.cwd });
|
|
277
|
+
if (active.length === 0) {
|
|
278
|
+
ctx.ui.setStatus('anchor', undefined);
|
|
279
|
+
return;
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
ctx.ui.setStatus('anchor', `⌖ ${active.length}`);
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
/**
|
|
286
|
+
* Render lightweight startup banner widget above the editor.
|
|
287
|
+
* Features 3-4 lines with quadrant badges and target date hints.
|
|
288
|
+
* Automatically cleared on agent_start (when user sends first message).
|
|
289
|
+
*/
|
|
290
|
+
export function updateStartupBanner(ctx: ExtensionContext, store: AnchorStore): void {
|
|
291
|
+
if (!ctx.hasUI || !ctx.ui) return;
|
|
292
|
+
|
|
293
|
+
const active = store.list({ status: 'active', cwd: ctx.cwd });
|
|
294
|
+
if (active.length === 0) {
|
|
295
|
+
ctx.ui.setWidget('anchor-startup', undefined);
|
|
296
|
+
return;
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
const groups = groupAnchorsByQuadrant(active);
|
|
300
|
+
const sorted = [...groups.today, ...groups.upcoming, ...groups.habits, ...groups.backlog];
|
|
301
|
+
|
|
302
|
+
const lines: string[] = [
|
|
303
|
+
`⌖ Anchor · ${active.length} active commitments:`
|
|
304
|
+
];
|
|
305
|
+
|
|
306
|
+
const topItems = sorted.slice(0, 3);
|
|
307
|
+
for (let i = 0; i < topItems.length; i++) {
|
|
308
|
+
const a = topItems[i];
|
|
309
|
+
const num = (i + 1).toString().padStart(2, '0');
|
|
310
|
+
const q = classifyAnchor(a);
|
|
311
|
+
const tgt = formatTargetDate(a);
|
|
312
|
+
const titlePadded = a.title.length > 34 ? a.title.slice(0, 32) + '..' : a.title;
|
|
313
|
+
lines.push(` • [${q}] ${num} ${titlePadded} (${tgt})`);
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
if (sorted.length > 3) {
|
|
317
|
+
lines.push(` (+${sorted.length - 3} more · run /anchor to inspect)`);
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
ctx.ui.setWidget('anchor-startup', lines, { placement: 'aboveEditor' });
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
/**
|
|
324
|
+
* Clean, columnar-aligned checklist with Target Date and Creation Time columns
|
|
325
|
+
*/
|
|
326
|
+
export async function openAnchorDashboard(
|
|
327
|
+
ctx: ExtensionContext,
|
|
328
|
+
store: AnchorStore
|
|
329
|
+
): Promise<void> {
|
|
330
|
+
if (!ctx.hasUI || !ctx.ui) return;
|
|
331
|
+
|
|
332
|
+
const list = store.list({ status: 'active', cwd: ctx.cwd });
|
|
333
|
+
if (list.length === 0) {
|
|
334
|
+
ctx.ui.notify('⌖ No active anchors. Use /pin <task> to record.', 'info');
|
|
335
|
+
return;
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
const groups = groupAnchorsByQuadrant(list);
|
|
339
|
+
const sortedList = [
|
|
340
|
+
...groups.today,
|
|
341
|
+
...groups.upcoming,
|
|
342
|
+
...groups.habits,
|
|
343
|
+
...groups.backlog
|
|
344
|
+
];
|
|
345
|
+
|
|
346
|
+
const optionMap = new Map<string, Anchor>();
|
|
347
|
+
const displayOptions: string[] = [];
|
|
348
|
+
|
|
349
|
+
for (let i = 0; i < sortedList.length; i++) {
|
|
350
|
+
const a = sortedList[i];
|
|
351
|
+
const num = (i + 1).toString().padStart(2, '0');
|
|
352
|
+
const category = padToWidth(`[${classifyAnchor(a)}]`, 12);
|
|
353
|
+
const origin = formatOrigin(a);
|
|
354
|
+
const target = formatTargetDate(a);
|
|
355
|
+
const created = formatCreationTime(a.createdAt);
|
|
356
|
+
|
|
357
|
+
const titleWithFiles = (a.files && a.files.length > 0)
|
|
358
|
+
? `${a.title} [${a.files.slice(0, 1).join(', ')}]`
|
|
359
|
+
: a.title;
|
|
360
|
+
|
|
361
|
+
// Clean tabular column alignment: ID, Category, Title, Project, Target, Created
|
|
362
|
+
const colNum = `${num} `;
|
|
363
|
+
const colTitle = padToWidth(titleWithFiles, 34);
|
|
364
|
+
const colOrigin = padToWidth(origin, 10);
|
|
365
|
+
const colTarget = padToWidth(target, 12);
|
|
366
|
+
const colCreated = created;
|
|
367
|
+
|
|
368
|
+
const label = `${colNum}${category}${colTitle} ${colOrigin} ${colTarget} ${colCreated}`.trimEnd();
|
|
369
|
+
|
|
370
|
+
optionMap.set(label, a);
|
|
371
|
+
displayOptions.push(label);
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
const selected = await ctx.ui.select('⌖ Anchors (enter to complete):', displayOptions);
|
|
375
|
+
if (!selected) return;
|
|
376
|
+
|
|
377
|
+
const anchor = optionMap.get(selected);
|
|
378
|
+
if (!anchor) return;
|
|
379
|
+
|
|
380
|
+
store.settle(anchor.id, { settledBy: 'manual-command' });
|
|
381
|
+
const successMsg = anchor.recurrence === 'daily'
|
|
382
|
+
? `⌖ Completed for today: "${anchor.title}" (resets tomorrow)`
|
|
383
|
+
: `⌖ Settled: "${anchor.title}"`;
|
|
384
|
+
ctx.ui.notify(successMsg, 'info');
|
|
385
|
+
updateAnchorStatusBar(ctx, store);
|
|
386
|
+
}
|
package/src/types.ts
ADDED
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Anchor Data Contracts & Type Definitions
|
|
3
|
+
* Single Global Source of Truth with Zero Workspace Contamination.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
export type AnchorStatus = 'active' | 'sleeping' | 'settled' | 'graveyard';
|
|
7
|
+
export type AnchorPriority = 'p0' | 'p1' | 'p2';
|
|
8
|
+
export type AnchorDurability = 'ephemeral' | 'durable';
|
|
9
|
+
|
|
10
|
+
export interface AnchorDecayPolicy {
|
|
11
|
+
/** Days before an untouched active anchor transitions to sleeping */
|
|
12
|
+
activeDays: number;
|
|
13
|
+
/** Days an anchor remains sleeping before dropping to graveyard */
|
|
14
|
+
sleepDays: number;
|
|
15
|
+
/** Total maximum days before permanent graveyard sweep (9999 = never) */
|
|
16
|
+
graveyardDays: number;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export interface AnchorEvidence {
|
|
20
|
+
settledAt?: number;
|
|
21
|
+
settledBy?: 'one-tap-settlement' | 'manual-command' | 'verification-test';
|
|
22
|
+
commitHash?: string;
|
|
23
|
+
summary?: string;
|
|
24
|
+
touchedFiles?: string[];
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export interface Anchor {
|
|
28
|
+
/** Unique sequential identifier, e.g., 'anc-1' */
|
|
29
|
+
id: string;
|
|
30
|
+
/** Short imperative title of the task */
|
|
31
|
+
title: string;
|
|
32
|
+
/** Optional detailed context or notes */
|
|
33
|
+
description?: string;
|
|
34
|
+
/** Task priority */
|
|
35
|
+
priority: AnchorPriority;
|
|
36
|
+
/** Current lifecycle status */
|
|
37
|
+
status: AnchorStatus;
|
|
38
|
+
/** Durability tier: ephemeral (short-lived reminder, 48h TTL) or durable (long-term architecture vision) */
|
|
39
|
+
durability: AnchorDurability;
|
|
40
|
+
/** Canonical project name (e.g. 'PPT', 'X', or 'global') */
|
|
41
|
+
project: string;
|
|
42
|
+
/** Canonical root directory where the task was anchored (empty string for global tasks) */
|
|
43
|
+
cwd: string;
|
|
44
|
+
/** Creation timestamp in ms */
|
|
45
|
+
createdAt: number;
|
|
46
|
+
/** Last update timestamp in ms */
|
|
47
|
+
updatedAt: number;
|
|
48
|
+
/** Last touched timestamp in ms (resets on file touch) */
|
|
49
|
+
lastTouchedAt: number;
|
|
50
|
+
/** Associated file paths or globs, e.g. ['src/auth/*.ts'] */
|
|
51
|
+
files: string[];
|
|
52
|
+
/** Expected completion date in format 'YYYY-MM-DD' (e.g. '2026-09-24') */
|
|
53
|
+
targetDate?: string;
|
|
54
|
+
/** Optional shell command for automated physical verification (e.g. 'npm test') */
|
|
55
|
+
verifyCommand?: string;
|
|
56
|
+
/** Topic or domain tags, e.g. ['auth', 'security'] */
|
|
57
|
+
tags: string[];
|
|
58
|
+
/** Optional recurrence pattern: 'daily' for recurring daily habits */
|
|
59
|
+
recurrence?: 'daily';
|
|
60
|
+
/** Last completed date string 'YYYY-MM-DD' for recurring tasks */
|
|
61
|
+
lastCompletedDate?: string;
|
|
62
|
+
/** Decay policy overrides */
|
|
63
|
+
decay: AnchorDecayPolicy;
|
|
64
|
+
/** Settlement evidence metadata upon closure */
|
|
65
|
+
evidence?: AnchorEvidence;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
export interface AnchorStoreState {
|
|
69
|
+
version: number;
|
|
70
|
+
anchors: Anchor[];
|
|
71
|
+
lastSweepAt: number;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
export interface TouchMatchResult {
|
|
75
|
+
anchor: Anchor;
|
|
76
|
+
score: number;
|
|
77
|
+
matchedFiles: string[];
|
|
78
|
+
reason: 'exact-file' | 'dir-prefix' | 'tag-keyword';
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
export interface SettlementProposal {
|
|
82
|
+
anchor: Anchor;
|
|
83
|
+
matchedFiles: string[];
|
|
84
|
+
reason: string;
|
|
85
|
+
recommendedAction: 'settle' | 'defer';
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
export const EPHEMERAL_DECAY_POLICY: AnchorDecayPolicy = {
|
|
89
|
+
activeDays: 1, // 24 hours active
|
|
90
|
+
sleepDays: 1, // 24 hours sleeping
|
|
91
|
+
graveyardDays: 2 // 48 hours total before auto-clearing
|
|
92
|
+
};
|
|
93
|
+
|
|
94
|
+
export const DURABLE_DECAY_POLICY: AnchorDecayPolicy = {
|
|
95
|
+
activeDays: 3,
|
|
96
|
+
sleepDays: 30,
|
|
97
|
+
graveyardDays: 9999 // Never permanently auto-dropped; preserved indefinitely
|
|
98
|
+
};
|
|
99
|
+
|
|
100
|
+
export const DEFAULT_DECAY_POLICY: AnchorDecayPolicy = {
|
|
101
|
+
activeDays: 3,
|
|
102
|
+
sleepDays: 7,
|
|
103
|
+
graveyardDays: 14
|
|
104
|
+
};
|