@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.
Files changed (61) hide show
  1. package/CHANGELOG.md +25 -0
  2. package/extensions/plan-mode/__tests__/git.test.ts +17 -0
  3. package/extensions/plan-mode/__tests__/initiative-status.test.ts +5 -5
  4. package/extensions/plan-mode/__tests__/plan-references-context.test.ts +4 -4
  5. package/extensions/plan-mode/__tests__/prompts.test.ts +23 -0
  6. package/extensions/plan-mode/__tests__/reconcile-plans.test.ts +3 -3
  7. package/extensions/plan-mode/__tests__/resolve-plan.test.ts +4 -4
  8. package/extensions/plan-mode/__tests__/revise-plan.test.ts +4 -4
  9. package/extensions/plan-mode/__tests__/submit-initiative.test.ts +2 -2
  10. package/extensions/plan-mode/__tests__/submit-plan.test.ts +3 -3
  11. package/extensions/plan-mode/__tests__/update-initiative.test.ts +2 -2
  12. package/extensions/plan-mode/__tests__/update-plan.test.ts +2 -2
  13. package/extensions/plan-mode/__tests__/utils.test.ts +2 -1
  14. package/extensions/plan-mode/commands/list-initiatives.ts +19 -109
  15. package/extensions/plan-mode/commands/list-plans.ts +21 -168
  16. package/extensions/plan-mode/exec-pending.ts +59 -0
  17. package/extensions/plan-mode/git.ts +21 -0
  18. package/extensions/plan-mode/index.ts +9 -7
  19. package/extensions/plan-mode/prompts.ts +6 -1
  20. package/extensions/plan-mode/references/context.ts +5 -5
  21. package/extensions/plan-mode/references/plan-index.ts +1 -1
  22. package/extensions/plan-mode/resolve-plan.ts +5 -4
  23. package/extensions/plan-mode/resume.ts +7 -5
  24. package/extensions/plan-mode/tools/add-task.ts +1 -1
  25. package/extensions/plan-mode/tools/initiative-status.ts +6 -6
  26. package/extensions/plan-mode/tools/preview-prototype.ts +3 -3
  27. package/extensions/plan-mode/tools/reconcile-plans.ts +2 -2
  28. package/extensions/plan-mode/tools/revise-plan.ts +9 -7
  29. package/extensions/plan-mode/tools/submit-initiative.ts +4 -4
  30. package/extensions/plan-mode/tools/submit-plan.ts +18 -8
  31. package/extensions/plan-mode/tools/update-initiative.ts +2 -2
  32. package/extensions/plan-mode/tools/update-plan.ts +3 -3
  33. package/extensions/plan-mode/types.ts +17 -51
  34. package/extensions/plan-mode/utils.ts +2 -29
  35. package/package.json +2 -1
  36. package/extensions/plan-mode/__tests__/atomic-write.test.ts +0 -45
  37. package/extensions/plan-mode/__tests__/concurrent-writes.test.ts +0 -85
  38. package/extensions/plan-mode/__tests__/initiative-readiness.test.ts +0 -159
  39. package/extensions/plan-mode/__tests__/initiatives-manifest.test.ts +0 -89
  40. package/extensions/plan-mode/__tests__/list-initiatives.test.ts +0 -59
  41. package/extensions/plan-mode/__tests__/list-plans.test.ts +0 -253
  42. package/extensions/plan-mode/__tests__/plan-storage.test.ts +0 -57
  43. package/extensions/plan-mode/__tests__/plans-manifest.test.ts +0 -120
  44. package/extensions/plan-mode/__tests__/reconcile.test.ts +0 -188
  45. package/extensions/plan-mode/__tests__/schema.test.ts +0 -319
  46. package/extensions/plan-mode/__tests__/task-status.test.ts +0 -74
  47. package/extensions/plan-mode/__tests__/task-storage.test.ts +0 -94
  48. package/extensions/plan-mode/effects/filesystem.ts +0 -65
  49. package/extensions/plan-mode/effects/runtime.ts +0 -24
  50. package/extensions/plan-mode/errors.ts +0 -105
  51. package/extensions/plan-mode/initiative.ts +0 -172
  52. package/extensions/plan-mode/plan-storage.ts +0 -6
  53. package/extensions/plan-mode/reconcile.ts +0 -235
  54. package/extensions/plan-mode/schema.ts +0 -97
  55. package/extensions/plan-mode/storage/atomic-write.ts +0 -75
  56. package/extensions/plan-mode/storage/file-lock.ts +0 -49
  57. package/extensions/plan-mode/storage/initiatives-manifest.ts +0 -154
  58. package/extensions/plan-mode/storage/plan-storage.ts +0 -88
  59. package/extensions/plan-mode/storage/plans-manifest.ts +0 -174
  60. package/extensions/plan-mode/storage/task-storage.ts +0 -111
  61. package/extensions/plan-mode/task-status.ts +0 -46
