@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 +55 -106
- package/dist/modules/approvals/review-runtime.js +3 -11
- package/dist/modules/improvement/generate-recommendations-runtime.js +3 -11
- package/dist/modules/task-engine/index.d.ts +2 -0
- package/dist/modules/task-engine/index.js +44 -46
- package/dist/modules/task-engine/migrate-task-persistence-runtime.d.ts +2 -0
- package/dist/modules/task-engine/migrate-task-persistence-runtime.js +192 -0
- package/dist/modules/task-engine/planning-config.d.ts +10 -0
- package/dist/modules/task-engine/planning-config.js +37 -0
- package/dist/modules/task-engine/planning-open.d.ts +16 -0
- package/dist/modules/task-engine/planning-open.js +34 -0
- package/dist/modules/task-engine/sqlite-dual-planning.d.ts +21 -0
- package/dist/modules/task-engine/sqlite-dual-planning.js +137 -0
- package/dist/modules/task-engine/store.d.ts +12 -3
- package/dist/modules/task-engine/store.js +62 -38
- package/dist/modules/task-engine/wishlist-store.d.ts +12 -3
- package/dist/modules/task-engine/wishlist-store.js +62 -40
- package/package.json +11 -2
- package/src/modules/documentation/README.md +1 -0
- package/src/modules/documentation/instructions/document-project.md +1 -0
- package/src/modules/documentation/instructions/generate-document.md +1 -0
- package/src/modules/documentation/templates/README.md +89 -0
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
|
-
|
|
4
|
-
|
|
5
|
-
|
|
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
|
|
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
|
-
-
|
|
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
|
-
##
|
|
11
|
+
## Quick start (clone this repo)
|
|
35
12
|
|
|
36
|
-
|
|
13
|
+
**Needs:** Node.js **22+** (see CI), **pnpm 10** (see `packageManager` in `package.json`).
|
|
37
14
|
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
-
|
|
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
|
-
|
|
22
|
+
Verify the kit sees your workspace:
|
|
43
23
|
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
24
|
+
```bash
|
|
25
|
+
node dist/cli.js doctor
|
|
26
|
+
node dist/cli.js --help
|
|
27
|
+
```
|
|
47
28
|
|
|
48
|
-
|
|
29
|
+
Try **read-only** task-engine queries:
|
|
49
30
|
|
|
50
|
-
|
|
31
|
+
```bash
|
|
32
|
+
node dist/cli.js run list-tasks '{}'
|
|
33
|
+
node dist/cli.js run get-next-actions '{}'
|
|
34
|
+
```
|
|
51
35
|
|
|
52
|
-
|
|
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
|
-
|
|
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
|
-
|
|
40
|
+
```bash
|
|
41
|
+
npm install @workflow-cannon/workspace-kit
|
|
42
|
+
npx workspace-kit --help
|
|
43
|
+
```
|
|
61
44
|
|
|
62
|
-
|
|
45
|
+
Or with pnpm: `pnpm add @workflow-cannon/workspace-kit` then `pnpm exec workspace-kit --help`.
|
|
63
46
|
|
|
64
|
-
|
|
47
|
+
## What this repo contains
|
|
65
48
|
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
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
|
-
|
|
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
|
-
##
|
|
58
|
+
## Policy and approvals (read this before mutating state)
|
|
76
59
|
|
|
77
|
-
-
|
|
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
|
-
|
|
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
|
-
|
|
65
|
+
## Project status and roadmap
|
|
86
66
|
|
|
87
|
-
|
|
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
|
-
|
|
69
|
+
Snapshot: [`docs/maintainers/data/workspace-kit-status.yaml`](docs/maintainers/data/workspace-kit-status.yaml).
|
|
92
70
|
|
|
93
|
-
|
|
71
|
+
## Where to go next
|
|
94
72
|
|
|
95
|
-
|
|
|
73
|
+
| Goal | Start here |
|
|
96
74
|
| --- | --- |
|
|
97
|
-
|
|
|
98
|
-
|
|
|
99
|
-
|
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
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
|
-
|
|
85
|
+
MIT. See [`LICENSE`](LICENSE).
|
|
@@ -1,15 +1,7 @@
|
|
|
1
1
|
import { appendLineageEvent } from "../../core/lineage-store.js";
|
|
2
|
-
import {
|
|
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
|
|
28
|
-
|
|
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 {
|
|
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
|
|
52
|
-
|
|
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 {
|
|
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
|
-
|
|
18
|
-
|
|
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.
|
|
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
|
-
|
|
253
|
+
if (command.name === "migrate-task-persistence") {
|
|
254
|
+
return runMigrateTaskPersistence(ctx, args);
|
|
255
|
+
}
|
|
256
|
+
let planning;
|
|
263
257
|
try {
|
|
264
|
-
await
|
|
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
|
|
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
|
-
|
|
564
|
+
let wishlistItems = [];
|
|
570
565
|
try {
|
|
571
|
-
await
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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
|
-
|
|
875
|
-
|
|
876
|
-
|
|
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,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
|
+
}
|