@workflow-cannon/workspace-kit 0.18.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.
- package/README.md +23 -9
- package/dist/cli/doctor-planning-issues.js +3 -22
- package/dist/cli/run-command.js +22 -38
- package/dist/cli.js +95 -4
- package/dist/contracts/command-manifest.d.ts +17 -0
- package/dist/contracts/command-manifest.js +1 -0
- package/dist/contracts/index.d.ts +1 -1
- package/dist/contracts/module-contract.d.ts +12 -11
- package/dist/core/agent-instruction-surface.d.ts +33 -0
- package/dist/core/agent-instruction-surface.js +46 -0
- package/dist/core/config-cli.js +13 -17
- package/dist/core/config-metadata.js +61 -2
- package/dist/core/index.d.ts +4 -1
- package/dist/core/index.js +3 -0
- package/dist/core/module-command-router.js +19 -1
- package/dist/core/module-registry-resolve.d.ts +27 -0
- package/dist/core/module-registry-resolve.js +91 -0
- package/dist/core/module-registry.d.ts +14 -0
- package/dist/core/module-registry.js +57 -0
- package/dist/core/planning/build-plan-session-file.d.ts +29 -0
- package/dist/core/planning/build-plan-session-file.js +58 -0
- package/dist/core/planning/index.d.ts +17 -0
- package/dist/core/planning/index.js +15 -0
- package/dist/core/policy.js +18 -8
- package/dist/core/state/unified-state-db.d.ts +21 -0
- package/dist/core/state/unified-state-db.js +80 -0
- package/dist/core/workspace-kit-config.js +8 -0
- package/dist/modules/agent-behavior/builtins.d.ts +3 -0
- package/dist/modules/agent-behavior/builtins.js +71 -0
- package/dist/modules/agent-behavior/explain.d.ts +6 -0
- package/dist/modules/agent-behavior/explain.js +46 -0
- package/dist/modules/agent-behavior/index.d.ts +4 -0
- package/dist/modules/agent-behavior/index.js +461 -0
- package/dist/modules/agent-behavior/interview-session-file.d.ts +9 -0
- package/dist/modules/agent-behavior/interview-session-file.js +43 -0
- package/dist/modules/agent-behavior/interview.d.ts +13 -0
- package/dist/modules/agent-behavior/interview.js +88 -0
- package/dist/modules/agent-behavior/persistence.d.ts +6 -0
- package/dist/modules/agent-behavior/persistence.js +89 -0
- package/dist/modules/agent-behavior/store.d.ts +34 -0
- package/dist/modules/agent-behavior/store.js +119 -0
- package/dist/modules/agent-behavior/types.d.ts +28 -0
- package/dist/modules/agent-behavior/types.js +1 -0
- package/dist/modules/agent-behavior/validate.d.ts +11 -0
- package/dist/modules/agent-behavior/validate.js +123 -0
- package/dist/modules/approvals/index.js +54 -51
- package/dist/modules/approvals/policy-sensitive-commands.d.ts +4 -0
- package/dist/modules/approvals/policy-sensitive-commands.js +4 -0
- package/dist/modules/approvals/review-runtime.js +1 -2
- package/dist/modules/documentation/index.js +47 -45
- package/dist/modules/documentation/normalizer.d.ts +3 -0
- package/dist/modules/documentation/normalizer.js +171 -0
- package/dist/modules/documentation/parser.d.ts +7 -0
- package/dist/modules/documentation/parser.js +39 -0
- package/dist/modules/documentation/policy-sensitive-commands.d.ts +5 -0
- package/dist/modules/documentation/policy-sensitive-commands.js +8 -0
- package/dist/modules/documentation/renderer.d.ts +23 -0
- package/dist/modules/documentation/renderer.js +105 -0
- package/dist/modules/documentation/runtime-batch.d.ts +10 -0
- package/dist/modules/documentation/runtime-batch.js +67 -0
- package/dist/modules/documentation/runtime-config.d.ts +11 -0
- package/dist/modules/documentation/runtime-config.js +54 -0
- package/dist/modules/documentation/runtime-render-support.d.ts +8 -0
- package/dist/modules/documentation/runtime-render-support.js +36 -0
- package/dist/modules/documentation/runtime.js +22 -510
- package/dist/modules/documentation/types.d.ts +182 -0
- package/dist/modules/documentation/validator.d.ts +8 -0
- package/dist/modules/documentation/validator.js +234 -0
- package/dist/modules/documentation/view-models.d.ts +3 -0
- package/dist/modules/documentation/view-models.js +124 -0
- package/dist/modules/improvement/generate-recommendations-runtime.js +3 -3
- package/dist/modules/improvement/improvement-state.d.ts +2 -2
- package/dist/modules/improvement/improvement-state.js +52 -23
- package/dist/modules/improvement/index.js +140 -138
- package/dist/modules/improvement/ingest.d.ts +1 -1
- package/dist/modules/improvement/policy-sensitive-commands.d.ts +4 -0
- package/dist/modules/improvement/policy-sensitive-commands.js +7 -0
- package/dist/modules/index.d.ts +6 -0
- package/dist/modules/index.js +17 -0
- package/dist/modules/planning/index.js +384 -50
- package/dist/modules/planning/question-engine.d.ts +2 -0
- package/dist/modules/planning/question-engine.js +8 -1
- package/dist/modules/task-engine/doctor-planning-persistence.js +21 -13
- package/dist/modules/task-engine/index.d.ts +1 -2
- package/dist/modules/task-engine/index.js +1 -1143
- package/dist/modules/task-engine/migrate-task-persistence-runtime.js +31 -4
- package/dist/modules/task-engine/migrate-wishlist-intake-runtime.d.ts +2 -0
- package/dist/modules/task-engine/migrate-wishlist-intake-runtime.js +146 -0
- package/dist/modules/task-engine/planning-open.d.ts +2 -9
- package/dist/modules/task-engine/planning-open.js +4 -15
- package/dist/modules/task-engine/policy-sensitive-commands.d.ts +5 -0
- package/dist/modules/task-engine/policy-sensitive-commands.js +5 -0
- package/dist/modules/task-engine/sqlite-dual-planning.d.ts +11 -2
- package/dist/modules/task-engine/sqlite-dual-planning.js +134 -28
- package/dist/modules/task-engine/strict-task-validation.js +3 -0
- package/dist/modules/task-engine/suggestions.js +2 -1
- package/dist/modules/task-engine/task-engine-internal.d.ts +2 -0
- package/dist/modules/task-engine/task-engine-internal.js +1304 -0
- package/dist/modules/task-engine/task-type-validation.js +40 -0
- package/dist/modules/task-engine/wishlist-intake.d.ts +22 -0
- package/dist/modules/task-engine/wishlist-intake.js +180 -0
- package/dist/modules/task-engine/wishlist-validation.d.ts +4 -0
- package/dist/modules/task-engine/wishlist-validation.js +19 -0
- package/dist/modules/workspace-config/index.js +9 -11
- package/package.json +2 -2
- package/schemas/agent-behavior-profile.schema.json +52 -0
- package/schemas/task-engine-run-contracts.schema.json +80 -5
- package/src/modules/documentation/README.md +16 -25
- package/src/modules/documentation/RULES.md +9 -9
- package/src/modules/documentation/index.ts +54 -49
- package/src/modules/documentation/instructions/document-project.md +6 -6
- package/src/modules/documentation/instructions/generate-document.md +4 -4
- package/src/modules/documentation/normalizer.ts +187 -0
- package/src/modules/documentation/parser.ts +41 -0
- package/src/modules/documentation/policy-sensitive-commands.ts +8 -0
- package/src/modules/documentation/renderer.ts +121 -0
- package/src/modules/documentation/runtime-batch.ts +74 -0
- package/src/modules/documentation/runtime-config.ts +68 -0
- package/src/modules/documentation/runtime-render-support.ts +39 -0
- package/src/modules/documentation/runtime.ts +28 -600
- package/src/modules/documentation/schemas/documentation-schema.md +37 -54
- package/src/modules/documentation/types.ts +228 -0
- package/src/modules/documentation/validator.ts +247 -0
- package/src/modules/documentation/view-models.ts +132 -0
- package/src/modules/documentation/views/agents.view.yaml +18 -0
- package/src/modules/documentation/views/architecture.view.yaml +18 -0
- package/src/modules/documentation/views/principles.view.yaml +18 -0
- package/src/modules/documentation/views/readme.view.yaml +18 -0
- package/src/modules/documentation/views/releasing.view.yaml +18 -0
- package/src/modules/documentation/views/roadmap.view.yaml +18 -0
- package/src/modules/documentation/views/runbooks-consumer-cadence.view.yaml +18 -0
- package/src/modules/documentation/views/runbooks-parity-validation-flow.view.yaml +18 -0
- package/src/modules/documentation/views/runbooks-release-channels.view.yaml +18 -0
- package/src/modules/documentation/views/security.view.yaml +18 -0
- package/src/modules/documentation/views/support.view.yaml +18 -0
- package/src/modules/documentation/views/terms.view.yaml +18 -0
- package/src/modules/documentation/views/workbooks-phase2-config-policy-workbook.view.yaml +18 -0
- package/src/modules/documentation/views/workbooks-task-engine-workbook.view.yaml +18 -0
- package/src/modules/documentation/views/workbooks-transcript-automation-baseline.view.yaml +18 -0
- 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" &&
|
|
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:
|
|
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,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
|
|
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();
|
|
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"]];
|
|
@@ -1,14 +1,17 @@
|
|
|
1
1
|
import type { TaskStoreDocument } from "./types.js";
|
|
2
2
|
import type { WishlistStoreDocument } from "./wishlist-types.js";
|
|
3
|
-
|
|
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
|
|
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
|
|
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
|
-
|
|
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 (
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
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
|
|
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 =
|
|
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
|
-
|
|
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 =
|
|
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
|
|
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
|
|
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
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
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("
|
|
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,
|