@pi-unipi/milestone 2.4.1 → 2.5.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 +11 -3
- package/hooks.ts +103 -29
- package/index.ts +1 -1
- package/package.json +5 -2
package/README.md
CHANGED
|
@@ -15,9 +15,17 @@ Workflow operates at the task level — brainstorm, plan, work, review. Project
|
|
|
15
15
|
|
|
16
16
|
### Session Start
|
|
17
17
|
|
|
18
|
-
On `before_agent_start`, milestone reads `.unipi/docs/MILESTONES.md` and appends a
|
|
18
|
+
On `before_agent_start`, milestone reads `.unipi/docs/MILESTONES.md` from `ctx.cwd` and appends a hidden `unipi-milestone-snapshot` custom message to the session:
|
|
19
19
|
|
|
20
20
|
```
|
|
21
|
+
# UniPi Milestone Snapshot
|
|
22
|
+
|
|
23
|
+
This snapshot supersedes all prior UniPi milestone snapshots; use only this snapshot for milestone status.
|
|
24
|
+
|
|
25
|
+
Workspace: /path/to/project
|
|
26
|
+
|
|
27
|
+
Status: active
|
|
28
|
+
|
|
21
29
|
## Project Milestones
|
|
22
30
|
Overall progress: 5/10 items (50%)
|
|
23
31
|
Phase 1: Foundation: 3/5 done
|
|
@@ -25,11 +33,11 @@ Overall progress: 5/10 items (50%)
|
|
|
25
33
|
Current focus: Phase 1: Foundation
|
|
26
34
|
```
|
|
27
35
|
|
|
28
|
-
|
|
36
|
+
Snapshots are append-only and hidden from the transcript. They keep the system-prompt prefix stable, persist milestone context in session history, and are deduplicated against the latest milestone custom message in the effective (compaction-aware) LLM context. If milestones disappear while an older active snapshot remains effective, an inactive snapshot is appended to supersede it. A clean workspace with no milestones and no effective snapshot receives no injected message.
|
|
29
37
|
|
|
30
38
|
### Session End
|
|
31
39
|
|
|
32
|
-
On `session_shutdown`, milestone scans workflow docs modified during the session.
|
|
40
|
+
On `session_shutdown`, milestone scans workflow docs modified during the session. It uses the workspace captured from `session_start`'s `ctx.cwd`, detects items that changed from `- [ ]` to `- [x]`, and auto-updates MILESTONES.md using exact text matching.
|
|
33
41
|
|
|
34
42
|
### Coexist Triggers
|
|
35
43
|
|
package/hooks.ts
CHANGED
|
@@ -1,22 +1,33 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* @pi-unipi/milestone — Lifecycle hooks
|
|
3
3
|
*
|
|
4
|
-
*
|
|
4
|
+
* Agent start: append milestone progress as a hidden, persistent context snapshot.
|
|
5
5
|
* Session end: auto-sync completed items from workflow docs.
|
|
6
6
|
*/
|
|
7
7
|
|
|
8
8
|
import * as fs from "node:fs";
|
|
9
9
|
import * as path from "node:path";
|
|
10
|
-
import
|
|
10
|
+
import {
|
|
11
|
+
buildSessionContext,
|
|
12
|
+
type ExtensionAPI,
|
|
13
|
+
type SessionEntry,
|
|
14
|
+
} from "@earendil-works/pi-coding-agent";
|
|
11
15
|
import { MILESTONE_DIRS, UNIPI_EVENTS, safeMtimeMs, tryRead } from "@pi-unipi/core";
|
|
12
|
-
import {
|
|
16
|
+
import { getProgressSummary, updateItemStatus } from "./milestone.js";
|
|
13
17
|
|
|
14
|
-
|
|
15
|
-
let sessionStartMs = 0;
|
|
18
|
+
export const MILESTONE_SNAPSHOT_TYPE = "unipi-milestone-snapshot";
|
|
16
19
|
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
+
interface MilestoneSnapshotDetails {
|
|
21
|
+
active: boolean;
|
|
22
|
+
workspace: string;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
interface EffectiveSnapshot {
|
|
26
|
+
content: unknown;
|
|
27
|
+
details?: MilestoneSnapshotDetails;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/** Format the active milestone progress included in a snapshot. */
|
|
20
31
|
function formatMilestoneContext(filePath: string): string | null {
|
|
21
32
|
const summary = getProgressSummary(filePath);
|
|
22
33
|
if (summary.totalItems === 0) return null;
|
|
@@ -39,23 +50,84 @@ function formatMilestoneContext(filePath: string): string | null {
|
|
|
39
50
|
.join("\n");
|
|
40
51
|
}
|
|
41
52
|
|
|
53
|
+
/** Build an append-only snapshot that explicitly invalidates earlier snapshots. */
|
|
54
|
+
function formatMilestoneSnapshot(workspace: string, context: string | null): string {
|
|
55
|
+
return [
|
|
56
|
+
"# UniPi Milestone Snapshot",
|
|
57
|
+
"This snapshot supersedes all prior UniPi milestone snapshots; use only this snapshot for milestone status.",
|
|
58
|
+
`Workspace: ${workspace}`,
|
|
59
|
+
`Status: ${context ? "active" : "inactive"}`,
|
|
60
|
+
context ?? "No milestones are active for this workspace.",
|
|
61
|
+
].join("\n\n");
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
function latestEffectiveSnapshot(branch: SessionEntry[]): EffectiveSnapshot | undefined {
|
|
65
|
+
const messages = buildSessionContext(branch).messages;
|
|
66
|
+
for (let index = messages.length - 1; index >= 0; index--) {
|
|
67
|
+
const message = messages[index];
|
|
68
|
+
if (message.role === "custom" && message.customType === MILESTONE_SNAPSHOT_TYPE) {
|
|
69
|
+
return {
|
|
70
|
+
content: message.content,
|
|
71
|
+
details: message.details as MilestoneSnapshotDetails | undefined,
|
|
72
|
+
};
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
return undefined;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/** Find state that may have been folded into a compaction summary. */
|
|
79
|
+
function latestHistoricalSnapshot(branch: SessionEntry[]): EffectiveSnapshot | undefined {
|
|
80
|
+
for (let index = branch.length - 1; index >= 0; index--) {
|
|
81
|
+
const entry = branch[index];
|
|
82
|
+
if (entry.type === "custom_message" && entry.customType === MILESTONE_SNAPSHOT_TYPE) {
|
|
83
|
+
return {
|
|
84
|
+
content: entry.content,
|
|
85
|
+
details: entry.details as MilestoneSnapshotDetails | undefined,
|
|
86
|
+
};
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
return undefined;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
function isActiveSnapshot(snapshot: EffectiveSnapshot): boolean {
|
|
93
|
+
if (typeof snapshot.details?.active === "boolean") return snapshot.details.active;
|
|
94
|
+
return typeof snapshot.content === "string" && snapshot.content.includes("Status: active");
|
|
95
|
+
}
|
|
96
|
+
|
|
42
97
|
/**
|
|
43
|
-
* Register
|
|
98
|
+
* Register the agent-start hook. Snapshots are hidden from the transcript but
|
|
99
|
+
* persist in the append-only session and therefore keep the system prefix stable.
|
|
44
100
|
*/
|
|
45
101
|
export function registerSessionStartHook(pi: ExtensionAPI): void {
|
|
46
|
-
pi.on("before_agent_start", (
|
|
47
|
-
|
|
102
|
+
pi.on("before_agent_start", (_event, ctx) => {
|
|
103
|
+
const workspace = ctx.cwd;
|
|
104
|
+
const milestonesPath = path.join(workspace, MILESTONE_DIRS.MILESTONES);
|
|
105
|
+
const context = formatMilestoneContext(milestonesPath);
|
|
106
|
+
const branch = ctx.sessionManager.getBranch();
|
|
107
|
+
const latest = latestEffectiveSnapshot(branch);
|
|
108
|
+
const historical = latestHistoricalSnapshot(branch);
|
|
48
109
|
|
|
49
|
-
|
|
50
|
-
|
|
110
|
+
// A genuinely clean workspace/session needs no synthetic context. Raw
|
|
111
|
+
// history is checked because compaction may have folded an old active
|
|
112
|
+
// snapshot into summary prose while removing its custom-message identity.
|
|
113
|
+
if (!context && !latest && !historical) return undefined;
|
|
51
114
|
|
|
52
|
-
const
|
|
53
|
-
if (!context) return undefined;
|
|
115
|
+
const prior = latest ?? historical;
|
|
116
|
+
if (!context && prior && !isActiveSnapshot(prior)) return undefined;
|
|
117
|
+
|
|
118
|
+
const content = formatMilestoneSnapshot(workspace, context);
|
|
119
|
+
if (latest?.content === content) return undefined;
|
|
54
120
|
|
|
55
|
-
// Append milestone context to the system prompt
|
|
56
|
-
const currentPrompt = (event as any).systemPrompt ?? "";
|
|
57
121
|
return {
|
|
58
|
-
|
|
122
|
+
message: {
|
|
123
|
+
customType: MILESTONE_SNAPSHOT_TYPE,
|
|
124
|
+
content,
|
|
125
|
+
display: false,
|
|
126
|
+
details: {
|
|
127
|
+
active: context !== null,
|
|
128
|
+
workspace,
|
|
129
|
+
} satisfies MilestoneSnapshotDetails,
|
|
130
|
+
},
|
|
59
131
|
};
|
|
60
132
|
});
|
|
61
133
|
}
|
|
@@ -140,19 +212,21 @@ function scanModifiedDocs(dirs: string[], since: number): string[] {
|
|
|
140
212
|
* scans modified docs, and auto-updates MILESTONES.md.
|
|
141
213
|
*/
|
|
142
214
|
export function registerSessionEndHook(pi: ExtensionAPI): void {
|
|
143
|
-
//
|
|
215
|
+
// Capture the session workspace because process.cwd() can change before shutdown.
|
|
144
216
|
const baselineSnapshots = new Map<string, string>();
|
|
217
|
+
let sessionStartMs = 0;
|
|
218
|
+
let sessionWorkspace: string | null = null;
|
|
145
219
|
|
|
146
220
|
// Capture baselines on session start
|
|
147
|
-
pi.on("session_start", () => {
|
|
221
|
+
pi.on("session_start", (_event, ctx) => {
|
|
148
222
|
sessionStartMs = Date.now();
|
|
223
|
+
sessionWorkspace = ctx.cwd;
|
|
149
224
|
baselineSnapshots.clear();
|
|
150
225
|
|
|
151
|
-
const cwd = process.cwd();
|
|
152
226
|
const scanDirs = [
|
|
153
|
-
path.join(
|
|
154
|
-
path.join(
|
|
155
|
-
path.join(
|
|
227
|
+
path.join(sessionWorkspace, ".unipi/docs/specs"),
|
|
228
|
+
path.join(sessionWorkspace, ".unipi/docs/plans"),
|
|
229
|
+
path.join(sessionWorkspace, ".unipi/docs/quick-work"),
|
|
156
230
|
];
|
|
157
231
|
|
|
158
232
|
for (const dir of scanDirs) {
|
|
@@ -168,15 +242,15 @@ export function registerSessionEndHook(pi: ExtensionAPI): void {
|
|
|
168
242
|
});
|
|
169
243
|
|
|
170
244
|
const syncModifiedDocs = () => {
|
|
171
|
-
|
|
172
|
-
const milestonesPath = path.join(cwd, MILESTONE_DIRS.MILESTONES);
|
|
245
|
+
if (!sessionWorkspace) return;
|
|
173
246
|
|
|
247
|
+
const milestonesPath = path.join(sessionWorkspace, MILESTONE_DIRS.MILESTONES);
|
|
174
248
|
if (!fs.existsSync(milestonesPath)) return;
|
|
175
249
|
|
|
176
250
|
const scanDirs = [
|
|
177
|
-
path.join(
|
|
178
|
-
path.join(
|
|
179
|
-
path.join(
|
|
251
|
+
path.join(sessionWorkspace, ".unipi/docs/specs"),
|
|
252
|
+
path.join(sessionWorkspace, ".unipi/docs/plans"),
|
|
253
|
+
path.join(sessionWorkspace, ".unipi/docs/quick-work"),
|
|
180
254
|
];
|
|
181
255
|
|
|
182
256
|
const modifiedFiles = scanModifiedDocs(scanDirs, sessionStartMs);
|
package/index.ts
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
* @pi-unipi/milestone — Extension entry point
|
|
3
3
|
*
|
|
4
4
|
* Lifecycle layer for project-level goals. Tracks progress via MILESTONES.md,
|
|
5
|
-
*
|
|
5
|
+
* appends hidden context snapshots before agent turns, and auto-syncs on session end.
|
|
6
6
|
*/
|
|
7
7
|
|
|
8
8
|
import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
package/package.json
CHANGED
|
@@ -1,9 +1,12 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@pi-unipi/milestone",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.5.0",
|
|
4
4
|
"description": "Lifecycle layer for project-level goals — MILESTONES.md tracking, session hooks, auto-sync",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "index.ts",
|
|
7
|
+
"scripts": {
|
|
8
|
+
"test": "npx tsx --test tests/hooks.test.ts"
|
|
9
|
+
},
|
|
7
10
|
"license": "MIT",
|
|
8
11
|
"author": "Neuron Mr White",
|
|
9
12
|
"repository": {
|
|
@@ -29,7 +32,7 @@
|
|
|
29
32
|
"access": "public"
|
|
30
33
|
},
|
|
31
34
|
"dependencies": {
|
|
32
|
-
"@pi-unipi/core": "2.
|
|
35
|
+
"@pi-unipi/core": "2.5.0"
|
|
33
36
|
},
|
|
34
37
|
"peerDependencies": {
|
|
35
38
|
"@earendil-works/pi-coding-agent": "^0.80.0",
|