@workflow-cannon/workspace-kit 0.15.0 → 0.16.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -1,136 +1,85 @@
1
1
  AI agents: `./.ai/` is the authoritative source of truth; read all files there before any analysis, planning, or code changes, and follow `./.ai/` over `README.md`, `./docs/`, and conflicting existing code patterns.
2
2
 
3
- # Workflow Cannon
4
-
5
- Workflow Cannon is the canonical home of `@workflow-cannon/workspace-kit` and the operational docs that drive its evolution.
6
-
7
- It is built for developers using VS Code who want safe, reproducible, package-first workflow automation with clear release evidence.
8
-
9
- ## Table of Contents
10
-
11
- - [What This Repository Is](#what-this-repository-is)
12
- - [Big-Picture Vision](#big-picture-vision)
13
- - [Current Status](#current-status)
14
- - [Goals](#goals)
15
- - [Package](#package)
16
- - [Repository Map](#repository-map)
17
- - [Documentation Index](#documentation-index)
18
- - [License](#license)
19
-
20
- ## What This Repository Is
3
+ <div align="center">
4
+ <img src="title_image.png" alt="Workflow Cannon" width="720" />
5
+ </div>
21
6
 
22
- Workflow Cannon is the source of truth for:
23
-
24
- - The `@workflow-cannon/workspace-kit` package
25
- - Maintainer planning and execution artifacts
26
- - Consumer validation and release-readiness evidence
27
-
28
- Guiding characteristics:
7
+ # Workflow Cannon
29
8
 
30
- - Package-first delivery and verification
31
- - Deterministic, auditable workflows
32
- - Safe-by-default operations (validation, traceability, rollback-friendly changes)
9
+ **[`@workflow-cannon/workspace-kit`](https://www.npmjs.com/package/@workflow-cannon/workspace-kit)** CLI, task engine, and workflow contracts for repos that want deterministic, policy-governed automation with clear evidence.
33
10
 
34
- ## Big-Picture Vision
11
+ ## Quick start (clone this repo)
35
12
 
36
- Workflow Cannon is evolving from a package and docs repository into a developer workflow platform that can:
13
+ **Needs:** Node.js **22+** (see CI), **pnpm 10** (see `packageManager` in `package.json`).
37
14
 
38
- - model planning, tasks, policy, and execution as first-class, versioned contracts
39
- - run repeatable workflows with deterministic outcomes and evidence capture
40
- - continuously improve itself based on observed friction and outcome data
15
+ ```bash
16
+ git clone https://github.com/NJLaPrell/workflow-cannon.git
17
+ cd workflow-cannon
18
+ pnpm install
19
+ pnpm run build
20
+ ```
41
21
 
42
- The long-term direction in this repository is to close the loop between:
22
+ Verify the kit sees your workspace:
43
23
 
44
- 1. **What happened** (transcripts, diffs, run artifacts, diagnostics)
45
- 2. **What should change** (recommendations to templates, rules, process, and config)
46
- 3. **What gets adopted** (human-reviewed approvals, policy checks, safe rollout)
24
+ ```bash
25
+ node dist/cli.js doctor
26
+ node dist/cli.js --help
27
+ ```
47
28
 
48
- ### Enhancement Engine (automatic learning and correction)
29
+ Try **read-only** task-engine queries:
49
30
 
50
- The Improvement/Enhancement Engine is intended to detect weak spots in workflows and rules, then generate high-signal fixes with supporting evidence. Instead of hard-coding static process forever, the system should learn from real usage patterns and propose better defaults.
31
+ ```bash
32
+ node dist/cli.js run list-tasks '{}'
33
+ node dist/cli.js run get-next-actions '{}'
34
+ ```
51
35
 
52
- In practice, this means:
36
+ **Developing:** after edits, `pnpm run build` then `pnpm test` (or `pnpm run phase5-gates` before larger changes). If `workspace-kit` is not on your `PATH`, use `node dist/cli.js …` from the repo root (same as above).
53
37
 
54
- - detect recurring failure patterns, manual rework, and template drift
55
- - emit recommendation items with confidence, deduping, and provenance
56
- - route recommendations through an approval queue (`accept`, `decline`, `accept edited`)
57
- - apply approved changes through guarded automation (dry-run, diff, rollback-ready)
58
- - measure post-change outcomes so future recommendations improve over time
38
+ ## Quick start (use the package in another project)
59
39
 
60
- This keeps automation adaptive without sacrificing safety, governance, or developer trust.
40
+ ```bash
41
+ npm install @workflow-cannon/workspace-kit
42
+ npx workspace-kit --help
43
+ ```
61
44
 
62
- ## Current Status
45
+ Or with pnpm: `pnpm add @workflow-cannon/workspace-kit` then `pnpm exec workspace-kit --help`.
63
46
 
64
- **Release and phase truth:** see `docs/maintainers/ROADMAP.md` and `docs/maintainers/data/workspace-kit-status.yaml`. **Task queue:** `.workspace-kit/tasks/state.json` (ids and `status` are authoritative for execution).
47
+ ## What this repo contains
65
48
 
66
- - **Phases 0–7** are complete through **`v0.9.0`** (see roadmap for slice ids).
67
- - **Phase 8** ships maintainer/onboarding hardening (`v0.10.0`): policy denial clarity, runbooks, and doc alignment for CLI vs `run` approval.
68
- - **Phase 9–10** ship agent/onboarding parity (`v0.11.0`): interactive policy opt-in, strict response-template mode, Agent CLI map (`docs/maintainers/AGENT-CLI-MAP.md`), and CLI-first Cursor guidance.
69
- - **Phase 11** ships architectural review follow-up hardening (`v0.12.0`): policy/session denial edge tests, persistence concurrency semantics, release doc-sweep checklist, and runtime path audit note.
70
- - **Phase 12** ships Cursor-native thin-client extension delivery (`v0.13.0`): dashboard/tasks/config UI flows, extension test suite, and operator/security docs.
71
- - **Phase 13** is the active queue: Task Engine lifecycle tightening (`T311+`).
49
+ | Area | What |
50
+ | --- | --- |
51
+ | **CLI** | `workspace-kit` `doctor`, `config`, `run <module-command>` (see `workspace-kit run` with no args for the list). |
52
+ | **Task engine** | Canonical queue in `.workspace-kit/tasks/state.json`; lifecycle via `run-transition`. Wishlist ideation uses ids `W###` (see [`docs/maintainers/runbooks/wishlist-workflow.md`](docs/maintainers/runbooks/wishlist-workflow.md)). |
53
+ | **Docs** | Maintainer process, roadmap, and changelog under `docs/maintainers/`. |
54
+ | **Cursor extension** (optional) | Thin UI in `extensions/cursor-workflow-cannon/` build with `pnpm run ui:prepare`. |
72
55
 
73
- Historical note: this file’s milestone list is not the live queue—always check task state for **`ready`** work.
56
+ There is **no** built-in IDE slash command like `/qt` from this package; editor integrations are **your** config (e.g. `.cursor/commands/`), while **`workspace-kit`** is the supported CLI.
74
57
 
75
- ## Goals
58
+ ## Policy and approvals (read this before mutating state)
76
59
 
77
- - Keep package implementation and release operations centralized here.
78
- - Preserve independent consumer validation and update cadence.
79
- - Grow modular capabilities for planning, tasking, configuration, policy, and improvement.
80
- - Build a human-governed enhancement loop that learns from usage and recommends better workflows/rules.
81
- - Maintain deterministic and auditable behavior as system complexity increases.
60
+ Sensitive `workspace-kit run` commands require JSON **`policyApproval`** in the third CLI argument. Chat approval is not enough. Env-based approval applies to `init` / `upgrade` / `config`, not the `run` path.
82
61
 
83
- ## Package
62
+ - **Human guide:** [`docs/maintainers/POLICY-APPROVAL.md`](docs/maintainers/POLICY-APPROVAL.md)
63
+ - **Copy-paste table:** [`docs/maintainers/AGENT-CLI-MAP.md`](docs/maintainers/AGENT-CLI-MAP.md)
84
64
 
85
- Install:
65
+ ## Project status and roadmap
86
66
 
87
- ```bash
88
- npm install @workflow-cannon/workspace-kit
89
- ```
67
+ Release cadence, phase history, and strategic decisions: [`docs/maintainers/ROADMAP.md`](docs/maintainers/ROADMAP.md). **Live execution queue:** `.workspace-kit/tasks/state.json` (`status` and `id` are authoritative — not this README’s milestone bullets).
90
68
 
91
- ### How to run the CLI (this repo and consumers)
69
+ Snapshot: [`docs/maintainers/data/workspace-kit-status.yaml`](docs/maintainers/data/workspace-kit-status.yaml).
92
70
 
93
- There is **no** IDE slash command like `/qt` defined by this package unless your own editor config adds one. Supported entrypoints:
71
+ ## Where to go next
94
72
 
95
- | Context | Command |
73
+ | Goal | Start here |
96
74
  | --- | --- |
97
- | **Installed package** | `npx @workflow-cannon/workspace-kit --help` or `pnpm exec workspace-kit --help` when the package is a dependency |
98
- | **Developing this repo** | `pnpm run build` then `node dist/cli.js --help` or `pnpm exec workspace-kit --help` if linked |
99
- | **Transcript helpers** | `pnpm run transcript:sync` / `pnpm run transcript:ingest` (see maintainer runbooks) |
100
-
101
- Mutating commands require policy approval: **`docs/maintainers/POLICY-APPROVAL.md`** (JSON **`policyApproval`** for `workspace-kit run`, env for `config`/`init`/`upgrade`).
102
-
103
- ## Repository Map
104
-
105
- - `README.md` - project entry point
106
- - `.ai/PRINCIPLES.md` - project goals and decision principles (canonical AI)
107
- - `docs/maintainers/ROADMAP.md` - roadmap and decision log
108
- - `.workspace-kit/tasks/state.json` - execution tracking
109
- - `docs/maintainers/ARCHITECTURE.md` - architecture direction
110
- - `docs/maintainers/DECISIONS.md` - focused design/decision notes
111
- - `docs/maintainers/RELEASING.md` - release checklist and validation expectations
112
- - `.ai/module-build.md` - canonical AI module build guidance
113
- - `docs/maintainers/` - maintainer process and boundary docs
114
- - `docs/maintainers/module-build-guide.md` - human-readable module build guidance
115
- - `docs/adr/` - ADR templates and records
116
-
117
- ## Documentation Index
118
-
119
- - Project goals and decision principles: `.ai/PRINCIPLES.md`
120
- - Strategy and long-range direction: `docs/maintainers/ROADMAP.md`
121
- - Active execution tasks: `.workspace-kit/tasks/state.json`
122
- - Glossary and agent-guidance terms: `docs/maintainers/TERMS.md`
123
- - Architecture direction: `docs/maintainers/ARCHITECTURE.md`
124
- - Project decisions: `docs/maintainers/DECISIONS.md`
125
- - Governance policy surface: `docs/maintainers/GOVERNANCE.md`
126
- - Release process and gates: `docs/maintainers/RELEASING.md`
127
- - Policy / approval surfaces: `docs/maintainers/POLICY-APPROVAL.md`
128
- - Canonical changelog: `docs/maintainers/CHANGELOG.md` (`CHANGELOG.md` at repo root is pointer-only)
129
- - Canonical AI module build guidance: `.ai/module-build.md`
130
- - Human module build guide: `docs/maintainers/module-build-guide.md`
131
- - Security, support, and governance: `docs/maintainers/SECURITY.md`, `docs/maintainers/SUPPORT.md`, `docs/maintainers/GOVERNANCE.md`
132
- - AI behavior rules and command wrappers: `.cursor/rules/`, `.cursor/commands/`
75
+ | Goals, trade-offs, gates | [`.ai/PRINCIPLES.md`](.ai/PRINCIPLES.md) |
76
+ | Roadmap & versions | [`docs/maintainers/ROADMAP.md`](docs/maintainers/ROADMAP.md) |
77
+ | Changelog | [`docs/maintainers/CHANGELOG.md`](docs/maintainers/CHANGELOG.md) |
78
+ | Release process | [`docs/maintainers/RELEASING.md`](docs/maintainers/RELEASING.md) |
79
+ | Glossary | [`docs/maintainers/TERMS.md`](docs/maintainers/TERMS.md) |
80
+ | Architecture | [`docs/maintainers/ARCHITECTURE.md`](docs/maintainers/ARCHITECTURE.md) |
81
+ | Agent/CLI execution | [`docs/maintainers/AGENTS.md`](docs/maintainers/AGENTS.md) |
133
82
 
134
83
  ## License
135
84
 
136
- Licensed under MIT. See `LICENSE`.
85
+ MIT. See [`LICENSE`](LICENSE).
@@ -1,15 +1,7 @@
1
1
  import { appendLineageEvent } from "../../core/lineage-store.js";
2
- import { TaskStore } from "../task-engine/store.js";
2
+ import { openPlanningStores } from "../task-engine/planning-open.js";
3
3
  import { TransitionService } from "../task-engine/service.js";
4
4
  import { appendDecisionRecord, computeDecisionFingerprint, readDecisionFingerprints } from "./decisions-store.js";
5
- function taskStoreRelativePath(ctx) {
6
- const tasks = ctx.effectiveConfig?.tasks;
7
- if (!tasks || typeof tasks !== "object" || Array.isArray(tasks)) {
8
- return undefined;
9
- }
10
- const p = tasks.storeRelativePath;
11
- return typeof p === "string" && p.trim().length > 0 ? p.trim() : undefined;
12
- }
13
5
  function getEvidenceKey(task) {
14
6
  const m = task.metadata;
15
7
  const k = m && typeof m.evidenceKey === "string" ? m.evidenceKey : "";
@@ -24,8 +16,8 @@ export async function runReviewItem(ctx, args, actor) {
24
16
  if (decision === "accept_edited" && !(typeof args.editedSummary === "string" && args.editedSummary.trim())) {
25
17
  return { ok: false, code: "invalid-args", message: "accept_edited requires non-empty editedSummary" };
26
18
  }
27
- const store = new TaskStore(ctx.workspacePath, taskStoreRelativePath(ctx));
28
- await store.load();
19
+ const planning = await openPlanningStores(ctx);
20
+ const store = planning.taskStore;
29
21
  const task = store.getTask(taskId);
30
22
  if (!task) {
31
23
  return { ok: false, code: "task-not-found", message: `Task '${taskId}' not found` };
@@ -1,17 +1,9 @@
1
1
  import { randomUUID } from "node:crypto";
2
- import { TaskStore } from "../task-engine/store.js";
2
+ import { openPlanningStores } from "../task-engine/planning-open.js";
3
3
  import { appendLineageEvent } from "../../core/lineage-store.js";
4
4
  import { loadImprovementState, saveImprovementState } from "./improvement-state.js";
5
5
  import { ingestAgentTranscripts, ingestConfigMutations, ingestGitDiffBetweenTags, ingestPolicyDenials, ingestTaskTransitionFriction, taskIdForEvidenceKey } from "./ingest.js";
6
6
  import { priorityForTier } from "./confidence.js";
7
- function taskStoreRelativePath(ctx) {
8
- const tasks = ctx.effectiveConfig?.tasks;
9
- if (!tasks || typeof tasks !== "object" || Array.isArray(tasks)) {
10
- return undefined;
11
- }
12
- const p = tasks.storeRelativePath;
13
- return typeof p === "string" && p.trim().length > 0 ? p.trim() : undefined;
14
- }
15
7
  function hasEvidenceKey(tasks, key) {
16
8
  return tasks.some((t) => {
17
9
  const m = t.metadata;
@@ -48,8 +40,8 @@ export function getMaxRecommendationCandidatesPerRun(ctx) {
48
40
  }
49
41
  export async function runGenerateRecommendations(ctx, args) {
50
42
  const runId = randomUUID();
51
- const store = new TaskStore(ctx.workspacePath, taskStoreRelativePath(ctx));
52
- await store.load();
43
+ const planning = await openPlanningStores(ctx);
44
+ const store = planning.taskStore;
53
45
  const state = await loadImprovementState(ctx.workspacePath);
54
46
  const transcriptsRoot = resolveTranscriptArchivePath(ctx, args);
55
47
  const fromTag = typeof args.fromTag === "string" ? args.fromTag.trim() : undefined;
@@ -8,4 +8,6 @@ export { readWorkspaceStatusSnapshot } from "./dashboard-status.js";
8
8
  export { WishlistStore } from "./wishlist-store.js";
9
9
  export type { WishlistItem, WishlistStatus, WishlistStoreDocument } from "./wishlist-types.js";
10
10
  export { validateWishlistIntakePayload, validateWishlistUpdatePayload, buildWishlistItemFromIntake, WISHLIST_ID_RE } from "./wishlist-validation.js";
11
+ export { openPlanningStores } from "./planning-open.js";
12
+ export { getTaskPersistenceBackend, planningSqliteDatabaseRelativePath, planningTaskStoreRelativePath, planningWishlistStoreRelativePath } from "./planning-config.js";
11
13
  export declare const taskEngineModule: WorkflowModule;
@@ -1,11 +1,11 @@
1
1
  import crypto from "node:crypto";
2
2
  import { maybeSpawnTranscriptHookAfterCompletion } from "../../core/transcript-completion-hook.js";
3
- import { TaskStore } from "./store.js";
4
3
  import { TransitionService } from "./service.js";
5
4
  import { TaskEngineError, getAllowedTransitionsFrom } from "./transitions.js";
6
5
  import { getNextActions } from "./suggestions.js";
7
6
  import { readWorkspaceStatusSnapshot } from "./dashboard-status.js";
8
- import { WishlistStore } from "./wishlist-store.js";
7
+ import { openPlanningStores } from "./planning-open.js";
8
+ import { runMigrateTaskPersistence } from "./migrate-task-persistence-runtime.js";
9
9
  import { buildWishlistItemFromIntake, validateWishlistIntakePayload, validateWishlistUpdatePayload, WISHLIST_ID_RE } from "./wishlist-validation.js";
10
10
  export { TaskStore } from "./store.js";
11
11
  export { TransitionService } from "./service.js";
@@ -14,22 +14,8 @@ export { getNextActions } from "./suggestions.js";
14
14
  export { readWorkspaceStatusSnapshot } from "./dashboard-status.js";
15
15
  export { WishlistStore } from "./wishlist-store.js";
16
16
  export { validateWishlistIntakePayload, validateWishlistUpdatePayload, buildWishlistItemFromIntake, WISHLIST_ID_RE } from "./wishlist-validation.js";
17
- function taskStorePath(ctx) {
18
- const tasks = ctx.effectiveConfig?.tasks;
19
- if (!tasks || typeof tasks !== "object" || Array.isArray(tasks)) {
20
- return undefined;
21
- }
22
- const p = tasks.storeRelativePath;
23
- return typeof p === "string" && p.trim().length > 0 ? p.trim() : undefined;
24
- }
25
- function wishlistStorePath(ctx) {
26
- const tasks = ctx.effectiveConfig?.tasks;
27
- if (!tasks || typeof tasks !== "object" || Array.isArray(tasks)) {
28
- return undefined;
29
- }
30
- const p = tasks.wishlistStoreRelativePath;
31
- return typeof p === "string" && p.trim().length > 0 ? p.trim() : undefined;
32
- }
17
+ export { openPlanningStores } from "./planning-open.js";
18
+ export { getTaskPersistenceBackend, planningSqliteDatabaseRelativePath, planningTaskStoreRelativePath, planningWishlistStoreRelativePath } from "./planning-config.js";
33
19
  const TASK_ID_RE = /^T\d+$/;
34
20
  const MUTABLE_TASK_FIELDS = new Set([
35
21
  "title",
@@ -126,7 +112,7 @@ function mutationEvidence(mutationType, taskId, actor, details) {
126
112
  export const taskEngineModule = {
127
113
  registration: {
128
114
  id: "task-engine",
129
- version: "0.5.0",
115
+ version: "0.6.0",
130
116
  contractVersion: "1",
131
117
  capabilities: ["task-engine"],
132
118
  dependsOn: [],
@@ -249,6 +235,11 @@ export const taskEngineModule = {
249
235
  file: "get-next-actions.md",
250
236
  description: "Get prioritized next-action suggestions with blocking analysis."
251
237
  },
238
+ {
239
+ name: "migrate-task-persistence",
240
+ file: "migrate-task-persistence.md",
241
+ description: "Copy task + wishlist state between JSON files and a single SQLite database (offline migration)."
242
+ },
252
243
  {
253
244
  name: "dashboard-summary",
254
245
  file: "dashboard-summary.md",
@@ -259,9 +250,12 @@ export const taskEngineModule = {
259
250
  },
260
251
  async onCommand(command, ctx) {
261
252
  const args = command.args ?? {};
262
- const store = new TaskStore(ctx.workspacePath, taskStorePath(ctx));
253
+ if (command.name === "migrate-task-persistence") {
254
+ return runMigrateTaskPersistence(ctx, args);
255
+ }
256
+ let planning;
263
257
  try {
264
- await store.load();
258
+ planning = await openPlanningStores(ctx);
265
259
  }
266
260
  catch (err) {
267
261
  if (err instanceof TaskEngineError) {
@@ -270,9 +264,10 @@ export const taskEngineModule = {
270
264
  return {
271
265
  ok: false,
272
266
  code: "storage-read-error",
273
- message: `Failed to load task store: ${err.message}`
267
+ message: `Failed to open task planning stores: ${err.message}`
274
268
  };
275
269
  }
270
+ const store = planning.taskStore;
276
271
  if (command.name === "run-transition") {
277
272
  const taskId = typeof args.taskId === "string" ? args.taskId : undefined;
278
273
  const action = typeof args.action === "string" ? args.action : undefined;
@@ -566,14 +561,14 @@ export const taskEngineModule = {
566
561
  phase: t.phase ?? null
567
562
  }));
568
563
  const blockedTop = suggestion.blockingAnalysis.slice(0, 15);
569
- const wishlistStore = new WishlistStore(ctx.workspacePath, wishlistStorePath(ctx));
564
+ let wishlistItems = [];
570
565
  try {
571
- await wishlistStore.load();
566
+ const wishlistStore = await planning.openWishlist();
567
+ wishlistItems = wishlistStore.getAllItems();
572
568
  }
573
569
  catch {
574
570
  /* wishlist store optional */
575
571
  }
576
- const wishlistItems = wishlistStore.getAllItems();
577
572
  const wishlistOpenCount = wishlistItems.filter((i) => i.status === "open").length;
578
573
  const data = {
579
574
  schemaVersion: 1,
@@ -690,8 +685,7 @@ export const taskEngineModule = {
690
685
  };
691
686
  }
692
687
  if (command.name === "create-wishlist") {
693
- const wishlistStore = new WishlistStore(ctx.workspacePath, wishlistStorePath(ctx));
694
- await wishlistStore.load();
688
+ const wishlistStore = await planning.openWishlist();
695
689
  const raw = args;
696
690
  const v = validateWishlistIntakePayload(raw);
697
691
  if (!v.ok) {
@@ -717,8 +711,7 @@ export const taskEngineModule = {
717
711
  };
718
712
  }
719
713
  if (command.name === "list-wishlist") {
720
- const wishlistStore = new WishlistStore(ctx.workspacePath, wishlistStorePath(ctx));
721
- await wishlistStore.load();
714
+ const wishlistStore = await planning.openWishlist();
722
715
  const statusFilter = typeof args.status === "string" ? args.status : undefined;
723
716
  let items = wishlistStore.getAllItems();
724
717
  if (statusFilter && ["open", "converted", "cancelled"].includes(statusFilter)) {
@@ -740,8 +733,7 @@ export const taskEngineModule = {
740
733
  if (!wishlistId) {
741
734
  return { ok: false, code: "invalid-task-schema", message: "get-wishlist requires 'wishlistId' or 'id'" };
742
735
  }
743
- const wishlistStore = new WishlistStore(ctx.workspacePath, wishlistStorePath(ctx));
744
- await wishlistStore.load();
736
+ const wishlistStore = await planning.openWishlist();
745
737
  const item = wishlistStore.getItem(wishlistId);
746
738
  if (!item) {
747
739
  return { ok: false, code: "task-not-found", message: `Wishlist item '${wishlistId}' not found` };
@@ -758,8 +750,7 @@ export const taskEngineModule = {
758
750
  if (!wishlistId || !updates) {
759
751
  return { ok: false, code: "invalid-task-schema", message: "update-wishlist requires wishlistId and updates" };
760
752
  }
761
- const wishlistStore = new WishlistStore(ctx.workspacePath, wishlistStorePath(ctx));
762
- await wishlistStore.load();
753
+ const wishlistStore = await planning.openWishlist();
763
754
  const existing = wishlistStore.getItem(wishlistId);
764
755
  if (!existing) {
765
756
  return { ok: false, code: "task-not-found", message: `Wishlist item '${wishlistId}' not found` };
@@ -817,8 +808,7 @@ export const taskEngineModule = {
817
808
  message: "convert-wishlist requires non-empty tasks array"
818
809
  };
819
810
  }
820
- const wishlistStore = new WishlistStore(ctx.workspacePath, wishlistStorePath(ctx));
821
- await wishlistStore.load();
811
+ const wishlistStore = await planning.openWishlist();
822
812
  const wlItem = wishlistStore.getItem(wishlistId);
823
813
  if (!wlItem) {
824
814
  return { ok: false, code: "task-not-found", message: `Wishlist item '${wishlistId}' not found` };
@@ -854,14 +844,6 @@ export const taskEngineModule = {
854
844
  }
855
845
  built.push(bt.task);
856
846
  }
857
- for (const t of built) {
858
- store.addTask(t);
859
- store.addMutationEvidence(mutationEvidence("create-task", t.id, actor, {
860
- initialStatus: t.status,
861
- source: "convert-wishlist",
862
- wishlistId
863
- }));
864
- }
865
847
  const convertedIds = built.map((t) => t.id);
866
848
  const updatedWishlist = {
867
849
  ...wlItem,
@@ -871,9 +853,25 @@ export const taskEngineModule = {
871
853
  convertedToTaskIds: convertedIds,
872
854
  conversionDecomposition: dec.value
873
855
  };
874
- wishlistStore.updateItem(updatedWishlist);
875
- await store.save();
876
- await wishlistStore.save();
856
+ const applyConvertMutations = () => {
857
+ for (const t of built) {
858
+ store.addTask(t);
859
+ store.addMutationEvidence(mutationEvidence("create-task", t.id, actor, {
860
+ initialStatus: t.status,
861
+ source: "convert-wishlist",
862
+ wishlistId
863
+ }));
864
+ }
865
+ wishlistStore.updateItem(updatedWishlist);
866
+ };
867
+ if (planning.kind === "sqlite") {
868
+ planning.sqliteDual.withTransaction(applyConvertMutations);
869
+ }
870
+ else {
871
+ applyConvertMutations();
872
+ await store.save();
873
+ await wishlistStore.save();
874
+ }
877
875
  return {
878
876
  ok: true,
879
877
  code: "wishlist-converted",
@@ -0,0 +1,2 @@
1
+ import type { ModuleCommandResult, ModuleLifecycleContext } from "../../contracts/module-contract.js";
2
+ export declare function runMigrateTaskPersistence(ctx: ModuleLifecycleContext, args: Record<string, unknown>): Promise<ModuleCommandResult>;
@@ -0,0 +1,192 @@
1
+ import fs from "node:fs/promises";
2
+ import fsSync from "node:fs";
3
+ import path from "node:path";
4
+ import crypto from "node:crypto";
5
+ import { TaskEngineError } from "./transitions.js";
6
+ import { SqliteDualPlanningStore } from "./sqlite-dual-planning.js";
7
+ import { planningSqliteDatabaseRelativePath, planningTaskStoreRelativePath, planningWishlistStoreRelativePath } from "./planning-config.js";
8
+ import { DEFAULT_TASK_STORE_PATH } from "./store.js";
9
+ import { DEFAULT_WISHLIST_PATH } from "./wishlist-store.js";
10
+ function emptyTaskDoc() {
11
+ return {
12
+ schemaVersion: 1,
13
+ tasks: [],
14
+ transitionLog: [],
15
+ mutationLog: [],
16
+ lastUpdated: new Date().toISOString()
17
+ };
18
+ }
19
+ function emptyWishDoc() {
20
+ return {
21
+ schemaVersion: 1,
22
+ items: [],
23
+ lastUpdated: new Date().toISOString()
24
+ };
25
+ }
26
+ async function atomicWriteJson(targetPath, body) {
27
+ const dir = path.dirname(targetPath);
28
+ const tmpPath = `${targetPath}.${crypto.randomUUID().slice(0, 8)}.tmp`;
29
+ await fs.mkdir(dir, { recursive: true });
30
+ await fs.writeFile(tmpPath, body, "utf8");
31
+ await fs.rename(tmpPath, targetPath);
32
+ }
33
+ export async function runMigrateTaskPersistence(ctx, args) {
34
+ const direction = typeof args.direction === "string" ? args.direction.trim() : "";
35
+ if (direction !== "json-to-sqlite" && direction !== "sqlite-to-json") {
36
+ return {
37
+ ok: false,
38
+ code: "invalid-task-schema",
39
+ message: "migrate-task-persistence requires direction: 'json-to-sqlite' | 'sqlite-to-json'"
40
+ };
41
+ }
42
+ const dryRun = args.dryRun === true;
43
+ const force = args.force === true;
44
+ const taskRel = planningTaskStoreRelativePath(ctx) ?? DEFAULT_TASK_STORE_PATH;
45
+ const wishRel = planningWishlistStoreRelativePath(ctx) ?? DEFAULT_WISHLIST_PATH;
46
+ const taskPath = path.resolve(ctx.workspacePath, taskRel);
47
+ const wishPath = path.resolve(ctx.workspacePath, wishRel);
48
+ const dbRel = planningSqliteDatabaseRelativePath(ctx);
49
+ const dual = new SqliteDualPlanningStore(ctx.workspacePath, dbRel);
50
+ if (direction === "json-to-sqlite") {
51
+ if (fsSync.existsSync(dual.dbPath) && !force) {
52
+ return {
53
+ ok: false,
54
+ code: "storage-write-error",
55
+ message: `SQLite database already exists at ${dual.dbPath} (pass force:true to overwrite)`
56
+ };
57
+ }
58
+ let taskDoc = emptyTaskDoc();
59
+ try {
60
+ const raw = await fs.readFile(taskPath, "utf8");
61
+ const parsed = JSON.parse(raw);
62
+ if (parsed.schemaVersion !== 1) {
63
+ throw new TaskEngineError("import-parse-error", `Unsupported task schema ${parsed.schemaVersion}`);
64
+ }
65
+ if (!Array.isArray(parsed.mutationLog)) {
66
+ parsed.mutationLog = [];
67
+ }
68
+ taskDoc = parsed;
69
+ }
70
+ catch (err) {
71
+ if (err.code === "ENOENT") {
72
+ taskDoc = emptyTaskDoc();
73
+ }
74
+ else if (err instanceof TaskEngineError) {
75
+ return { ok: false, code: err.code, message: err.message };
76
+ }
77
+ else {
78
+ return {
79
+ ok: false,
80
+ code: "import-parse-error",
81
+ message: `Failed to read task JSON: ${err.message}`
82
+ };
83
+ }
84
+ }
85
+ let wishDoc = emptyWishDoc();
86
+ try {
87
+ const raw = await fs.readFile(wishPath, "utf8");
88
+ const parsed = JSON.parse(raw);
89
+ if (parsed.schemaVersion !== 1) {
90
+ throw new TaskEngineError("import-parse-error", `Unsupported wishlist schema ${parsed.schemaVersion}`);
91
+ }
92
+ if (!Array.isArray(parsed.items)) {
93
+ throw new TaskEngineError("import-parse-error", "Wishlist items must be an array");
94
+ }
95
+ wishDoc = parsed;
96
+ }
97
+ catch (err) {
98
+ if (err.code === "ENOENT") {
99
+ wishDoc = emptyWishDoc();
100
+ }
101
+ else if (err instanceof TaskEngineError) {
102
+ return { ok: false, code: err.code, message: err.message };
103
+ }
104
+ else {
105
+ return {
106
+ ok: false,
107
+ code: "import-parse-error",
108
+ message: `Failed to read wishlist JSON: ${err.message}`
109
+ };
110
+ }
111
+ }
112
+ if (dryRun) {
113
+ return {
114
+ ok: true,
115
+ code: "migrate-dry-run",
116
+ message: "Dry run: would import JSON task/wishlist documents into SQLite",
117
+ data: {
118
+ dbPath: dual.dbPath,
119
+ taskPath,
120
+ wishPath,
121
+ taskCount: taskDoc.tasks.length,
122
+ wishlistCount: wishDoc.items.length
123
+ }
124
+ };
125
+ }
126
+ dual.seedFromDocuments(taskDoc, wishDoc);
127
+ try {
128
+ dual.persistSync();
129
+ }
130
+ catch (err) {
131
+ return {
132
+ ok: false,
133
+ code: "storage-write-error",
134
+ message: `Failed to write SQLite database: ${err.message}`
135
+ };
136
+ }
137
+ return {
138
+ ok: true,
139
+ code: "migrated-json-to-sqlite",
140
+ message: `Imported task and wishlist JSON into ${dual.dbPath}`,
141
+ data: { dbPath: dual.dbPath, taskPath, wishPath }
142
+ };
143
+ }
144
+ // sqlite-to-json
145
+ if (!fsSync.existsSync(dual.dbPath)) {
146
+ return {
147
+ ok: false,
148
+ code: "storage-read-error",
149
+ message: `SQLite database not found at ${dual.dbPath}`
150
+ };
151
+ }
152
+ dual.loadFromDisk();
153
+ if (!force && (fsSync.existsSync(taskPath) || fsSync.existsSync(wishPath))) {
154
+ return {
155
+ ok: false,
156
+ code: "storage-write-error",
157
+ message: `Target JSON path already exists (task or wishlist); pass force:true to overwrite`,
158
+ data: { taskPath, wishPath }
159
+ };
160
+ }
161
+ if (dryRun) {
162
+ return {
163
+ ok: true,
164
+ code: "migrate-dry-run",
165
+ message: "Dry run: would export SQLite documents to JSON files",
166
+ data: {
167
+ dbPath: dual.dbPath,
168
+ taskPath,
169
+ wishPath,
170
+ taskCount: dual.taskDocument.tasks.length,
171
+ wishlistCount: dual.wishlistDocument.items.length
172
+ }
173
+ };
174
+ }
175
+ try {
176
+ await atomicWriteJson(taskPath, JSON.stringify(dual.taskDocument, null, 2) + "\n");
177
+ await atomicWriteJson(wishPath, JSON.stringify(dual.wishlistDocument, null, 2) + "\n");
178
+ }
179
+ catch (err) {
180
+ return {
181
+ ok: false,
182
+ code: "storage-write-error",
183
+ message: `Failed to write JSON export: ${err.message}`
184
+ };
185
+ }
186
+ return {
187
+ ok: true,
188
+ code: "migrated-sqlite-to-json",
189
+ message: `Exported SQLite planning state to ${taskPath} and ${wishPath}`,
190
+ data: { dbPath: dual.dbPath, taskPath, wishPath }
191
+ };
192
+ }