@trevonistrevon/pi-loop 0.5.1 → 0.5.2
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/goal-store.d.ts +2 -0
- package/dist/goal-store.js +16 -4
- package/dist/store.d.ts +2 -0
- package/dist/store.js +16 -4
- package/dist/task-store.d.ts +2 -0
- package/dist/task-store.js +16 -4
- package/dist/tools/native-task-tools.js +6 -3
- package/dist/ui/widget.d.ts +0 -1
- package/dist/ui/widget.js +1 -14
- package/package.json +1 -1
- package/src/goal-store.ts +15 -4
- package/src/store.ts +15 -4
- package/src/task-store.ts +15 -4
- package/src/tools/native-task-tools.ts +6 -3
- package/src/ui/widget.ts +1 -16
- package/tmp/perf-cap-data/loops.json +455 -0
- package/tmp/perf-cap-data/tasks.json +1 -0
- package/tmp/perf-data/loops-0.json +1 -0
- package/tmp/perf-data/loops-10.json +1 -0
- package/tmp/perf-data/loops-100.json +1 -0
- package/tmp/perf-data/loops-1000.json +1 -0
- package/tmp/perf-data/loops-500.json +1 -0
- package/tmp/perf-data/tasks-0.json +1 -0
- package/tmp/perf-data/tasks-10.json +1 -0
- package/tmp/perf-data/tasks-100.json +1 -0
- package/tmp/perf-data/tasks-1000.json +1 -0
- package/tmp/perf-data/tasks-500.json +1 -0
- package/tmp/perf-data-session/loops-0.json +4 -0
- package/tmp/perf-data-session/loops-100.json +1805 -0
- package/tmp/perf-data-session/loops-1000.json +18005 -0
- package/tmp/perf-data-session/loops-25.json +455 -0
- package/tmp/perf-data-session/loops-500.json +9005 -0
- package/tmp/perf-data-session/tasks-0.json +1 -0
- package/tmp/perf-data-session/tasks-100.json +1 -0
- package/tmp/perf-data-session/tasks-1000.json +1 -0
- package/tmp/perf-data-session/tasks-25.json +1 -0
- package/tmp/perf-data-session/tasks-500.json +1 -0
- package/tmp/perf-session-init.js +98 -0
- package/tmp/perf-startup-audit.js +80 -0
package/dist/goal-store.d.ts
CHANGED
|
@@ -2,9 +2,11 @@ import type { GoalCriteria, GoalEntry, GoalProgressSnapshot, GoalScope } from ".
|
|
|
2
2
|
export declare class GoalStore {
|
|
3
3
|
private filePath;
|
|
4
4
|
private lockPath;
|
|
5
|
+
private lastLoadedSignature;
|
|
5
6
|
private nextId;
|
|
6
7
|
private goals;
|
|
7
8
|
constructor(listIdOrPath?: string);
|
|
9
|
+
private getFileSignature;
|
|
8
10
|
private load;
|
|
9
11
|
private save;
|
|
10
12
|
private withLock;
|
package/dist/goal-store.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { existsSync, mkdirSync, readFileSync, renameSync, unlinkSync, writeFileSync } from "node:fs";
|
|
1
|
+
import { existsSync, mkdirSync, readFileSync, renameSync, statSync, unlinkSync, writeFileSync } from "node:fs";
|
|
2
2
|
import { homedir } from "node:os";
|
|
3
3
|
import { dirname, isAbsolute, join } from "node:path";
|
|
4
4
|
import { reduceGoalState } from "./goal-reducer.js";
|
|
@@ -52,6 +52,7 @@ function isProcessRunning(pid) {
|
|
|
52
52
|
export class GoalStore {
|
|
53
53
|
filePath;
|
|
54
54
|
lockPath;
|
|
55
|
+
lastLoadedSignature;
|
|
55
56
|
nextId = 1;
|
|
56
57
|
goals = new Map();
|
|
57
58
|
constructor(listIdOrPath) {
|
|
@@ -64,10 +65,19 @@ export class GoalStore {
|
|
|
64
65
|
this.lockPath = filePath + ".lock";
|
|
65
66
|
this.load();
|
|
66
67
|
}
|
|
67
|
-
|
|
68
|
+
getFileSignature() {
|
|
69
|
+
if (!this.filePath || !existsSync(this.filePath))
|
|
70
|
+
return undefined;
|
|
71
|
+
const stat = statSync(this.filePath);
|
|
72
|
+
return `${stat.mtimeMs}:${stat.size}`;
|
|
73
|
+
}
|
|
74
|
+
load(force = false) {
|
|
68
75
|
if (!this.filePath)
|
|
69
76
|
return;
|
|
70
|
-
|
|
77
|
+
const signature = this.getFileSignature();
|
|
78
|
+
if (!signature)
|
|
79
|
+
return;
|
|
80
|
+
if (!force && signature === this.lastLoadedSignature)
|
|
71
81
|
return;
|
|
72
82
|
try {
|
|
73
83
|
const data = JSON.parse(readFileSync(this.filePath, "utf-8"));
|
|
@@ -76,6 +86,7 @@ export class GoalStore {
|
|
|
76
86
|
for (const goal of data.goals) {
|
|
77
87
|
this.goals.set(goal.id, goal);
|
|
78
88
|
}
|
|
89
|
+
this.lastLoadedSignature = signature;
|
|
79
90
|
}
|
|
80
91
|
catch { /* corrupt file — start fresh */ }
|
|
81
92
|
}
|
|
@@ -89,13 +100,14 @@ export class GoalStore {
|
|
|
89
100
|
const tmpPath = this.filePath + ".tmp";
|
|
90
101
|
writeFileSync(tmpPath, JSON.stringify(data, null, 2));
|
|
91
102
|
renameSync(tmpPath, this.filePath);
|
|
103
|
+
this.lastLoadedSignature = this.getFileSignature();
|
|
92
104
|
}
|
|
93
105
|
withLock(fn) {
|
|
94
106
|
if (!this.lockPath)
|
|
95
107
|
return fn();
|
|
96
108
|
acquireLock(this.lockPath);
|
|
97
109
|
try {
|
|
98
|
-
this.load();
|
|
110
|
+
this.load(true);
|
|
99
111
|
const result = fn();
|
|
100
112
|
this.save();
|
|
101
113
|
return result;
|
package/dist/store.d.ts
CHANGED
|
@@ -2,9 +2,11 @@ import type { LoopEntry, Trigger } from "./types.js";
|
|
|
2
2
|
export declare class LoopStore {
|
|
3
3
|
private filePath;
|
|
4
4
|
private lockPath;
|
|
5
|
+
private lastLoadedSignature;
|
|
5
6
|
private nextId;
|
|
6
7
|
private loops;
|
|
7
8
|
constructor(listIdOrPath?: string);
|
|
9
|
+
private getFileSignature;
|
|
8
10
|
private load;
|
|
9
11
|
private save;
|
|
10
12
|
private withLock;
|
package/dist/store.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { existsSync, mkdirSync, readFileSync, renameSync, unlinkSync, writeFileSync } from "node:fs";
|
|
1
|
+
import { existsSync, mkdirSync, readFileSync, renameSync, statSync, unlinkSync, writeFileSync } from "node:fs";
|
|
2
2
|
import { homedir } from "node:os";
|
|
3
3
|
import { dirname, isAbsolute, join } from "node:path";
|
|
4
4
|
import { reduceLoopState } from "./loop-reducer.js";
|
|
@@ -52,6 +52,7 @@ function isProcessRunning(pid) {
|
|
|
52
52
|
export class LoopStore {
|
|
53
53
|
filePath;
|
|
54
54
|
lockPath;
|
|
55
|
+
lastLoadedSignature;
|
|
55
56
|
nextId = 1;
|
|
56
57
|
loops = new Map();
|
|
57
58
|
constructor(listIdOrPath) {
|
|
@@ -64,10 +65,19 @@ export class LoopStore {
|
|
|
64
65
|
this.lockPath = filePath + ".lock";
|
|
65
66
|
this.load();
|
|
66
67
|
}
|
|
67
|
-
|
|
68
|
+
getFileSignature() {
|
|
69
|
+
if (!this.filePath || !existsSync(this.filePath))
|
|
70
|
+
return undefined;
|
|
71
|
+
const stat = statSync(this.filePath);
|
|
72
|
+
return `${stat.mtimeMs}:${stat.size}`;
|
|
73
|
+
}
|
|
74
|
+
load(force = false) {
|
|
68
75
|
if (!this.filePath)
|
|
69
76
|
return;
|
|
70
|
-
|
|
77
|
+
const signature = this.getFileSignature();
|
|
78
|
+
if (!signature)
|
|
79
|
+
return;
|
|
80
|
+
if (!force && signature === this.lastLoadedSignature)
|
|
71
81
|
return;
|
|
72
82
|
try {
|
|
73
83
|
const data = JSON.parse(readFileSync(this.filePath, "utf-8"));
|
|
@@ -76,6 +86,7 @@ export class LoopStore {
|
|
|
76
86
|
for (const loop of data.loops) {
|
|
77
87
|
this.loops.set(loop.id, loop);
|
|
78
88
|
}
|
|
89
|
+
this.lastLoadedSignature = signature;
|
|
79
90
|
}
|
|
80
91
|
catch { /* corrupt file — start fresh */ }
|
|
81
92
|
}
|
|
@@ -89,13 +100,14 @@ export class LoopStore {
|
|
|
89
100
|
const tmpPath = this.filePath + ".tmp";
|
|
90
101
|
writeFileSync(tmpPath, JSON.stringify(data, null, 2));
|
|
91
102
|
renameSync(tmpPath, this.filePath);
|
|
103
|
+
this.lastLoadedSignature = this.getFileSignature();
|
|
92
104
|
}
|
|
93
105
|
withLock(fn) {
|
|
94
106
|
if (!this.lockPath)
|
|
95
107
|
return fn();
|
|
96
108
|
acquireLock(this.lockPath);
|
|
97
109
|
try {
|
|
98
|
-
this.load();
|
|
110
|
+
this.load(true);
|
|
99
111
|
const result = fn();
|
|
100
112
|
this.save();
|
|
101
113
|
return result;
|
package/dist/task-store.d.ts
CHANGED
|
@@ -2,9 +2,11 @@ import type { TaskEntry } from "./task-types.js";
|
|
|
2
2
|
export declare class TaskStore {
|
|
3
3
|
private filePath;
|
|
4
4
|
private lockPath;
|
|
5
|
+
private lastLoadedSignature;
|
|
5
6
|
private nextId;
|
|
6
7
|
private tasks;
|
|
7
8
|
constructor(listIdOrPath?: string);
|
|
9
|
+
private getFileSignature;
|
|
8
10
|
private load;
|
|
9
11
|
private save;
|
|
10
12
|
private withLock;
|
package/dist/task-store.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { existsSync, mkdirSync, readFileSync, renameSync, unlinkSync, writeFileSync } from "node:fs";
|
|
1
|
+
import { existsSync, mkdirSync, readFileSync, renameSync, statSync, unlinkSync, writeFileSync } from "node:fs";
|
|
2
2
|
import { homedir } from "node:os";
|
|
3
3
|
import { dirname, isAbsolute, join } from "node:path";
|
|
4
4
|
import { reduceTaskState } from "./task-reducer.js";
|
|
@@ -52,6 +52,7 @@ function isProcessRunning(pid) {
|
|
|
52
52
|
export class TaskStore {
|
|
53
53
|
filePath;
|
|
54
54
|
lockPath;
|
|
55
|
+
lastLoadedSignature;
|
|
55
56
|
nextId = 1;
|
|
56
57
|
tasks = new Map();
|
|
57
58
|
constructor(listIdOrPath) {
|
|
@@ -64,10 +65,19 @@ export class TaskStore {
|
|
|
64
65
|
this.lockPath = filePath + ".lock";
|
|
65
66
|
this.load();
|
|
66
67
|
}
|
|
67
|
-
|
|
68
|
+
getFileSignature() {
|
|
69
|
+
if (!this.filePath || !existsSync(this.filePath))
|
|
70
|
+
return undefined;
|
|
71
|
+
const stat = statSync(this.filePath);
|
|
72
|
+
return `${stat.mtimeMs}:${stat.size}`;
|
|
73
|
+
}
|
|
74
|
+
load(force = false) {
|
|
68
75
|
if (!this.filePath)
|
|
69
76
|
return;
|
|
70
|
-
|
|
77
|
+
const signature = this.getFileSignature();
|
|
78
|
+
if (!signature)
|
|
79
|
+
return;
|
|
80
|
+
if (!force && signature === this.lastLoadedSignature)
|
|
71
81
|
return;
|
|
72
82
|
try {
|
|
73
83
|
const data = JSON.parse(readFileSync(this.filePath, "utf-8"));
|
|
@@ -76,6 +86,7 @@ export class TaskStore {
|
|
|
76
86
|
for (const task of data.tasks) {
|
|
77
87
|
this.tasks.set(task.id, task);
|
|
78
88
|
}
|
|
89
|
+
this.lastLoadedSignature = signature;
|
|
79
90
|
}
|
|
80
91
|
catch { /* corrupt file — start fresh */ }
|
|
81
92
|
}
|
|
@@ -89,13 +100,14 @@ export class TaskStore {
|
|
|
89
100
|
const tmpPath = this.filePath + ".tmp";
|
|
90
101
|
writeFileSync(tmpPath, JSON.stringify(data, null, 2));
|
|
91
102
|
renameSync(tmpPath, this.filePath);
|
|
103
|
+
this.lastLoadedSignature = this.getFileSignature();
|
|
92
104
|
}
|
|
93
105
|
withLock(fn) {
|
|
94
106
|
if (!this.lockPath)
|
|
95
107
|
return fn();
|
|
96
108
|
acquireLock(this.lockPath);
|
|
97
109
|
try {
|
|
98
|
-
this.load();
|
|
110
|
+
this.load(true);
|
|
99
111
|
const result = fn();
|
|
100
112
|
this.save();
|
|
101
113
|
return result;
|
|
@@ -7,14 +7,17 @@ export function registerNativeTaskTools(options) {
|
|
|
7
7
|
pi.registerTool({
|
|
8
8
|
name: "TaskCreate",
|
|
9
9
|
label: "TaskCreate",
|
|
10
|
-
description: `Create a task for tracking work across turns. Use when you need to track progress on complex multi-step tasks.
|
|
10
|
+
description: `Create a task for tracking work across turns. Use when you need to track progress on complex multi-step tasks or turn a broad user goal into a concrete backlog.
|
|
11
11
|
|
|
12
12
|
Fields:
|
|
13
13
|
- subject: brief actionable title
|
|
14
|
-
- description: detailed requirements
|
|
15
|
-
- metadata: optional tags/metadata`,
|
|
14
|
+
- description: detailed requirements and done condition`,
|
|
16
15
|
promptGuidelines: [
|
|
17
16
|
"Use TaskCreate to track complex multi-step work across turns.",
|
|
17
|
+
"When the user gives a broad goal, use multiple TaskCreate calls to decompose it into a small backlog of concrete tasks rather than one oversized task.",
|
|
18
|
+
"Prefer 2-5 tasks that separate investigation, implementation, validation, and reporting or commit-prep when those phases are distinct.",
|
|
19
|
+
"Make each `subject` a short verb-object action.",
|
|
20
|
+
"Make each `description` include the expected artifact, outcome, or done condition so another turn can pick the task up cleanly.",
|
|
18
21
|
"Break work into small, independently completable tasks. A task should be finishable in one focused session — if a task would take multiple turns, split it further.",
|
|
19
22
|
"TaskCreate accepts `subject` and `description` parameters only — do not invent extra fields unless the schema explicitly adds them.",
|
|
20
23
|
],
|
package/dist/ui/widget.d.ts
CHANGED
package/dist/ui/widget.js
CHANGED
|
@@ -2,7 +2,6 @@ export class LoopWidget {
|
|
|
2
2
|
store;
|
|
3
3
|
monitorManager;
|
|
4
4
|
uiCtx;
|
|
5
|
-
interval;
|
|
6
5
|
taskSummaryProvider;
|
|
7
6
|
constructor(store, monitorManager) {
|
|
8
7
|
this.store = store;
|
|
@@ -20,15 +19,7 @@ export class LoopWidget {
|
|
|
20
19
|
update() {
|
|
21
20
|
if (!this.uiCtx)
|
|
22
21
|
return;
|
|
23
|
-
|
|
24
|
-
if (status && !this.interval) {
|
|
25
|
-
this.interval = setInterval(() => this.update(), 5000);
|
|
26
|
-
}
|
|
27
|
-
if (!status && this.interval) {
|
|
28
|
-
clearInterval(this.interval);
|
|
29
|
-
this.interval = undefined;
|
|
30
|
-
}
|
|
31
|
-
this.uiCtx.setStatus("loops", status);
|
|
22
|
+
this.uiCtx.setStatus("loops", this.computeStatus());
|
|
32
23
|
}
|
|
33
24
|
computeStatus() {
|
|
34
25
|
const loops = this.store.list().filter(l => l.status === "active");
|
|
@@ -50,10 +41,6 @@ export class LoopWidget {
|
|
|
50
41
|
return line;
|
|
51
42
|
}
|
|
52
43
|
dispose() {
|
|
53
|
-
if (this.interval) {
|
|
54
|
-
clearInterval(this.interval);
|
|
55
|
-
this.interval = undefined;
|
|
56
|
-
}
|
|
57
44
|
this.uiCtx?.setStatus("loops", undefined);
|
|
58
45
|
}
|
|
59
46
|
}
|
package/package.json
CHANGED
package/src/goal-store.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { existsSync, mkdirSync, readFileSync, renameSync, unlinkSync, writeFileSync } from "node:fs";
|
|
1
|
+
import { existsSync, mkdirSync, readFileSync, renameSync, statSync, unlinkSync, writeFileSync } from "node:fs";
|
|
2
2
|
import { homedir } from "node:os";
|
|
3
3
|
import { dirname, isAbsolute, join } from "node:path";
|
|
4
4
|
import { type GoalReducerEvent, reduceGoalState } from "./goal-reducer.js";
|
|
@@ -51,6 +51,7 @@ function isProcessRunning(pid: number): boolean {
|
|
|
51
51
|
export class GoalStore {
|
|
52
52
|
private filePath: string | undefined;
|
|
53
53
|
private lockPath: string | undefined;
|
|
54
|
+
private lastLoadedSignature: string | undefined;
|
|
54
55
|
|
|
55
56
|
private nextId = 1;
|
|
56
57
|
private goals = new Map<string, GoalEntry>();
|
|
@@ -65,9 +66,17 @@ export class GoalStore {
|
|
|
65
66
|
this.load();
|
|
66
67
|
}
|
|
67
68
|
|
|
68
|
-
private
|
|
69
|
+
private getFileSignature(): string | undefined {
|
|
70
|
+
if (!this.filePath || !existsSync(this.filePath)) return undefined;
|
|
71
|
+
const stat = statSync(this.filePath);
|
|
72
|
+
return `${stat.mtimeMs}:${stat.size}`;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
private load(force = false): void {
|
|
69
76
|
if (!this.filePath) return;
|
|
70
|
-
|
|
77
|
+
const signature = this.getFileSignature();
|
|
78
|
+
if (!signature) return;
|
|
79
|
+
if (!force && signature === this.lastLoadedSignature) return;
|
|
71
80
|
try {
|
|
72
81
|
const data: GoalStoreData = JSON.parse(readFileSync(this.filePath, "utf-8"));
|
|
73
82
|
this.nextId = data.nextId;
|
|
@@ -75,6 +84,7 @@ export class GoalStore {
|
|
|
75
84
|
for (const goal of data.goals) {
|
|
76
85
|
this.goals.set(goal.id, goal);
|
|
77
86
|
}
|
|
87
|
+
this.lastLoadedSignature = signature;
|
|
78
88
|
} catch { /* corrupt file — start fresh */ }
|
|
79
89
|
}
|
|
80
90
|
|
|
@@ -87,13 +97,14 @@ export class GoalStore {
|
|
|
87
97
|
const tmpPath = this.filePath + ".tmp";
|
|
88
98
|
writeFileSync(tmpPath, JSON.stringify(data, null, 2));
|
|
89
99
|
renameSync(tmpPath, this.filePath);
|
|
100
|
+
this.lastLoadedSignature = this.getFileSignature();
|
|
90
101
|
}
|
|
91
102
|
|
|
92
103
|
private withLock<T>(fn: () => T): T {
|
|
93
104
|
if (!this.lockPath) return fn();
|
|
94
105
|
acquireLock(this.lockPath);
|
|
95
106
|
try {
|
|
96
|
-
this.load();
|
|
107
|
+
this.load(true);
|
|
97
108
|
const result = fn();
|
|
98
109
|
this.save();
|
|
99
110
|
return result;
|
package/src/store.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { existsSync, mkdirSync, readFileSync, renameSync, unlinkSync, writeFileSync } from "node:fs";
|
|
1
|
+
import { existsSync, mkdirSync, readFileSync, renameSync, statSync, unlinkSync, writeFileSync } from "node:fs";
|
|
2
2
|
import { homedir } from "node:os";
|
|
3
3
|
import { dirname, isAbsolute, join } from "node:path";
|
|
4
4
|
import { type LoopReducerEvent, type LoopReducerState, reduceLoopState } from "./loop-reducer.js";
|
|
@@ -44,6 +44,7 @@ function isProcessRunning(pid: number): boolean {
|
|
|
44
44
|
export class LoopStore {
|
|
45
45
|
private filePath: string | undefined;
|
|
46
46
|
private lockPath: string | undefined;
|
|
47
|
+
private lastLoadedSignature: string | undefined;
|
|
47
48
|
|
|
48
49
|
private nextId = 1;
|
|
49
50
|
private loops = new Map<string, LoopEntry>();
|
|
@@ -58,9 +59,17 @@ export class LoopStore {
|
|
|
58
59
|
this.load();
|
|
59
60
|
}
|
|
60
61
|
|
|
61
|
-
private
|
|
62
|
+
private getFileSignature(): string | undefined {
|
|
63
|
+
if (!this.filePath || !existsSync(this.filePath)) return undefined;
|
|
64
|
+
const stat = statSync(this.filePath);
|
|
65
|
+
return `${stat.mtimeMs}:${stat.size}`;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
private load(force = false): void {
|
|
62
69
|
if (!this.filePath) return;
|
|
63
|
-
|
|
70
|
+
const signature = this.getFileSignature();
|
|
71
|
+
if (!signature) return;
|
|
72
|
+
if (!force && signature === this.lastLoadedSignature) return;
|
|
64
73
|
try {
|
|
65
74
|
const data: LoopStoreData = JSON.parse(readFileSync(this.filePath, "utf-8"));
|
|
66
75
|
this.nextId = data.nextId;
|
|
@@ -68,6 +77,7 @@ export class LoopStore {
|
|
|
68
77
|
for (const loop of data.loops) {
|
|
69
78
|
this.loops.set(loop.id, loop);
|
|
70
79
|
}
|
|
80
|
+
this.lastLoadedSignature = signature;
|
|
71
81
|
} catch { /* corrupt file — start fresh */ }
|
|
72
82
|
}
|
|
73
83
|
|
|
@@ -80,13 +90,14 @@ export class LoopStore {
|
|
|
80
90
|
const tmpPath = this.filePath + ".tmp";
|
|
81
91
|
writeFileSync(tmpPath, JSON.stringify(data, null, 2));
|
|
82
92
|
renameSync(tmpPath, this.filePath);
|
|
93
|
+
this.lastLoadedSignature = this.getFileSignature();
|
|
83
94
|
}
|
|
84
95
|
|
|
85
96
|
private withLock<T>(fn: () => T): T {
|
|
86
97
|
if (!this.lockPath) return fn();
|
|
87
98
|
acquireLock(this.lockPath);
|
|
88
99
|
try {
|
|
89
|
-
this.load();
|
|
100
|
+
this.load(true);
|
|
90
101
|
const result = fn();
|
|
91
102
|
this.save();
|
|
92
103
|
return result;
|
package/src/task-store.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { existsSync, mkdirSync, readFileSync, renameSync, unlinkSync, writeFileSync } from "node:fs";
|
|
1
|
+
import { existsSync, mkdirSync, readFileSync, renameSync, statSync, unlinkSync, writeFileSync } from "node:fs";
|
|
2
2
|
import { homedir } from "node:os";
|
|
3
3
|
import { dirname, isAbsolute, join } from "node:path";
|
|
4
4
|
import { reduceTaskState, type TaskReducerEvent, type TaskReducerState } from "./task-reducer.js";
|
|
@@ -44,6 +44,7 @@ function isProcessRunning(pid: number): boolean {
|
|
|
44
44
|
export class TaskStore {
|
|
45
45
|
private filePath: string | undefined;
|
|
46
46
|
private lockPath: string | undefined;
|
|
47
|
+
private lastLoadedSignature: string | undefined;
|
|
47
48
|
|
|
48
49
|
private nextId = 1;
|
|
49
50
|
private tasks = new Map<string, TaskEntry>();
|
|
@@ -58,9 +59,17 @@ export class TaskStore {
|
|
|
58
59
|
this.load();
|
|
59
60
|
}
|
|
60
61
|
|
|
61
|
-
private
|
|
62
|
+
private getFileSignature(): string | undefined {
|
|
63
|
+
if (!this.filePath || !existsSync(this.filePath)) return undefined;
|
|
64
|
+
const stat = statSync(this.filePath);
|
|
65
|
+
return `${stat.mtimeMs}:${stat.size}`;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
private load(force = false): void {
|
|
62
69
|
if (!this.filePath) return;
|
|
63
|
-
|
|
70
|
+
const signature = this.getFileSignature();
|
|
71
|
+
if (!signature) return;
|
|
72
|
+
if (!force && signature === this.lastLoadedSignature) return;
|
|
64
73
|
try {
|
|
65
74
|
const data: TaskStoreData = JSON.parse(readFileSync(this.filePath, "utf-8"));
|
|
66
75
|
this.nextId = data.nextId;
|
|
@@ -68,6 +77,7 @@ export class TaskStore {
|
|
|
68
77
|
for (const task of data.tasks) {
|
|
69
78
|
this.tasks.set(task.id, task);
|
|
70
79
|
}
|
|
80
|
+
this.lastLoadedSignature = signature;
|
|
71
81
|
} catch { /* corrupt file — start fresh */ }
|
|
72
82
|
}
|
|
73
83
|
|
|
@@ -80,13 +90,14 @@ export class TaskStore {
|
|
|
80
90
|
const tmpPath = this.filePath + ".tmp";
|
|
81
91
|
writeFileSync(tmpPath, JSON.stringify(data, null, 2));
|
|
82
92
|
renameSync(tmpPath, this.filePath);
|
|
93
|
+
this.lastLoadedSignature = this.getFileSignature();
|
|
83
94
|
}
|
|
84
95
|
|
|
85
96
|
private withLock<T>(fn: () => T): T {
|
|
86
97
|
if (!this.lockPath) return fn();
|
|
87
98
|
acquireLock(this.lockPath);
|
|
88
99
|
try {
|
|
89
|
-
this.load();
|
|
100
|
+
this.load(true);
|
|
90
101
|
const result = fn();
|
|
91
102
|
this.save();
|
|
92
103
|
return result;
|
|
@@ -24,14 +24,17 @@ export function registerNativeTaskTools(options: NativeTaskToolsOptions): void {
|
|
|
24
24
|
pi.registerTool({
|
|
25
25
|
name: "TaskCreate",
|
|
26
26
|
label: "TaskCreate",
|
|
27
|
-
description: `Create a task for tracking work across turns. Use when you need to track progress on complex multi-step tasks.
|
|
27
|
+
description: `Create a task for tracking work across turns. Use when you need to track progress on complex multi-step tasks or turn a broad user goal into a concrete backlog.
|
|
28
28
|
|
|
29
29
|
Fields:
|
|
30
30
|
- subject: brief actionable title
|
|
31
|
-
- description: detailed requirements
|
|
32
|
-
- metadata: optional tags/metadata`,
|
|
31
|
+
- description: detailed requirements and done condition`,
|
|
33
32
|
promptGuidelines: [
|
|
34
33
|
"Use TaskCreate to track complex multi-step work across turns.",
|
|
34
|
+
"When the user gives a broad goal, use multiple TaskCreate calls to decompose it into a small backlog of concrete tasks rather than one oversized task.",
|
|
35
|
+
"Prefer 2-5 tasks that separate investigation, implementation, validation, and reporting or commit-prep when those phases are distinct.",
|
|
36
|
+
"Make each `subject` a short verb-object action.",
|
|
37
|
+
"Make each `description` include the expected artifact, outcome, or done condition so another turn can pick the task up cleanly.",
|
|
35
38
|
"Break work into small, independently completable tasks. A task should be finishable in one focused session — if a task would take multiple turns, split it further.",
|
|
36
39
|
"TaskCreate accepts `subject` and `description` parameters only — do not invent extra fields unless the schema explicitly adds them.",
|
|
37
40
|
],
|
package/src/ui/widget.ts
CHANGED
|
@@ -9,7 +9,6 @@ interface TaskSummary {
|
|
|
9
9
|
|
|
10
10
|
export class LoopWidget {
|
|
11
11
|
private uiCtx: ExtensionUIContext | undefined;
|
|
12
|
-
private interval: ReturnType<typeof setInterval> | undefined;
|
|
13
12
|
private taskSummaryProvider: (() => TaskSummary) | undefined;
|
|
14
13
|
|
|
15
14
|
constructor(
|
|
@@ -31,17 +30,7 @@ export class LoopWidget {
|
|
|
31
30
|
|
|
32
31
|
update() {
|
|
33
32
|
if (!this.uiCtx) return;
|
|
34
|
-
|
|
35
|
-
const status = this.computeStatus();
|
|
36
|
-
if (status && !this.interval) {
|
|
37
|
-
this.interval = setInterval(() => this.update(), 5000);
|
|
38
|
-
}
|
|
39
|
-
if (!status && this.interval) {
|
|
40
|
-
clearInterval(this.interval);
|
|
41
|
-
this.interval = undefined;
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
this.uiCtx.setStatus("loops", status);
|
|
33
|
+
this.uiCtx.setStatus("loops", this.computeStatus());
|
|
45
34
|
}
|
|
46
35
|
|
|
47
36
|
private computeStatus(): string | undefined {
|
|
@@ -64,10 +53,6 @@ export class LoopWidget {
|
|
|
64
53
|
}
|
|
65
54
|
|
|
66
55
|
dispose() {
|
|
67
|
-
if (this.interval) {
|
|
68
|
-
clearInterval(this.interval);
|
|
69
|
-
this.interval = undefined;
|
|
70
|
-
}
|
|
71
56
|
this.uiCtx?.setStatus("loops", undefined);
|
|
72
57
|
}
|
|
73
58
|
}
|