@dreki-gg/pi-plan-mode 0.26.0 → 0.26.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/CHANGELOG.md +19 -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__/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/index.ts +8 -7
- 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 +4 -4
- package/extensions/plan-mode/resume.ts +6 -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 +7 -7
- package/extensions/plan-mode/tools/submit-initiative.ts +4 -4
- package/extensions/plan-mode/tools/submit-plan.ts +7 -7
- 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 -59
- 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 -334
- package/extensions/plan-mode/__tests__/task-status.test.ts +0 -74
- package/extensions/plan-mode/__tests__/task-storage.test.ts +0 -101
- 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 -99
- 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
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,24 @@
|
|
|
1
1
|
# @dreki-gg/pi-plan-mode
|
|
2
2
|
|
|
3
|
+
## 0.26.2
|
|
4
|
+
|
|
5
|
+
### Patch Changes
|
|
6
|
+
|
|
7
|
+
- Updated dependencies
|
|
8
|
+
- @dreki-gg/taskman@0.3.0
|
|
9
|
+
|
|
10
|
+
## 0.26.1
|
|
11
|
+
|
|
12
|
+
### Patch Changes
|
|
13
|
+
|
|
14
|
+
- Internal refactor: the task-management engine (storage, schema, reconcile,
|
|
15
|
+
initiative projection, task-status, plan resolution) now lives in the new
|
|
16
|
+
`@dreki-gg/taskman` package, which plan-mode depends on. No behavior change —
|
|
17
|
+
the same tools, commands, and `.plans/` layout, with the engine shared so other
|
|
18
|
+
harnesses can drive the same ledger via the `taskman` CLI.
|
|
19
|
+
- Updated dependencies
|
|
20
|
+
- @dreki-gg/taskman@0.2.0
|
|
21
|
+
|
|
3
22
|
## 0.26.0
|
|
4
23
|
|
|
5
24
|
### Minor Changes
|
|
@@ -4,11 +4,11 @@ import { chdir } from 'node:process';
|
|
|
4
4
|
import { mkdtemp, rm } from 'node:fs/promises';
|
|
5
5
|
import { join } from 'node:path';
|
|
6
6
|
import { tmpdir } from 'node:os';
|
|
7
|
-
import { FileSystem, nodeFileSystemService } from '
|
|
8
|
-
import { makePlanRuntime } from '
|
|
9
|
-
import { upsertPlanEntry } from '
|
|
10
|
-
import { upsertInitiativeEntry } from '
|
|
11
|
-
import { writeTasksJsonl } from '
|
|
7
|
+
import { FileSystem, nodeFileSystemService } from '@dreki-gg/taskman';
|
|
8
|
+
import { makePlanRuntime } from '@dreki-gg/taskman';
|
|
9
|
+
import { upsertPlanEntry } from '@dreki-gg/taskman';
|
|
10
|
+
import { upsertInitiativeEntry } from '@dreki-gg/taskman';
|
|
11
|
+
import { writeTasksJsonl } from '@dreki-gg/taskman';
|
|
12
12
|
import type { TaskRecord } from '../types.js';
|
|
13
13
|
import { registerInitiativeStatusTool } from '../tools/initiative-status.js';
|
|
14
14
|
|
|
@@ -3,10 +3,10 @@ import { chdir } from 'node:process';
|
|
|
3
3
|
import { mkdtemp, rm } from 'node:fs/promises';
|
|
4
4
|
import { join } from 'node:path';
|
|
5
5
|
import { tmpdir } from 'node:os';
|
|
6
|
-
import { makePlanRuntime } from '
|
|
7
|
-
import { upsertPlanEntry } from '
|
|
8
|
-
import { writeTasksJsonl } from '
|
|
9
|
-
import { saveHandoff } from '
|
|
6
|
+
import { makePlanRuntime } from '@dreki-gg/taskman';
|
|
7
|
+
import { upsertPlanEntry } from '@dreki-gg/taskman';
|
|
8
|
+
import { writeTasksJsonl } from '@dreki-gg/taskman';
|
|
9
|
+
import { saveHandoff } from '@dreki-gg/taskman';
|
|
10
10
|
import type { TaskMeta, TaskRecord } from '../types.js';
|
|
11
11
|
import { buildPlanContextPack, resolvePlanReference } from '../references/context.js';
|
|
12
12
|
|
|
@@ -3,9 +3,9 @@ import { chdir } from 'node:process';
|
|
|
3
3
|
import { mkdtemp, rm } from 'node:fs/promises';
|
|
4
4
|
import { join } from 'node:path';
|
|
5
5
|
import { tmpdir } from 'node:os';
|
|
6
|
-
import { makePlanRuntime } from '
|
|
7
|
-
import { writeTasksJsonl } from '
|
|
8
|
-
import { readPlansManifest, upsertPlanEntry } from '
|
|
6
|
+
import { makePlanRuntime } from '@dreki-gg/taskman';
|
|
7
|
+
import { writeTasksJsonl } from '@dreki-gg/taskman';
|
|
8
|
+
import { readPlansManifest, upsertPlanEntry } from '@dreki-gg/taskman';
|
|
9
9
|
import { registerReconcilePlansTool } from '../tools/reconcile-plans.js';
|
|
10
10
|
import type { TaskMeta, TaskRecord } from '../types.js';
|
|
11
11
|
|
|
@@ -3,10 +3,10 @@ import { chdir } from 'node:process';
|
|
|
3
3
|
import { mkdtemp, rm } from 'node:fs/promises';
|
|
4
4
|
import { join } from 'node:path';
|
|
5
5
|
import { tmpdir } from 'node:os';
|
|
6
|
-
import { makePlanRuntime } from '
|
|
7
|
-
import { writeTasksJsonl } from '
|
|
8
|
-
import { upsertPlanEntry } from '
|
|
9
|
-
import { saveHandoff } from '
|
|
6
|
+
import { makePlanRuntime } from '@dreki-gg/taskman';
|
|
7
|
+
import { writeTasksJsonl } from '@dreki-gg/taskman';
|
|
8
|
+
import { upsertPlanEntry } from '@dreki-gg/taskman';
|
|
9
|
+
import { saveHandoff } from '@dreki-gg/taskman';
|
|
10
10
|
import { PlanModeState } from '../state.js';
|
|
11
11
|
import { resolveActivePlan } from '../resolve-plan.js';
|
|
12
12
|
import type { TaskMeta, TaskRecord } from '../types.js';
|
|
@@ -3,11 +3,11 @@ import { chdir } from 'node:process';
|
|
|
3
3
|
import { mkdtemp, rm } from 'node:fs/promises';
|
|
4
4
|
import { join } from 'node:path';
|
|
5
5
|
import { tmpdir } from 'node:os';
|
|
6
|
-
import { makePlanRuntime } from '
|
|
6
|
+
import { makePlanRuntime } from '@dreki-gg/taskman';
|
|
7
7
|
import { registerRevisePlanTool } from '../tools/revise-plan.js';
|
|
8
|
-
import { readTasksJsonl, writeTasksJsonl } from '
|
|
9
|
-
import { saveHandoff, loadHandoff } from '
|
|
10
|
-
import { readPlansManifest, upsertPlanEntry } from '
|
|
8
|
+
import { readTasksJsonl, writeTasksJsonl } from '@dreki-gg/taskman';
|
|
9
|
+
import { saveHandoff, loadHandoff } from '@dreki-gg/taskman';
|
|
10
|
+
import { readPlansManifest, upsertPlanEntry } from '@dreki-gg/taskman';
|
|
11
11
|
import type { PlanData, TaskRecord } from '../types.js';
|
|
12
12
|
|
|
13
13
|
const runPlanIO = makePlanRuntime();
|
|
@@ -3,8 +3,8 @@ import { chdir } from 'node:process';
|
|
|
3
3
|
import { mkdtemp, readFile, rm } from 'node:fs/promises';
|
|
4
4
|
import { join } from 'node:path';
|
|
5
5
|
import { tmpdir } from 'node:os';
|
|
6
|
-
import { makePlanRuntime } from '
|
|
7
|
-
import { readInitiativesManifest } from '
|
|
6
|
+
import { makePlanRuntime } from '@dreki-gg/taskman';
|
|
7
|
+
import { readInitiativesManifest } from '@dreki-gg/taskman';
|
|
8
8
|
import { registerSubmitInitiativeTool } from '../tools/submit-initiative.js';
|
|
9
9
|
|
|
10
10
|
const runPlanIO = makePlanRuntime();
|
|
@@ -3,12 +3,12 @@ import { chdir } from 'node:process';
|
|
|
3
3
|
import { mkdtemp, rm } from 'node:fs/promises';
|
|
4
4
|
import { join } from 'node:path';
|
|
5
5
|
import { tmpdir } from 'node:os';
|
|
6
|
-
import { makePlanRuntime } from '
|
|
7
|
-
import { readPlansManifest } from '
|
|
6
|
+
import { makePlanRuntime } from '@dreki-gg/taskman';
|
|
7
|
+
import { readPlansManifest } from '@dreki-gg/taskman';
|
|
8
8
|
import {
|
|
9
9
|
readInitiativesManifest,
|
|
10
10
|
upsertInitiativeEntry,
|
|
11
|
-
} from '
|
|
11
|
+
} from '@dreki-gg/taskman';
|
|
12
12
|
import { registerSubmitPlanTool } from '../tools/submit-plan.js';
|
|
13
13
|
|
|
14
14
|
const runPlanIO = makePlanRuntime();
|
|
@@ -3,11 +3,11 @@ import { chdir } from 'node:process';
|
|
|
3
3
|
import { mkdtemp, rm } from 'node:fs/promises';
|
|
4
4
|
import { join } from 'node:path';
|
|
5
5
|
import { tmpdir } from 'node:os';
|
|
6
|
-
import { makePlanRuntime } from '
|
|
6
|
+
import { makePlanRuntime } from '@dreki-gg/taskman';
|
|
7
7
|
import {
|
|
8
8
|
readInitiativesManifest,
|
|
9
9
|
upsertInitiativeEntry,
|
|
10
|
-
} from '
|
|
10
|
+
} from '@dreki-gg/taskman';
|
|
11
11
|
import { registerUpdateInitiativeTool } from '../tools/update-initiative.js';
|
|
12
12
|
|
|
13
13
|
const runPlanIO = makePlanRuntime();
|
|
@@ -3,8 +3,8 @@ import { chdir } from 'node:process';
|
|
|
3
3
|
import { mkdtemp, rm } from 'node:fs/promises';
|
|
4
4
|
import { join } from 'node:path';
|
|
5
5
|
import { tmpdir } from 'node:os';
|
|
6
|
-
import { makePlanRuntime } from '
|
|
7
|
-
import { readPlansManifest, upsertPlanEntry } from '
|
|
6
|
+
import { makePlanRuntime } from '@dreki-gg/taskman';
|
|
7
|
+
import { readPlansManifest, upsertPlanEntry } from '@dreki-gg/taskman';
|
|
8
8
|
import { registerUpdatePlanTool } from '../tools/update-plan.js';
|
|
9
9
|
|
|
10
10
|
const runPlanIO = makePlanRuntime();
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { describe, expect, test } from 'bun:test';
|
|
2
|
-
import { isSafeCommand, isPlanPath
|
|
2
|
+
import { isSafeCommand, isPlanPath } from '../utils.js';
|
|
3
|
+
import { nextTaskId } from '@dreki-gg/taskman';
|
|
3
4
|
|
|
4
5
|
describe('nextTaskId', () => {
|
|
5
6
|
test('increments the max numeric suffix', () => {
|
|
@@ -1,121 +1,29 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* /initiatives command — list, filter, and sort initiatives interactively.
|
|
3
3
|
*
|
|
4
|
-
* The
|
|
5
|
-
*
|
|
4
|
+
* The pure listing logic lives in `@dreki-gg/taskman`; this file is the
|
|
5
|
+
* pi-interactive shell around it.
|
|
6
6
|
*/
|
|
7
7
|
|
|
8
|
-
import { Effect } from 'effect';
|
|
9
8
|
import type { ExtensionCommandContext } from '@earendil-works/pi-coding-agent';
|
|
10
|
-
import type { RunPlanIO } from '
|
|
11
|
-
import {
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
export interface InitiativeListItem {
|
|
23
|
-
name: string;
|
|
24
|
-
title: string;
|
|
25
|
-
status: InitiativeStatus;
|
|
26
|
-
created_at: string;
|
|
27
|
-
totalPlans: number;
|
|
28
|
-
donePlans: number;
|
|
29
|
-
ready: number;
|
|
30
|
-
blocked: number;
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
export function filterInitiatives(
|
|
34
|
-
items: InitiativeListItem[],
|
|
35
|
-
filter: StatusFilter,
|
|
36
|
-
): InitiativeListItem[] {
|
|
37
|
-
if (filter === 'all') return items;
|
|
38
|
-
return items.filter((i) => i.status === filter);
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
const STATUS_ICON: Record<InitiativeStatus, string> = {
|
|
42
|
-
'in-progress': '🔵',
|
|
43
|
-
done: '✅',
|
|
44
|
-
superseded: '🔄',
|
|
45
|
-
abandoned: '❌',
|
|
46
|
-
};
|
|
47
|
-
|
|
48
|
-
export function formatInitiativeList(items: InitiativeListItem[], filter: StatusFilter): string {
|
|
49
|
-
if (items.length === 0) {
|
|
50
|
-
return filter === 'all'
|
|
51
|
-
? 'No initiatives found in .plans/initiatives.jsonl'
|
|
52
|
-
: `No initiatives with status "${filter}"`;
|
|
53
|
-
}
|
|
54
|
-
const header =
|
|
55
|
-
filter === 'all'
|
|
56
|
-
? `All initiatives (${items.length})`
|
|
57
|
-
: `Initiatives: ${filter} (${items.length})`;
|
|
58
|
-
const lines = items.map((i) => {
|
|
59
|
-
const icon = STATUS_ICON[i.status];
|
|
60
|
-
const progress =
|
|
61
|
-
i.totalPlans > 0
|
|
62
|
-
? ` [${i.donePlans}/${i.totalPlans} plans, ready ${i.ready}, blocked ${i.blocked}]`
|
|
63
|
-
: ' [no plans]';
|
|
64
|
-
const date = i.created_at.slice(0, 10);
|
|
65
|
-
return ` ${icon} ${i.name} — ${i.title}${progress} (${date})`;
|
|
66
|
-
});
|
|
67
|
-
return `${header}\n${lines.join('\n')}`;
|
|
68
|
-
}
|
|
69
|
-
|
|
70
|
-
export function loadInitiativeListItems(): Effect.Effect<InitiativeListItem[], never, FileSystem> {
|
|
71
|
-
return Effect.gen(function* () {
|
|
72
|
-
const initiatives = yield* Effect.orElseSucceed(
|
|
73
|
-
readInitiativesManifest(),
|
|
74
|
-
() => [] as InitiativeManifestEntry[],
|
|
75
|
-
);
|
|
76
|
-
const plans = yield* Effect.orElseSucceed(
|
|
77
|
-
readPlansManifest(),
|
|
78
|
-
() => [] as PlanManifestEntry[],
|
|
79
|
-
);
|
|
80
|
-
return initiatives.map((entry): InitiativeListItem => {
|
|
81
|
-
const r = initiativeRollup(entry.name, plans);
|
|
82
|
-
return {
|
|
83
|
-
name: entry.name,
|
|
84
|
-
title: entry.title,
|
|
85
|
-
status: entry.status,
|
|
86
|
-
created_at: entry.created_at,
|
|
87
|
-
totalPlans: r.total,
|
|
88
|
-
donePlans: r.done,
|
|
89
|
-
ready: r.ready,
|
|
90
|
-
blocked: r.blocked,
|
|
91
|
-
};
|
|
92
|
-
});
|
|
93
|
-
});
|
|
94
|
-
}
|
|
95
|
-
|
|
96
|
-
const FILTER_ALIASES: Record<string, StatusFilter> = {
|
|
97
|
-
all: 'all',
|
|
98
|
-
'in-progress': 'in-progress',
|
|
99
|
-
active: 'in-progress',
|
|
100
|
-
done: 'done',
|
|
101
|
-
completed: 'done',
|
|
102
|
-
superseded: 'superseded',
|
|
103
|
-
abandoned: 'abandoned',
|
|
104
|
-
};
|
|
105
|
-
|
|
106
|
-
export function parseFilter(raw: string): StatusFilter {
|
|
107
|
-
for (const token of raw.toLowerCase().split(/\s+/)) {
|
|
108
|
-
if (FILTER_ALIASES[token]) return FILTER_ALIASES[token];
|
|
109
|
-
}
|
|
110
|
-
return 'all';
|
|
111
|
-
}
|
|
9
|
+
import type { RunPlanIO } from '@dreki-gg/taskman';
|
|
10
|
+
import { InitiativeListing } from '@dreki-gg/taskman';
|
|
11
|
+
|
|
12
|
+
export type StatusFilter = InitiativeListing.StatusFilter;
|
|
13
|
+
export type InitiativeListItem = InitiativeListing.InitiativeListItem;
|
|
14
|
+
export const {
|
|
15
|
+
filterInitiatives,
|
|
16
|
+
formatInitiativeList,
|
|
17
|
+
loadInitiativeListItems,
|
|
18
|
+
parseInitiativeFilter: parseFilter,
|
|
19
|
+
} = InitiativeListing;
|
|
112
20
|
|
|
113
21
|
export async function handleListInitiatives(
|
|
114
22
|
ctx: ExtensionCommandContext,
|
|
115
23
|
runPlanIO: RunPlanIO,
|
|
116
24
|
args?: string,
|
|
117
25
|
): Promise<void> {
|
|
118
|
-
const items = await runPlanIO(loadInitiativeListItems());
|
|
26
|
+
const items = await runPlanIO(InitiativeListing.loadInitiativeListItems());
|
|
119
27
|
if (items.length === 0) {
|
|
120
28
|
ctx.ui.notify('No initiatives found in .plans/initiatives.jsonl', 'info');
|
|
121
29
|
return;
|
|
@@ -123,7 +31,7 @@ export async function handleListInitiatives(
|
|
|
123
31
|
|
|
124
32
|
let filter: StatusFilter = 'all';
|
|
125
33
|
if (args?.trim()) {
|
|
126
|
-
filter =
|
|
34
|
+
filter = InitiativeListing.parseInitiativeFilter(args.trim());
|
|
127
35
|
} else {
|
|
128
36
|
const choice = await ctx.ui.select('Filter initiatives by status:', [
|
|
129
37
|
'All',
|
|
@@ -143,6 +51,8 @@ export async function handleListInitiatives(
|
|
|
143
51
|
filter = map[choice] ?? 'all';
|
|
144
52
|
}
|
|
145
53
|
|
|
146
|
-
|
|
147
|
-
|
|
54
|
+
ctx.ui.notify(
|
|
55
|
+
InitiativeListing.formatInitiativeList(InitiativeListing.filterInitiatives(items, filter), filter),
|
|
56
|
+
'info',
|
|
57
|
+
);
|
|
148
58
|
}
|
|
@@ -1,155 +1,47 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* /plans command — list, filter, and sort plans interactively.
|
|
3
3
|
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
* sort by different criteria via interactive selectors.
|
|
4
|
+
* The pure listing logic (filter / sort / format / load) lives in
|
|
5
|
+
* `@dreki-gg/taskman`; this file is the pi-interactive shell around it.
|
|
7
6
|
*/
|
|
8
7
|
|
|
9
|
-
import { Effect } from 'effect';
|
|
10
8
|
import type { ExtensionCommandContext } from '@earendil-works/pi-coding-agent';
|
|
11
|
-
import type { RunPlanIO } from '
|
|
12
|
-
import {
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
status: PlanStatus;
|
|
26
|
-
created_at: string;
|
|
27
|
-
completed_at: string | null;
|
|
28
|
-
totalTasks: number;
|
|
29
|
-
doneTasks: number;
|
|
30
|
-
pendingTasks: number;
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
// ── Core logic (pure, testable) ──────────────────────────────────────────────
|
|
34
|
-
|
|
35
|
-
export function filterPlans(plans: PlanListItem[], filter: StatusFilter): PlanListItem[] {
|
|
36
|
-
if (filter === 'all') return plans;
|
|
37
|
-
return plans.filter((p) => p.status === filter);
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
export function sortPlans(plans: PlanListItem[], sort: SortField): PlanListItem[] {
|
|
41
|
-
const sorted = [...plans];
|
|
42
|
-
switch (sort) {
|
|
43
|
-
case 'name':
|
|
44
|
-
sorted.sort((a, b) => a.name.localeCompare(b.name));
|
|
45
|
-
break;
|
|
46
|
-
case 'date-asc':
|
|
47
|
-
sorted.sort((a, b) => a.created_at.localeCompare(b.created_at));
|
|
48
|
-
break;
|
|
49
|
-
case 'date-desc':
|
|
50
|
-
sorted.sort((a, b) => b.created_at.localeCompare(a.created_at));
|
|
51
|
-
break;
|
|
52
|
-
case 'tasks':
|
|
53
|
-
sorted.sort((a, b) => b.totalTasks - a.totalTasks);
|
|
54
|
-
break;
|
|
55
|
-
}
|
|
56
|
-
return sorted;
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
const STATUS_ICON: Record<PlanStatus, string> = {
|
|
60
|
-
'in-progress': '🔵',
|
|
61
|
-
done: '✅',
|
|
62
|
-
superseded: '🔄',
|
|
63
|
-
abandoned: '❌',
|
|
64
|
-
};
|
|
65
|
-
|
|
66
|
-
export function formatPlanList(plans: PlanListItem[], filter: StatusFilter, sort: SortField): string {
|
|
67
|
-
if (plans.length === 0) {
|
|
68
|
-
return filter === 'all'
|
|
69
|
-
? 'No plans found in .plans/plans.jsonl'
|
|
70
|
-
: `No plans with status "${filter}"`;
|
|
71
|
-
}
|
|
72
|
-
|
|
73
|
-
const sortLabel: Record<SortField, string> = {
|
|
74
|
-
name: 'name',
|
|
75
|
-
'date-asc': 'oldest first',
|
|
76
|
-
'date-desc': 'newest first',
|
|
77
|
-
tasks: 'most tasks first',
|
|
78
|
-
};
|
|
79
|
-
|
|
80
|
-
const header =
|
|
81
|
-
filter === 'all'
|
|
82
|
-
? `All plans (${plans.length}) — sorted by ${sortLabel[sort]}`
|
|
83
|
-
: `Plans: ${filter} (${plans.length}) — sorted by ${sortLabel[sort]}`;
|
|
84
|
-
|
|
85
|
-
const lines = plans.map((p) => {
|
|
86
|
-
const icon = STATUS_ICON[p.status];
|
|
87
|
-
const progress =
|
|
88
|
-
p.totalTasks > 0 ? ` [${p.doneTasks}/${p.totalTasks} tasks]` : ' [no tasks]';
|
|
89
|
-
const date = p.created_at.slice(0, 10);
|
|
90
|
-
return ` ${icon} ${p.name} — ${p.title}${progress} (${date})`;
|
|
91
|
-
});
|
|
92
|
-
|
|
93
|
-
return `${header}\n${lines.join('\n')}`;
|
|
94
|
-
}
|
|
95
|
-
|
|
96
|
-
// ── Data loading (Effect-based) ──────────────────────────────────────────────
|
|
97
|
-
|
|
98
|
-
export function loadPlanListItems(): Effect.Effect<PlanListItem[], never, FileSystem> {
|
|
99
|
-
return Effect.gen(function* () {
|
|
100
|
-
const manifest = yield* Effect.orElseSucceed(readPlansManifest(), () => [] as PlanManifestEntry[]);
|
|
101
|
-
const items: PlanListItem[] = [];
|
|
102
|
-
|
|
103
|
-
for (const entry of manifest) {
|
|
104
|
-
const dir = `.plans/${entry.name}`;
|
|
105
|
-
const snapshot = yield* Effect.orElseSucceed(readTasksJsonl(dir), () => undefined);
|
|
106
|
-
const totalTasks = snapshot?.tasks.length ?? 0;
|
|
107
|
-
const doneTasks = snapshot?.tasks.filter(
|
|
108
|
-
(t) => t.status === 'done' || t.status === 'skipped',
|
|
109
|
-
).length ?? 0;
|
|
110
|
-
const pendingTasks = snapshot?.tasks.filter((t) => t.status === 'pending').length ?? 0;
|
|
111
|
-
|
|
112
|
-
items.push({
|
|
113
|
-
name: entry.name,
|
|
114
|
-
title: entry.title,
|
|
115
|
-
status: entry.status,
|
|
116
|
-
created_at: entry.created_at,
|
|
117
|
-
completed_at: entry.completed_at,
|
|
118
|
-
totalTasks,
|
|
119
|
-
doneTasks,
|
|
120
|
-
pendingTasks,
|
|
121
|
-
});
|
|
122
|
-
}
|
|
123
|
-
|
|
124
|
-
return items;
|
|
125
|
-
});
|
|
126
|
-
}
|
|
127
|
-
|
|
128
|
-
// ── Interactive command handler ──────────────────────────────────────────────
|
|
9
|
+
import type { RunPlanIO } from '@dreki-gg/taskman';
|
|
10
|
+
import { PlanListing } from '@dreki-gg/taskman';
|
|
11
|
+
|
|
12
|
+
// Re-export the engine listing types/helpers for the rest of the extension.
|
|
13
|
+
export type SortField = PlanListing.SortField;
|
|
14
|
+
export type StatusFilter = PlanListing.StatusFilter;
|
|
15
|
+
export type PlanListItem = PlanListing.PlanListItem;
|
|
16
|
+
export const {
|
|
17
|
+
filterPlans,
|
|
18
|
+
sortPlans,
|
|
19
|
+
formatPlanList,
|
|
20
|
+
loadPlanListItems,
|
|
21
|
+
parseListArgs: parseArgs,
|
|
22
|
+
} = PlanListing;
|
|
129
23
|
|
|
130
24
|
export async function handleListPlans(
|
|
131
25
|
ctx: ExtensionCommandContext,
|
|
132
26
|
runPlanIO: RunPlanIO,
|
|
133
27
|
args?: string,
|
|
134
28
|
): Promise<void> {
|
|
135
|
-
const allItems = await runPlanIO(loadPlanListItems());
|
|
29
|
+
const allItems = await runPlanIO(PlanListing.loadPlanListItems());
|
|
136
30
|
|
|
137
31
|
if (allItems.length === 0) {
|
|
138
32
|
ctx.ui.notify('No plans found in .plans/plans.jsonl', 'info');
|
|
139
33
|
return;
|
|
140
34
|
}
|
|
141
35
|
|
|
142
|
-
// Parse inline args: /plans [filter] [sort]
|
|
143
|
-
// e.g. /plans done tasks, /plans in-progress date-asc
|
|
36
|
+
// Parse inline args: /plans [filter] [sort] — e.g. /plans done tasks
|
|
144
37
|
let filter: StatusFilter = 'all';
|
|
145
38
|
let sort: SortField = 'date-desc';
|
|
146
39
|
|
|
147
40
|
if (args?.trim()) {
|
|
148
|
-
const parsed =
|
|
41
|
+
const parsed = PlanListing.parseListArgs(args.trim());
|
|
149
42
|
filter = parsed.filter;
|
|
150
43
|
sort = parsed.sort;
|
|
151
44
|
} else {
|
|
152
|
-
// Interactive mode: ask the user
|
|
153
45
|
const filterChoice = await ctx.ui.select('Filter plans by status:', [
|
|
154
46
|
'All',
|
|
155
47
|
'In-progress',
|
|
@@ -185,45 +77,6 @@ export async function handleListPlans(
|
|
|
185
77
|
sort = sortMap[sortChoice] ?? 'date-desc';
|
|
186
78
|
}
|
|
187
79
|
|
|
188
|
-
const
|
|
189
|
-
|
|
190
|
-
const output = formatPlanList(sorted, filter, sort);
|
|
191
|
-
|
|
192
|
-
ctx.ui.notify(output, 'info');
|
|
193
|
-
}
|
|
194
|
-
|
|
195
|
-
// ── Arg parsing ──────────────────────────────────────────────────────────────
|
|
196
|
-
|
|
197
|
-
const FILTER_ALIASES: Record<string, StatusFilter> = {
|
|
198
|
-
all: 'all',
|
|
199
|
-
'in-progress': 'in-progress',
|
|
200
|
-
pending: 'in-progress',
|
|
201
|
-
active: 'in-progress',
|
|
202
|
-
done: 'done',
|
|
203
|
-
completed: 'done',
|
|
204
|
-
superseded: 'superseded',
|
|
205
|
-
abandoned: 'abandoned',
|
|
206
|
-
};
|
|
207
|
-
|
|
208
|
-
const SORT_ALIASES: Record<string, SortField> = {
|
|
209
|
-
name: 'name',
|
|
210
|
-
'date-asc': 'date-asc',
|
|
211
|
-
oldest: 'date-asc',
|
|
212
|
-
'date-desc': 'date-desc',
|
|
213
|
-
newest: 'date-desc',
|
|
214
|
-
tasks: 'tasks',
|
|
215
|
-
'task-count': 'tasks',
|
|
216
|
-
};
|
|
217
|
-
|
|
218
|
-
export function parseArgs(raw: string): { filter: StatusFilter; sort: SortField } {
|
|
219
|
-
const tokens = raw.toLowerCase().split(/\s+/);
|
|
220
|
-
let filter: StatusFilter = 'all';
|
|
221
|
-
let sort: SortField = 'date-desc';
|
|
222
|
-
|
|
223
|
-
for (const token of tokens) {
|
|
224
|
-
if (FILTER_ALIASES[token]) filter = FILTER_ALIASES[token];
|
|
225
|
-
else if (SORT_ALIASES[token]) sort = SORT_ALIASES[token];
|
|
226
|
-
}
|
|
227
|
-
|
|
228
|
-
return { filter, sort };
|
|
80
|
+
const sorted = PlanListing.sortPlans(PlanListing.filterPlans(allItems, filter), sort);
|
|
81
|
+
ctx.ui.notify(PlanListing.formatPlanList(sorted, filter, sort), 'info');
|
|
229
82
|
}
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Exec-pending marker I/O — the pi-mode one-shot handoff from planning to
|
|
3
|
+
* execution. This is pi-workflow-specific (model + thinking preset for the
|
|
4
|
+
* execution run), so it stays in the extension rather than the engine package.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { Effect, Either, Option } from 'effect';
|
|
8
|
+
import {
|
|
9
|
+
FileSystem,
|
|
10
|
+
decodeExecPendingConfig,
|
|
11
|
+
type ExecPendingConfig,
|
|
12
|
+
type PlanWriteError,
|
|
13
|
+
} from '@dreki-gg/taskman';
|
|
14
|
+
import { EXEC_PENDING_FILE } from './constants.js';
|
|
15
|
+
|
|
16
|
+
const PLANS_DIR = '.plans';
|
|
17
|
+
|
|
18
|
+
export function writeExecPending(
|
|
19
|
+
dir: string,
|
|
20
|
+
config: ExecPendingConfig,
|
|
21
|
+
): Effect.Effect<void, PlanWriteError, FileSystem> {
|
|
22
|
+
return Effect.gen(function* () {
|
|
23
|
+
const fs = yield* FileSystem;
|
|
24
|
+
yield* fs.makeDir(dir);
|
|
25
|
+
yield* fs.writeFileString(`${dir}/${EXEC_PENDING_FILE}`, JSON.stringify(config, null, 2) + '\n');
|
|
26
|
+
});
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export function readAndClearExecPending(): Effect.Effect<
|
|
30
|
+
{ planDir: string; config: ExecPendingConfig } | undefined,
|
|
31
|
+
never,
|
|
32
|
+
FileSystem
|
|
33
|
+
> {
|
|
34
|
+
return Effect.gen(function* () {
|
|
35
|
+
const fs = yield* FileSystem;
|
|
36
|
+
const maybeDirs = yield* Effect.option(fs.listDirectories(PLANS_DIR));
|
|
37
|
+
if (Option.isNone(maybeDirs)) return undefined;
|
|
38
|
+
|
|
39
|
+
for (const name of maybeDirs.value) {
|
|
40
|
+
const dir = `${PLANS_DIR}/${name}`;
|
|
41
|
+
const markerPath = `${dir}/${EXEC_PENDING_FILE}`;
|
|
42
|
+
const maybeText = yield* Effect.option(fs.readFileString(markerPath));
|
|
43
|
+
if (Option.isNone(maybeText)) continue;
|
|
44
|
+
|
|
45
|
+
let parsed: unknown;
|
|
46
|
+
try {
|
|
47
|
+
parsed = JSON.parse(maybeText.value);
|
|
48
|
+
} catch {
|
|
49
|
+
continue;
|
|
50
|
+
}
|
|
51
|
+
const decoded = decodeExecPendingConfig(parsed);
|
|
52
|
+
if (Either.isLeft(decoded)) continue;
|
|
53
|
+
|
|
54
|
+
yield* Effect.ignore(fs.removeFile(markerPath));
|
|
55
|
+
return { planDir: dir, config: decoded.right };
|
|
56
|
+
}
|
|
57
|
+
return undefined;
|
|
58
|
+
});
|
|
59
|
+
}
|
|
@@ -28,19 +28,20 @@ import {
|
|
|
28
28
|
} from './constants.js';
|
|
29
29
|
import type { ThinkingLevel, TaskStatus } from './types.js';
|
|
30
30
|
import { PlanModeState } from './state.js';
|
|
31
|
-
import { makePlanRuntime } from '
|
|
32
|
-
import { loadHandoff
|
|
33
|
-
import {
|
|
34
|
-
import {
|
|
31
|
+
import { makePlanRuntime } from '@dreki-gg/taskman';
|
|
32
|
+
import { loadHandoff } from '@dreki-gg/taskman';
|
|
33
|
+
import { readAndClearExecPending } from './exec-pending.js';
|
|
34
|
+
import { readTasksJsonl, writeTasksJsonl } from '@dreki-gg/taskman';
|
|
35
|
+
import { upsertPlanEntry, reconcilePlanStatus } from '@dreki-gg/taskman';
|
|
35
36
|
import { updateUI } from './ui.js';
|
|
36
37
|
import { buildPlanModePrompt, buildExecutionPrompt } from './prompts.js';
|
|
37
38
|
import { filterExecutionMessages, filterStalePlanMessages } from './context-filter.js';
|
|
38
|
-
import { activeTasksResolved, deferredTasks, isPlanFinalizable } from '
|
|
39
|
+
import { activeTasksResolved, deferredTasks, isPlanFinalizable } from '@dreki-gg/taskman';
|
|
39
40
|
import { enterPlanMode, exitPlanMode, switchModel } from './phase-transitions.js';
|
|
40
41
|
import { resumePlan, executeInNewSession } from './resume.js';
|
|
41
42
|
import { resolveActivePlan, focusActivePlan } from './resolve-plan.js';
|
|
42
|
-
import { reconcileInitiativeForPlan } from '
|
|
43
|
-
import { collectPlanDrift } from '
|
|
43
|
+
import { reconcileInitiativeForPlan } from '@dreki-gg/taskman';
|
|
44
|
+
import { collectPlanDrift } from '@dreki-gg/taskman';
|
|
44
45
|
import { registerSubmitPlanTool } from './tools/submit-plan.js';
|
|
45
46
|
import { registerSubmitInitiativeTool } from './tools/submit-initiative.js';
|
|
46
47
|
import { registerRevisePlanTool } from './tools/revise-plan.js';
|
|
@@ -10,12 +10,12 @@
|
|
|
10
10
|
|
|
11
11
|
import { Effect } from 'effect';
|
|
12
12
|
import type { ExtensionAPI } from '@earendil-works/pi-coding-agent';
|
|
13
|
-
import type { FileSystem } from '
|
|
14
|
-
import type { RunPlanIO } from '
|
|
13
|
+
import type { FileSystem } from '@dreki-gg/taskman';
|
|
14
|
+
import type { RunPlanIO } from '@dreki-gg/taskman';
|
|
15
15
|
import type { PlanStatus, TaskRecord, TaskStatus } from '../types.js';
|
|
16
|
-
import { readPlansManifest } from '
|
|
17
|
-
import { readTasksJsonl } from '
|
|
18
|
-
import { loadHandoff } from '
|
|
16
|
+
import { readPlansManifest } from '@dreki-gg/taskman';
|
|
17
|
+
import { readTasksJsonl } from '@dreki-gg/taskman';
|
|
18
|
+
import { loadHandoff } from '@dreki-gg/taskman';
|
|
19
19
|
import { firstPlanReference } from './tokens.js';
|
|
20
20
|
|
|
21
21
|
export interface ResolvedPlanReference {
|