@workflow-cannon/workspace-kit 0.15.0 → 0.16.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 (28) hide show
  1. package/README.md +55 -106
  2. package/dist/cli/doctor-planning-issues.d.ts +6 -0
  3. package/dist/cli/doctor-planning-issues.js +37 -0
  4. package/dist/cli.js +3 -0
  5. package/dist/core/config-metadata.js +56 -1
  6. package/dist/modules/approvals/review-runtime.js +3 -11
  7. package/dist/modules/improvement/generate-recommendations-runtime.js +3 -11
  8. package/dist/modules/task-engine/doctor-planning-persistence.d.ts +9 -0
  9. package/dist/modules/task-engine/doctor-planning-persistence.js +77 -0
  10. package/dist/modules/task-engine/index.d.ts +2 -0
  11. package/dist/modules/task-engine/index.js +44 -46
  12. package/dist/modules/task-engine/migrate-task-persistence-runtime.d.ts +2 -0
  13. package/dist/modules/task-engine/migrate-task-persistence-runtime.js +192 -0
  14. package/dist/modules/task-engine/planning-config.d.ts +10 -0
  15. package/dist/modules/task-engine/planning-config.js +37 -0
  16. package/dist/modules/task-engine/planning-open.d.ts +16 -0
  17. package/dist/modules/task-engine/planning-open.js +34 -0
  18. package/dist/modules/task-engine/sqlite-dual-planning.d.ts +21 -0
  19. package/dist/modules/task-engine/sqlite-dual-planning.js +137 -0
  20. package/dist/modules/task-engine/store.d.ts +12 -3
  21. package/dist/modules/task-engine/store.js +62 -38
  22. package/dist/modules/task-engine/wishlist-store.d.ts +12 -3
  23. package/dist/modules/task-engine/wishlist-store.js +62 -40
  24. package/package.json +11 -2
  25. package/src/modules/documentation/README.md +1 -0
  26. package/src/modules/documentation/instructions/document-project.md +1 -0
  27. package/src/modules/documentation/instructions/generate-document.md +1 -0
  28. package/src/modules/documentation/templates/README.md +89 -0
@@ -2,7 +2,7 @@ import fs from "node:fs/promises";
2
2
  import path from "node:path";
3
3
  import crypto from "node:crypto";
4
4
  import { TaskEngineError } from "./transitions.js";
