@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.
Files changed (57) hide show
  1. package/CHANGELOG.md +19 -0
  2. package/extensions/plan-mode/__tests__/initiative-status.test.ts +5 -5
  3. package/extensions/plan-mode/__tests__/plan-references-context.test.ts +4 -4
  4. package/extensions/plan-mode/__tests__/reconcile-plans.test.ts +3 -3
  5. package/extensions/plan-mode/__tests__/resolve-plan.test.ts +4 -4
  6. package/extensions/plan-mode/__tests__/revise-plan.test.ts +4 -4
  7. package/extensions/plan-mode/__tests__/submit-initiative.test.ts +2 -2
  8. package/extensions/plan-mode/__tests__/submit-plan.test.ts +3 -3
  9. package/extensions/plan-mode/__tests__/update-initiative.test.ts +2 -2
  10. package/extensions/plan-mode/__tests__/update-plan.test.ts +2 -2
  11. package/extensions/plan-mode/__tests__/utils.test.ts +2 -1
  12. package/extensions/plan-mode/commands/list-initiatives.ts +19 -109
  13. package/extensions/plan-mode/commands/list-plans.ts +21 -168
  14. package/extensions/plan-mode/exec-pending.ts +59 -0
  15. package/extensions/plan-mode/index.ts +8 -7
  16. package/extensions/plan-mode/references/context.ts +5 -5
  17. package/extensions/plan-mode/references/plan-index.ts +1 -1
  18. package/extensions/plan-mode/resolve-plan.ts +4 -4
  19. package/extensions/plan-mode/resume.ts +6 -5
  20. package/extensions/plan-mode/tools/add-task.ts +1 -1
  21. package/extensions/plan-mode/tools/initiative-status.ts +6 -6
  22. package/extensions/plan-mode/tools/preview-prototype.ts +3 -3
  23. package/extensions/plan-mode/tools/reconcile-plans.ts +2 -2
  24. package/extensions/plan-mode/tools/revise-plan.ts +7 -7
  25. package/extensions/plan-mode/tools/submit-initiative.ts +4 -4
  26. package/extensions/plan-mode/tools/submit-plan.ts +7 -7
  27. package/extensions/plan-mode/tools/update-initiative.ts +2 -2
  28. package/extensions/plan-mode/tools/update-plan.ts +3 -3
  29. package/extensions/plan-mode/types.ts +17 -59
  30. package/extensions/plan-mode/utils.ts +2 -29
  31. package/package.json +2 -1
  32. package/extensions/plan-mode/__tests__/atomic-write.test.ts +0 -45
  33. package/extensions/plan-mode/__tests__/concurrent-writes.test.ts +0 -85
  34. package/extensions/plan-mode/__tests__/initiative-readiness.test.ts +0 -159
  35. package/extensions/plan-mode/__tests__/initiatives-manifest.test.ts +0 -89
  36. package/extensions/plan-mode/__tests__/list-initiatives.test.ts +0 -59
  37. package/extensions/plan-mode/__tests__/list-plans.test.ts +0 -253
  38. package/extensions/plan-mode/__tests__/plan-storage.test.ts +0 -57
  39. package/extensions/plan-mode/__tests__/plans-manifest.test.ts +0 -120
  40. package/extensions/plan-mode/__tests__/reconcile.test.ts +0 -188
  41. package/extensions/plan-mode/__tests__/schema.test.ts +0 -334
  42. package/extensions/plan-mode/__tests__/task-status.test.ts +0 -74
  43. package/extensions/plan-mode/__tests__/task-storage.test.ts +0 -101
  44. package/extensions/plan-mode/effects/filesystem.ts +0 -65
  45. package/extensions/plan-mode/effects/runtime.ts +0 -24
  46. package/extensions/plan-mode/errors.ts +0 -105
  47. package/extensions/plan-mode/initiative.ts +0 -172
  48. package/extensions/plan-mode/plan-storage.ts +0 -6
  49. package/extensions/plan-mode/reconcile.ts +0 -235
  50. package/extensions/plan-mode/schema.ts +0 -99
  51. package/extensions/plan-mode/storage/atomic-write.ts +0 -75
  52. package/extensions/plan-mode/storage/file-lock.ts +0 -49
  53. package/extensions/plan-mode/storage/initiatives-manifest.ts +0 -154
  54. package/extensions/plan-mode/storage/plan-storage.ts +0 -88
  55. package/extensions/plan-mode/storage/plans-manifest.ts +0 -174
  56. package/extensions/plan-mode/storage/task-storage.ts +0 -111
  57. 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 '../effects/filesystem.js';
