@pi-unipi/milestone 0.1.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/README.md +123 -0
- package/coexist.ts +123 -0
- package/commands.ts +97 -0
- package/hooks.ts +212 -0
- package/index.ts +72 -0
- package/milestone.ts +238 -0
- package/package.json +50 -0
- package/skills/milestone-onboard/SKILL.md +114 -0
- package/skills/milestone-update/SKILL.md +109 -0
- package/types.ts +61 -0
package/README.md
ADDED
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
# @pi-unipi/milestone
|
|
2
|
+
|
|
3
|
+
Lifecycle layer for project-level goals. Track progress across multiple workflow cycles via a `MILESTONES.md` file with automatic context injection and sync.
|
|
4
|
+
|
|
5
|
+
## Why
|
|
6
|
+
|
|
7
|
+
Workflow operates at the task level — brainstorm, plan, work, review. But project goals are scattered across specs, plans, and quick-work docs. Milestone provides a unified view of "what's left to do" and keeps the agent aligned with project goals across sessions.
|
|
8
|
+
|
|
9
|
+
## How It Works
|
|
10
|
+
|
|
11
|
+
1. **MILESTONES.md** — A markdown file with phases and checkbox items that tracks your project goals
|
|
12
|
+
2. **Session start hook** — Reads milestones and injects progress summary into the system prompt
|
|
13
|
+
3. **Session end hook** — Scans modified workflow docs, detects completed items, auto-updates milestones
|
|
14
|
+
4. **Coexist triggers** — Hooks into brainstorm/plan/consolidate to suggest milestone updates
|
|
15
|
+
|
|
16
|
+
## MILESTONES.md Format
|
|
17
|
+
|
|
18
|
+
```markdown
|
|
19
|
+
---
|
|
20
|
+
title: "Project Milestones"
|
|
21
|
+
created: 2026-04-28
|
|
22
|
+
updated: 2026-04-28
|
|
23
|
+
---
|
|
24
|
+
|
|
25
|
+
# Project Milestones
|
|
26
|
+
|
|
27
|
+
## Phase 1: Foundation
|
|
28
|
+
> Set up the core infrastructure
|
|
29
|
+
|
|
30
|
+
- [x] Project scaffold and build system
|
|
31
|
+
- [x] Database schema design
|
|
32
|
+
- [ ] Authentication system
|
|
33
|
+
- [ ] API routing layer
|
|
34
|
+
|
|
35
|
+
## Phase 2: Core Features
|
|
36
|
+
> Build the primary user-facing features
|
|
37
|
+
|
|
38
|
+
- [ ] User dashboard
|
|
39
|
+
- [ ] File upload system
|
|
40
|
+
- [ ] Notification service
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
## Skills
|
|
44
|
+
|
|
45
|
+
### `/unipi:milestone-onboard`
|
|
46
|
+
|
|
47
|
+
Create MILESTONES.md from existing workflow docs. Scans specs, plans, quick-work, debug, fix, and chore docs to group scattered tasks into coherent milestone phases.
|
|
48
|
+
|
|
49
|
+
**Phases:** Explore → Propose → Refine → Write → Report
|
|
50
|
+
|
|
51
|
+
### `/unipi:milestone-update`
|
|
52
|
+
|
|
53
|
+
Sync MILESTONES.md with completed work. Detects checkbox changes in workflow docs and updates milestone items.
|
|
54
|
+
|
|
55
|
+
**Phases:** Scan → Diff → Resolve → Write → Report
|
|
56
|
+
|
|
57
|
+
## API Exports
|
|
58
|
+
|
|
59
|
+
```typescript
|
|
60
|
+
import {
|
|
61
|
+
parseMilestones, // Parse MILESTONES.md → MilestoneDoc
|
|
62
|
+
writeMilestones, // Write MilestoneDoc → MILESTONES.md
|
|
63
|
+
updateItemStatus, // Toggle a checkbox item
|
|
64
|
+
getProgressSummary, // Get progress stats
|
|
65
|
+
} from "@pi-unipi/milestone";
|
|
66
|
+
|
|
67
|
+
import type {
|
|
68
|
+
MilestoneDoc,
|
|
69
|
+
MilestonePhase,
|
|
70
|
+
MilestoneItem,
|
|
71
|
+
ProgressSummary,
|
|
72
|
+
PhaseProgress,
|
|
73
|
+
} from "@pi-unipi/milestone";
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
## Lifecycle Hooks
|
|
77
|
+
|
|
78
|
+
### Session Start
|
|
79
|
+
|
|
80
|
+
On `before_agent_start`, reads `.unipi/docs/MILESTONES.md` and appends a progress summary to the system prompt:
|
|
81
|
+
|
|
82
|
+
```
|
|
83
|
+
## Project Milestones
|
|
84
|
+
Overall progress: 5/10 items (50%)
|
|
85
|
+
Phase 1: Foundation: 3/5 done
|
|
86
|
+
Phase 2: Features: 2/5 done
|
|
87
|
+
Current focus: Phase 1: Foundation
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
If MILESTONES.md doesn't exist, no context is injected.
|
|
91
|
+
|
|
92
|
+
### Session End
|
|
93
|
+
|
|
94
|
+
On `session_shutdown`, scans workflow docs modified during the session. Detects items that changed from `- [ ]` to `- [x]` and auto-updates MILESTONES.md using exact text matching.
|
|
95
|
+
|
|
96
|
+
Unmatched items are logged as warnings — resolve manually with `/unipi:milestone-update`.
|
|
97
|
+
|
|
98
|
+
## Coexist Triggers
|
|
99
|
+
|
|
100
|
+
| Trigger | Behavior |
|
|
101
|
+
|---------|----------|
|
|
102
|
+
| After brainstorm | Checks if new spec items map to milestones, logs suggestions |
|
|
103
|
+
| After plan | Maps plan tasks to milestone items, logs coverage |
|
|
104
|
+
| After consolidate | References auto-sync from session shutdown |
|
|
105
|
+
|
|
106
|
+
All triggers are non-blocking and skip gracefully if MILESTONES.md doesn't exist.
|
|
107
|
+
|
|
108
|
+
## Info Screen
|
|
109
|
+
|
|
110
|
+
Registers a "Milestones" group in the info-screen dashboard showing:
|
|
111
|
+
- **Progress** — completed/total items with percentage
|
|
112
|
+
- **Current Phase** — phase name with per-phase breakdown
|
|
113
|
+
- **Remaining** — items left to complete
|
|
114
|
+
|
|
115
|
+
## Configuration
|
|
116
|
+
|
|
117
|
+
No configuration needed. Place MILESTONES.md at `.unipi/docs/MILESTONES.md` and the extension handles the rest.
|
|
118
|
+
|
|
119
|
+
## Dependencies
|
|
120
|
+
|
|
121
|
+
- `@pi-unipi/core` — shared constants and utilities
|
|
122
|
+
- `@mariozechner/pi-coding-agent` — extension API
|
|
123
|
+
- `@mariozechner/pi-tui` — TUI types
|
package/coexist.ts
ADDED
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @pi-unipi/milestone — Coexist triggers
|
|
3
|
+
*
|
|
4
|
+
* Hooks into workflow skill completions to offer milestone integration.
|
|
5
|
+
* Non-blocking — if MILESTONES.md doesn't exist, triggers silently skip.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import * as path from "node:path";
|
|
9
|
+
import { MILESTONE_DIRS, tryRead } from "@pi-unipi/core";
|
|
10
|
+
import { parseMilestones, updateItemStatus, writeMilestones } from "./milestone.js";
|
|
11
|
+
import type { MilestoneDoc } from "./types.js";
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* After brainstorm completes: check if new spec items map to milestones.
|
|
15
|
+
* Offers to mark matching items as planned.
|
|
16
|
+
*/
|
|
17
|
+
export function onBrainstormComplete(specPath: string): void {
|
|
18
|
+
const cwd = process.cwd();
|
|
19
|
+
const milestonesPath = path.join(cwd, MILESTONE_DIRS.MILESTONES);
|
|
20
|
+
|
|
21
|
+
// Silently skip if no MILESTONES.md
|
|
22
|
+
if (!tryRead(milestonesPath)) return;
|
|
23
|
+
|
|
24
|
+
const specContent = tryRead(specPath);
|
|
25
|
+
if (!specContent) return;
|
|
26
|
+
|
|
27
|
+
// Extract checklist items from the new spec
|
|
28
|
+
const specItems: string[] = [];
|
|
29
|
+
for (const line of specContent.split("\n")) {
|
|
30
|
+
const match = line.match(/^-\s+\[([ xX])\]\s+(.+)$/);
|
|
31
|
+
if (match) {
|
|
32
|
+
specItems.push(match[2].trim().toLowerCase());
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
if (specItems.length === 0) return;
|
|
37
|
+
|
|
38
|
+
// Check against milestones
|
|
39
|
+
const doc = parseMilestones(milestonesPath);
|
|
40
|
+
const matched: string[] = [];
|
|
41
|
+
|
|
42
|
+
for (const phase of doc.phases) {
|
|
43
|
+
for (const item of phase.items) {
|
|
44
|
+
const normalized = item.text.toLowerCase().trim();
|
|
45
|
+
if (specItems.includes(normalized) && !item.checked) {
|
|
46
|
+
matched.push(`"${item.text}" in ${phase.name}`);
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
if (matched.length > 0) {
|
|
52
|
+
console.log(
|
|
53
|
+
`[milestone] Brainstorm spec contains items that map to milestones: ${matched.join(", ")}`,
|
|
54
|
+
);
|
|
55
|
+
console.log(
|
|
56
|
+
`[milestone] Run /unipi:milestone-update to sync after completing work.`,
|
|
57
|
+
);
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* After plan completes: check if plan tasks map to milestone items.
|
|
63
|
+
* Logs matching items for awareness.
|
|
64
|
+
*/
|
|
65
|
+
export function onPlanComplete(planPath: string): void {
|
|
66
|
+
const cwd = process.cwd();
|
|
67
|
+
const milestonesPath = path.join(cwd, MILESTONE_DIRS.MILESTONES);
|
|
68
|
+
|
|
69
|
+
// Silently skip if no MILESTONES.md
|
|
70
|
+
if (!tryRead(milestonesPath)) return;
|
|
71
|
+
|
|
72
|
+
const planContent = tryRead(planPath);
|
|
73
|
+
if (!planContent) return;
|
|
74
|
+
|
|
75
|
+
// Extract task names from plan (### Task N — Name pattern)
|
|
76
|
+
const planTasks: string[] = [];
|
|
77
|
+
for (const line of planContent.split("\n")) {
|
|
78
|
+
const match = line.match(/^###\s+Task\s+\d+\s*[—–-]\s*(.+)$/);
|
|
79
|
+
if (match) {
|
|
80
|
+
planTasks.push(match[1].trim().toLowerCase());
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
if (planTasks.length === 0) return;
|
|
85
|
+
|
|
86
|
+
// Check against milestones
|
|
87
|
+
const doc = parseMilestones(milestonesPath);
|
|
88
|
+
const matched: string[] = [];
|
|
89
|
+
|
|
90
|
+
for (const phase of doc.phases) {
|
|
91
|
+
for (const item of phase.items) {
|
|
92
|
+
const normalized = item.text.toLowerCase().trim();
|
|
93
|
+
// Check if any plan task contains the milestone item text or vice versa
|
|
94
|
+
for (const task of planTasks) {
|
|
95
|
+
if (task.includes(normalized) || normalized.includes(task)) {
|
|
96
|
+
matched.push(`"${item.text}" → plan task`);
|
|
97
|
+
break;
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
if (matched.length > 0) {
|
|
104
|
+
console.log(
|
|
105
|
+
`[milestone] Plan covers milestone items: ${matched.join(", ")}`,
|
|
106
|
+
);
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
/**
|
|
111
|
+
* After consolidate: reference milestone sync that already happened.
|
|
112
|
+
*/
|
|
113
|
+
export function onConsolidate(): void {
|
|
114
|
+
const cwd = process.cwd();
|
|
115
|
+
const milestonesPath = path.join(cwd, MILESTONE_DIRS.MILESTONES);
|
|
116
|
+
|
|
117
|
+
if (!tryRead(milestonesPath)) return;
|
|
118
|
+
|
|
119
|
+
// Just log that milestone sync was handled by session_shutdown hook
|
|
120
|
+
console.log(
|
|
121
|
+
`[milestone] Milestone progress was auto-synced during session shutdown.`,
|
|
122
|
+
);
|
|
123
|
+
}
|
package/commands.ts
ADDED
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @pi-unipi/milestone — Command registration
|
|
3
|
+
*
|
|
4
|
+
* Registers milestone-onboard and milestone-update commands.
|
|
5
|
+
* Follows the same pattern as workflow/commands.ts:
|
|
6
|
+
* loads SKILL.md content and sends it as a user message.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import type { ExtensionAPI } from "@mariozechner/pi-coding-agent";
|
|
10
|
+
import { UNIPI_PREFIX, MILESTONE_COMMANDS, MILESTONE_DIRS } from "@pi-unipi/core";
|
|
11
|
+
import { parseMilestones } from "./milestone.js";
|
|
12
|
+
import { readFileSync } from "node:fs";
|
|
13
|
+
import { join } from "node:path";
|
|
14
|
+
|
|
15
|
+
/** Resolve the skills directory relative to this file */
|
|
16
|
+
const SKILLS_DIR = join(new URL(".", import.meta.url).pathname, "skills");
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Load SKILL.md content for a given skill name.
|
|
20
|
+
*/
|
|
21
|
+
function loadSkill(skillName: string): string {
|
|
22
|
+
try {
|
|
23
|
+
return readFileSync(join(SKILLS_DIR, skillName, "SKILL.md"), "utf-8");
|
|
24
|
+
} catch {
|
|
25
|
+
return "";
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Register milestone commands with the extension API.
|
|
31
|
+
*/
|
|
32
|
+
export function registerCommands(pi: ExtensionAPI): void {
|
|
33
|
+
// milestone-onboard — create milestones from existing work
|
|
34
|
+
pi.registerCommand(`${UNIPI_PREFIX}${MILESTONE_COMMANDS.ONBOARD}`, {
|
|
35
|
+
description: "Create MILESTONES.md from existing workflow docs — scan, propose, refine, write",
|
|
36
|
+
handler: async (args: string, ctx: any) => {
|
|
37
|
+
const skillContent = loadSkill("milestone-onboard");
|
|
38
|
+
|
|
39
|
+
let message = "Execute the milestone-onboard workflow.";
|
|
40
|
+
if (args?.trim()) {
|
|
41
|
+
message += `\n\nArguments: ${args.trim()}`;
|
|
42
|
+
}
|
|
43
|
+
if (skillContent) {
|
|
44
|
+
message += `\n\n<skill_content>\n${skillContent}\n</skill_content>`;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
pi.sendUserMessage(message, { deliverAs: "followUp" });
|
|
48
|
+
|
|
49
|
+
if (ctx.hasUI) {
|
|
50
|
+
ctx.ui.notify("Running /unipi:milestone-onboard", "info");
|
|
51
|
+
}
|
|
52
|
+
},
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
// milestone-update — sync milestones with completed work
|
|
56
|
+
pi.registerCommand(`${UNIPI_PREFIX}${MILESTONE_COMMANDS.UPDATE}`, {
|
|
57
|
+
description: "Sync MILESTONES.md with completed work — scan docs, diff checkboxes, auto-update",
|
|
58
|
+
handler: async (args: string, ctx: any) => {
|
|
59
|
+
const skillContent = loadSkill("milestone-update");
|
|
60
|
+
|
|
61
|
+
let message = "Execute the milestone-update workflow.";
|
|
62
|
+
if (args?.trim()) {
|
|
63
|
+
message += `\n\nArguments: ${args.trim()}`;
|
|
64
|
+
}
|
|
65
|
+
if (skillContent) {
|
|
66
|
+
message += `\n\n<skill_content>\n${skillContent}\n</skill_content>`;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
pi.sendUserMessage(message, { deliverAs: "followUp" });
|
|
70
|
+
|
|
71
|
+
if (ctx.hasUI) {
|
|
72
|
+
ctx.ui.notify("Running /unipi:milestone-update", "info");
|
|
73
|
+
}
|
|
74
|
+
},
|
|
75
|
+
getArgumentCompletions: () => {
|
|
76
|
+
// Suggest phase names from existing MILESTONES.md
|
|
77
|
+
const cwd = process.cwd();
|
|
78
|
+
const milestonesPath = join(cwd, MILESTONE_DIRS.MILESTONES);
|
|
79
|
+
const doc = parseMilestones(milestonesPath);
|
|
80
|
+
|
|
81
|
+
const suggestions = doc.phases.map((phase) => ({
|
|
82
|
+
value: phase.name,
|
|
83
|
+
label: phase.name,
|
|
84
|
+
description: `${phase.items.filter((i) => i.checked).length}/${phase.items.length} done`,
|
|
85
|
+
}));
|
|
86
|
+
|
|
87
|
+
// Add "all" option
|
|
88
|
+
suggestions.unshift({
|
|
89
|
+
value: "all",
|
|
90
|
+
label: "all",
|
|
91
|
+
description: "Update all phases",
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
return suggestions;
|
|
95
|
+
},
|
|
96
|
+
});
|
|
97
|
+
}
|
package/hooks.ts
ADDED
|
@@ -0,0 +1,212 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @pi-unipi/milestone — Lifecycle hooks
|
|
3
|
+
*
|
|
4
|
+
* Session start: inject milestone progress as system context.
|
|
5
|
+
* Session end: auto-sync completed items from workflow docs.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import * as fs from "node:fs";
|
|
9
|
+
import * as path from "node:path";
|
|
10
|
+
import type { ExtensionAPI } from "@mariozechner/pi-coding-agent";
|
|
11
|
+
import { MILESTONE_DIRS, safeMtimeMs, tryRead } from "@pi-unipi/core";
|
|
12
|
+
import { parseMilestones, getProgressSummary, updateItemStatus } from "./milestone.js";
|
|
13
|
+
|
|
14
|
+
/** Track when the session started for diffing modified files */
|
|
15
|
+
let sessionStartMs = 0;
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Format a progress summary as a context string for the system prompt.
|
|
19
|
+
*/
|
|
20
|
+
function formatMilestoneContext(filePath: string): string | null {
|
|
21
|
+
const summary = getProgressSummary(filePath);
|
|
22
|
+
if (summary.totalItems === 0) return null;
|
|
23
|
+
|
|
24
|
+
const phaseLines = summary.phases
|
|
25
|
+
.filter((p) => p.total > 0)
|
|
26
|
+
.map((p) => ` ${p.name}: ${p.done}/${p.total} done`);
|
|
27
|
+
|
|
28
|
+
const focus = summary.currentPhase
|
|
29
|
+
? `Current focus: ${summary.currentPhase}`
|
|
30
|
+
: "";
|
|
31
|
+
|
|
32
|
+
return [
|
|
33
|
+
"## Project Milestones",
|
|
34
|
+
`Overall progress: ${summary.completedItems}/${summary.totalItems} items (${summary.percentComplete}%)`,
|
|
35
|
+
...phaseLines,
|
|
36
|
+
focus,
|
|
37
|
+
]
|
|
38
|
+
.filter(Boolean)
|
|
39
|
+
.join("\n");
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Register session start hook — injects milestone progress into system context.
|
|
44
|
+
*/
|
|
45
|
+
export function registerSessionStartHook(pi: ExtensionAPI): void {
|
|
46
|
+
pi.on("before_agent_start", (event) => {
|
|
47
|
+
sessionStartMs = Date.now();
|
|
48
|
+
|
|
49
|
+
const cwd = process.cwd();
|
|
50
|
+
const milestonesPath = path.join(cwd, MILESTONE_DIRS.MILESTONES);
|
|
51
|
+
|
|
52
|
+
const context = formatMilestoneContext(milestonesPath);
|
|
53
|
+
if (!context) return undefined;
|
|
54
|
+
|
|
55
|
+
// Append milestone context to the system prompt
|
|
56
|
+
const currentPrompt = (event as any).systemPrompt ?? "";
|
|
57
|
+
return {
|
|
58
|
+
systemPrompt: currentPrompt + "\n\n" + context,
|
|
59
|
+
};
|
|
60
|
+
});
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* Extract checkbox items that changed from [ ] to [x] in a file.
|
|
65
|
+
* Compares current state against a baseline snapshot.
|
|
66
|
+
*/
|
|
67
|
+
function extractNewCompletions(
|
|
68
|
+
filePath: string,
|
|
69
|
+
baselineContent: string,
|
|
70
|
+
): Array<{ text: string; phase: string }> {
|
|
71
|
+
const currentContent = tryRead(filePath);
|
|
72
|
+
if (!currentContent) return [];
|
|
73
|
+
|
|
74
|
+
const baselineLines = baselineContent.split("\n");
|
|
75
|
+
const currentLines = currentContent.split("\n");
|
|
76
|
+
const results: Array<{ text: string; phase: string }> = [];
|
|
77
|
+
let currentPhase = "";
|
|
78
|
+
|
|
79
|
+
for (let i = 0; i < currentLines.length; i++) {
|
|
80
|
+
const line = currentLines[i];
|
|
81
|
+
|
|
82
|
+
// Track phase
|
|
83
|
+
const phaseMatch = line.match(/^##\s+(.+)$/);
|
|
84
|
+
if (phaseMatch) {
|
|
85
|
+
currentPhase = phaseMatch[1].trim();
|
|
86
|
+
continue;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
// Check if this line is a newly checked item
|
|
90
|
+
const currentItemMatch = line.match(/^-\s+\[x\]\s+(.+)$/);
|
|
91
|
+
if (currentItemMatch && currentPhase) {
|
|
92
|
+
// Check if baseline had this as unchecked
|
|
93
|
+
const baselineLine = baselineLines[i] ?? "";
|
|
94
|
+
const baselineItemMatch = baselineLine.match(/^-\s+\[([ xX])\]\s+(.+)$/);
|
|
95
|
+
if (baselineItemMatch && baselineItemMatch[1] === " ") {
|
|
96
|
+
// Same position, was unchecked, now checked
|
|
97
|
+
results.push({ text: currentItemMatch[1].trim(), phase: currentPhase });
|
|
98
|
+
} else if (!baselineItemMatch) {
|
|
99
|
+
// Line didn't exist or wasn't a checkbox — check by text match in same phase
|
|
100
|
+
const text = currentItemMatch[1].trim().toLowerCase();
|
|
101
|
+
const foundUnchecked = baselineLines.some((bl) => {
|
|
102
|
+
const m = bl.match(/^-\s+\[\s\]\s+(.+)$/);
|
|
103
|
+
return m && m[1].trim().toLowerCase() === text;
|
|
104
|
+
});
|
|
105
|
+
if (foundUnchecked) {
|
|
106
|
+
results.push({ text: currentItemMatch[1].trim(), phase: currentPhase });
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
return results;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
/**
|
|
116
|
+
* Scan workflow docs for files modified since session start.
|
|
117
|
+
*/
|
|
118
|
+
function scanModifiedDocs(dirs: string[], since: number): string[] {
|
|
119
|
+
const modified: string[] = [];
|
|
120
|
+
|
|
121
|
+
for (const dir of dirs) {
|
|
122
|
+
if (!fs.existsSync(dir)) continue;
|
|
123
|
+
|
|
124
|
+
const entries = fs.readdirSync(dir, { withFileTypes: true });
|
|
125
|
+
for (const entry of entries) {
|
|
126
|
+
if (!entry.isFile()) continue;
|
|
127
|
+
const filePath = path.join(dir, entry.name);
|
|
128
|
+
const mtime = safeMtimeMs(filePath);
|
|
129
|
+
if (mtime > since) {
|
|
130
|
+
modified.push(filePath);
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
return modified;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
/**
|
|
139
|
+
* Register session end hook — listens for WORKFLOW_END events,
|
|
140
|
+
* scans modified docs, and auto-updates MILESTONES.md.
|
|
141
|
+
*/
|
|
142
|
+
export function registerSessionEndHook(pi: ExtensionAPI): void {
|
|
143
|
+
// Store baseline snapshots at session start
|
|
144
|
+
const baselineSnapshots = new Map<string, string>();
|
|
145
|
+
|
|
146
|
+
// Capture baselines on session start
|
|
147
|
+
pi.on("session_start", () => {
|
|
148
|
+
sessionStartMs = Date.now();
|
|
149
|
+
baselineSnapshots.clear();
|
|
150
|
+
|
|
151
|
+
const cwd = process.cwd();
|
|
152
|
+
const scanDirs = [
|
|
153
|
+
path.join(cwd, ".unipi/docs/specs"),
|
|
154
|
+
path.join(cwd, ".unipi/docs/plans"),
|
|
155
|
+
path.join(cwd, ".unipi/docs/quick-work"),
|
|
156
|
+
];
|
|
157
|
+
|
|
158
|
+
for (const dir of scanDirs) {
|
|
159
|
+
if (!fs.existsSync(dir)) continue;
|
|
160
|
+
const entries = fs.readdirSync(dir, { withFileTypes: true });
|
|
161
|
+
for (const entry of entries) {
|
|
162
|
+
if (!entry.isFile()) continue;
|
|
163
|
+
const filePath = path.join(dir, entry.name);
|
|
164
|
+
const content = tryRead(filePath);
|
|
165
|
+
if (content) baselineSnapshots.set(filePath, content);
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
});
|
|
169
|
+
|
|
170
|
+
// Listen for WORKFLOW_END events
|
|
171
|
+
pi.on("input", (event) => {
|
|
172
|
+
// Check if this is a unipi event emission for WORKFLOW_END
|
|
173
|
+
// The input event fires for tool calls; we need to detect when
|
|
174
|
+
// the workflow ends. We'll use the events system instead.
|
|
175
|
+
return undefined;
|
|
176
|
+
});
|
|
177
|
+
|
|
178
|
+
// Use tool_result to detect workflow end
|
|
179
|
+
// Actually, we should listen for the UNIPI_EVENTS.WORKFLOW_END via pi.events
|
|
180
|
+
// But the ExtensionAPI doesn't expose pi.events.on() directly.
|
|
181
|
+
// Instead, we'll hook into session_shutdown to do a final sync.
|
|
182
|
+
pi.on("session_shutdown", () => {
|
|
183
|
+
const cwd = process.cwd();
|
|
184
|
+
const milestonesPath = path.join(cwd, MILESTONE_DIRS.MILESTONES);
|
|
185
|
+
|
|
186
|
+
if (!fs.existsSync(milestonesPath)) return;
|
|
187
|
+
|
|
188
|
+
const scanDirs = [
|
|
189
|
+
path.join(cwd, ".unipi/docs/specs"),
|
|
190
|
+
path.join(cwd, ".unipi/docs/plans"),
|
|
191
|
+
path.join(cwd, ".unipi/docs/quick-work"),
|
|
192
|
+
];
|
|
193
|
+
|
|
194
|
+
const modifiedFiles = scanModifiedDocs(scanDirs, sessionStartMs);
|
|
195
|
+
|
|
196
|
+
for (const filePath of modifiedFiles) {
|
|
197
|
+
const baseline = baselineSnapshots.get(filePath);
|
|
198
|
+
if (!baseline) continue;
|
|
199
|
+
|
|
200
|
+
const completions = extractNewCompletions(filePath, baseline);
|
|
201
|
+
for (const { text, phase } of completions) {
|
|
202
|
+
// Try exact match update
|
|
203
|
+
const updated = updateItemStatus(milestonesPath, phase, text, true);
|
|
204
|
+
if (!updated) {
|
|
205
|
+
console.warn(
|
|
206
|
+
`[milestone] Could not auto-update "${text}" in phase "${phase}" — no exact match found`,
|
|
207
|
+
);
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
});
|
|
212
|
+
}
|
package/index.ts
ADDED
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @pi-unipi/milestone — Extension entry point
|
|
3
|
+
*
|
|
4
|
+
* Lifecycle layer for project-level goals. Tracks progress via MILESTONES.md,
|
|
5
|
+
* injects context on session start, auto-syncs on session end.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import type { ExtensionAPI } from "@mariozechner/pi-coding-agent";
|
|
9
|
+
import { MODULES, MILESTONE_COMMANDS, MILESTONE_DIRS, emitEvent, UNIPI_EVENTS } from "@pi-unipi/core";
|
|
10
|
+
import { registerSessionStartHook, registerSessionEndHook } from "./hooks.js";
|
|
11
|
+
import { registerCommands } from "./commands.js";
|
|
12
|
+
import { getProgressSummary } from "./milestone.js";
|
|
13
|
+
import * as path from "node:path";
|
|
14
|
+
|
|
15
|
+
export default function milestoneExtension(pi: ExtensionAPI): void {
|
|
16
|
+
// Register lifecycle hooks
|
|
17
|
+
registerSessionStartHook(pi);
|
|
18
|
+
registerSessionEndHook(pi);
|
|
19
|
+
|
|
20
|
+
// Register commands
|
|
21
|
+
registerCommands(pi);
|
|
22
|
+
|
|
23
|
+
// Register info-screen group
|
|
24
|
+
const globalObj = globalThis as any;
|
|
25
|
+
const registry = globalObj.__unipi_info_registry;
|
|
26
|
+
if (registry) {
|
|
27
|
+
registry.registerGroup({
|
|
28
|
+
id: "milestone",
|
|
29
|
+
name: "Milestones",
|
|
30
|
+
icon: "🎯",
|
|
31
|
+
priority: 40,
|
|
32
|
+
config: {
|
|
33
|
+
showByDefault: true,
|
|
34
|
+
stats: [
|
|
35
|
+
{ id: "progress", label: "Progress", show: true },
|
|
36
|
+
{ id: "current_phase", label: "Current Phase", show: true },
|
|
37
|
+
{ id: "remaining", label: "Remaining", show: true },
|
|
38
|
+
],
|
|
39
|
+
},
|
|
40
|
+
dataProvider: async () => {
|
|
41
|
+
const cwd = process.cwd();
|
|
42
|
+
const milestonesPath = path.join(cwd, MILESTONE_DIRS.MILESTONES);
|
|
43
|
+
const summary = getProgressSummary(milestonesPath);
|
|
44
|
+
|
|
45
|
+
return {
|
|
46
|
+
progress: {
|
|
47
|
+
value: `${summary.completedItems}/${summary.totalItems}`,
|
|
48
|
+
detail: `${summary.percentComplete}% complete`,
|
|
49
|
+
},
|
|
50
|
+
current_phase: {
|
|
51
|
+
value: summary.currentPhase || "None",
|
|
52
|
+
detail: summary.phases.length > 0
|
|
53
|
+
? summary.phases.map((p) => `${p.name}: ${p.done}/${p.total}`).join(", ")
|
|
54
|
+
: "No milestones defined",
|
|
55
|
+
},
|
|
56
|
+
remaining: {
|
|
57
|
+
value: String(summary.totalItems - summary.completedItems),
|
|
58
|
+
detail: "items remaining",
|
|
59
|
+
},
|
|
60
|
+
};
|
|
61
|
+
},
|
|
62
|
+
});
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
// Emit module ready event
|
|
66
|
+
emitEvent(pi as any, UNIPI_EVENTS.MODULE_READY, {
|
|
67
|
+
name: MODULES.MILESTONE,
|
|
68
|
+
version: "0.1.0",
|
|
69
|
+
commands: Object.values(MILESTONE_COMMANDS),
|
|
70
|
+
tools: [],
|
|
71
|
+
});
|
|
72
|
+
}
|
package/milestone.ts
ADDED
|
@@ -0,0 +1,238 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @pi-unipi/milestone — MILESTONES.md parser, writer, and updater
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import * as fs from "node:fs";
|
|
6
|
+
import * as path from "node:path";
|
|
7
|
+
import { ensureDir, tryRead } from "@pi-unipi/core";
|
|
8
|
+
import type { MilestoneDoc, MilestonePhase, MilestoneItem, ProgressSummary } from "./types.js";
|
|
9
|
+
|
|
10
|
+
/** Default empty milestone doc */
|
|
11
|
+
function emptyDoc(filePath: string): MilestoneDoc {
|
|
12
|
+
return {
|
|
13
|
+
title: "Project Milestones",
|
|
14
|
+
created: new Date().toISOString().split("T")[0],
|
|
15
|
+
updated: new Date().toISOString().split("T")[0],
|
|
16
|
+
phases: [],
|
|
17
|
+
filePath,
|
|
18
|
+
};
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Parse a MILESTONES.md file into a MilestoneDoc.
|
|
23
|
+
* Handles missing file (returns empty doc) and malformed input (skips unparseable lines).
|
|
24
|
+
*/
|
|
25
|
+
export function parseMilestones(filePath: string): MilestoneDoc {
|
|
26
|
+
const content = tryRead(filePath);
|
|
27
|
+
if (!content) return emptyDoc(filePath);
|
|
28
|
+
|
|
29
|
+
const lines = content.split("\n");
|
|
30
|
+
const doc = emptyDoc(filePath);
|
|
31
|
+
let currentPhase: MilestonePhase | null = null;
|
|
32
|
+
let inFrontmatter = false;
|
|
33
|
+
let frontmatterDone = false;
|
|
34
|
+
|
|
35
|
+
for (let i = 0; i < lines.length; i++) {
|
|
36
|
+
const line = lines[i];
|
|
37
|
+
const lineNum = i + 1; // 1-indexed
|
|
38
|
+
|
|
39
|
+
// Frontmatter parsing
|
|
40
|
+
if (lineNum === 1 && line.trim() === "---") {
|
|
41
|
+
inFrontmatter = true;
|
|
42
|
+
continue;
|
|
43
|
+
}
|
|
44
|
+
if (inFrontmatter) {
|
|
45
|
+
if (line.trim() === "---") {
|
|
46
|
+
inFrontmatter = false;
|
|
47
|
+
frontmatterDone = true;
|
|
48
|
+
continue;
|
|
49
|
+
}
|
|
50
|
+
const match = line.match(/^(\w+):\s*(.+)$/);
|
|
51
|
+
if (match) {
|
|
52
|
+
const [, key, value] = match;
|
|
53
|
+
if (key === "title") doc.title = value.replace(/^["']|["']$/g, "");
|
|
54
|
+
if (key === "created") doc.created = value.trim();
|
|
55
|
+
if (key === "updated") doc.updated = value.trim();
|
|
56
|
+
}
|
|
57
|
+
continue;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
// Phase header: ## Phase N: Name or ## Name
|
|
61
|
+
const phaseMatch = line.match(/^##\s+(.+)$/);
|
|
62
|
+
if (phaseMatch) {
|
|
63
|
+
currentPhase = {
|
|
64
|
+
name: phaseMatch[1].trim(),
|
|
65
|
+
items: [],
|
|
66
|
+
};
|
|
67
|
+
doc.phases.push(currentPhase);
|
|
68
|
+
continue;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
// Phase description: > text
|
|
72
|
+
if (currentPhase && line.match(/^>\s*(.*)$/)) {
|
|
73
|
+
const desc = line.replace(/^>\s*/, "").trim();
|
|
74
|
+
if (desc) {
|
|
75
|
+
currentPhase.description = currentPhase.description
|
|
76
|
+
? `${currentPhase.description} ${desc}`
|
|
77
|
+
: desc;
|
|
78
|
+
}
|
|
79
|
+
continue;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
// Checkbox item: - [ ] text or - [x] text
|
|
83
|
+
const itemMatch = line.match(/^-\s+\[([ xX])\]\s+(.+)$/);
|
|
84
|
+
if (itemMatch && currentPhase) {
|
|
85
|
+
const checked = itemMatch[1].toLowerCase() === "x";
|
|
86
|
+
const text = itemMatch[2].trim();
|
|
87
|
+
currentPhase.items.push({
|
|
88
|
+
text,
|
|
89
|
+
checked,
|
|
90
|
+
lineNumber: lineNum,
|
|
91
|
+
});
|
|
92
|
+
continue;
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
return doc;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* Write a MilestoneDoc to a MILESTONES.md file.
|
|
101
|
+
* Generates frontmatter, phase headers, descriptions, and checkbox items.
|
|
102
|
+
*/
|
|
103
|
+
export function writeMilestones(filePath: string, doc: MilestoneDoc): void {
|
|
104
|
+
const lines: string[] = [];
|
|
105
|
+
|
|
106
|
+
// Frontmatter
|
|
107
|
+
lines.push("---");
|
|
108
|
+
lines.push(`title: "${doc.title}"`);
|
|
109
|
+
lines.push(`created: ${doc.created}`);
|
|
110
|
+
lines.push(`updated: ${doc.updated}`);
|
|
111
|
+
lines.push("---");
|
|
112
|
+
lines.push("");
|
|
113
|
+
lines.push(`# ${doc.title}`);
|
|
114
|
+
lines.push("");
|
|
115
|
+
|
|
116
|
+
// Phases
|
|
117
|
+
for (const phase of doc.phases) {
|
|
118
|
+
lines.push(`## ${phase.name}`);
|
|
119
|
+
if (phase.description) {
|
|
120
|
+
lines.push(`> ${phase.description}`);
|
|
121
|
+
}
|
|
122
|
+
lines.push("");
|
|
123
|
+
for (const item of phase.items) {
|
|
124
|
+
const check = item.checked ? "[x]" : "[ ]";
|
|
125
|
+
lines.push(`- ${check} ${item.text}`);
|
|
126
|
+
}
|
|
127
|
+
lines.push("");
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
ensureDir(filePath);
|
|
131
|
+
// Atomic write: write to temp, rename
|
|
132
|
+
const tmpPath = filePath + ".tmp";
|
|
133
|
+
fs.writeFileSync(tmpPath, lines.join("\n"), "utf-8");
|
|
134
|
+
fs.renameSync(tmpPath, filePath);
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
/**
|
|
138
|
+
* Toggle a checkbox item's status in a MILESTONES.md file.
|
|
139
|
+
* Matches by normalized (lowercase, trimmed) phase name and item text.
|
|
140
|
+
* Returns true if item was found and updated.
|
|
141
|
+
*/
|
|
142
|
+
export function updateItemStatus(
|
|
143
|
+
filePath: string,
|
|
144
|
+
phaseName: string,
|
|
145
|
+
itemText: string,
|
|
146
|
+
checked: boolean,
|
|
147
|
+
): boolean {
|
|
148
|
+
const content = tryRead(filePath);
|
|
149
|
+
if (!content) return false;
|
|
150
|
+
|
|
151
|
+
const lines = content.split("\n");
|
|
152
|
+
const normalizedPhase = phaseName.toLowerCase().trim();
|
|
153
|
+
const normalizedItem = itemText.toLowerCase().trim();
|
|
154
|
+
let currentPhase = "";
|
|
155
|
+
let found = false;
|
|
156
|
+
|
|
157
|
+
for (let i = 0; i < lines.length; i++) {
|
|
158
|
+
const line = lines[i];
|
|
159
|
+
|
|
160
|
+
// Track current phase
|
|
161
|
+
const phaseMatch = line.match(/^##\s+(.+)$/);
|
|
162
|
+
if (phaseMatch) {
|
|
163
|
+
currentPhase = phaseMatch[1].trim().toLowerCase();
|
|
164
|
+
continue;
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
// Match checkbox in the target phase
|
|
168
|
+
if (currentPhase === normalizedPhase) {
|
|
169
|
+
const itemMatch = line.match(/^-\s+\[([ xX])\]\s+(.+)$/);
|
|
170
|
+
if (itemMatch) {
|
|
171
|
+
const lineText = itemMatch[2].trim().toLowerCase();
|
|
172
|
+
if (lineText === normalizedItem) {
|
|
173
|
+
const mark = checked ? "x" : " ";
|
|
174
|
+
lines[i] = `- [${mark}] ${itemMatch[2].trim()}`;
|
|
175
|
+
found = true;
|
|
176
|
+
break;
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
if (found) {
|
|
183
|
+
// Update the 'updated' frontmatter date
|
|
184
|
+
const today = new Date().toISOString().split("T")[0];
|
|
185
|
+
for (let i = 0; i < lines.length; i++) {
|
|
186
|
+
if (lines[i].match(/^updated:\s*/)) {
|
|
187
|
+
lines[i] = `updated: ${today}`;
|
|
188
|
+
break;
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
// Atomic write
|
|
193
|
+
const tmpPath = filePath + ".tmp";
|
|
194
|
+
fs.writeFileSync(tmpPath, lines.join("\n"), "utf-8");
|
|
195
|
+
fs.renameSync(tmpPath, filePath);
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
return found;
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
/**
|
|
202
|
+
* Get a progress summary from a MILESTONES.md file.
|
|
203
|
+
* Returns empty summary if file doesn't exist.
|
|
204
|
+
*/
|
|
205
|
+
export function getProgressSummary(filePath: string): ProgressSummary {
|
|
206
|
+
const doc = parseMilestones(filePath);
|
|
207
|
+
|
|
208
|
+
let totalItems = 0;
|
|
209
|
+
let completedItems = 0;
|
|
210
|
+
let currentPhase = "";
|
|
211
|
+
const phases: ProgressSummary["phases"] = [];
|
|
212
|
+
|
|
213
|
+
for (const phase of doc.phases) {
|
|
214
|
+
const done = phase.items.filter((i) => i.checked).length;
|
|
215
|
+
const total = phase.items.length;
|
|
216
|
+
totalItems += total;
|
|
217
|
+
completedItems += done;
|
|
218
|
+
|
|
219
|
+
phases.push({
|
|
220
|
+
name: phase.name,
|
|
221
|
+
done,
|
|
222
|
+
total,
|
|
223
|
+
});
|
|
224
|
+
|
|
225
|
+
// Current phase: first phase with incomplete items
|
|
226
|
+
if (!currentPhase && done < total) {
|
|
227
|
+
currentPhase = phase.name;
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
return {
|
|
232
|
+
totalItems,
|
|
233
|
+
completedItems,
|
|
234
|
+
percentComplete: totalItems > 0 ? Math.round((completedItems / totalItems) * 100) : 0,
|
|
235
|
+
currentPhase: currentPhase || (doc.phases.length > 0 ? doc.phases[0].name : "None"),
|
|
236
|
+
phases,
|
|
237
|
+
};
|
|
238
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@pi-unipi/milestone",
|
|
3
|
+
"version": "0.1.2",
|
|
4
|
+
"description": "Lifecycle layer for project-level goals — MILESTONES.md tracking, session hooks, auto-sync",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"license": "MIT",
|
|
7
|
+
"author": "Neuron Mr White",
|
|
8
|
+
"repository": {
|
|
9
|
+
"type": "git",
|
|
10
|
+
"url": "git+https://github.com/Neuron-Mr-White/unipi.git",
|
|
11
|
+
"directory": "packages/milestone"
|
|
12
|
+
},
|
|
13
|
+
"keywords": [
|
|
14
|
+
"pi-package",
|
|
15
|
+
"pi-extension",
|
|
16
|
+
"pi-coding-agent",
|
|
17
|
+
"unipi",
|
|
18
|
+
"milestone",
|
|
19
|
+
"goals",
|
|
20
|
+
"progress"
|
|
21
|
+
],
|
|
22
|
+
"files": [
|
|
23
|
+
"*.ts",
|
|
24
|
+
"skills/**/*",
|
|
25
|
+
"README.md"
|
|
26
|
+
],
|
|
27
|
+
"pi": {
|
|
28
|
+
"extensions": [
|
|
29
|
+
"index.ts"
|
|
30
|
+
],
|
|
31
|
+
"skills": [
|
|
32
|
+
"skills"
|
|
33
|
+
]
|
|
34
|
+
},
|
|
35
|
+
"publishConfig": {
|
|
36
|
+
"access": "public"
|
|
37
|
+
},
|
|
38
|
+
"dependencies": {
|
|
39
|
+
"@pi-unipi/core": "*"
|
|
40
|
+
},
|
|
41
|
+
"peerDependencies": {
|
|
42
|
+
"@mariozechner/pi-coding-agent": "*",
|
|
43
|
+
"@mariozechner/pi-tui": "*",
|
|
44
|
+
"@sinclair/typebox": "*"
|
|
45
|
+
},
|
|
46
|
+
"devDependencies": {
|
|
47
|
+
"@types/node": "^25.6.0",
|
|
48
|
+
"typescript": "^6.0.0"
|
|
49
|
+
}
|
|
50
|
+
}
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: milestone-onboard
|
|
3
|
+
description: "Create MILESTONES.md from existing workflow docs — scan, propose, refine, write."
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Onboard Milestones
|
|
7
|
+
|
|
8
|
+
Create a MILESTONES.md file by scanning existing workflow documentation. Groups scattered tasks into coherent milestone phases.
|
|
9
|
+
|
|
10
|
+
## Boundaries
|
|
11
|
+
|
|
12
|
+
**This skill MAY:** read `.unipi/docs/`, read existing specs/plans/quick-work/debug/fix/chore docs, write `.unipi/docs/MILESTONES.md`.
|
|
13
|
+
**This skill MAY NOT:** modify existing workflow docs, delete files, merge branches.
|
|
14
|
+
|
|
15
|
+
---
|
|
16
|
+
|
|
17
|
+
## Phase 1: Explore
|
|
18
|
+
|
|
19
|
+
Scan `.unipi/docs/` for existing workflow documentation. Understand what's been done and what's planned.
|
|
20
|
+
|
|
21
|
+
1. List all files in `.unipi/docs/{specs,plans,quick-work,debug,fix,chore}/`
|
|
22
|
+
2. For each file, extract:
|
|
23
|
+
- Checkbox items (`- [ ]` and `- [x]`)
|
|
24
|
+
- Task statuses (`unstarted:`, `in-progress:`, `completed:`)
|
|
25
|
+
- File modification dates
|
|
26
|
+
3. Categorize findings:
|
|
27
|
+
- **Completed work** — checked items, completed tasks
|
|
28
|
+
- **In progress** — in-progress tasks, partially checked lists
|
|
29
|
+
- **Planned** — unstarted tasks, unchecked items
|
|
30
|
+
4. Present summary to user:
|
|
31
|
+
> "Found X completed items, Y in-progress, Z planned across N documents."
|
|
32
|
+
|
|
33
|
+
---
|
|
34
|
+
|
|
35
|
+
## Phase 2: Propose
|
|
36
|
+
|
|
37
|
+
Group findings into logical milestone phases. Present with rationale.
|
|
38
|
+
|
|
39
|
+
1. Analyze themes across documents — group related items together
|
|
40
|
+
2. Suggest 2-5 phases with clear names and descriptions
|
|
41
|
+
3. For each phase, list proposed items (both done and todo)
|
|
42
|
+
4. Present proposal:
|
|
43
|
+
> "**Phase 1: Foundation** (3/5 done)
|
|
44
|
+
> - [x] Project scaffold
|
|
45
|
+
> - [x] Core parser
|
|
46
|
+
> - [ ] Type definitions
|
|
47
|
+
> - [ ] Error handling
|
|
48
|
+
> - [ ] Documentation
|
|
49
|
+
>
|
|
50
|
+
> **Phase 2: Features** (0/3 done)
|
|
51
|
+
> - [ ] User dashboard
|
|
52
|
+
> - [ ] File upload
|
|
53
|
+
> - [ ] Notifications
|
|
54
|
+
>
|
|
55
|
+
> Does this grouping look right?"
|
|
56
|
+
|
|
57
|
+
5. **One question at a time** — ask if phases are correct before proceeding
|
|
58
|
+
|
|
59
|
+
---
|
|
60
|
+
|
|
61
|
+
## Phase 3: Refine
|
|
62
|
+
|
|
63
|
+
User approves/adjusts phases. Iterate until satisfied.
|
|
64
|
+
|
|
65
|
+
1. If user wants changes:
|
|
66
|
+
- **Add phase**: Ask for name and items
|
|
67
|
+
- **Remove phase**: Confirm removal
|
|
68
|
+
- **Move items**: Ask which item, which phase
|
|
69
|
+
- **Rename phase**: Ask for new name
|
|
70
|
+
- **Add items**: Ask for text and target phase
|
|
71
|
+
2. After each change, show updated proposal
|
|
72
|
+
3. Continue until user says "looks good" or "write it"
|
|
73
|
+
|
|
74
|
+
---
|
|
75
|
+
|
|
76
|
+
## Phase 4: Write
|
|
77
|
+
|
|
78
|
+
Save MILESTONES.md using the milestone parser.
|
|
79
|
+
|
|
80
|
+
1. Build `MilestoneDoc` from approved phases:
|
|
81
|
+
- `title`: "Project Milestones" (or ask user for custom title)
|
|
82
|
+
- `created`: today's date
|
|
83
|
+
- `updated`: today's date
|
|
84
|
+
- `phases`: approved phases with items
|
|
85
|
+
2. Call `writeMilestones(".unipi/docs/MILESTONES.md", doc)`
|
|
86
|
+
3. Verify file was written correctly
|
|
87
|
+
|
|
88
|
+
---
|
|
89
|
+
|
|
90
|
+
## Phase 5: Report
|
|
91
|
+
|
|
92
|
+
Show summary and suggest next steps.
|
|
93
|
+
|
|
94
|
+
1. Display what was written:
|
|
95
|
+
> "✅ MILESTONES.md created with N phases and M items.
|
|
96
|
+
> - Phase 1: Foundation (3/5 done)
|
|
97
|
+
> - Phase 2: Features (0/3 done)
|
|
98
|
+
>
|
|
99
|
+
> **Next steps:**
|
|
100
|
+
> - `/unipi:milestone-update` — sync milestones with completed work
|
|
101
|
+
> - Milestones will auto-inject context on session start
|
|
102
|
+
> - Completed items auto-sync on session end"
|
|
103
|
+
|
|
104
|
+
---
|
|
105
|
+
|
|
106
|
+
## Validation Checklist
|
|
107
|
+
|
|
108
|
+
Before completing, verify:
|
|
109
|
+
- [ ] MILESTONES.md exists at `.unipi/docs/MILESTONES.md`
|
|
110
|
+
- [ ] File has valid frontmatter (title, created, updated)
|
|
111
|
+
- [ ] All phases have names
|
|
112
|
+
- [ ] All items have checkbox format (`- [ ]` or `- [x]`)
|
|
113
|
+
- [ ] Previously completed items are marked `[x]`
|
|
114
|
+
- [ ] File parses correctly via `parseMilestones()`
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: milestone-update
|
|
3
|
+
description: "Sync MILESTONES.md with completed work — scan docs, diff checkboxes, auto-update."
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Update Milestones
|
|
7
|
+
|
|
8
|
+
Sync MILESTONES.md with work completed in workflow docs. Detects checkbox changes and updates milestone items.
|
|
9
|
+
|
|
10
|
+
## Boundaries
|
|
11
|
+
|
|
12
|
+
**This skill MAY:** read `.unipi/docs/`, update `.unipi/docs/MILESTONES.md`, ask user for conflict resolution.
|
|
13
|
+
**This skill MAY NOT:** modify workflow docs, delete files, create new milestones (use `/unipi:milestone-onboard`).
|
|
14
|
+
|
|
15
|
+
---
|
|
16
|
+
|
|
17
|
+
## Phase 1: Scan
|
|
18
|
+
|
|
19
|
+
Read all workflow docs modified since last milestone update.
|
|
20
|
+
|
|
21
|
+
1. Read `.unipi/docs/MILESTONES.md` — if missing, suggest `/unipi:milestone-onboard`
|
|
22
|
+
2. Record `updated` date from MILESTONES.md frontmatter
|
|
23
|
+
3. Scan `.unipi/docs/{specs,plans,quick-work}/` for files modified after the `updated` date
|
|
24
|
+
4. If no modified files found:
|
|
25
|
+
> "No workflow docs have been modified since the last milestone update."
|
|
26
|
+
5. List modified files and present to user
|
|
27
|
+
|
|
28
|
+
---
|
|
29
|
+
|
|
30
|
+
## Phase 2: Diff
|
|
31
|
+
|
|
32
|
+
Compare checkbox states between current docs and baseline.
|
|
33
|
+
|
|
34
|
+
1. For each modified file:
|
|
35
|
+
- Extract all checkbox items (`- [ ]` and `- [x]`)
|
|
36
|
+
- Extract task statuses from plans (`completed:`)
|
|
37
|
+
2. Compare against MILESTONES.md items:
|
|
38
|
+
- **Exact match** (normalized): item text matches a milestone item
|
|
39
|
+
- **No match**: item not found in milestones
|
|
40
|
+
3. Categorize:
|
|
41
|
+
- **Newly completed**: item is `[x]` in doc but `[ ]` in milestones
|
|
42
|
+
- **Already synced**: item matches milestone state
|
|
43
|
+
- **Unmatched**: item not in milestones at all
|
|
44
|
+
4. Present diff:
|
|
45
|
+
> "**Found 3 changes:**
|
|
46
|
+
> - ✅ `Authentication system` — completed in spec.md (exact match)
|
|
47
|
+
> - ✅ `API routing` — completed in plan.md (exact match)
|
|
48
|
+
> - ⚠️ `New feature idea` — not found in milestones (skipped)"
|
|
49
|
+
|
|
50
|
+
---
|
|
51
|
+
|
|
52
|
+
## Phase 3: Resolve
|
|
53
|
+
|
|
54
|
+
Auto-update clear matches, ask user on conflicts.
|
|
55
|
+
|
|
56
|
+
1. **Exact matches**: Update automatically via `updateItemStatus()`
|
|
57
|
+
2. **Unmatched completions**: Present to user via `ask_user`:
|
|
58
|
+
> "Found completed items not in milestones:
|
|
59
|
+
> 1. `New feature idea` (from spec.md)
|
|
60
|
+
>
|
|
61
|
+
> What should I do?"
|
|
62
|
+
> - Skip (don't update milestones)
|
|
63
|
+
> - Add to a phase (specify which)
|
|
64
|
+
3. If user wants to add: ask which phase, then add item and mark as completed
|
|
65
|
+
4. Log all changes made
|
|
66
|
+
|
|
67
|
+
---
|
|
68
|
+
|
|
69
|
+
## Phase 4: Write
|
|
70
|
+
|
|
71
|
+
Apply resolved changes to MILESTONES.md.
|
|
72
|
+
|
|
73
|
+
1. For each resolved change:
|
|
74
|
+
- Call `updateItemStatus(milestonesPath, phase, text, true)` for completions
|
|
75
|
+
- Or modify doc directly for new items
|
|
76
|
+
2. Update `updated` frontmatter date to today
|
|
77
|
+
3. Verify file still parses correctly
|
|
78
|
+
|
|
79
|
+
---
|
|
80
|
+
|
|
81
|
+
## Phase 5: Report
|
|
82
|
+
|
|
83
|
+
Show what changed, what was skipped, suggest next steps.
|
|
84
|
+
|
|
85
|
+
1. Display summary:
|
|
86
|
+
> "✅ **Milestones updated:**
|
|
87
|
+
> - 2 items marked complete
|
|
88
|
+
> - 1 item skipped (not in milestones)
|
|
89
|
+
>
|
|
90
|
+
> **Progress:** 5/10 items (50%)
|
|
91
|
+
> **Current phase:** Phase 1: Foundation (3/5 done)
|
|
92
|
+
>
|
|
93
|
+
> **Next steps:**
|
|
94
|
+
> - Continue with remaining items
|
|
95
|
+
> - `/unipi:milestone-onboard` — restructure milestones"
|
|
96
|
+
|
|
97
|
+
2. If no changes were made:
|
|
98
|
+
> "No milestone items needed updating. Everything is in sync."
|
|
99
|
+
|
|
100
|
+
---
|
|
101
|
+
|
|
102
|
+
## Validation Checklist
|
|
103
|
+
|
|
104
|
+
Before completing, verify:
|
|
105
|
+
- [ ] MILESTONES.md still exists and is valid
|
|
106
|
+
- [ ] All auto-updated items match their source docs
|
|
107
|
+
- [ ] `updated` date reflects today
|
|
108
|
+
- [ ] No items were accidentally unchecked
|
|
109
|
+
- [ ] File parses correctly via `parseMilestones()`
|
package/types.ts
ADDED
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @pi-unipi/milestone — Type definitions
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
/** A single item within a milestone phase */
|
|
6
|
+
export interface MilestoneItem {
|
|
7
|
+
/** Item text (without checkbox) */
|
|
8
|
+
text: string;
|
|
9
|
+
/** Whether item is checked off */
|
|
10
|
+
checked: boolean;
|
|
11
|
+
/** Line number in the source file (1-indexed) */
|
|
12
|
+
lineNumber: number;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
/** A phase grouping milestone items */
|
|
16
|
+
export interface MilestonePhase {
|
|
17
|
+
/** Phase name (e.g., "Phase 1: Foundation") */
|
|
18
|
+
name: string;
|
|
19
|
+
/** Optional description (from `>` blockquote lines) */
|
|
20
|
+
description?: string;
|
|
21
|
+
/** Items in this phase */
|
|
22
|
+
items: MilestoneItem[];
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/** Parsed representation of a MILESTONES.md file */
|
|
26
|
+
export interface MilestoneDoc {
|
|
27
|
+
/** Document title from frontmatter */
|
|
28
|
+
title: string;
|
|
29
|
+
/** Creation date (ISO string) */
|
|
30
|
+
created: string;
|
|
31
|
+
/** Last update date (ISO string) */
|
|
32
|
+
updated: string;
|
|
33
|
+
/** Ordered list of phases */
|
|
34
|
+
phases: MilestonePhase[];
|
|
35
|
+
/** Source file path */
|
|
36
|
+
filePath: string;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/** Per-phase progress */
|
|
40
|
+
export interface PhaseProgress {
|
|
41
|
+
/** Phase name */
|
|
42
|
+
name: string;
|
|
43
|
+
/** Completed items in this phase */
|
|
44
|
+
done: number;
|
|
45
|
+
/** Total items in this phase */
|
|
46
|
+
total: number;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/** Progress summary across all phases */
|
|
50
|
+
export interface ProgressSummary {
|
|
51
|
+
/** Total items across all phases */
|
|
52
|
+
totalItems: number;
|
|
53
|
+
/** Completed items across all phases */
|
|
54
|
+
completedItems: number;
|
|
55
|
+
/** Overall completion percentage (0-100) */
|
|
56
|
+
percentComplete: number;
|
|
57
|
+
/** Name of the current phase (first with incomplete items) */
|
|
58
|
+
currentPhase: string;
|
|
59
|
+
/** Per-phase progress */
|
|
60
|
+
phases: PhaseProgress[];
|
|
61
|
+
}
|