@workflow-cannon/workspace-kit 0.17.0 → 0.24.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.
Files changed (144) hide show
  1. package/README.md +23 -9
  2. package/dist/cli/doctor-planning-issues.js +3 -22
  3. package/dist/cli/run-command.js +22 -38
  4. package/dist/cli.js +95 -4
  5. package/dist/contracts/command-manifest.d.ts +17 -0
  6. package/dist/contracts/command-manifest.js +1 -0
  7. package/dist/contracts/index.d.ts +1 -1
  8. package/dist/contracts/module-contract.d.ts +12 -11
  9. package/dist/core/agent-instruction-surface.d.ts +33 -0
  10. package/dist/core/agent-instruction-surface.js +46 -0
  11. package/dist/core/config-cli.js +13 -17
  12. package/dist/core/config-metadata.js +101 -2
  13. package/dist/core/index.d.ts +4 -1
  14. package/dist/core/index.js +3 -0
  15. package/dist/core/module-command-router.js +19 -1
  16. package/dist/core/module-registry-resolve.d.ts +27 -0
  17. package/dist/core/module-registry-resolve.js +91 -0
  18. package/dist/core/module-registry.d.ts +14 -0
  19. package/dist/core/module-registry.js +57 -0
  20. package/dist/core/planning/build-plan-session-file.d.ts +29 -0
  21. package/dist/core/planning/build-plan-session-file.js +58 -0
  22. package/dist/core/planning/index.d.ts +17 -0
  23. package/dist/core/planning/index.js +15 -0
  24. package/dist/core/policy.js +18 -8
  25. package/dist/core/state/unified-state-db.d.ts +21 -0
  26. package/dist/core/state/unified-state-db.js +80 -0
  27. package/dist/core/workspace-kit-config.js +13 -1
  28. package/dist/modules/agent-behavior/builtins.d.ts +3 -0
  29. package/dist/modules/agent-behavior/builtins.js +71 -0
  30. package/dist/modules/agent-behavior/explain.d.ts +6 -0
  31. package/dist/modules/agent-behavior/explain.js +46 -0
  32. package/dist/modules/agent-behavior/index.d.ts +4 -0
  33. package/dist/modules/agent-behavior/index.js +461 -0
  34. package/dist/modules/agent-behavior/interview-session-file.d.ts +9 -0
  35. package/dist/modules/agent-behavior/interview-session-file.js +43 -0
  36. package/dist/modules/agent-behavior/interview.d.ts +13 -0
  37. package/dist/modules/agent-behavior/interview.js +88 -0
  38. package/dist/modules/agent-behavior/persistence.d.ts +6 -0
  39. package/dist/modules/agent-behavior/persistence.js +89 -0
  40. package/dist/modules/agent-behavior/store.d.ts +34 -0
  41. package/dist/modules/agent-behavior/store.js +119 -0
  42. package/dist/modules/agent-behavior/types.d.ts +28 -0
  43. package/dist/modules/agent-behavior/types.js +1 -0
  44. package/dist/modules/agent-behavior/validate.d.ts +11 -0
  45. package/dist/modules/agent-behavior/validate.js +123 -0
  46. package/dist/modules/approvals/index.js +54 -51
  47. package/dist/modules/approvals/policy-sensitive-commands.d.ts +4 -0
  48. package/dist/modules/approvals/policy-sensitive-commands.js +4 -0
  49. package/dist/modules/approvals/review-runtime.js +1 -2
  50. package/dist/modules/documentation/index.js +47 -45
  51. package/dist/modules/documentation/normalizer.d.ts +3 -0
  52. package/dist/modules/documentation/normalizer.js +171 -0
  53. package/dist/modules/documentation/parser.d.ts +7 -0
  54. package/dist/modules/documentation/parser.js +39 -0
  55. package/dist/modules/documentation/policy-sensitive-commands.d.ts +5 -0
  56. package/dist/modules/documentation/policy-sensitive-commands.js +8 -0
  57. package/dist/modules/documentation/renderer.d.ts +23 -0
  58. package/dist/modules/documentation/renderer.js +105 -0
  59. package/dist/modules/documentation/runtime-batch.d.ts +10 -0
  60. package/dist/modules/documentation/runtime-batch.js +67 -0
  61. package/dist/modules/documentation/runtime-config.d.ts +11 -0
  62. package/dist/modules/documentation/runtime-config.js +54 -0
  63. package/dist/modules/documentation/runtime-render-support.d.ts +8 -0
  64. package/dist/modules/documentation/runtime-render-support.js +36 -0
  65. package/dist/modules/documentation/runtime.js +22 -510
  66. package/dist/modules/documentation/types.d.ts +182 -0
  67. package/dist/modules/documentation/validator.d.ts +8 -0
  68. package/dist/modules/documentation/validator.js +234 -0
  69. package/dist/modules/documentation/view-models.d.ts +3 -0
  70. package/dist/modules/documentation/view-models.js +124 -0
  71. package/dist/modules/improvement/generate-recommendations-runtime.js +3 -3
  72. package/dist/modules/improvement/improvement-state.d.ts +2 -2
  73. package/dist/modules/improvement/improvement-state.js +52 -23
  74. package/dist/modules/improvement/index.js +140 -138
  75. package/dist/modules/improvement/ingest.d.ts +1 -1
  76. package/dist/modules/improvement/policy-sensitive-commands.d.ts +4 -0
  77. package/dist/modules/improvement/policy-sensitive-commands.js +7 -0
  78. package/dist/modules/index.d.ts +6 -0
  79. package/dist/modules/index.js +17 -0
  80. package/dist/modules/planning/artifact.d.ts +19 -0
  81. package/dist/modules/planning/artifact.js +72 -0
  82. package/dist/modules/planning/index.js +605 -6
  83. package/dist/modules/planning/question-engine.d.ts +25 -0
  84. package/dist/modules/planning/question-engine.js +284 -0
  85. package/dist/modules/planning/types.d.ts +9 -0
  86. package/dist/modules/planning/types.js +39 -0
  87. package/dist/modules/task-engine/doctor-planning-persistence.js +21 -13
  88. package/dist/modules/task-engine/index.d.ts +1 -2
  89. package/dist/modules/task-engine/index.js +1 -1143
  90. package/dist/modules/task-engine/migrate-task-persistence-runtime.js +31 -4
  91. package/dist/modules/task-engine/migrate-wishlist-intake-runtime.d.ts +2 -0
  92. package/dist/modules/task-engine/migrate-wishlist-intake-runtime.js +146 -0
  93. package/dist/modules/task-engine/planning-open.d.ts +2 -9
  94. package/dist/modules/task-engine/planning-open.js +4 -15
  95. package/dist/modules/task-engine/policy-sensitive-commands.d.ts +5 -0
  96. package/dist/modules/task-engine/policy-sensitive-commands.js +5 -0
  97. package/dist/modules/task-engine/sqlite-dual-planning.d.ts +11 -2
  98. package/dist/modules/task-engine/sqlite-dual-planning.js +134 -28
  99. package/dist/modules/task-engine/strict-task-validation.js +3 -0
  100. package/dist/modules/task-engine/suggestions.js +2 -1
  101. package/dist/modules/task-engine/task-engine-internal.d.ts +2 -0
  102. package/dist/modules/task-engine/task-engine-internal.js +1304 -0
  103. package/dist/modules/task-engine/task-type-validation.js +40 -0
  104. package/dist/modules/task-engine/wishlist-intake.d.ts +22 -0
  105. package/dist/modules/task-engine/wishlist-intake.js +180 -0
  106. package/dist/modules/task-engine/wishlist-validation.d.ts +4 -0
  107. package/dist/modules/task-engine/wishlist-validation.js +19 -0
  108. package/dist/modules/workspace-config/index.js +9 -11
  109. package/package.json +2 -2
  110. package/schemas/agent-behavior-profile.schema.json +52 -0
  111. package/schemas/task-engine-run-contracts.schema.json +80 -5
  112. package/src/modules/documentation/README.md +16 -25
  113. package/src/modules/documentation/RULES.md +9 -9
  114. package/src/modules/documentation/index.ts +54 -49
  115. package/src/modules/documentation/instructions/document-project.md +6 -6
  116. package/src/modules/documentation/instructions/generate-document.md +4 -4
  117. package/src/modules/documentation/normalizer.ts +187 -0
  118. package/src/modules/documentation/parser.ts +41 -0
  119. package/src/modules/documentation/policy-sensitive-commands.ts +8 -0
  120. package/src/modules/documentation/renderer.ts +121 -0
  121. package/src/modules/documentation/runtime-batch.ts +74 -0
  122. package/src/modules/documentation/runtime-config.ts +68 -0
  123. package/src/modules/documentation/runtime-render-support.ts +39 -0
  124. package/src/modules/documentation/runtime.ts +28 -600
  125. package/src/modules/documentation/schemas/documentation-schema.md +37 -54
  126. package/src/modules/documentation/types.ts +228 -0
  127. package/src/modules/documentation/validator.ts +247 -0
  128. package/src/modules/documentation/view-models.ts +132 -0
  129. package/src/modules/documentation/views/agents.view.yaml +18 -0
  130. package/src/modules/documentation/views/architecture.view.yaml +18 -0
  131. package/src/modules/documentation/views/principles.view.yaml +18 -0
  132. package/src/modules/documentation/views/readme.view.yaml +18 -0
  133. package/src/modules/documentation/views/releasing.view.yaml +18 -0
  134. package/src/modules/documentation/views/roadmap.view.yaml +18 -0
  135. package/src/modules/documentation/views/runbooks-consumer-cadence.view.yaml +18 -0
  136. package/src/modules/documentation/views/runbooks-parity-validation-flow.view.yaml +18 -0
  137. package/src/modules/documentation/views/runbooks-release-channels.view.yaml +18 -0
  138. package/src/modules/documentation/views/security.view.yaml +18 -0
  139. package/src/modules/documentation/views/support.view.yaml +18 -0
  140. package/src/modules/documentation/views/terms.view.yaml +18 -0
  141. package/src/modules/documentation/views/workbooks-phase2-config-policy-workbook.view.yaml +18 -0
  142. package/src/modules/documentation/views/workbooks-task-engine-workbook.view.yaml +18 -0
  143. package/src/modules/documentation/views/workbooks-transcript-automation-baseline.view.yaml +18 -0
  144. package/src/modules/documentation/state.md +0 -8