@@ -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
+ }
@@ -0,0 +1,21 @@
1
+ /**
2
+ * Read-only git helpers. Failure-tolerant by design: plan-mode must never block
3
+ * or throw because git metadata is unavailable (no repo, detached HEAD, no
4
+ * commits yet). Runs on Node via execFile — no Bun APIs.
5
+ */
6
+
7
+ import { execFile } from 'node:child_process';
8
+
9
+ /**
10
+ * Resolve the current HEAD commit SHA, or undefined when it cannot be read.
11
+ * Never rejects — callers treat undefined as "no drift baseline".
12
+ */
13
+ export function readHeadCommit(cwd: string = process.cwd()): Promise<string | undefined> {
14
+ return new Promise((resolve) => {
15
+ execFile('git', ['rev-parse', 'HEAD'], { cwd }, (error, stdout) => {
16
+ if (error) return resolve(undefined);
17
+ const sha = stdout.trim();
18
+ resolve(sha.length > 0 ? sha : undefined);
19
+ });
20
+ });
21
+ }
@@ -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';
@@ -626,6 +627,7 @@ export default function planMode(pi: ExtensionAPI): void {
626
627
  planName: snapshot.meta.plan_name,
627
628
  handoff: (await runPlanIO(loadHandoff(pending.planDir))) ?? '',
628
629
  tasks: snapshot.tasks,
630
+ base_commit: snapshot.meta.base_commit,
629
631
  }
630
632
  : undefined;
631
633
  }
@@ -28,7 +28,7 @@ When you are ready to finalize the plan, call submit_plan with:
28
28
  - tasks: an array of tasks with id (e.g. "t-001"), description (≤60 chars), optional details, and optional depends_on task IDs
29
29
 
30
30
  Plan weight:
31
- - **Delegation plans** (different agent/human executes): include full details in each task so an executor with zero context can follow them.
31
+ - **Delegation plans** (different agent/human executes): include full details in each task so an executor with zero context can follow them. End each task's details with a **verification gate** — a concrete command and its expected output (e.g. \`bun test\` → all pass) so the executor can prove success without judgement, plus any **STOP conditions** ("if X, stop and report" instead of improvising when reality doesn't match the plan).
32
32
  - **Self-execution plans** (you plan and execute in the same session): use lightweight checklist-style tasks — just id + description, skip details. The handoff doc carries the real context.
33
33
 
34
34
  submit_plan is finalization, not the starting point. It records tasks and the handoff — it does not generate HTML.