5
- const DEFAULT_STORE_PATH = ".workspace-kit/tasks/state.json";
5
+ export const DEFAULT_TASK_STORE_PATH = ".workspace-kit/tasks/state.json";
6
6
  function emptyStore() {
7
7
  return {
8
8
  schemaVersion: 1,
@@ -14,49 +14,73 @@ function emptyStore() {
14
14
  }
15
15
  export class TaskStore {
16
16
  document;
17
- filePath;
18
- constructor(workspacePath, storePath) {
19
- this.filePath = path.resolve(workspacePath, storePath ?? DEFAULT_STORE_PATH);
17
+ persistence;
18
+ constructor(persistence) {
19
+ this.persistence = persistence;
20
20
  this.document = emptyStore();
21
21
  }
22
- async load() {
23
- try {
24
- const raw = await fs.readFile(this.filePath, "utf8");
25
- const parsed = JSON.parse(raw);
26
- if (parsed.schemaVersion !== 1) {
27
- throw new TaskEngineError("storage-read-error", `Unsupported schema version: ${parsed.schemaVersion}`);
28
- }
29
- this.document = parsed;
30
- if (!Array.isArray(this.document.mutationLog)) {
31
- this.document.mutationLog = [];
22
+ static forJsonFile(workspacePath, storeRelativePath) {
23
+ const filePath = path.resolve(workspacePath, storeRelativePath ?? DEFAULT_TASK_STORE_PATH);
24
+ return new TaskStore({
25
+ pathLabel: filePath,
26
+ loadDocument: async () => {
27
+ try {
28
+ const raw = await fs.readFile(filePath, "utf8");
29
+ const parsed = JSON.parse(raw);
30
+ if (parsed.schemaVersion !== 1) {
31
+ throw new TaskEngineError("storage-read-error", `Unsupported schema version: ${parsed.schemaVersion}`);
32
+ }
33
+ if (!Array.isArray(parsed.mutationLog)) {
34
+ parsed.mutationLog = [];
35
+ }
36
+ return parsed;
37
+ }
38
+ catch (err) {
39
+ if (err.code === "ENOENT") {
40
+ return emptyStore();
41
+ }
42
+ if (err instanceof TaskEngineError) {
43
+ throw err;
44
+ }
45
+ throw new TaskEngineError("storage-read-error", `Failed to read task store: ${err.message}`);
46
+ }
47
+ },
48
+ saveDocument: async (doc) => {
49
+ const dir = path.dirname(filePath);
50
+ const tmpPath = `${filePath}.${crypto.randomUUID().slice(0, 8)}.tmp`;
51
+ try {
52
+ await fs.mkdir(dir, { recursive: true });
53
+ await fs.writeFile(tmpPath, JSON.stringify(doc, null, 2) + "\n", "utf8");
54
+ await fs.rename(tmpPath, filePath);
55
+ }
56
+ catch (err) {
57
+ try {
58
+ await fs.unlink(tmpPath);
59
+ }
60
+ catch {
61
+ /* cleanup best-effort */
62
+ }
63
+ throw new TaskEngineError("storage-write-error", `Failed to write task store: ${err.message}`);
64
+ }
32
65
  }
33
- }
34
- catch (err) {
35
- if (err.code === "ENOENT") {
36
- this.document = emptyStore();
37
- return;
66
+ });
67
+ }
68
+ static forSqliteDual(dual) {
69
+ return new TaskStore({
70
+ pathLabel: `${dual.getDisplayPath()}#task_engine`,
71
+ loadDocument: async () => dual.taskDocument,
72
+ saveDocument: async (doc) => {
73
+ dual.seedFromDocuments(doc, dual.wishlistDocument);
74
+ dual.persistSync();
38
75
  }
39
- if (err instanceof TaskEngineError)
40
- throw err;
41
- throw new TaskEngineError("storage-read-error", `Failed to read task store: ${err.message}`);
42
- }
76
+ });
77
+ }
78
+ async load() {
79
+ this.document = await this.persistence.loadDocument();
43
80
  }
44
81
  async save() {
45
82
  this.document.lastUpdated = new Date().toISOString();
46
- const dir = path.dirname(this.filePath);
47
- const tmpPath = `${this.filePath}.${crypto.randomUUID().slice(0, 8)}.tmp`;
48
- try {
49
- await fs.mkdir(dir, { recursive: true });
50
- await fs.writeFile(tmpPath, JSON.stringify(this.document, null, 2) + "\n", "utf8");
51
- await fs.rename(tmpPath, this.filePath);
52
- }
53
- catch (err) {
54
- try {
55
- await fs.unlink(tmpPath);
56
- }
57
- catch { /* cleanup best-effort */ }
58
- throw new TaskEngineError("storage-write-error", `Failed to write task store: ${err.message}`);
59
- }
83
+ await this.persistence.saveDocument(this.document);
60
84
  }
61
85
  getAllTasks() {
62
86
  return [...this.document.tasks];
@@ -99,7 +123,7 @@ export class TaskStore {
99
123
  this.document.tasks = tasks.map((t) => ({ ...t }));
100
124
  }
101
125
  getFilePath() {
102
- return this.filePath;
126
+ return this.persistence.pathLabel;
103
127
  }
104
128
  getLastUpdated() {
105
129
  return this.document.lastUpdated;
@@ -1,8 +1,17 @@
1
- import type { WishlistItem } from "./wishlist-types.js";
1
+ import type { WishlistItem, WishlistStoreDocument } from "./wishlist-types.js";
2
+ import type { SqliteDualPlanningStore } from "./sqlite-dual-planning.js";
3
+ export declare const DEFAULT_WISHLIST_PATH = ".workspace-kit/wishlist/state.json";
4
+ export type WishlistStorePersistence = {
5
+ loadDocument: () => Promise<WishlistStoreDocument>;
6
+ saveDocument: (doc: WishlistStoreDocument) => Promise<void>;
7
+ pathLabel: string;
8
+ };
2
9
  export declare class WishlistStore {
3
10
  private document;
4
- private readonly filePath;
5
- constructor(workspacePath: string, storeRelativePath?: string);
11
+ private readonly persistence;
12
+ constructor(persistence: WishlistStorePersistence);
13
+ static forJsonFile(workspacePath: string, storeRelativePath?: string): WishlistStore;
14
+ static forSqliteDual(dual: SqliteDualPlanningStore): WishlistStore;
6
15
  load(): Promise<void>;
7
16
  save(): Promise<void>;
8
17
  getAllItems(): WishlistItem[];
@@ -2,7 +2,7 @@ import fs from "node:fs/promises";
2
2
  import path from "node:path";
3
3
  import crypto from "node:crypto";
4
4
  import { TaskEngineError } from "./transitions.js";
5
- const DEFAULT_WISHLIST_PATH = ".workspace-kit/wishlist/state.json";
5
+ export const DEFAULT_WISHLIST_PATH = ".workspace-kit/wishlist/state.json";
6
6
  function emptyWishlistDoc() {
7
7
  return {
8
8
  schemaVersion: 1,
@@ -12,51 +12,73 @@ function emptyWishlistDoc() {
12
12
  }
13
13
  export class WishlistStore {
14
14
  document;
15
- filePath;
16
- constructor(workspacePath, storeRelativePath) {
17
- this.filePath = path.resolve(workspacePath, storeRelativePath ?? DEFAULT_WISHLIST_PATH);
15
+ persistence;
16
+ constructor(persistence) {
17
+ this.persistence = persistence;
18
18
  this.document = emptyWishlistDoc();
19
19
  }
20
- async load() {
21
- try {
22
- const raw = await fs.readFile(this.filePath, "utf8");
23
- const parsed = JSON.parse(raw);
24
- if (parsed.schemaVersion !== 1) {
25
- throw new TaskEngineError("storage-read-error", `Unsupported wishlist schema version: ${parsed.schemaVersion}`);
26
- }
27
- if (!Array.isArray(parsed.items)) {
28
- throw new TaskEngineError("storage-read-error", "Wishlist store 'items' must be an array");
20
+ static forJsonFile(workspacePath, storeRelativePath) {
21
+ const filePath = path.resolve(workspacePath, storeRelativePath ?? DEFAULT_WISHLIST_PATH);
22
+ return new WishlistStore({
23
+ pathLabel: filePath,
24
+ loadDocument: async () => {
25
+ try {
26
+ const raw = await fs.readFile(filePath, "utf8");
27
+ const parsed = JSON.parse(raw);
28
+ if (parsed.schemaVersion !== 1) {
29
+ throw new TaskEngineError("storage-read-error", `Unsupported wishlist schema version: ${parsed.schemaVersion}`);
30
+ }
31
+ if (!Array.isArray(parsed.items)) {
32
+ throw new TaskEngineError("storage-read-error", "Wishlist store 'items' must be an array");
33
+ }
34
+ return parsed;
35
+ }
36
+ catch (err) {
37
+ if (err.code === "ENOENT") {
38
+ return emptyWishlistDoc();
39
+ }
40
+ if (err instanceof TaskEngineError) {
41
+ throw err;
42
+ }
43
+ throw new TaskEngineError("storage-read-error", `Failed to read wishlist store: ${err.message}`);
44
+ }
45
+ },
46
+ saveDocument: async (doc) => {
47
+ const dir = path.dirname(filePath);
48
+ const tmpPath = `${filePath}.${crypto.randomUUID().slice(0, 8)}.tmp`;
49
+ try {
50
+ await fs.mkdir(dir, { recursive: true });
51
+ await fs.writeFile(tmpPath, JSON.stringify(doc, null, 2) + "\n", "utf8");
52
+ await fs.rename(tmpPath, filePath);
53
+ }
54
+ catch (err) {
55
+ try {
56
+ await fs.unlink(tmpPath);
57
+ }
58
+ catch {
59
+ /* cleanup best-effort */
60
+ }
61
+ throw new TaskEngineError("storage-write-error", `Failed to write wishlist store: ${err.message}`);
62
+ }
29
63
  }
30
- this.document = parsed;
31
- }
32
- catch (err) {
33
- if (err.code === "ENOENT") {
34
- this.document = emptyWishlistDoc();
35
- return;
64
+ });
65
+ }
66
+ static forSqliteDual(dual) {
67
+ return new WishlistStore({
68
+ pathLabel: `${dual.getDisplayPath()}#wishlist`,
69
+ loadDocument: async () => dual.wishlistDocument,
70
+ saveDocument: async (doc) => {
71
+ dual.seedFromDocuments(dual.taskDocument, doc);
72
+ dual.persistSync();
36
73
  }
37
- if (err instanceof TaskEngineError)
38
- throw err;
39
- throw new TaskEngineError("storage-read-error", `Failed to read wishlist store: ${err.message}`);
40
- }
74
+ });
75
+ }
76
+ async load() {
77
+ this.document = await this.persistence.loadDocument();
41
78
  }
42
79
  async save() {
43
80
  this.document.lastUpdated = new Date().toISOString();
44
- const dir = path.dirname(this.filePath);
45
- const tmpPath = `${this.filePath}.${crypto.randomUUID().slice(0, 8)}.tmp`;
46
- try {
47
- await fs.mkdir(dir, { recursive: true });
48
- await fs.writeFile(tmpPath, JSON.stringify(this.document, null, 2) + "\n", "utf8");
49
- await fs.rename(tmpPath, this.filePath);
50
- }
51
- catch (err) {
52
- try {
53
- await fs.unlink(tmpPath);
54
- }
55
- catch {
56
- /* cleanup best-effort */
57
- }
58
- throw new TaskEngineError("storage-write-error", `Failed to write wishlist store: ${err.message}`);
59
- }
81
+ await this.persistence.saveDocument(this.document);
60
82
  }
61
83
  getAllItems() {
62
84
  return [...this.document.items];
@@ -78,7 +100,7 @@ export class WishlistStore {
78
100
  this.document.items[idx] = { ...item };
79
101
  }
80
102
  getFilePath() {
81
- return this.filePath;
103
+ return this.persistence.pathLabel;
82
104
  }
83
105
  getLastUpdated() {
84
106
  return this.document.lastUpdated;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@workflow-cannon/workspace-kit",
3
- "version": "0.15.0",
3
+ "version": "0.16.1",
4
4
  "private": false,
5
5
  "packageManager": "pnpm@10.0.0",
6
6
  "license": "MIT",
@@ -41,6 +41,7 @@
41
41
  "ui:watch": "cd extensions/cursor-workflow-cannon && npm run watch"
42
42
  },
43
43
  "devDependencies": {
44
+ "@types/better-sqlite3": "^7.6.13",
44
45
  "@types/node": "^25.5.0",
45
46
  "typescript": "^5.9.3"
46
47
  },
@@ -48,5 +49,13 @@
48
49
  "dist",
49
50
  "src/modules/documentation",
50
51
  "package.json"
51
- ]
52
+ ],
53
+ "dependencies": {
54
+ "better-sqlite3": "^12.8.0"
55
+ },
56
+ "pnpm": {
57
+ "onlyBuiltDependencies": [
58
+ "better-sqlite3"
59
+ ]
60
+ }
52
61
  }
@@ -24,6 +24,7 @@ Files under `templates/`; `documentType` is the filename (basename). Keep this l
24
24
  - `AGENTS.md`
25
25
  - `ARCHITECTURE.md`
26
26
  - `PRINCIPLES.md`
27
+ - `README.md`
27
28
  - `RELEASING.md`
28
29
  - `ROADMAP.md`
29
30
  - `SECURITY.md`
@@ -18,6 +18,7 @@ All `.md` files under `sources.templatesRoot` (default `src/modules/documentatio
18
18
  - `AGENTS.md`
19
19
  - `ARCHITECTURE.md`
20
20
  - `PRINCIPLES.md`
21
+ - `README.md`
21
22
  - `RELEASING.md`
22
23
  - `ROADMAP.md`
23
24
  - `SECURITY.md`
@@ -8,6 +8,7 @@ Generate a single document for both canonical AI and human-readable surfaces usi
8
8
  - `AGENTS.md`
9
9
  - `ARCHITECTURE.md`
10
10
  - `PRINCIPLES.md`
11
+ - `README.md`
11
12
  - `RELEASING.md`
12
13
  - `ROADMAP.md`
13
14
  - `SECURITY.md`
@@ -0,0 +1,89 @@
1
+ <!--
2
+ Human output path: docs/maintainers/README.md (image uses ../title_image.png from that location).
3
+ Root README.md: mirror this body but use title_image.png and keep the agent notice line at the very top.
4
+ Audience: developers cloning the repo or adding @workflow-cannon/workspace-kit to a project who want to run something useful in minutes.
5
+ -->
6
+
7
+ <div align="center">
8
+ <img src="../title_image.png" alt="Workflow Cannon" width="720" />
9
+ </div>
10
+
11
+ # Workflow Cannon
12
+
13
+ **[`@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.
14
+
15
+ ## Quick start (clone this repo)
16
+
17
+ **Needs:** Node.js **22+** (see CI), **pnpm 10** (see `packageManager` in `package.json`).
18
+
19
+ ```bash
20
+ git clone https://github.com/NJLaPrell/workflow-cannon.git
21
+ cd workflow-cannon
22
+ pnpm install
23
+ pnpm run build
24
+ ```
25
+
26
+ Verify the kit sees your workspace:
27
+
28
+ ```bash
29
+ node dist/cli.js doctor
30
+ node dist/cli.js --help
31
+ ```
32
+
33
+ Try **read-only** task-engine queries:
34
+
35
+ ```bash
36
+ node dist/cli.js run list-tasks '{}'
37
+ node dist/cli.js run get-next-actions '{}'
38
+ ```
39
+
40
+ **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).
41
+
42
+ ## Quick start (use the package in another project)
43
+
44
+ ```bash
45
+ npm install @workflow-cannon/workspace-kit
46
+ npx workspace-kit --help
47
+ ```
48
+
49
+ Or with pnpm: `pnpm add @workflow-cannon/workspace-kit` then `pnpm exec workspace-kit --help`.
50
+
51
+ ## What this repo contains
52
+
53
+ | Area | What |
54
+ | --- | --- |
55
+ | **CLI** | `workspace-kit` — `doctor`, `config`, `run <module-command>` (see `workspace-kit run` with no args for the list). |
56
+ | **Task engine** | Canonical queue in `.workspace-kit/tasks/state.json`; lifecycle via `run-transition`. Wishlist ideation uses ids `W###` (see maintainer runbooks). |
57
+ | **Docs** | Maintainer process, roadmap, and changelog under `docs/maintainers/`. |
58
+ | **Cursor extension** (optional) | Thin UI in `extensions/cursor-workflow-cannon/` — build with `pnpm run ui:prepare`. |
59
+
60
+ 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.
61
+
62
+ ## Policy and approvals (read this before mutating state)
63
+
64
+ 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.
65
+
66
+ - **Human guide:** [`docs/maintainers/POLICY-APPROVAL.md`](POLICY-APPROVAL.md)
67
+ - **Copy-paste table:** [`docs/maintainers/AGENT-CLI-MAP.md`](AGENT-CLI-MAP.md)
68
+
69
+ ## Project status and roadmap
70
+
71
+ Release cadence, phase history, and strategic decisions: [`docs/maintainers/ROADMAP.md`](ROADMAP.md). **Live execution queue:** `.workspace-kit/tasks/state.json` (`status` and `id` are authoritative — not this README’s milestone bullets).
72
+
73
+ Snapshot: [`docs/maintainers/data/workspace-kit-status.yaml`](data/workspace-kit-status.yaml).
74
+
75
+ ## Where to go next
76
+
77
+ | Goal | Start here |
78
+ | --- | --- |
79
+ | Goals, trade-offs, gates | [`.ai/PRINCIPLES.md`](../.ai/PRINCIPLES.md) |
80
+ | Roadmap & versions | [`ROADMAP.md`](ROADMAP.md) |
81
+ | Changelog | [`CHANGELOG.md`](CHANGELOG.md) |
82
+ | Release process | [`RELEASING.md`](RELEASING.md) |
83
+ | Glossary | [`TERMS.md`](TERMS.md) |
84
+ | Architecture | [`ARCHITECTURE.md`](ARCHITECTURE.md) |
85
+ | Agent/CLI execution | [`AGENTS.md`](AGENTS.md) |
86
+
87
+ ## License
88
+
89
+ MIT. See [`LICENSE`](../LICENSE) at the repository root.