@@ -2,6 +2,7 @@ import fs from "node:fs/promises";
2
2
  import fsSync from "node:fs";
3
3
  import path from "node:path";
4
4
  import crypto from "node:crypto";
5
+ import { UnifiedStateDb } from "../../core/state/unified-state-db.js";
5
6
  import { TaskEngineError } from "./transitions.js";
6
7
  import { SqliteDualPlanningStore } from "./sqlite-dual-planning.js";
7
8
  import { planningSqliteDatabaseRelativePath, planningTaskStoreRelativePath, planningWishlistStoreRelativePath } from "./planning-config.js";
@@ -32,11 +33,13 @@ async function atomicWriteJson(targetPath, body) {
32
33
  }
33
34
  export async function runMigrateTaskPersistence(ctx, args) {
34
35
  const direction = typeof args.direction === "string" ? args.direction.trim() : "";
35
- if (direction !== "json-to-sqlite" && direction !== "sqlite-to-json") {
36
+ if (direction !== "json-to-sqlite" &&
37
+ direction !== "sqlite-to-json" &&
38
+ direction !== "json-to-unified-sqlite") {
36
39
  return {
37
40
  ok: false,
38
41
  code: "invalid-task-schema",
39
- message: "migrate-task-persistence requires direction: 'json-to-sqlite' | 'sqlite-to-json'"
42
+ message: "migrate-task-persistence requires direction: 'json-to-sqlite' | 'sqlite-to-json' | 'json-to-unified-sqlite'"
40
43
  };
41
44
  }
42
45
  const dryRun = args.dryRun === true;
@@ -47,7 +50,7 @@ export async function runMigrateTaskPersistence(ctx, args) {
47
50
  const wishPath = path.resolve(ctx.workspacePath, wishRel);
48
51
  const dbRel = planningSqliteDatabaseRelativePath(ctx);
49
52
  const dual = new SqliteDualPlanningStore(ctx.workspacePath, dbRel);
50
- if (direction === "json-to-sqlite") {
53
+ if (direction === "json-to-sqlite" || direction === "json-to-unified-sqlite") {
51
54
  if (fsSync.existsSync(dual.dbPath) && !force) {
52
55
  return {
53
56
  ok: false,
@@ -113,7 +116,9 @@ export async function runMigrateTaskPersistence(ctx, args) {
113
116
  return {
114
117
  ok: true,
115
118
  code: "migrate-dry-run",
116
- message: "Dry run: would import JSON task/wishlist documents into SQLite",
119
+ message: direction === "json-to-unified-sqlite"
120
+ ? "Dry run: would import JSON task/wishlist documents into unified SQLite module state"
121
+ : "Dry run: would import JSON task/wishlist documents into SQLite",
117
122
  data: {
118
123
  dbPath: dual.dbPath,
119
124
  taskPath,
@@ -123,6 +128,28 @@ export async function runMigrateTaskPersistence(ctx, args) {
123
128
  }
124
129
  };
125
130
  }
131
+ if (direction === "json-to-unified-sqlite") {
132
+ try {
133
+ const unified = new UnifiedStateDb(ctx.workspacePath, dbRel);
134
+ unified.setModuleState("task-engine", 1, {
135
+ taskStore: taskDoc,
136
+ wishlistStore: wishDoc
137
+ });
138
+ }
139
+ catch (err) {
140
+ return {
141
+ ok: false,
142
+ code: "storage-write-error",
143
+ message: `Failed to write unified SQLite module state: ${err.message}`
144
+ };
145
+ }
146
+ return {
147
+ ok: true,
148
+ code: "migrated-json-to-unified-sqlite",
149
+ message: `Imported task and wishlist JSON into unified module state at ${dual.dbPath}`,
150
+ data: { dbPath: dual.dbPath, taskPath, wishPath, moduleId: "task-engine" }
151
+ };
152
+ }
126
153
  dual.seedFromDocuments(taskDoc, wishDoc);
127
154
  try {
128
155
  dual.persistSync();
@@ -0,0 +1,2 @@
1
+ import type { ModuleCommandResult, ModuleLifecycleContext } from "../../contracts/module-contract.js";
2
+ export declare function runMigrateWishlistIntake(ctx: ModuleLifecycleContext, args: Record<string, unknown>): Promise<ModuleCommandResult>;
@@ -0,0 +1,146 @@
1
+ import fs from "node:fs/promises";
2
+ import fsSync from "node:fs";
3
+ import path from "node:path";
4
+ import { TaskStore } from "./store.js";
5
+ import { WishlistStore } from "./wishlist-store.js";
6
+ import { SqliteDualPlanningStore } from "./sqlite-dual-planning.js";
7
+ import { getTaskPersistenceBackend, 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
+ import { allocateNextTaskNumericId, isWishlistIntakeTask, LEGACY_WISHLIST_ID_METADATA_KEY, taskEntityFromWishlistItem } from "./wishlist-intake.js";
11
+ function emptyWishDoc() {
12
+ return {
13
+ schemaVersion: 1,
14
+ items: [],
15
+ lastUpdated: new Date().toISOString()
16
+ };
17
+ }
18
+ export async function runMigrateWishlistIntake(ctx, args) {
19
+ const dryRun = args.dryRun === true;
20
+ const backend = getTaskPersistenceBackend(ctx.effectiveConfig);
21
+ if (backend === "sqlite") {
22
+ return migrateSqlite(ctx, dryRun);
23
+ }
24
+ return migrateJson(ctx, dryRun);
25
+ }
26
+ async function migrateJson(ctx, dryRun) {
27
+ const taskRel = planningTaskStoreRelativePath(ctx) ?? DEFAULT_TASK_STORE_PATH;
28
+ const wishRel = planningWishlistStoreRelativePath(ctx) ?? DEFAULT_WISHLIST_PATH;
29
+ const taskPath = path.resolve(ctx.workspacePath, taskRel);
30
+ const wishPath = path.resolve(ctx.workspacePath, wishRel);
31
+ const taskStore = TaskStore.forJsonFile(ctx.workspacePath, taskRel);
32
+ await taskStore.load();
33
+ const wishStore = WishlistStore.forJsonFile(ctx.workspacePath, wishRel);
34
+ await wishStore.load();
35
+ const items = wishStore.getAllItems();
36
+ if (items.length === 0) {
37
+ return {
38
+ ok: true,
39
+ code: "wishlist-intake-migrate-noop",
40
+ message: "No legacy wishlist items to migrate (JSON mode)",
41
+ data: { migratedCount: 0, wishPath, taskPath }
42
+ };
43
+ }
44
+ const tasks = taskStore.getAllTasks();
45
+ const existingLegacy = new Set(tasks
46
+ .filter((t) => isWishlistIntakeTask(t))
47
+ .map((t) => t.metadata?.[LEGACY_WISHLIST_ID_METADATA_KEY])
48
+ .filter((x) => typeof x === "string"));
49
+ const toAdd = items.filter((it) => !existingLegacy.has(it.id));
50
+ if (dryRun) {
51
+ return {
52
+ ok: true,
53
+ code: "migrate-dry-run",
54
+ message: `Dry run: would migrate ${toAdd.length} wishlist item(s) into tasks and clear ${wishPath}`,
55
+ data: { wishlistCount: items.length, wouldMigrate: toAdd.length, wishPath, taskPath }
56
+ };
57
+ }
58
+ const now = new Date().toISOString();
59
+ let working = [...tasks];
60
+ for (const item of toAdd) {
61
+ const newId = allocateNextTaskNumericId(working);
62
+ const entity = taskEntityFromWishlistItem(item, newId, now);
63
+ working.push(entity);
64
+ taskStore.addTask(entity);
65
+ }
66
+ await taskStore.save();
67
+ await fs.mkdir(path.dirname(wishPath), { recursive: true });
68
+ await fs.writeFile(wishPath, `${JSON.stringify(emptyWishDoc(), null, 2)}\n`, "utf8");
69
+ return {
70
+ ok: true,
71
+ code: "wishlist-intake-migrated",
72
+ message: `Migrated ${toAdd.length} wishlist item(s) to wishlist_intake tasks; cleared ${wishPath}`,
73
+ data: { migratedCount: toAdd.length, wishPath, taskPath }
74
+ };
75
+ }
76
+ function migrateSqlite(ctx, dryRun) {
77
+ const dbRel = planningSqliteDatabaseRelativePath(ctx);
78
+ const dual = new SqliteDualPlanningStore(ctx.workspacePath, dbRel);
79
+ if (!fsSync.existsSync(dual.dbPath)) {
80
+ return {
81
+ ok: false,
82
+ code: "storage-read-error",
83
+ message: `SQLite planning database not found at ${dual.dbPath}`
84
+ };
85
+ }
86
+ dual.loadFromDisk();
87
+ const wishItems = dual.wishlistDocument.items;
88
+ const taskDoc = dual.taskDocument;
89
+ const existingLegacy = new Set(taskDoc.tasks
90
+ .filter((t) => isWishlistIntakeTask(t))
91
+ .map((t) => t.metadata?.[LEGACY_WISHLIST_ID_METADATA_KEY])
92
+ .filter((x) => typeof x === "string"));
93
+ const toAdd = wishItems.filter((it) => !existingLegacy.has(it.id));
94
+ const needsWishlistClear = wishItems.length > 0;
95
+ const needsSchemaShrink = dual.tableShape === "legacy-dual";
96
+ if (dryRun) {
97
+ return {
98
+ ok: true,
99
+ code: "migrate-dry-run",
100
+ message: `Dry run: would migrate ${toAdd.length} wishlist row(s), clear wishlist blob when needed, and shrink SQLite schema when applicable`,
101
+ data: {
102
+ dbPath: dual.dbPath,
103
+ wishlistCount: wishItems.length,
104
+ wouldMigrate: toAdd.length,
105
+ tableShape: dual.tableShape,
106
+ needsWishlistClear,
107
+ needsSchemaShrink
108
+ }
109
+ };
110
+ }
111
+ if (toAdd.length === 0 && !needsWishlistClear && !needsSchemaShrink) {
112
+ return {
113
+ ok: true,
114
+ code: "wishlist-intake-migrate-noop",
115
+ message: "No legacy wishlist data and planning SQLite already uses task-only row shape",
116
+ data: { migratedCount: 0, dbPath: dual.dbPath }
117
+ };
118
+ }
119
+ const now = new Date().toISOString();
120
+ const store = TaskStore.forSqliteDual(dual);
121
+ const apply = () => {
122
+ let working = [...dual.taskDocument.tasks];
123
+ for (const item of toAdd) {
124
+ const newId = allocateNextTaskNumericId(working);
125
+ const entity = taskEntityFromWishlistItem(item, newId, now);
126
+ working.push(entity);
127
+ store.addTask(entity);
128
+ }
129
+ if (needsWishlistClear) {
130
+ dual.seedFromDocuments(dual.taskDocument, emptyWishDoc());
131
+ }
132
+ };
133
+ dual.withTransaction(apply);
134
+ if (needsSchemaShrink) {
135
+ dual.migrateToTaskOnlyTableSchema();
136
+ }
137
+ else {
138
+ dual.persistSync();
139
+ }
140
+ return {
141
+ ok: true,
142
+ code: "wishlist-intake-migrated",
143
+ message: `Migrated ${toAdd.length} wishlist item(s) to tasks; cleared legacy wishlist blob where applicable`,
144
+ data: { migratedCount: toAdd.length, dbPath: dual.dbPath, schemaShrunk: needsSchemaShrink }
145
+ };
146
+ }
@@ -1,16 +1,9 @@
1
1
  import type { ModuleLifecycleContext } from "../../contracts/module-contract.js";
2
2
  import { TaskStore } from "./store.js";
3
- import { WishlistStore } from "./wishlist-store.js";
4
3
  import { SqliteDualPlanningStore } from "./sqlite-dual-planning.js";
5
4
  export type OpenedPlanningStores = {
6
- kind: "json";
5
+ kind: "json" | "sqlite";
7
6
  taskStore: TaskStore;
8
- sqliteDual: null;
9
- openWishlist: () => Promise<WishlistStore>;
10
- } | {
11
- kind: "sqlite";
12
- taskStore: TaskStore;
13
- sqliteDual: SqliteDualPlanningStore;
14
- openWishlist: () => Promise<WishlistStore>;
7
+ sqliteDual: SqliteDualPlanningStore | null;
15
8
  };
16
9
  export declare function openPlanningStores(ctx: ModuleLifecycleContext): Promise<OpenedPlanningStores>;
@@ -1,22 +1,16 @@
1
1
  import { TaskStore } from "./store.js";
2
- import { WishlistStore } from "./wishlist-store.js";
3
2
  import { SqliteDualPlanningStore } from "./sqlite-dual-planning.js";
4
- import { getTaskPersistenceBackend, planningSqliteDatabaseRelativePath, planningTaskStoreRelativePath, planningWishlistStoreRelativePath } from "./planning-config.js";
3
+ import { getTaskPersistenceBackend, planningSqliteDatabaseRelativePath, planningTaskStoreRelativePath } from "./planning-config.js";
5
4
  export async function openPlanningStores(ctx) {
6
5
  if (getTaskPersistenceBackend(ctx.effectiveConfig) === "sqlite") {
7
6
  const dual = new SqliteDualPlanningStore(ctx.workspacePath, planningSqliteDatabaseRelativePath(ctx));
8
7
  dual.loadFromDisk();
9
8
  const taskStore = TaskStore.forSqliteDual(dual);
10
- await taskStore.load(); // binds task document reference from dual
9
+ await taskStore.load();
11
10
  return {
12
11
  kind: "sqlite",
13
12
  sqliteDual: dual,
14
- taskStore,
15
- openWishlist: async () => {
16
- const w = WishlistStore.forSqliteDual(dual);
17
- await w.load();
18
- return w;
19
- }
13
+ taskStore
20
14
  };
21
15
  }
22
16
  const taskStore = TaskStore.forJsonFile(ctx.workspacePath, planningTaskStoreRelativePath(ctx));
@@ -24,11 +18,6 @@ export async function openPlanningStores(ctx) {
24
18
  return {
25
19
  kind: "json",
26
20
  sqliteDual: null,
27
- taskStore,
28
- openWishlist: async () => {
29
- const w = WishlistStore.forJsonFile(ctx.workspacePath, planningWishlistStoreRelativePath(ctx));
30
- await w.load();
31
- return w;
32
- }
21
+ taskStore
33
22
  };
34
23
  }
@@ -0,0 +1,5 @@
1
+ /**
2
+ * Policy-gated `workspace-kit run` commands owned by the task-engine module.
3
+ * Keep in sync with instruction names in `task-engine-internal.ts` / registration.
4
+ */
5
+ export declare const TASK_ENGINE_POLICY_COMMAND_NAMES: readonly [readonly ["run-transition", "tasks.run-transition"]];
@@ -0,0 +1,5 @@
1
+ /**
2
+ * Policy-gated `workspace-kit run` commands owned by the task-engine module.
3
+ * Keep in sync with instruction names in `task-engine-internal.ts` / registration.
4
+ */
5
+ export const TASK_ENGINE_POLICY_COMMAND_NAMES = [["run-transition", "tasks.run-transition"]];
@@ -1,14 +1,17 @@
1
1
  import type { TaskStoreDocument } from "./types.js";
2
2
  import type { WishlistStoreDocument } from "./wishlist-types.js";
3
- /** Single-file SQLite backing for task + wishlist JSON documents (atomic convert-wishlist). */
3
+ type TableShape = "legacy-dual" | "task-only";
4
+ /** Single-file SQLite backing for task JSON document; legacy rows may include a second wishlist blob until migrated. */
4
5
  export declare class SqliteDualPlanningStore {
5
6
  private db;
6
7
  readonly dbPath: string;
7
8
  private _taskDoc;
8
9
  private _wishlistDoc;
10
+ private _tableShape;
9
11
  constructor(workspacePath: string, databaseRelativePath: string);
10
12
  get taskDocument(): TaskStoreDocument;
11
13
  get wishlistDocument(): WishlistStoreDocument;
14
+ get tableShape(): TableShape;
12
15
  getDisplayPath(): string;
13
16
  private ensureDb;
14
17
  /** Load documents from an existing database file; otherwise start empty (no file created). */
@@ -16,6 +19,12 @@ export declare class SqliteDualPlanningStore {
16
19
  /** Replace in-memory documents (used by migrate). */
17
20
  seedFromDocuments(taskDoc: TaskStoreDocument, wishlistDoc: WishlistStoreDocument): void;
18
21
  persistSync(): void;
19
- /** Run synchronous work inside one SQLite transaction and flush both blobs at the end. */
22
+ /** Run synchronous work inside one SQLite transaction and flush blob(s) at the end. */
20
23
  withTransaction(work: () => void): void;
24
+ /**
25
+ * Drop legacy `wishlist_store_json` column by recreating the table (task JSON only).
26
+ * Caller must persist an empty wishlist document first if migrating off legacy data.
27
+ */
28
+ migrateToTaskOnlyTableSchema(): void;
21
29
  }
30
+ export {};
@@ -2,11 +2,10 @@ import fs from "node:fs";
2
2
  import path from "node:path";
3
3
  import Database from "better-sqlite3";
4
4
  import { TaskEngineError } from "./transitions.js";
5
- const DDL = `
5
+ const TASK_ONLY_DDL = `
6
6
  CREATE TABLE IF NOT EXISTS workspace_planning_state (
7
7
  id INTEGER PRIMARY KEY CHECK (id = 1),
8
- task_store_json TEXT NOT NULL,
9
- wishlist_store_json TEXT NOT NULL
8
+ task_store_json TEXT NOT NULL
10
9
  );
11
10
  `;
12
11
  function emptyTaskStoreDocument() {
@@ -25,12 +24,20 @@ function emptyWishlistDocument() {
25
24
  lastUpdated: new Date().toISOString()
26
25
  };
27
26
  }
28
- /** Single-file SQLite backing for task + wishlist JSON documents (atomic convert-wishlist). */
27
+ function detectTableShape(db) {
28
+ const rows = db.prepare("PRAGMA table_info(workspace_planning_state)").all();
29
+ if (rows.some((r) => r.name === "wishlist_store_json")) {
30
+ return "legacy-dual";
31
+ }
32
+ return "task-only";
33
+ }
34
+ /** Single-file SQLite backing for task JSON document; legacy rows may include a second wishlist blob until migrated. */
29
35
  export class SqliteDualPlanningStore {
30
36
  db = null;
31
37
  dbPath;
32
38
  _taskDoc;
33
39
  _wishlistDoc;
40
+ _tableShape = "task-only";
34
41
  constructor(workspacePath, databaseRelativePath) {
35
42
  this.dbPath = path.resolve(workspacePath, databaseRelativePath);
36
43
  this._taskDoc = emptyTaskStoreDocument();
@@ -42,16 +49,36 @@ export class SqliteDualPlanningStore {
42
49
  get wishlistDocument() {
43
50
  return this._wishlistDoc;
44
51
  }
52
+ get tableShape() {
53
+ return this._tableShape;
54
+ }
45
55
  getDisplayPath() {
46
56
  return this.dbPath;
47
57
  }
48
58
  ensureDb() {
49
- if (!this.db) {
50
- const dir = path.dirname(this.dbPath);
51
- fs.mkdirSync(dir, { recursive: true });
52
- this.db = new Database(this.dbPath);
53
- this.db.pragma("journal_mode = WAL");
54
- this.db.exec(DDL);
59
+ if (this.db) {
60
+ return this.db;
61
+ }
62
+ const dir = path.dirname(this.dbPath);
63
+ fs.mkdirSync(dir, { recursive: true });
64
+ const fileExisted = fs.existsSync(this.dbPath);
65
+ this.db = new Database(this.dbPath);
66
+ this.db.pragma("journal_mode = WAL");
67
+ if (!fileExisted) {
68
+ this.db.exec(TASK_ONLY_DDL);
69
+ this._tableShape = "task-only";
70
+ }
71
+ else {
72
+ const master = this.db
73
+ .prepare("SELECT name FROM sqlite_master WHERE type='table' AND name='workspace_planning_state'")
74
+ .get();
75
+ if (!master) {
76
+ this.db.exec(TASK_ONLY_DDL);
77
+ this._tableShape = "task-only";
78
+ }
79
+ else {
80
+ this._tableShape = detectTableShape(this.db);
81
+ }
55
82
  }
56
83
  return this.db;
57
84
  }
@@ -60,11 +87,48 @@ export class SqliteDualPlanningStore {
60
87
  if (!fs.existsSync(this.dbPath)) {
61
88
  this._taskDoc = emptyTaskStoreDocument();
62
89
  this._wishlistDoc = emptyWishlistDocument();
90
+ this._tableShape = "task-only";
63
91
  return;
64
92
  }
65
93
  const db = this.ensureDb();
94
+ this._tableShape = detectTableShape(db);
95
+ if (this._tableShape === "legacy-dual") {
96
+ const row = db
97
+ .prepare("SELECT task_store_json, wishlist_store_json FROM workspace_planning_state WHERE id = 1")
98
+ .get();
99
+ if (!row) {
100
+ this._taskDoc = emptyTaskStoreDocument();
101
+ this._wishlistDoc = emptyWishlistDocument();
102
+ return;
103
+ }
104
+ try {
105
+ const taskParsed = JSON.parse(row.task_store_json);
106
+ const wishParsed = JSON.parse(row.wishlist_store_json);
107
+ if (taskParsed.schemaVersion !== 1) {
108
+ throw new TaskEngineError("storage-read-error", `Unsupported task store schema in SQLite: ${taskParsed.schemaVersion}`);
109
+ }
110
+ if (wishParsed.schemaVersion !== 1) {
111
+ throw new TaskEngineError("storage-read-error", `Unsupported wishlist schema in SQLite: ${wishParsed.schemaVersion}`);
112
+ }
113
+ if (!Array.isArray(taskParsed.mutationLog)) {
114
+ taskParsed.mutationLog = [];
115
+ }
116
+ if (!Array.isArray(wishParsed.items)) {
117
+ throw new TaskEngineError("storage-read-error", "Wishlist items missing in SQLite row");
118
+ }
119
+ this._taskDoc = taskParsed;
120
+ this._wishlistDoc = wishParsed;
121
+ }
122
+ catch (err) {
123
+ if (err instanceof TaskEngineError) {
124
+ throw err;
125
+ }
126
+ throw new TaskEngineError("storage-read-error", `Failed to parse SQLite planning row: ${err.message}`);
127
+ }
128
+ return;
129
+ }
66
130
  const row = db
67
- .prepare("SELECT task_store_json, wishlist_store_json FROM workspace_planning_state WHERE id = 1")
131
+ .prepare("SELECT task_store_json FROM workspace_planning_state WHERE id = 1")
68
132
  .get();
69
133
  if (!row) {
70
134
  this._taskDoc = emptyTaskStoreDocument();
@@ -73,21 +137,14 @@ export class SqliteDualPlanningStore {
73
137
  }
74
138
  try {
75
139
  const taskParsed = JSON.parse(row.task_store_json);
76
- const wishParsed = JSON.parse(row.wishlist_store_json);
77
140
  if (taskParsed.schemaVersion !== 1) {
78
141
  throw new TaskEngineError("storage-read-error", `Unsupported task store schema in SQLite: ${taskParsed.schemaVersion}`);
79
142
  }
80
- if (wishParsed.schemaVersion !== 1) {
81
- throw new TaskEngineError("storage-read-error", `Unsupported wishlist schema in SQLite: ${wishParsed.schemaVersion}`);
82
- }
83
143
  if (!Array.isArray(taskParsed.mutationLog)) {
84
144
  taskParsed.mutationLog = [];
85
145
  }
86
- if (!Array.isArray(wishParsed.items)) {
87
- throw new TaskEngineError("storage-read-error", "Wishlist items missing in SQLite row");
88
- }
89
146
  this._taskDoc = taskParsed;
90
- this._wishlistDoc = wishParsed;
147
+ this._wishlistDoc = emptyWishlistDocument();
91
148
  }
92
149
  catch (err) {
93
150
  if (err instanceof TaskEngineError) {
@@ -105,33 +162,82 @@ export class SqliteDualPlanningStore {
105
162
  this._taskDoc.lastUpdated = new Date().toISOString();
106
163
  this._wishlistDoc.lastUpdated = new Date().toISOString();
107
164
  const db = this.ensureDb();
165
+ this._tableShape = detectTableShape(db);
108
166
  const t = JSON.stringify(this._taskDoc);
109
- const w = JSON.stringify(this._wishlistDoc);
167
+ if (this._tableShape === "legacy-dual") {
168
+ const w = JSON.stringify(this._wishlistDoc);
169
+ const exists = db.prepare("SELECT 1 AS ok FROM workspace_planning_state WHERE id = 1").get();
170
+ if (exists) {
171
+ db.prepare("UPDATE workspace_planning_state SET task_store_json = ?, wishlist_store_json = ? WHERE id = 1").run(t, w);
172
+ }
173
+ else {
174
+ db.prepare("INSERT INTO workspace_planning_state (id, task_store_json, wishlist_store_json) VALUES (1, ?, ?)").run(t, w);
175
+ }
176
+ return;
177
+ }
110
178
  const exists = db.prepare("SELECT 1 AS ok FROM workspace_planning_state WHERE id = 1").get();
111
179
  if (exists) {
112
- db.prepare("UPDATE workspace_planning_state SET task_store_json = ?, wishlist_store_json = ? WHERE id = 1").run(t, w);
180
+ db.prepare("UPDATE workspace_planning_state SET task_store_json = ? WHERE id = 1").run(t);
113
181
  }
114
182
  else {
115
- db.prepare("INSERT INTO workspace_planning_state (id, task_store_json, wishlist_store_json) VALUES (1, ?, ?)").run(t, w);
183
+ db.prepare("INSERT INTO workspace_planning_state (id, task_store_json) VALUES (1, ?)").run(t);
116
184
  }
117
185
  }
118
- /** Run synchronous work inside one SQLite transaction and flush both blobs at the end. */
186
+ /** Run synchronous work inside one SQLite transaction and flush blob(s) at the end. */
119
187
  withTransaction(work) {
120
188
  const db = this.ensureDb();
189
+ this._tableShape = detectTableShape(db);
121
190
  const txn = db.transaction(() => {
122
191
  work();
123
192
  this._taskDoc.lastUpdated = new Date().toISOString();
124
193
  this._wishlistDoc.lastUpdated = new Date().toISOString();
125
194
  const t = JSON.stringify(this._taskDoc);
126
- const w = JSON.stringify(this._wishlistDoc);
127
- const exists = db.prepare("SELECT 1 AS ok FROM workspace_planning_state WHERE id = 1").get();
128
- if (exists) {
129
- db.prepare("UPDATE workspace_planning_state SET task_store_json = ?, wishlist_store_json = ? WHERE id = 1").run(t, w);
195
+ if (this._tableShape === "legacy-dual") {
196
+ const w = JSON.stringify(this._wishlistDoc);
197
+ const exists = db.prepare("SELECT 1 AS ok FROM workspace_planning_state WHERE id = 1").get();
198
+ if (exists) {
199
+ db.prepare("UPDATE workspace_planning_state SET task_store_json = ?, wishlist_store_json = ? WHERE id = 1").run(t, w);
200
+ }
201
+ else {
202
+ db.prepare("INSERT INTO workspace_planning_state (id, task_store_json, wishlist_store_json) VALUES (1, ?, ?)").run(t, w);
203
+ }
130
204
  }
131
205
  else {
132
- db.prepare("INSERT INTO workspace_planning_state (id, task_store_json, wishlist_store_json) VALUES (1, ?, ?)").run(t, w);
206
+ const exists = db.prepare("SELECT 1 AS ok FROM workspace_planning_state WHERE id = 1").get();
207
+ if (exists) {
208
+ db.prepare("UPDATE workspace_planning_state SET task_store_json = ? WHERE id = 1").run(t);
209
+ }
210
+ else {
211
+ db.prepare("INSERT INTO workspace_planning_state (id, task_store_json) VALUES (1, ?)").run(t);
212
+ }
133
213
  }
134
214
  });
135
215
  txn();
136
216
  }
217
+ /**
218
+ * Drop legacy `wishlist_store_json` column by recreating the table (task JSON only).
219
+ * Caller must persist an empty wishlist document first if migrating off legacy data.
220
+ */
221
+ migrateToTaskOnlyTableSchema() {
222
+ const db = this.ensureDb();
223
+ if (detectTableShape(db) !== "legacy-dual") {
224
+ this._tableShape = "task-only";
225
+ return;
226
+ }
227
+ db.exec(`
228
+ CREATE TABLE IF NOT EXISTS workspace_planning_state_new (
229
+ id INTEGER PRIMARY KEY CHECK (id = 1),
230
+ task_store_json TEXT NOT NULL
231
+ );
232
+ `);
233
+ const row = db
234
+ .prepare("SELECT task_store_json FROM workspace_planning_state WHERE id = 1")
235
+ .get();
236
+ const taskJson = row?.task_store_json ?? JSON.stringify(emptyTaskStoreDocument());
237
+ db.prepare("INSERT OR REPLACE INTO workspace_planning_state_new (id, task_store_json) VALUES (1, ?)").run(taskJson);
238
+ db.exec("DROP TABLE workspace_planning_state");
239
+ db.exec("ALTER TABLE workspace_planning_state_new RENAME TO workspace_planning_state");
240
+ this._tableShape = "task-only";
241
+ this._wishlistDoc = emptyWishlistDocument();
242
+ }
137
243
  }
@@ -35,6 +35,9 @@ export function validateTaskEntityForStrictMode(task) {
35
35
  if (task.acceptanceCriteria !== undefined && !isStringArray(task.acceptanceCriteria)) {
36
36
  return `task '${task.id}' has invalid acceptanceCriteria values`;
37
37
  }
38
+ if (task.type === "wishlist_intake" && task.phase !== undefined && String(task.phase).trim().length > 0) {
39
+ return `task '${task.id}': wishlist_intake tasks must not set phase (ideation-only until converted)`;
40
+ }
38
41
  const knownTypeValidation = validateKnownTaskTypeRequirements(task);
39
42
  if (knownTypeValidation) {
40
43
  return `task '${task.id}': ${knownTypeValidation.message}`;
@@ -1,3 +1,4 @@
1
+ import { isWishlistIntakeTask } from "./wishlist-intake.js";
1
2
  const PRIORITY_ORDER = {
2
3
  P1: 0,
3
4
  P2: 1,
@@ -40,7 +41,7 @@ function buildBlockingAnalysis(tasks) {
40
41
  }
41
42
  export function getNextActions(tasks) {
42
43
  const readyQueue = tasks
43
- .filter((t) => t.status === "ready")
44
+ .filter((t) => t.status === "ready" && !isWishlistIntakeTask(t))
44
45
  .sort((a, b) => priorityRank(a) - priorityRank(b));
45
46
  return {
46
47
  readyQueue,
@@ -0,0 +1,2 @@
1
+ import type { WorkflowModule } from "../../contracts/module-contract.js";
2
+ export declare const taskEngineModule: WorkflowModule;