@@ -59,6 +59,10 @@ export function buildExecutionPrompt(plan: PlanData): string | undefined {
59
59
  const currentTask = remaining[0];
60
60
  const currentDetails = currentTask.details ? `\nDetails: ${currentTask.details}` : '';
61
61
 
62
+ const driftCheck = plan.base_commit
63
+ ? `\n## Drift check (do this FIRST)\nThis plan was written against git commit ${plan.base_commit}. Before editing, run \`git rev-parse HEAD\`. If it differs, the codebase has moved since the plan was written: run \`git diff ${plan.base_commit} --stat\`, re-read any files the current task touches, and proceed with caution — adjust to what the code actually looks like now. This is a warning, not a stop.\n`
64
+ : '';
65
+
62
66
  return `[EXECUTING PLAN — FOLLOW THE PLAN EXACTLY]
63
67
 
64
68
  You are executing a structured plan. Your ONLY job is to implement the plan tasks below, one at a time.
@@ -73,6 +77,7 @@ Rules:
73
77
 
74
78
  ## Current task
75
79
  ${currentTask.id}: ${currentTask.description}${currentDetails}
80
+ ${driftCheck}
76
81
 
77
82
  ## Handoff
78
83
  ${plan.handoff}
@@ -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 {
@@ -7,7 +7,7 @@
7
7
  * per-keystroke disk hit.
8
8
  */
9
9
 
10
- import type { RunPlanIO } from '../effects/runtime.js';
10
+ import type { RunPlanIO } from '@dreki-gg/taskman';
11
11
  import { loadPlanListItems, type PlanListItem } from '../commands/list-plans.js';
12
12
 
13
13
  const DEFAULT_TTL_MS = 2_000;
@@ -17,10 +17,10 @@
17
17
  import type { ExtensionAPI } from '@earendil-works/pi-coding-agent';
18
18
  import type { PlanModeState } from './state.js';
19
19
  import type { PlanData } from './types.js';
20
- import type { RunPlanIO } from './effects/runtime.js';
21
- import { readPlansManifest } from './storage/plans-manifest.js';
22
- import { readTasksJsonl } from './storage/task-storage.js';
23
- import { loadHandoff } from './storage/plan-storage.js';
20
+ import type { RunPlanIO } from '@dreki-gg/taskman';
21
+ import { readPlansManifest } from '@dreki-gg/taskman';
22
+ import { readTasksJsonl } from '@dreki-gg/taskman';
23
+ import { loadHandoff } from '@dreki-gg/taskman';
24
24
 
25
25
  export interface ResolvedPlan {
26
26
  /** The attached plan, when resolvable. Already written into `state`. */
@@ -57,6 +57,7 @@ async function attach(
57
57
  planName: snapshot.meta.plan_name,
58
58
  handoff: (await runPlanIO(loadHandoff(dir))) ?? '',
59
59
  tasks: snapshot.tasks,
60
+ base_commit: snapshot.meta.base_commit,
60
61
  };
61
62
  state.plan = plan;
62
63
  state.planDir = dir;
@@ -9,13 +9,14 @@ import type {
9
9
  } from '@earendil-works/pi-coding-agent';
10
10
  import type { PlanModeState } from './state.js';
11
11
  import type { PlanData } from './types.js';
12
- import type { RunPlanIO } from './effects/runtime.js';
12
+ import type { RunPlanIO } from '@dreki-gg/taskman';
13
13
  import { EXEC_THINKING, EXEC_MODEL_OPTIONS } from './constants.js';
14
- import { readPlansManifest } from './storage/plans-manifest.js';
15
- import { loadHandoff, writeExecPending } from './storage/plan-storage.js';
16
- import { readTasksJsonl, writeTasksJsonl } from './storage/task-storage.js';
14
+ import { readPlansManifest } from '@dreki-gg/taskman';
15
+ import { loadHandoff } from '@dreki-gg/taskman';
16
+ import { writeExecPending } from './exec-pending.js';
17
+ import { readTasksJsonl, writeTasksJsonl } from '@dreki-gg/taskman';
17
18
  import { enterPlanMode } from './phase-transitions.js';
18
- import { reactivateForExecution } from './task-status.js';
19
+ import { reactivateForExecution } from '@dreki-gg/taskman';
19
20
 
20
21
  export async function pickExecutionModel(
21
22
  ctx: ExtensionContext,
@@ -82,6 +83,7 @@ export async function resumePlan(
82
83
  planName: snapshot.meta.plan_name,
83
84
  handoff: (await runPlanIO(loadHandoff(dir))) ?? '',
84
85
  tasks: snapshot.tasks,
86
+ base_commit: snapshot.meta.base_commit,
85
87
  };
86
88
 
87
89
  const doneCount = state.plan.tasks.filter(
@@ -11,7 +11,7 @@ import { Text } from '@earendil-works/pi-tui';
11
11
  import { Type } from 'typebox';
12
12
  import type { TaskRecord } from '../types.js';
13
13
  import type { ResolvedPlan } from '../resolve-plan.js';
14
- import { nextTaskId } from '../utils.js';
14
+ import { nextTaskId } from '@dreki-gg/taskman';
15
15
 
16
16
  export interface AddTaskCallbacks {
17
17
  /** Resolve the active plan, attaching from disk when none is in memory. */
@@ -12,12 +12,12 @@ import type { ExtensionAPI } from '@earendil-works/pi-coding-agent';
12
12
  import { Text } from '@earendil-works/pi-tui';
13
13
  import { Type } from 'typebox';
14
14
  import { Effect } from 'effect';
15
- import type { RunPlanIO } from '../effects/runtime.js';
16
- import { FileSystem } from '../effects/filesystem.js';
17
- import { readPlansManifest, type PlanManifestEntry } from '../storage/plans-manifest.js';
18
- import { readInitiativesManifest } from '../storage/initiatives-manifest.js';
19
- import { readTasksJsonl } from '../storage/task-storage.js';
20
- import { initiativeRollup, membersOf, type InitiativeRollup } from '../initiative.js';
15
+ import type { RunPlanIO } from '@dreki-gg/taskman';
16
+ import { FileSystem } from '@dreki-gg/taskman';
17
+ import { readPlansManifest, type PlanManifestEntry } from '@dreki-gg/taskman';
18
+ import { readInitiativesManifest } from '@dreki-gg/taskman';
19
+ import { readTasksJsonl } from '@dreki-gg/taskman';
20
+ import { initiativeRollup, membersOf, type InitiativeRollup } from '@dreki-gg/taskman';
21
21
 
22
22
  /** Normalize an initiative hint (`x` or `.plans/x`) to a bare name. */
23
23
  function normalizeName(hint: string): string {
@@ -12,10 +12,10 @@ import { Type } from 'typebox';
12
12
  import { Effect } from 'effect';
13
13
  import { spawn } from 'node:child_process';
14
14
  import { join } from 'node:path';
15
- import { FileSystem } from '../effects/filesystem.js';
16
- import type { RunPlanIO } from '../effects/runtime.js';
15
+ import { FileSystem } from '@dreki-gg/taskman';
16
+ import type { RunPlanIO } from '@dreki-gg/taskman';
17
17
  import { buildPrototypeDocument } from '../html/render.js';
18
- import { toKebabCase } from '../utils.js';
18
+ import { toKebabCase } from '@dreki-gg/taskman';
19
19
 
20
20
  const PREVIEW_DIR = '.plans/_prototypes';
21
21
 
@@ -10,7 +10,7 @@
10
10
  import type { ExtensionAPI } from '@earendil-works/pi-coding-agent';
11
11
  import { Text } from '@earendil-works/pi-tui';
12
12
  import { Type } from 'typebox';
13
- import type { RunPlanIO } from '../effects/runtime.js';
13
+ import type { RunPlanIO } from '@dreki-gg/taskman';
14
14
  import {
15
15
  applyInitiativeReconcile,
16
16
  applyReconcile,
@@ -18,7 +18,7 @@ import {
18
18
  collectPlanDrift,
19
19
  type InitiativeDriftRow,
20
20
  type PlanDriftRow,
21
- } from '../reconcile.js';
21
+ } from '@dreki-gg/taskman';
22
22
 
23
23
  function describeRow(row: PlanDriftRow): string {
24
24
  const progress = row.hasTasks ? ` (${row.resolved}/${row.total})` : '';
@@ -16,17 +16,17 @@ import type { ExtensionAPI } from '@earendil-works/pi-coding-agent';
16
16
  import { Text } from '@earendil-works/pi-tui';
17
17
  import { Type } from 'typebox';
18
18
  import { Effect } from 'effect';
19
- import { saveHandoff } from '../storage/plan-storage.js';
20
- import { writeTasksJsonl } from '../storage/task-storage.js';
19
+ import { saveHandoff } from '@dreki-gg/taskman';
20
+ import { writeTasksJsonl } from '@dreki-gg/taskman';
21
21
  import {
22
22
  readPlansManifest,
23
23
  reconcilePlanStatus,
24
24
  upsertPlanEntry,
25
- } from '../storage/plans-manifest.js';
26
- import { reconcileInitiativeForPlan, reconcileInitiativeStatus } from '../initiative.js';
27
- import { isPlanFinalizable } from '../task-status.js';
28
- import { toKebabCase } from '../utils.js';
29
- import type { RunPlanIO } from '../effects/runtime.js';
25
+ } from '@dreki-gg/taskman';
26
+ import { reconcileInitiativeForPlan, reconcileInitiativeStatus } from '@dreki-gg/taskman';
27
+ import { isPlanFinalizable } from '@dreki-gg/taskman';
28
+ import { toKebabCase } from '@dreki-gg/taskman';
29
+ import type { RunPlanIO } from '@dreki-gg/taskman';
30
30
  import type { PlanData, TaskMeta, TaskRecord } from '../types.js';
31
31
 
32
32
  export interface RevisePlanCallbacks {
@@ -129,12 +129,14 @@ export function registerRevisePlanTool(
129
129
  title: newTitle,
130
130
  plan_name: plan.planName,
131
131
  created_at: plan.tasks[0]?.created_at ?? now,
132
+ base_commit: plan.base_commit,
132
133
  };
133
134
  const revised: PlanData = {
134
135
  title: newTitle,
135
136
  planName: plan.planName,
136
137
  handoff: newHandoff,
137
138
  tasks,
139
+ base_commit: plan.base_commit,
138
140
  };
139
141
  const planDir = `.plans/${plan.planName}`;
140
142
 
@@ -12,10 +12,10 @@ import type { ExtensionAPI } from '@earendil-works/pi-coding-agent';
12
12
  import { Text } from '@earendil-works/pi-tui';
13
13
  import { Type } from 'typebox';
14
14
  import { Effect } from 'effect';
15
- import { saveInitiative } from '../storage/plan-storage.js';
16
- import { upsertInitiativeEntry } from '../storage/initiatives-manifest.js';
17
- import type { RunPlanIO } from '../effects/runtime.js';
18
- import { toKebabCase } from '../utils.js';
15
+ import { saveInitiative } from '@dreki-gg/taskman';
16
+ import { upsertInitiativeEntry } from '@dreki-gg/taskman';
17
+ import type { RunPlanIO } from '@dreki-gg/taskman';
18
+ import { toKebabCase } from '@dreki-gg/taskman';
19
19
 
20
20
  export function registerSubmitInitiativeTool(pi: ExtensionAPI, runPlanIO: RunPlanIO): void {
21
21
  pi.registerTool({
@@ -6,13 +6,14 @@ import type { ExtensionAPI } from '@earendil-works/pi-coding-agent';
6
6
  import { Text } from '@earendil-works/pi-tui';
7
7
  import { Type } from 'typebox';
8
8
  import { Effect } from 'effect';
9
- import { saveHandoff } from '../storage/plan-storage.js';
10
- import { writeTasksJsonl } from '../storage/task-storage.js';
11
- import { upsertPlanEntry } from '../storage/plans-manifest.js';
12
- import { readInitiativesManifest } from '../storage/initiatives-manifest.js';
13
- import { reconcileInitiativeForPlan } from '../initiative.js';
14
- import type { RunPlanIO } from '../effects/runtime.js';
15
- import { toKebabCase } from '../utils.js';
9
+ import { saveHandoff } from '@dreki-gg/taskman';
10
+ import { writeTasksJsonl } from '@dreki-gg/taskman';
11
+ import { upsertPlanEntry } from '@dreki-gg/taskman';
12
+ import { readInitiativesManifest } from '@dreki-gg/taskman';
13
+ import { reconcileInitiativeForPlan } from '@dreki-gg/taskman';
14
+ import type { RunPlanIO } from '@dreki-gg/taskman';
15
+ import { toKebabCase } from '@dreki-gg/taskman';
16
+ import { readHeadCommit } from '../git.js';
16
17
  import type { PlanData, TaskMeta, TaskRecord } from '../types.js';
17
18
 
18
19
  export interface SubmitPlanCallbacks {
@@ -33,6 +34,7 @@ export function registerSubmitPlanTool(
33
34
  'Only call submit_plan after shared understanding has been reached with the user.',
34
35
  'Each task needs an id like t-001, a short description, and optional depends_on task IDs.',
35
36
  "When a different agent or human will execute the plan, include detailed implementation instructions in each task's details field.",
37
+ "For delegation tasks (those with details), end the details with a verification gate: a concrete command and its expected output, plus any STOP conditions, so a zero-context executor can prove success without judgement.",
36
38
  'When you are planning and executing yourself (same session), use lightweight checklist-style tasks: just id + description, omit details. Put the real context in the handoff document instead.',
37
39
  'The handoff must be thorough enough that both a human reviewer and executor agent with zero prior context can understand the plan.',
38
40
  'For visual/UI work, preview a prototype with preview_prototype during planning — before submit_plan, not as part of it.',
@@ -81,11 +83,13 @@ export function registerSubmitPlanTool(
81
83
  const initiative = params.initiative ? toKebabCase(params.initiative) : undefined;
82
84
  const dependsOnPlans = params.depends_on_plans?.map(toKebabCase);
83
85
  const now = new Date().toISOString();
86
+ const baseCommit = await readHeadCommit();
84
87
  const meta: TaskMeta = {
85
88
  _type: 'meta',
86
89
  title: params.title,
87
90
  plan_name: planName,
88
91
  created_at: now,
92
+ base_commit: baseCommit,
89
93
  };
90
94
  const tasks: TaskRecord[] = params.tasks.map((task) => ({
91
95
  _type: 'task',
@@ -97,7 +101,13 @@ export function registerSubmitPlanTool(
97
101
  created_at: now,
98
102
  updated_at: now,
99
103
  }));
100
- const plan: PlanData = { title: params.title, planName, handoff: params.handoff, tasks };
104
+ const plan: PlanData = {
105
+ title: params.title,
106
+ planName,
107
+ handoff: params.handoff,
108
+ tasks,
109
+ base_commit: baseCommit,
110
+ };
101
111
 
102
112
  const unknownInitiative = await runPlanIO(
103
113
  Effect.gen(function* () {
@@ -12,11 +12,11 @@ import { StringEnum } from '@earendil-works/pi-ai';
12
12
  import { Text } from '@earendil-works/pi-tui';
13
13
  import { Type } from 'typebox';
14
14
  import type { InitiativeStatus } from '../types.js';
15
- import type { RunPlanIO } from '../effects/runtime.js';
15
+ import type { RunPlanIO } from '@dreki-gg/taskman';
16
16
  import {
17
17
  readInitiativesManifest,
18
18
  upsertInitiativeEntry,
19
- } from '../storage/initiatives-manifest.js';
19
+ } from '@dreki-gg/taskman';
20
20
 
21
21
  /** Normalize an initiative hint (`x` or `.plans/x`) to a bare name. */
22
22
  function normalizeName(hint: string): string {
@@ -16,9 +16,9 @@ import { StringEnum } from '@earendil-works/pi-ai';
16
16
  import { Text } from '@earendil-works/pi-tui';
17
17
  import { Type } from 'typebox';
18
18
  import type { PlanStatus } from '../types.js';
19
- import type { RunPlanIO } from '../effects/runtime.js';
20
- import { readPlansManifest, upsertPlanEntry } from '../storage/plans-manifest.js';
21
- import { reconcileInitiativeForPlan } from '../initiative.js';
19
+ import type { RunPlanIO } from '@dreki-gg/taskman';
20
+ import { readPlansManifest, upsertPlanEntry } from '@dreki-gg/taskman';
21
+ import { reconcileInitiativeForPlan } from '@dreki-gg/taskman';
22
22
 
23
23
  /** Normalize a plan hint (`my-plan` or `.plans/my-plan`) to a bare name. */
24
24
  function normalizeName(hint: string): string {
@@ -1,55 +1,24 @@
1
1
  /**
2
- * Shared types for plan mode.
2
+ * Plan-mode types.
3
+ *
4
+ * Engine record/value types now live in `@dreki-gg/taskman` and are re-exported
5
+ * here so the rest of the extension keeps importing from `./types.js`. The
6
+ * pi-session-only types (`PersistedState`) stay local.
3
7
  */
4
8
 
5
- export type TaskStatus = 'pending' | 'done' | 'skipped' | 'blocked' | 'deferred';
9
+ export type {
10
+ TaskStatus,
11
+ TaskOrigin,
12
+ PlanStatus,
13
+ InitiativeStatus,
14
+ TaskRecord,
15
+ TaskMeta,
16
+ PlanData,
17
+ ThinkingLevel,
18
+ ExecPendingConfig,
19
+ } from '@dreki-gg/taskman';
6
20
 
7
- /** Where a task came from: the original submitted plan, or discovered during execution. */
8
- export type TaskOrigin = 'plan' | 'discovered';
9
-
10
- /**
11
- * Plan lifecycle status. Only `in-progress` is active; `done`, `superseded`,
12
- * and `abandoned` are terminal and drop out of active-plan resolution.
13
- */
14
- export type PlanStatus = 'in-progress' | 'done' | 'superseded' | 'abandoned';
15
-
16
- /** Initiative lifecycle reuses the plan lifecycle literals. */
17
- export type InitiativeStatus = PlanStatus;
18
-
19
- export interface TaskRecord {
20
- _type: 'task';
21
- id: string;
22
- description: string;
23
- details?: string;
24
- status: TaskStatus;
25
- /** Defaults to 'plan' when absent (back-compat with older tasks.jsonl files). */
26
- origin?: TaskOrigin;
27
- depends_on?: string[];
28
- notes?: string;
29
- created_at: string;
30
- updated_at: string;
31
- }
32
-
33
- export interface TaskMeta {
34
- _type: 'meta';
35
- title: string;
36
- plan_name: string;
37
- created_at: string;
38
- }
39
-
40
- export interface PlanData {
41
- title: string;
42
- planName: string;
43
- handoff: string;
44
- tasks: TaskRecord[];
45
- }
46
-
47
- export type ThinkingLevel = 'off' | 'minimal' | 'low' | 'medium' | 'high' | 'xhigh';
48
-
49
- export interface ExecPendingConfig {
50
- model: { provider: string; id: string };
51
- thinking: string;
52
- }
21
+ import type { PlanData } from '@dreki-gg/taskman';
53
22
 
54
23
  export interface PersistedState {
55
24
  planEnabled: boolean;
@@ -58,6 +27,3 @@ export interface PersistedState {
58
27
  plan: PlanData | undefined;
59
28
  executionStartIdx: number | undefined;
60
29
  }
61
-
62
- // Record validation lives in `schema.ts` (Effect Schema). The interfaces above
63
- // remain the mutable shapes used by the imperative orchestration code.
@@ -33,32 +33,5 @@ export function isPlanPath(filePath: string): boolean {
33
33
  return /(?:^|\/)?\.plans\//.test(normalized);
34
34
  }
35
35
 
36
- // ── Plan name utilities ─────────────────────────────────────────────────────
37
-
38
- export function toKebabCase(name: string): string {
39
- return name
40
- .toLowerCase()
41
- .replace(/[^a-z0-9]+/g, '-')
42
- .replace(/^-+|-+$/g, '')
43
- .slice(0, 60);
44
- }
45
-
46
- /**
47
- * Generate the next sequential task id (`t-NNN`) given existing ids.
48
- *
49
- * Uses the max numeric suffix of `t-<digits>` ids + 1, zero-padded to 3.
50
- * Falls back to `t-<count+1>` when no ids match the pattern.
51
- */
52
- export function nextTaskId(existingIds: readonly string[]): string {
53
- let max = 0;
54
- let matched = false;
55
- for (const id of existingIds) {
56
- const m = /^t-(\d+)$/.exec(id);
57
- if (!m) continue;
58
- matched = true;
59
- const n = Number.parseInt(m[1], 10);
60
- if (n > max) max = n;
61
- }
62
- const next = matched ? max + 1 : existingIds.length + 1;
63
- return `t-${String(next).padStart(3, '0')}`;
64
- }
36
+ // Plan name / task id helpers (`toKebabCase`, `nextTaskId`) now live in
37
+ // `@dreki-gg/taskman`.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@dreki-gg/pi-plan-mode",
3
- "version": "0.25.0",
3
+ "version": "0.26.1",
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,6 +41,7 @@
41
41
  },
42
42
  "dependencies": {
43
43
  "@dreki-gg/pi-command-sandbox": "^0.3.0",
44
+ "@dreki-gg/taskman": "^0.2.0",
44
45
  "effect": "^3.21.2"
45
46
  },
46
47
  "devDependencies": {
@@ -1,45 +0,0 @@
1
- import { afterEach, beforeEach, describe, expect, test } from 'bun:test';
2
- import { Effect, Exit } from 'effect';
3
- import { mkdtemp, readFile, rm, writeFile } from 'node:fs/promises';
4
- import { join } from 'node:path';
5
- import { tmpdir } from 'node:os';
6
- import { writeFileAtomic } from '../storage/atomic-write.js';
7
-
8
- let dir: string;
9
-
10
- beforeEach(async () => {
11
- dir = await mkdtemp(join(tmpdir(), 'plan-mode-atomic-'));
12
- });
13
-
14
- afterEach(async () => {
15
- await rm(dir, { recursive: true, force: true });
16
- });
17
-
18
- describe('writeFileAtomic', () => {
19
- test('writes complete content to the target path', async () => {
20
- const target = join(dir, 'data.txt');
21
-
22
- await Effect.runPromise(writeFileAtomic(target, 'hello world'));
23
-
24
- expect(await readFile(target, 'utf8')).toBe('hello world');
25
- });
26
-
27
- test('replaces existing content atomically from the caller perspective', async () => {
28
- const target = join(dir, 'data.txt');
29
- await writeFile(target, 'old');
30
-
31
- await Effect.runPromise(writeFileAtomic(target, 'new'));
32
-
33
- expect(await readFile(target, 'utf8')).toBe('new');
34
- });
35
-
36
- test('fails with PlanWriteError and leaves target untouched when the write fails', async () => {
37
- const target = join(dir, 'data.txt');
38
- await writeFile(target, 'original');
39
-
40
- const exit = await Effect.runPromiseExit(writeFileAtomic(target, 'next', { mode: 0o400 }));
41
-
42
- expect(Exit.isFailure(exit)).toBe(true);
43
- expect(await readFile(target, 'utf8')).toBe('original');
44
- });
45
- });