@dreki-gg/pi-plan-mode 0.25.0 → 0.26.1
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/extensions/plan-mode/__tests__/git.test.ts +17 -0
- package/extensions/plan-mode/__tests__/initiative-status.test.ts +5 -5
- package/extensions/plan-mode/__tests__/plan-references-context.test.ts +4 -4
- package/extensions/plan-mode/__tests__/prompts.test.ts +23 -0
- package/extensions/plan-mode/__tests__/reconcile-plans.test.ts +3 -3
- package/extensions/plan-mode/__tests__/resolve-plan.test.ts +4 -4
- package/extensions/plan-mode/__tests__/revise-plan.test.ts +4 -4
- package/extensions/plan-mode/__tests__/submit-initiative.test.ts +2 -2
- package/extensions/plan-mode/__tests__/submit-plan.test.ts +3 -3
- package/extensions/plan-mode/__tests__/update-initiative.test.ts +2 -2
- package/extensions/plan-mode/__tests__/update-plan.test.ts +2 -2
- package/extensions/plan-mode/__tests__/utils.test.ts +2 -1
- package/extensions/plan-mode/commands/list-initiatives.ts +19 -109
- package/extensions/plan-mode/commands/list-plans.ts +21 -168
- package/extensions/plan-mode/exec-pending.ts +59 -0
- package/extensions/plan-mode/git.ts +21 -0
- package/extensions/plan-mode/index.ts +9 -7
- package/extensions/plan-mode/prompts.ts +6 -1
- package/extensions/plan-mode/references/context.ts +5 -5
- package/extensions/plan-mode/references/plan-index.ts +1 -1
- package/extensions/plan-mode/resolve-plan.ts +5 -4
- package/extensions/plan-mode/resume.ts +7 -5
- package/extensions/plan-mode/tools/add-task.ts +1 -1
- package/extensions/plan-mode/tools/initiative-status.ts +6 -6
- package/extensions/plan-mode/tools/preview-prototype.ts +3 -3
- package/extensions/plan-mode/tools/reconcile-plans.ts +2 -2
- package/extensions/plan-mode/tools/revise-plan.ts +9 -7
- package/extensions/plan-mode/tools/submit-initiative.ts +4 -4
- package/extensions/plan-mode/tools/submit-plan.ts +18 -8
- package/extensions/plan-mode/tools/update-initiative.ts +2 -2
- package/extensions/plan-mode/tools/update-plan.ts +3 -3
- package/extensions/plan-mode/types.ts +17 -51
- package/extensions/plan-mode/utils.ts +2 -29
- package/package.json +2 -1
- package/extensions/plan-mode/__tests__/atomic-write.test.ts +0 -45
- package/extensions/plan-mode/__tests__/concurrent-writes.test.ts +0 -85
- package/extensions/plan-mode/__tests__/initiative-readiness.test.ts +0 -159
- package/extensions/plan-mode/__tests__/initiatives-manifest.test.ts +0 -89
- package/extensions/plan-mode/__tests__/list-initiatives.test.ts +0 -59
- package/extensions/plan-mode/__tests__/list-plans.test.ts +0 -253
- package/extensions/plan-mode/__tests__/plan-storage.test.ts +0 -57
- package/extensions/plan-mode/__tests__/plans-manifest.test.ts +0 -120
- package/extensions/plan-mode/__tests__/reconcile.test.ts +0 -188
- package/extensions/plan-mode/__tests__/schema.test.ts +0 -319
- package/extensions/plan-mode/__tests__/task-status.test.ts +0 -74
- package/extensions/plan-mode/__tests__/task-storage.test.ts +0 -94
- package/extensions/plan-mode/effects/filesystem.ts +0 -65
- package/extensions/plan-mode/effects/runtime.ts +0 -24
- package/extensions/plan-mode/errors.ts +0 -105
- package/extensions/plan-mode/initiative.ts +0 -172
- package/extensions/plan-mode/plan-storage.ts +0 -6
- package/extensions/plan-mode/reconcile.ts +0 -235
- package/extensions/plan-mode/schema.ts +0 -97
- package/extensions/plan-mode/storage/atomic-write.ts +0 -75
- package/extensions/plan-mode/storage/file-lock.ts +0 -49
- package/extensions/plan-mode/storage/initiatives-manifest.ts +0 -154
- package/extensions/plan-mode/storage/plan-storage.ts +0 -88
- package/extensions/plan-mode/storage/plans-manifest.ts +0 -174
- package/extensions/plan-mode/storage/task-storage.ts +0 -111
- package/extensions/plan-mode/task-status.ts +0 -46
|
@@ -1,174 +0,0 @@
|
|
|
1
|
-
import { Effect, Either, Option } from 'effect';
|
|
2
|
-
import { FileSystem } from '../effects/filesystem.js';
|
|
3
|
-
import { JsonlParseError, JsonlValidationError, PlanWriteError } from '../errors.js';
|
|
4
|
-
import { decodePlanManifestEntry } from '../schema.js';
|
|
5
|
-
import type { PlanStatus } from '../types.js';
|
|
6
|
-
import { withFileLock } from './file-lock.js';
|
|
7
|
-
|
|
8
|
-
const MANIFEST_DIR = '.plans';
|
|
9
|
-
const MANIFEST_PATH = '.plans/plans.jsonl';
|
|
10
|
-
|
|
11
|
-
export interface PlanManifestEntry {
|
|
12
|
-
_type: 'plan';
|
|
13
|
-
name: string;
|
|
14
|
-
status: PlanStatus;
|
|
15
|
-
title: string;
|
|
16
|
-
created_at: string;
|
|
17
|
-
completed_at: string | null;
|
|
18
|
-
reason?: string;
|
|
19
|
-
/** Parent initiative name (kebab). Absent = standalone flat plan. */
|
|
20
|
-
initiative?: string;
|
|
21
|
-
/** Plan-level dependencies (plan names). Cross-initiative allowed. */
|
|
22
|
-
depends_on?: string[];
|
|
23
|
-
}
|
|
24
|
-
|
|
25
|
-
/** A status is terminal (closed) when it is anything other than in-progress. */
|
|
26
|
-
export function isTerminalStatus(status: PlanStatus): boolean {
|
|
27
|
-
return status !== 'in-progress';
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
type ReadError = JsonlParseError | JsonlValidationError;
|
|
31
|
-
|
|
32
|
-
export function readPlansManifest(): Effect.Effect<PlanManifestEntry[], ReadError, FileSystem> {
|
|
33
|
-
return Effect.gen(function* () {
|
|
34
|
-
const fs = yield* FileSystem;
|
|
35
|
-
// A missing or unreadable manifest is treated as "no plans".
|
|
36
|
-
const maybeText = yield* Effect.option(fs.readFileString(MANIFEST_PATH));
|
|
37
|
-
if (Option.isNone(maybeText)) return [];
|
|
38
|
-
|
|
39
|
-
const entries: PlanManifestEntry[] = [];
|
|
40
|
-
for (const [index, raw] of maybeText.value.split(/\r?\n/).entries()) {
|
|
41
|
-
if (!raw.trim()) continue;
|
|
42
|
-
const line = index + 1;
|
|
43
|
-
|
|
44
|
-
let parsed: unknown;
|
|
45
|
-
try {
|
|
46
|
-
parsed = JSON.parse(raw);
|
|
47
|
-
} catch (cause) {
|
|
48
|
-
return yield* Effect.fail(new JsonlParseError({ path: MANIFEST_PATH, line, cause }));
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
const decoded = decodePlanManifestEntry(parsed);
|
|
52
|
-
if (Either.isLeft(decoded)) {
|
|
53
|
-
return yield* Effect.fail(
|
|
54
|
-
new JsonlValidationError({ path: MANIFEST_PATH, line, reason: decoded.left.message }),
|
|
55
|
-
);
|
|
56
|
-
}
|
|
57
|
-
entries.push(decoded.right);
|
|
58
|
-
}
|
|
59
|
-
return entries;
|
|
60
|
-
});
|
|
61
|
-
}
|
|
62
|
-
|
|
63
|
-
export function writePlansManifest(
|
|
64
|
-
entries: PlanManifestEntry[],
|
|
65
|
-
): Effect.Effect<void, PlanWriteError, FileSystem> {
|
|
66
|
-
return Effect.gen(function* () {
|
|
67
|
-
const fs = yield* FileSystem;
|
|
68
|
-
yield* fs.makeDir(MANIFEST_DIR);
|
|
69
|
-
const content =
|
|
70
|
-
entries.map((entry) => JSON.stringify(entry)).join('\n') + (entries.length ? '\n' : '');
|
|
71
|
-
yield* fs.writeFileAtomic(MANIFEST_PATH, content);
|
|
72
|
-
});
|
|
73
|
-
}
|
|
74
|
-
|
|
75
|
-
export interface PlanUpsert {
|
|
76
|
-
status: PlanStatus;
|
|
77
|
-
title?: string;
|
|
78
|
-
reason?: string;
|
|
79
|
-
/** Parent initiative name; preserved when omitted. */
|
|
80
|
-
initiative?: string;
|
|
81
|
-
/** Plan-level dependencies (plan names); preserved when omitted. */
|
|
82
|
-
depends_on?: string[];
|
|
83
|
-
}
|
|
84
|
-
|
|
85
|
-
/**
|
|
86
|
-
* Pure transform: upsert `name` into the in-memory `entries` array, preserving
|
|
87
|
-
* created_at / membership / deps from any existing entry. No IO — shared by the
|
|
88
|
-
* locked `upsertPlanEntry` and `reconcilePlanStatus` so both flow through one
|
|
89
|
-
* serialized read-modify-write and never nest locks.
|
|
90
|
-
*/
|
|
91
|
-
export function applyPlanUpsert(
|
|
92
|
-
entries: PlanManifestEntry[],
|
|
93
|
-
name: string,
|
|
94
|
-
updates: PlanUpsert,
|
|
95
|
-
): void {
|
|
96
|
-
const now = new Date().toISOString();
|
|
97
|
-
const index = entries.findIndex((entry) => entry.name === name);
|
|
98
|
-
const existing = index === -1 ? undefined : entries[index];
|
|
99
|
-
const entry: PlanManifestEntry = {
|
|
100
|
-
_type: 'plan',
|
|
101
|
-
name,
|
|
102
|
-
status: updates.status,
|
|
103
|
-
title: updates.title ?? existing?.title ?? 'Untitled plan',
|
|
104
|
-
created_at: existing?.created_at ?? now,
|
|
105
|
-
// Terminal statuses record a completion timestamp; reopening clears it.
|
|
106
|
-
completed_at: isTerminalStatus(updates.status) ? (existing?.completed_at ?? now) : null,
|
|
107
|
-
reason: updates.reason ?? existing?.reason,
|
|
108
|
-
// Membership + plan-level deps are preserved across status-only upserts.
|
|
109
|
-
initiative: updates.initiative ?? existing?.initiative,
|
|
110
|
-
depends_on: updates.depends_on ?? existing?.depends_on,
|
|
111
|
-
};
|
|
112
|
-
if (index === -1) entries.push(entry);
|
|
113
|
-
else entries[index] = entry;
|
|
114
|
-
}
|
|
115
|
-
|
|
116
|
-
/**
|
|
117
|
-
* Serialized read-modify-write of the plans registry. Holds a process-wide lock
|
|
118
|
-
* on the manifest path across the whole read → transform → write so concurrent
|
|
119
|
-
* tool calls cannot clobber each other (lost-update race). `transform` mutates
|
|
120
|
-
* the entries array in place and returns `true` when it changed something
|
|
121
|
-
* (return `false` to skip the rewrite).
|
|
122
|
-
*/
|
|
123
|
-
export function mutatePlansManifest(
|
|
124
|
-
transform: (entries: PlanManifestEntry[]) => boolean,
|
|
125
|
-
): Effect.Effect<void, ReadError | PlanWriteError, FileSystem> {
|
|
126
|
-
return withFileLock(
|
|
127
|
-
MANIFEST_PATH,
|
|
128
|
-
Effect.gen(function* () {
|
|
129
|
-
const entries = yield* readPlansManifest();
|
|
130
|
-
const changed = transform(entries);
|
|
131
|
-
if (changed) yield* writePlansManifest(entries);
|
|
132
|
-
}),
|
|
133
|
-
);
|
|
134
|
-
}
|
|
135
|
-
|
|
136
|
-
export function upsertPlanEntry(
|
|
137
|
-
name: string,
|
|
138
|
-
updates: PlanUpsert,
|
|
139
|
-
): Effect.Effect<void, ReadError | PlanWriteError, FileSystem> {
|
|
140
|
-
return mutatePlansManifest((entries) => {
|
|
141
|
-
applyPlanUpsert(entries, name, updates);
|
|
142
|
-
return true;
|
|
143
|
-
});
|
|
144
|
-
}
|
|
145
|
-
|
|
146
|
-
/**
|
|
147
|
-
* Reconcile a plan's registry status from its task state.
|
|
148
|
-
*
|
|
149
|
-
* The registry `status` is a PROJECTION of task state, not a parallel flag.
|
|
150
|
-
* Call this wherever tasks are written so completion is never coupled to a
|
|
151
|
-
* formal in-session execution run (see FEEDBACK #1). `finalizable` means every
|
|
152
|
-
* active task is resolved AND no deferred follow-ups remain.
|
|
153
|
-
*
|
|
154
|
-
* Guard: a manually-set terminal status (`superseded` / `abandoned`) is never
|
|
155
|
-
* auto-overridden — only `in-progress` ⇄ `done` is derived from tasks.
|
|
156
|
-
*/
|
|
157
|
-
export function reconcilePlanStatus(
|
|
158
|
-
name: string,
|
|
159
|
-
finalizable: boolean,
|
|
160
|
-
title?: string,
|
|
161
|
-
): Effect.Effect<void, ReadError | PlanWriteError, FileSystem> {
|
|
162
|
-
return mutatePlansManifest((entries) => {
|
|
163
|
-
const existing = entries.find((entry) => entry.name === name);
|
|
164
|
-
// Reconcile only reflects task state for KNOWN plans; never conjure an
|
|
165
|
-
// entry for an unregistered plan (orphans are surfaced, not auto-created).
|
|
166
|
-
if (!existing) return false;
|
|
167
|
-
// Do not resurrect / clobber an explicitly closed plan.
|
|
168
|
-
if (existing.status === 'superseded' || existing.status === 'abandoned') return false;
|
|
169
|
-
const status: PlanStatus = finalizable ? 'done' : 'in-progress';
|
|
170
|
-
if (existing.status === status) return false; // no change
|
|
171
|
-
applyPlanUpsert(entries, name, { status, title });
|
|
172
|
-
return true;
|
|
173
|
-
});
|
|
174
|
-
}
|
|
@@ -1,111 +0,0 @@
|
|
|
1
|
-
import { Effect, Either, Option } from 'effect';
|
|
2
|
-
import { join } from 'node:path';
|
|
3
|
-
import { FileSystem } from '../effects/filesystem.js';
|
|
4
|
-
import {
|
|
5
|
-
JsonlParseError,
|
|
6
|
-
JsonlValidationError,
|
|
7
|
-
MissingMetaRecord,
|
|
8
|
-
PlanWriteError,
|
|
9
|
-
TaskNotFound,
|
|
10
|
-
TasksFileNotFound,
|
|
11
|
-
} from '../errors.js';
|
|
12
|
-
import { decodeTasksLine } from '../schema.js';
|
|
13
|
-
import type { TaskMeta, TaskRecord } from '../types.js';
|
|
14
|
-
import { withFileLock } from './file-lock.js';
|
|
15
|
-
|
|
16
|
-
const TASKS_FILE = 'tasks.jsonl';
|
|
17
|
-
|
|
18
|
-
export interface TasksSnapshot {
|
|
19
|
-
meta: TaskMeta;
|
|
20
|
-
tasks: TaskRecord[];
|
|
21
|
-
}
|
|
22
|
-
|
|
23
|
-
type ReadError = JsonlParseError | JsonlValidationError | MissingMetaRecord;
|
|
24
|
-
|
|
25
|
-
export function readTasksJsonl(
|
|
26
|
-
planDir: string,
|
|
27
|
-
): Effect.Effect<TasksSnapshot | undefined, ReadError, FileSystem> {
|
|
28
|
-
const path = join(planDir, TASKS_FILE);
|
|
29
|
-
return Effect.gen(function* () {
|
|
30
|
-
const fs = yield* FileSystem;
|
|
31
|
-
// A read failure (missing or unreadable file) is treated as "no snapshot".
|
|
32
|
-
const maybeText = yield* Effect.option(fs.readFileString(path));
|
|
33
|
-
if (Option.isNone(maybeText)) return undefined;
|
|
34
|
-
|
|
35
|
-
const text = maybeText.value;
|
|
36
|
-
if (!text.trim()) return yield* Effect.fail(new MissingMetaRecord({ path }));
|
|
37
|
-
|
|
38
|
-
let meta: TaskMeta | undefined;
|
|
39
|
-
const tasks: TaskRecord[] = [];
|
|
40
|
-
for (const [index, raw] of text.split(/\r?\n/).entries()) {
|
|
41
|
-
if (!raw.trim()) continue;
|
|
42
|
-
const line = index + 1;
|
|
43
|
-
|
|
44
|
-
let parsed: unknown;
|
|
45
|
-
try {
|
|
46
|
-
parsed = JSON.parse(raw);
|
|
47
|
-
} catch (cause) {
|
|
48
|
-
return yield* Effect.fail(new JsonlParseError({ path, line, cause }));
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
const decoded = decodeTasksLine(parsed);
|
|
52
|
-
if (Either.isLeft(decoded)) {
|
|
53
|
-
return yield* Effect.fail(
|
|
54
|
-
new JsonlValidationError({ path, line, reason: decoded.left.message }),
|
|
55
|
-
);
|
|
56
|
-
}
|
|
57
|
-
|
|
58
|
-
const record = decoded.right;
|
|
59
|
-
if (record._type === 'meta') meta = record;
|
|
60
|
-
else tasks.push(record);
|
|
61
|
-
}
|
|
62
|
-
|
|
63
|
-
if (!meta) return yield* Effect.fail(new MissingMetaRecord({ path }));
|
|
64
|
-
return { meta, tasks };
|
|
65
|
-
});
|
|
66
|
-
}
|
|
67
|
-
|
|
68
|
-
export function writeTasksJsonl(
|
|
69
|
-
planDir: string,
|
|
70
|
-
meta: TaskMeta,
|
|
71
|
-
tasks: TaskRecord[],
|
|
72
|
-
): Effect.Effect<void, PlanWriteError, FileSystem> {
|
|
73
|
-
return Effect.gen(function* () {
|
|
74
|
-
const fs = yield* FileSystem;
|
|
75
|
-
yield* fs.makeDir(planDir);
|
|
76
|
-
const content = [meta, ...tasks].map((record) => JSON.stringify(record)).join('\n') + '\n';
|
|
77
|
-
yield* fs.writeFileAtomic(join(planDir, TASKS_FILE), content);
|
|
78
|
-
});
|
|
79
|
-
}
|
|
80
|
-
|
|
81
|
-
export function updateTask(
|
|
82
|
-
planDir: string,
|
|
83
|
-
taskId: string,
|
|
84
|
-
updates: Partial<Omit<TaskRecord, '_type' | 'id' | 'created_at'>>,
|
|
85
|
-
): Effect.Effect<
|
|
86
|
-
TaskRecord,
|
|
87
|
-
ReadError | PlanWriteError | TasksFileNotFound | TaskNotFound,
|
|
88
|
-
FileSystem
|
|
89
|
-
> {
|
|
90
|
-
// Serialize the read-modify-write so concurrent task updates to the same plan
|
|
91
|
-
// (e.g. parallel update_task / update_tasks calls) cannot clobber each other.
|
|
92
|
-
return withFileLock(
|
|
93
|
-
join(planDir, TASKS_FILE),
|
|
94
|
-
Effect.gen(function* () {
|
|
95
|
-
const snapshot = yield* readTasksJsonl(planDir);
|
|
96
|
-
if (!snapshot) return yield* Effect.fail(new TasksFileNotFound({ planDir }));
|
|
97
|
-
|
|
98
|
-
const index = snapshot.tasks.findIndex((task) => task.id === taskId);
|
|
99
|
-
if (index === -1) return yield* Effect.fail(new TaskNotFound({ planDir, taskId }));
|
|
100
|
-
|
|
101
|
-
const updated: TaskRecord = {
|
|
102
|
-
...snapshot.tasks[index],
|
|
103
|
-
...updates,
|
|
104
|
-
updated_at: new Date().toISOString(),
|
|
105
|
-
};
|
|
106
|
-
snapshot.tasks[index] = updated;
|
|
107
|
-
yield* writeTasksJsonl(planDir, snapshot.meta, snapshot.tasks);
|
|
108
|
-
return updated;
|
|
109
|
-
}),
|
|
110
|
-
);
|
|
111
|
-
}
|
|
@@ -1,46 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Pure helpers for reasoning about task status at execution checkpoints.
|
|
3
|
-
*
|
|
4
|
-
* `deferred` tasks are discovered follow-ups kept out of the active queue, so
|
|
5
|
-
* they are excluded from "resolved" checks but block plan finalization.
|
|
6
|
-
*/
|
|
7
|
-
|
|
8
|
-
import type { TaskRecord } from './types.js';
|
|
9
|
-
|
|
10
|
-
export function deferredTasks(tasks: readonly TaskRecord[]): TaskRecord[] {
|
|
11
|
-
return tasks.filter((task) => task.status === 'deferred');
|
|
12
|
-
}
|
|
13
|
-
|
|
14
|
-
/**
|
|
15
|
-
* True when no active work remains — every task is done, skipped, or deferred
|
|
16
|
-
* (nothing pending or blocked).
|
|
17
|
-
*/
|
|
18
|
-
export function activeTasksResolved(tasks: readonly TaskRecord[]): boolean {
|
|
19
|
-
return tasks.every(
|
|
20
|
-
(task) => task.status === 'done' || task.status === 'skipped' || task.status === 'deferred',
|
|
21
|
-
);
|
|
22
|
-
}
|
|
23
|
-
|
|
24
|
-
/**
|
|
25
|
-
* True when the plan can be marked complete: active work is resolved AND there
|
|
26
|
-
* are no deferred follow-ups awaiting the user's decision.
|
|
27
|
-
*/
|
|
28
|
-
export function isPlanFinalizable(tasks: readonly TaskRecord[]): boolean {
|
|
29
|
-
return activeTasksResolved(tasks) && !tasks.some((task) => task.status === 'deferred');
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
/**
|
|
33
|
-
* Reactivate tasks for a resumed run: blocked tasks and deferred follow-ups
|
|
34
|
-
* become pending (mutated in place). Returns true if anything changed.
|
|
35
|
-
*/
|
|
36
|
-
export function reactivateForExecution(tasks: TaskRecord[], timestamp: string): boolean {
|
|
37
|
-
let changed = false;
|
|
38
|
-
for (const task of tasks) {
|
|
39
|
-
if (task.status === 'blocked' || task.status === 'deferred') {
|
|
40
|
-
task.status = 'pending';
|
|
41
|
-
task.updated_at = timestamp;
|
|
42
|
-
changed = true;
|
|
43
|
-
}
|
|
44
|
-
}
|
|
45
|
-
return changed;
|
|
46
|
-
}
|