@dreki-gg/pi-plan-mode 0.21.0 → 0.24.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/CHANGELOG.md +25 -0
- package/README.md +48 -5
- package/bin/clean-plans.js +67 -49
- package/extensions/plan-mode/__tests__/html-render.test.ts +37 -23
- package/extensions/plan-mode/__tests__/initiative-readiness.test.ts +159 -0
- package/extensions/plan-mode/__tests__/initiative-status.test.ts +98 -0
- package/extensions/plan-mode/__tests__/initiatives-manifest.test.ts +89 -0
- package/extensions/plan-mode/__tests__/list-initiatives.test.ts +59 -0
- package/extensions/plan-mode/__tests__/list-plans.test.ts +253 -0
- package/extensions/plan-mode/__tests__/reconcile.test.ts +38 -1
- package/extensions/plan-mode/__tests__/revise-plan.test.ts +38 -0
- package/extensions/plan-mode/__tests__/schema.test.ts +88 -0
- package/extensions/plan-mode/__tests__/submit-initiative.test.ts +58 -0
- package/extensions/plan-mode/__tests__/submit-plan.test.ts +97 -0
- package/extensions/plan-mode/__tests__/update-initiative.test.ts +72 -0
- package/extensions/plan-mode/commands/list-initiatives.ts +148 -0
- package/extensions/plan-mode/commands/list-plans.ts +229 -0
- package/extensions/plan-mode/constants.ts +5 -0
- package/extensions/plan-mode/html/render.ts +41 -18
- package/extensions/plan-mode/index.ts +31 -0
- package/extensions/plan-mode/initiative.ts +168 -0
- package/extensions/plan-mode/prompts.ts +5 -1
- package/extensions/plan-mode/reconcile.ts +65 -0
- package/extensions/plan-mode/schema.ts +28 -0
- package/extensions/plan-mode/storage/initiatives-manifest.ts +112 -0
- package/extensions/plan-mode/storage/plan-storage.ts +11 -0
- package/extensions/plan-mode/storage/plans-manifest.ts +16 -1
- package/extensions/plan-mode/tools/initiative-status.ts +191 -0
- package/extensions/plan-mode/tools/preview-prototype.ts +14 -9
- package/extensions/plan-mode/tools/reconcile-plans.ts +33 -6
- package/extensions/plan-mode/tools/revise-plan.ts +22 -0
- package/extensions/plan-mode/tools/submit-initiative.ts +86 -0
- package/extensions/plan-mode/tools/submit-plan.ts +37 -4
- package/extensions/plan-mode/tools/update-initiative.ts +120 -0
- package/extensions/plan-mode/tools/update-plan.ts +3 -0
- package/extensions/plan-mode/types.ts +3 -0
- package/package.json +2 -3
- package/skills/planning-context/SKILL.md +11 -0
- package/skills/visual-prototype/SKILL.md +4 -2
- package/extensions/plan-mode/html/templates/prototype.pug +0 -25
- package/extensions/plan-mode/pug.d.ts +0 -5
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* update_initiative tool — initiative-level lifecycle control.
|
|
3
|
+
*
|
|
4
|
+
* The initiative sibling of update_plan. An initiative's `done` status is a
|
|
5
|
+
* projection of its member plans, but `superseded` / `abandoned` (and reopen)
|
|
6
|
+
* are explicit decisions recorded here with an optional `reason`. A manually
|
|
7
|
+
* set terminal status is never auto-overridden by projection.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import type { ExtensionAPI } from '@earendil-works/pi-coding-agent';
|
|
11
|
+
import { StringEnum } from '@earendil-works/pi-ai';
|
|
12
|
+
import { Text } from '@earendil-works/pi-tui';
|
|
13
|
+
import { Type } from 'typebox';
|
|
14
|
+
import type { InitiativeStatus } from '../types.js';
|
|
15
|
+
import type { RunPlanIO } from '../effects/runtime.js';
|
|
16
|
+
import {
|
|
17
|
+
readInitiativesManifest,
|
|
18
|
+
upsertInitiativeEntry,
|
|
19
|
+
} from '../storage/initiatives-manifest.js';
|
|
20
|
+
|
|
21
|
+
/** Normalize an initiative hint (`x` or `.plans/x`) to a bare name. */
|
|
22
|
+
function normalizeName(hint: string): string {
|
|
23
|
+
return hint
|
|
24
|
+
.replace(/^\.plans\//, '')
|
|
25
|
+
.replace(/\/+$/, '')
|
|
26
|
+
.trim();
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export function registerUpdateInitiativeTool(pi: ExtensionAPI, runPlanIO: RunPlanIO): void {
|
|
30
|
+
pi.registerTool({
|
|
31
|
+
name: 'update_initiative',
|
|
32
|
+
label: 'Update Initiative',
|
|
33
|
+
description:
|
|
34
|
+
'Set an initiative-level status (done, superseded, abandoned, or reopen to in-progress) with an optional reason.',
|
|
35
|
+
promptSnippet: 'Close or reopen an initiative (done/superseded/abandoned/in-progress)',
|
|
36
|
+
promptGuidelines: [
|
|
37
|
+
'Use update_initiative to close an initiative that will not complete via its plans: superseded (another initiative shipped it) or abandoned (won\u2019t do).',
|
|
38
|
+
'Always pass a reason for superseded/abandoned so the registry keeps honest history.',
|
|
39
|
+
'An initiative usually reaches done automatically once every member plan is closed \u2014 prefer letting projection handle it.',
|
|
40
|
+
],
|
|
41
|
+
parameters: Type.Object({
|
|
42
|
+
initiative: Type.String({ description: 'Initiative name (or .plans/<name>) to update' }),
|
|
43
|
+
status: StringEnum(['in-progress', 'done', 'superseded', 'abandoned'] as const),
|
|
44
|
+
reason: Type.Optional(
|
|
45
|
+
Type.String({ description: 'Why — recorded in the registry (esp. superseded/abandoned)' }),
|
|
46
|
+
),
|
|
47
|
+
}),
|
|
48
|
+
|
|
49
|
+
async execute(_toolCallId, params) {
|
|
50
|
+
const name = normalizeName(params.initiative);
|
|
51
|
+
const manifest = await runPlanIO(readInitiativesManifest());
|
|
52
|
+
const existing = manifest.find((entry) => entry.name === name);
|
|
53
|
+
if (!existing) {
|
|
54
|
+
const names = manifest.map((entry) => entry.name).join(', ');
|
|
55
|
+
return {
|
|
56
|
+
content: [
|
|
57
|
+
{
|
|
58
|
+
type: 'text' as const,
|
|
59
|
+
text: `Initiative not found: ${name}. Known initiatives: ${names || '(none)'}`,
|
|
60
|
+
},
|
|
61
|
+
],
|
|
62
|
+
details: { error: 'not_found', initiative: name } as Record<string, unknown>,
|
|
63
|
+
};
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
await runPlanIO(
|
|
67
|
+
upsertInitiativeEntry(name, {
|
|
68
|
+
status: params.status as InitiativeStatus,
|
|
69
|
+
title: existing.title,
|
|
70
|
+
reason: params.reason,
|
|
71
|
+
}),
|
|
72
|
+
);
|
|
73
|
+
|
|
74
|
+
const reasonSuffix = params.reason ? ` — ${params.reason}` : '';
|
|
75
|
+
return {
|
|
76
|
+
content: [
|
|
77
|
+
{
|
|
78
|
+
type: 'text' as const,
|
|
79
|
+
text: `Initiative ${name}: ${existing.status} → ${params.status}${reasonSuffix}`,
|
|
80
|
+
},
|
|
81
|
+
],
|
|
82
|
+
details: {
|
|
83
|
+
initiative: name,
|
|
84
|
+
from: existing.status,
|
|
85
|
+
status: params.status,
|
|
86
|
+
reason: params.reason,
|
|
87
|
+
} as Record<string, unknown>,
|
|
88
|
+
};
|
|
89
|
+
},
|
|
90
|
+
|
|
91
|
+
renderCall(args, theme) {
|
|
92
|
+
const name = (args as { initiative?: string }).initiative ?? 'initiative';
|
|
93
|
+
const status = (args as { status?: string }).status ?? '';
|
|
94
|
+
let content = theme.fg('toolTitle', theme.bold('update_initiative '));
|
|
95
|
+
content += theme.fg('accent', name);
|
|
96
|
+
if (status) content += ' ' + theme.fg('muted', status);
|
|
97
|
+
return new Text(content, 0, 0);
|
|
98
|
+
},
|
|
99
|
+
|
|
100
|
+
renderResult(result, _options, theme) {
|
|
101
|
+
const details = result.details as
|
|
102
|
+
| { initiative?: string; from?: string; status?: string }
|
|
103
|
+
| undefined;
|
|
104
|
+
if (!details?.status) return new Text(theme.fg('dim', 'Initiative updated'), 0, 0);
|
|
105
|
+
const color =
|
|
106
|
+
details.status === 'done'
|
|
107
|
+
? 'success'
|
|
108
|
+
: details.status === 'in-progress'
|
|
109
|
+
? 'accent'
|
|
110
|
+
: 'warning';
|
|
111
|
+
return new Text(
|
|
112
|
+
theme.fg('muted', `${details.initiative ?? 'initiative'} `) +
|
|
113
|
+
theme.fg('dim', `${details.from ?? ''} → `) +
|
|
114
|
+
theme.fg(color, details.status),
|
|
115
|
+
0,
|
|
116
|
+
0,
|
|
117
|
+
);
|
|
118
|
+
},
|
|
119
|
+
});
|
|
120
|
+
}
|
|
@@ -18,6 +18,7 @@ import { Type } from 'typebox';
|
|
|
18
18
|
import type { PlanStatus } from '../types.js';
|
|
19
19
|
import type { RunPlanIO } from '../effects/runtime.js';
|
|
20
20
|
import { readPlansManifest, upsertPlanEntry } from '../storage/plans-manifest.js';
|
|
21
|
+
import { reconcileInitiativeForPlan } from '../initiative.js';
|
|
21
22
|
|
|
22
23
|
/** Normalize a plan hint (`my-plan` or `.plans/my-plan`) to a bare name. */
|
|
23
24
|
function normalizeName(hint: string): string {
|
|
@@ -72,6 +73,8 @@ export function registerUpdatePlanTool(pi: ExtensionAPI, runPlanIO: RunPlanIO):
|
|
|
72
73
|
reason: params.reason,
|
|
73
74
|
}),
|
|
74
75
|
);
|
|
76
|
+
// A plan-level status change can flip its parent initiative's projection.
|
|
77
|
+
await runPlanIO(reconcileInitiativeForPlan(name));
|
|
75
78
|
|
|
76
79
|
const reasonSuffix = params.reason ? ` — ${params.reason}` : '';
|
|
77
80
|
const okDetails: Record<string, unknown> = {
|
|
@@ -13,6 +13,9 @@ export type TaskOrigin = 'plan' | 'discovered';
|
|
|
13
13
|
*/
|
|
14
14
|
export type PlanStatus = 'in-progress' | 'done' | 'superseded' | 'abandoned';
|
|
15
15
|
|
|
16
|
+
/** Initiative lifecycle reuses the plan lifecycle literals. */
|
|
17
|
+
export type InitiativeStatus = PlanStatus;
|
|
18
|
+
|
|
16
19
|
export interface TaskRecord {
|
|
17
20
|
_type: 'task';
|
|
18
21
|
id: string;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@dreki-gg/pi-plan-mode",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.24.0",
|
|
4
4
|
"description": "Two-phase planning workflow for pi — plan with claude-opus-4-6:medium, execute with gpt-5.5:low, with .plans/ file-based handoff",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"pi-package"
|
|
@@ -41,8 +41,7 @@
|
|
|
41
41
|
},
|
|
42
42
|
"dependencies": {
|
|
43
43
|
"@dreki-gg/pi-command-sandbox": "^0.3.0",
|
|
44
|
-
"effect": "^3.21.2"
|
|
45
|
-
"pug": "^3.0.3"
|
|
44
|
+
"effect": "^3.21.2"
|
|
46
45
|
},
|
|
47
46
|
"devDependencies": {
|
|
48
47
|
"@types/node": "24",
|
|
@@ -37,6 +37,17 @@ Caveman-lite: professional, tight, no filler. Bullet points over prose. Update i
|
|
|
37
37
|
|
|
38
38
|
`context.md` is the input to `submit_plan`, not a duplicate of it. Before finalizing, re-read it: every open question resolved, every decision justified. The handoff is the distilled conclusion; `context.md` is the reasoning that earned it.
|
|
39
39
|
|
|
40
|
+
## Large work: initiatives
|
|
41
|
+
|
|
42
|
+
When the work is too large for a single coherent execution session, or spans several
|
|
43
|
+
subsystems with dependencies between chunks, do not force it into one general plan. Create
|
|
44
|
+
an **initiative** (`submit_initiative`) and decompose the work into multiple plans, each
|
|
45
|
+
linked with `initiative` and ordered with `depends_on_plans`. The initiative's status is a
|
|
46
|
+
projection of its member plans; `initiative_status` shows which plans are *ready* (all
|
|
47
|
+
dependencies done) so work can be divided across sessions or subagents. Keep the
|
|
48
|
+
initiative-level deliberation (why this breakdown, the ordering, what each plan owns) in
|
|
49
|
+
the initiative's `INITIATIVE.md` overview; each member plan keeps its own `context.md`.
|
|
50
|
+
|
|
40
51
|
## Relationship to prototypes
|
|
41
52
|
|
|
42
53
|
For visual/UI work, a prototype is the visual sibling of `context.md` — see the `visual-prototype` skill. Both are deliberation artifacts. Written reasoning lives here; visual reasoning lives in the prototype.
|
|
@@ -30,9 +30,11 @@ Call `preview_prototype` with:
|
|
|
30
30
|
|
|
31
31
|
- `title` — short name for the prototype
|
|
32
32
|
- `intent` — one line describing what it shows
|
|
33
|
-
- `
|
|
33
|
+
- `html` — a complete, self-contained HTML document you author **with full freedom**. There is no template engine and no imposed theme: pick your own markup, fonts, colors, layout, and inline `<style>`/`<script>`. Assume nothing about a host page.
|
|
34
34
|
|
|
35
|
-
The tool
|
|
35
|
+
The tool persists your HTML to `.plans/_prototypes/<slug>.html` and opens it for review. It does not wrap, restyle, or theme your markup — what you write is what the user sees. (A bare fragment is tolerated and dropped into a minimal unstyled shell, but prefer sending a full document.)
|
|
36
|
+
|
|
37
|
+
**Avoid generic boilerplate.** A dark dashboard with a purple accent and a card is not a design — it is slop. Design something that fits the actual product. For real design taste, delegate the markup to the `ux-designer` subagent and pass its HTML straight through `preview_prototype`.
|
|
36
38
|
|
|
37
39
|
### 3. Get a reaction before submitting
|
|
38
40
|
|
|
@@ -1,25 +0,0 @@
|
|
|
1
|
-
doctype html
|
|
2
|
-
html(lang="en")
|
|
3
|
-
head
|
|
4
|
-
meta(charset="utf-8")
|
|
5
|
-
meta(name="viewport" content="width=device-width, initial-scale=1")
|
|
6
|
-
title= title
|
|
7
|
-
style.
|
|
8
|
-
:root { color-scheme: dark; --bg:#0b0d12; --panel:#11141b; --muted:#8b93a7; --text:#eef1f7; --line:#242936; --accent:#8b5cf6; }
|
|
9
|
-
* { box-sizing: border-box; }
|
|
10
|
-
body { margin:0; background:radial-gradient(circle at top left,#1c1730,transparent 34rem),var(--bg); color:var(--text); font:14px/1.5 Inter, ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif; }
|
|
11
|
-
main { width:min(1040px, calc(100% - 48px)); margin:48px auto; }
|
|
12
|
-
header { margin-bottom:24px; }
|
|
13
|
-
h1 { font-size:30px; line-height:1.1; margin:0 0 8px; letter-spacing:-0.04em; }
|
|
14
|
-
.intent { color:var(--muted); margin:0; }
|
|
15
|
-
.badge { display:inline-block; border:1px solid var(--line); border-radius:999px; padding:5px 10px; background:rgba(255,255,255,0.03); color:var(--muted); font-size:12px; margin-bottom:14px; }
|
|
16
|
-
.prototype { background:rgba(17,20,27,0.82); border:1px solid var(--line); border-radius:18px; padding:22px; box-shadow:0 24px 80px rgba(0,0,0,0.24); }
|
|
17
|
-
body
|
|
18
|
-
main
|
|
19
|
-
header
|
|
20
|
-
span.badge Prototype · #{generatedAt}
|
|
21
|
-
h1= title
|
|
22
|
-
if intent
|
|
23
|
-
p.intent= intent
|
|
24
|
-
section.prototype
|
|
25
|
-
!= prototypeHtml
|