8
- import { makePlanRuntime } from '../effects/runtime.js';
9
- import { upsertPlanEntry } from '../storage/plans-manifest.js';
10
- import { upsertInitiativeEntry } from '../storage/initiatives-manifest.js';
11
- import { writeTasksJsonl } from '../storage/task-storage.js';
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 '../effects/runtime.js';
7
- import { upsertPlanEntry } from '../storage/plans-manifest.js';
8
- import { writeTasksJsonl } from '../storage/task-storage.js';
9
- import { saveHandoff } from '../storage/plan-storage.js';
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 '../effects/runtime.js';
7
- import { writeTasksJsonl } from '../storage/task-storage.js';
8
- import { readPlansManifest, upsertPlanEntry } from '../storage/plans-manifest.js';
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 '../effects/runtime.js';
7
- import { writeTasksJsonl } from '../storage/task-storage.js';
8
- import { upsertPlanEntry } from '../storage/plans-manifest.js';
9
- import { saveHandoff } from '../storage/plan-storage.js';
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 '../effects/runtime.js';
6
+ import { makePlanRuntime } from '@dreki-gg/taskman';
7
7
  import { registerRevisePlanTool } from '../tools/revise-plan.js';
8
- import { readTasksJsonl, writeTasksJsonl } from '../storage/task-storage.js';
9
- import { saveHandoff, loadHandoff } from '../storage/plan-storage.js';
10
- import { readPlansManifest, upsertPlanEntry } from '../storage/plans-manifest.js';
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 '../effects/runtime.js';
7
- import { readInitiativesManifest } from '../storage/initiatives-manifest.js';
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 '../effects/runtime.js';
7
- import { readPlansManifest } from '../storage/plans-manifest.js';
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 '../storage/initiatives-manifest.js';
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 '../effects/runtime.js';
6
+ import { makePlanRuntime } from '@dreki-gg/taskman';
7
7
  import {
8
8
  readInitiativesManifest,
9
9
  upsertInitiativeEntry,
10
- } from '../storage/initiatives-manifest.js';
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 '../effects/runtime.js';
7
- import { readPlansManifest, upsertPlanEntry } from '../storage/plans-manifest.js';
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, nextTaskId } from '../utils.js';
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 initiative sibling of /plans. Reads the initiatives registry plus the
5
- * plans manifest to roll up member-plan progress (done/total, ready/blocked).
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 '../effects/runtime.js';
11
- import { FileSystem } from '../effects/filesystem.js';
12
- import { readPlansManifest, type PlanManifestEntry } from '../storage/plans-manifest.js';
13
- import {
14
- readInitiativesManifest,
15
- type InitiativeManifestEntry,
16
- } from '../storage/initiatives-manifest.js';
17
- import { initiativeRollup } from '../initiative.js';
18
- import type { InitiativeStatus } from '../types.js';
19
-
20
- export type StatusFilter = 'all' | InitiativeStatus;
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 = parseFilter(args.trim());
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
- const filtered = filterInitiatives(items, filter);
147
- ctx.ui.notify(formatInitiativeList(filtered, filter), 'info');
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
- * Reads from the plans manifest AND task snapshots to build a rich table
5
- * with status, task counts, and dates. The user can filter by status and
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 '../effects/runtime.js';
12
- import { FileSystem } from '../effects/filesystem.js';
13
- import { readPlansManifest, type PlanManifestEntry } from '../storage/plans-manifest.js';
14
- import { readTasksJsonl } from '../storage/task-storage.js';
15
- import type { PlanStatus } from '../types.js';
16
-
17
- // ── Types ────────────────────────────────────────────────────────────────────
18
-
19
- export type SortField = 'name' | 'date-asc' | 'date-desc' | 'tasks';
20
- export type StatusFilter = 'all' | PlanStatus;
21
-
22
- export interface PlanListItem {
23
- name: string;
24
- title: string;
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 = parseArgs(args.trim());
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 filtered = filterPlans(allItems, filter);
189
- const sorted = sortPlans(filtered, sort);
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 './effects/runtime.js';
32
- import { loadHandoff, readAndClearExecPending } from './storage/plan-storage.js';
33
- import { readTasksJsonl, writeTasksJsonl } from './storage/task-storage.js';
34
- import { upsertPlanEntry, reconcilePlanStatus } from './storage/plans-manifest.js';
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 './task-status.js';
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 './initiative.js';
43
- import { collectPlanDrift } from './reconcile.js';
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 '../effects/filesystem.js';
14
- import type { RunPlanIO } from '../effects/runtime.js';
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 '../storage/plans-manifest.js';
17
- import { readTasksJsonl } from '../storage/task-storage.js';
18
- import { loadHandoff } from '../storage/plan-storage.js';
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 {