ndomo 0.1.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/.bun-version +1 -0
- package/.dockerignore +79 -0
- package/.editorconfig +18 -0
- package/.env.example +19 -0
- package/.github/CODEOWNERS +8 -0
- package/.github/ISSUE_TEMPLATE/bug_report.yml +62 -0
- package/.github/ISSUE_TEMPLATE/config.yml +2 -0
- package/.github/ISSUE_TEMPLATE/feature_request.yml +34 -0
- package/.github/dependabot.yml +36 -0
- package/.github/pull_request_template.md +24 -0
- package/.github/release.yml +30 -0
- package/.github/workflows/gitleaks.yml +28 -0
- package/.github/workflows/release-please.yml +27 -0
- package/.github/workflows/smoke.yml +29 -0
- package/.husky/commit-msg +1 -0
- package/CHANGELOG.md +114 -0
- package/Dockerfile +32 -0
- package/README.es.md +174 -0
- package/README.md +187 -0
- package/agents/chronicler.md +98 -0
- package/agents/ci-smith.md +136 -0
- package/agents/craftsman.md +341 -0
- package/agents/deploy-smith.md +138 -0
- package/agents/foreman.md +377 -0
- package/agents/go-smith.md +164 -0
- package/agents/guild.md +188 -0
- package/agents/inspector.md +83 -0
- package/agents/js-smith.md +127 -0
- package/agents/ops-scout.md +173 -0
- package/agents/painter.md +200 -0
- package/agents/python-smith.md +120 -0
- package/agents/ranger.md +307 -0
- package/agents/release-smith.md +165 -0
- package/agents/rust-smith.md +159 -0
- package/agents/sage.md +178 -0
- package/agents/scout.md +144 -0
- package/agents/scribe.md +156 -0
- package/agents/smith.md +201 -0
- package/agents/vue-smith.md +155 -0
- package/agents/warden.md +216 -0
- package/agents/zig-smith.md +156 -0
- package/bin/ndomo-analyses.ts +4 -0
- package/bin/ndomo-status.ts +4 -0
- package/biome.json +57 -0
- package/bun.lock +514 -0
- package/commitlint.config.js +3 -0
- package/config/ndomo.config.json +258 -0
- package/config/ndomo.schema.json +166 -0
- package/docs/agents.md +375 -0
- package/docs/bugs/plan-create-orphan-fk.md +131 -0
- package/docs/bugs/task_create_batch-order-index-collision.md +158 -0
- package/docs/configuration.md +276 -0
- package/docs/database.md +364 -0
- package/docs/features/feature-flexible-builder-v1.md +724 -0
- package/docs/features/feature-flexible-builder-v2.md +882 -0
- package/docs/features/feature-flexible-builder.md +974 -0
- package/docs/http-server.md +244 -0
- package/docs/installation.md +259 -0
- package/docs/integrations.md +129 -0
- package/docs/operations/anti-pattern-sub-agent-verify-2026-06-21.md +32 -0
- package/docs/operations/audit-v1.md +417 -0
- package/docs/operations/audit-v2.md +197 -0
- package/docs/operations/audit-v3.md +306 -0
- package/docs/operations/db-optimize-foundations.md +123 -0
- package/docs/operations/verify-gate-architecture.md +82 -0
- package/docs/workflows.md +448 -0
- package/opencode.json +5 -0
- package/package.json +65 -0
- package/release-please-config.json +11 -0
- package/scripts/dev-bust-cache.sh +164 -0
- package/scripts/install.sh +688 -0
- package/scripts/smoke-e2e.ts +704 -0
- package/scripts/smoke-hot.ts +417 -0
- package/scripts/smoke-http.sh +228 -0
- package/scripts/smoke-v4.ts +256 -0
- package/scripts/smoke-v5.ts +397 -0
- package/scripts/smoke.sh +9 -0
- package/scripts/uninstall.sh +224 -0
- package/skills/api-security-best-practices/SKILL.md +915 -0
- package/skills/bash-scripting/SKILL.md +201 -0
- package/skills/bun/SKILL.md +313 -0
- package/skills/cavecrew/SKILL.md +82 -0
- package/skills/caveman/SKILL.md +74 -0
- package/skills/caveman-review/README.md +33 -0
- package/skills/caveman-review/SKILL.md +55 -0
- package/skills/find-skills/SKILL.md +142 -0
- package/skills/frontend-design/LICENSE.txt +177 -0
- package/skills/frontend-design/SKILL.md +55 -0
- package/skills/golang-patterns/SKILL.md +674 -0
- package/skills/golang-security/SKILL.md +185 -0
- package/skills/golang-security/evals/evals.json +595 -0
- package/skills/golang-security/references/architecture.md +268 -0
- package/skills/golang-security/references/checklist.md +80 -0
- package/skills/golang-security/references/cookies.md +200 -0
- package/skills/golang-security/references/cryptography.md +424 -0
- package/skills/golang-security/references/filesystem.md +285 -0
- package/skills/golang-security/references/injection.md +315 -0
- package/skills/golang-security/references/logging.md +163 -0
- package/skills/golang-security/references/memory-safety.md +241 -0
- package/skills/golang-security/references/network.md +253 -0
- package/skills/golang-security/references/secrets.md +189 -0
- package/skills/golang-security/references/third-party.md +159 -0
- package/skills/golang-security/references/threat-modeling.md +189 -0
- package/skills/golang-testing/SKILL.md +720 -0
- package/skills/grill-me/SKILL.md +7 -0
- package/skills/javascript-testing-patterns/SKILL.md +537 -0
- package/skills/javascript-testing-patterns/references/advanced-testing-patterns.md +513 -0
- package/skills/modern-javascript-patterns/SKILL.md +43 -0
- package/skills/modern-javascript-patterns/references/advanced-patterns.md +487 -0
- package/skills/modern-javascript-patterns/references/details.md +457 -0
- package/skills/python-anti-patterns/SKILL.md +349 -0
- package/skills/python-design-patterns/SKILL.md +85 -0
- package/skills/python-design-patterns/references/details.md +353 -0
- package/skills/python-error-handling/SKILL.md +193 -0
- package/skills/python-error-handling/references/details.md +171 -0
- package/skills/python-testing-patterns/SKILL.md +278 -0
- package/skills/python-testing-patterns/references/advanced-patterns.md +411 -0
- package/skills/python-testing-patterns/references/details.md +349 -0
- package/skills/rust-patterns/SKILL.md +500 -0
- package/skills/rust-testing/SKILL.md +501 -0
- package/skills/security-review/SKILL.md +504 -0
- package/skills/security-review/cloud-infrastructure-security.md +361 -0
- package/skills/vue-best-practices/SKILL.md +154 -0
- package/skills/vue-best-practices/references/animation-class-based-technique.md +254 -0
- package/skills/vue-best-practices/references/animation-state-driven-technique.md +291 -0
- package/skills/vue-best-practices/references/component-async.md +97 -0
- package/skills/vue-best-practices/references/component-data-flow.md +307 -0
- package/skills/vue-best-practices/references/component-fallthrough-attrs.md +174 -0
- package/skills/vue-best-practices/references/component-keep-alive.md +137 -0
- package/skills/vue-best-practices/references/component-slots.md +216 -0
- package/skills/vue-best-practices/references/component-suspense.md +228 -0
- package/skills/vue-best-practices/references/component-teleport.md +108 -0
- package/skills/vue-best-practices/references/component-transition-group.md +128 -0
- package/skills/vue-best-practices/references/component-transition.md +125 -0
- package/skills/vue-best-practices/references/composables.md +290 -0
- package/skills/vue-best-practices/references/directives.md +162 -0
- package/skills/vue-best-practices/references/perf-avoid-component-abstraction-in-lists.md +159 -0
- package/skills/vue-best-practices/references/perf-v-once-v-memo-directives.md +182 -0
- package/skills/vue-best-practices/references/perf-virtualize-large-lists.md +187 -0
- package/skills/vue-best-practices/references/plugins.md +166 -0
- package/skills/vue-best-practices/references/reactivity.md +344 -0
- package/skills/vue-best-practices/references/render-functions.md +201 -0
- package/skills/vue-best-practices/references/sfc.md +310 -0
- package/skills/vue-best-practices/references/state-management.md +135 -0
- package/skills/vue-best-practices/references/updated-hook-performance.md +187 -0
- package/skills/vue-pinia-best-practices/SKILL.md +21 -0
- package/skills/vue-pinia-best-practices/reference/pinia-no-active-pinia-error.md +248 -0
- package/skills/vue-pinia-best-practices/reference/pinia-setup-store-return-all-state.md +227 -0
- package/skills/vue-pinia-best-practices/reference/pinia-store-destructuring-breaks-reactivity.md +193 -0
- package/skills/vue-pinia-best-practices/reference/state-url-for-ephemeral-filters.md +238 -0
- package/skills/vue-pinia-best-practices/reference/state-use-pinia-for-large-apps.md +262 -0
- package/skills/vue-pinia-best-practices/reference/store-method-binding-parentheses.md +191 -0
- package/skills/zig-0.16/SKILL.md +840 -0
- package/skills/zig-0.16/scripts/check-zig-version.sh +21 -0
- package/src/cli/analyses.ts +280 -0
- package/src/cli/index.ts +108 -0
- package/src/cli/serve.ts +192 -0
- package/src/cli/smoke.ts +131 -0
- package/src/cli/status.test.ts +204 -0
- package/src/cli/status.ts +263 -0
- package/src/cli/vacuum.test.ts +82 -0
- package/src/cli/vacuum.ts +96 -0
- package/src/config/schema.test.ts +88 -0
- package/src/config/schema.ts +64 -0
- package/src/db/analyses-migration.test.ts +210 -0
- package/src/db/analyses.test.ts +466 -0
- package/src/db/analyses.ts +375 -0
- package/src/db/auto-checkpoint.ts +131 -0
- package/src/db/client.test.ts +129 -0
- package/src/db/client.ts +55 -0
- package/src/db/fts-escape.ts +20 -0
- package/src/db/incidents.test.ts +201 -0
- package/src/db/incidents.ts +93 -0
- package/src/db/index.ts +86 -0
- package/src/db/migrations-v13.test.ts +141 -0
- package/src/db/migrations-v8.test.ts +301 -0
- package/src/db/migrations.ts +147 -0
- package/src/db/plan-archive.test.ts +180 -0
- package/src/db/plan-archive.ts +274 -0
- package/src/db/plan-create.test.ts +276 -0
- package/src/db/plan-create.ts +78 -0
- package/src/db/plan-files.test.ts +289 -0
- package/src/db/plan-update-status.ts +287 -0
- package/src/db/plans.test.ts +490 -0
- package/src/db/plans.ts +534 -0
- package/src/db/resolve-project-dir.test.ts +143 -0
- package/src/db/resolve-project-dir.ts +75 -0
- package/src/db/rollbacks.test.ts +150 -0
- package/src/db/rollbacks.ts +67 -0
- package/src/db/schema.ts +907 -0
- package/src/db/sessions.test.ts +80 -0
- package/src/db/sessions.ts +135 -0
- package/src/db/shutdown.test.ts +147 -0
- package/src/db/shutdown.ts +45 -0
- package/src/db/tasks.test.ts +921 -0
- package/src/db/tasks.ts +747 -0
- package/src/db/types.ts +619 -0
- package/src/http/__tests__/auth.test.ts +196 -0
- package/src/http/__tests__/routes.test.ts +465 -0
- package/src/http/__tests__/sse.test.ts +317 -0
- package/src/http/auth.ts +72 -0
- package/src/http/middleware/cors.ts +53 -0
- package/src/http/middleware/security-headers.ts +21 -0
- package/src/http/routes/events.ts +112 -0
- package/src/http/routes/health.ts +51 -0
- package/src/http/routes/plans.ts +66 -0
- package/src/http/routes/sessions.ts +50 -0
- package/src/http/routes/tasks.ts +60 -0
- package/src/http/server.ts +95 -0
- package/src/http/sse.ts +116 -0
- package/src/index.ts +37 -0
- package/src/lib.ts +65 -0
- package/src/mem/scoped.ts +65 -0
- package/src/orchestrator/background.test.ts +268 -0
- package/src/orchestrator/background.ts +293 -0
- package/src/orchestrator/memory-hook.ts +182 -0
- package/src/orchestrator/reconciler.ts +123 -0
- package/src/orchestrator/scheduler.test.ts +300 -0
- package/src/orchestrator/scheduler.ts +243 -0
- package/src/plugin.test.ts +2574 -0
- package/src/plugin.ts +1690 -0
- package/src/sdk/client.ts +66 -0
- package/src/worktrees/manager.ts +236 -0
- package/src/worktrees/state.ts +87 -0
- package/tests/integration/ranger-flow.test.ts +257 -0
- package/tools/analysis_archive.ts +28 -0
- package/tools/analysis_create.ts +55 -0
- package/tools/analysis_get.ts +33 -0
- package/tools/analysis_link_plan.ts +44 -0
- package/tools/analysis_list.ts +48 -0
- package/tools/analysis_search.ts +36 -0
- package/tools/analysis_update.ts +44 -0
- package/tools/plan_approve.ts +31 -0
- package/tools/plan_create.ts +58 -0
- package/tools/plan_get.ts +40 -0
- package/tools/plan_list.ts +37 -0
- package/tools/plan_search.ts +34 -0
- package/tools/plan_update_status.ts +71 -0
- package/tools/session_checkpoint.ts +31 -0
- package/tools/session_end.ts +26 -0
- package/tools/session_start.ts +43 -0
- package/tools/task_create_batch.ts +70 -0
- package/tools/task_list.ts +35 -0
- package/tools/task_next_for_agent.ts +30 -0
- package/tools/task_search.ts +34 -0
- package/tools/task_update_status.ts +37 -0
- package/tsconfig.json +31 -0
|
@@ -0,0 +1,301 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tests for v6+v8 migrations — write-once audit fields.
|
|
3
|
+
*
|
|
4
|
+
* Validates:
|
|
5
|
+
* - original_plan_data is set on plan creation and NOT overwritten on status updates
|
|
6
|
+
* - created_by_agent is set on plan creation
|
|
7
|
+
* - executed_by_agent/session can be set but are not overwritten if already set
|
|
8
|
+
*
|
|
9
|
+
* Uses in-memory SQLite via bun:sqlite. Each test gets a fresh DB
|
|
10
|
+
* with the full schema applied by runMigrations.
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
import { Database } from "bun:sqlite";
|
|
14
|
+
import { beforeEach, describe, expect, test } from "bun:test";
|
|
15
|
+
import { runMigrations } from "./migrations.ts";
|
|
16
|
+
import { approvePlan, createPlan, getPlan, getPlanProgress, updatePlanStatus } from "./plans.ts";
|
|
17
|
+
import { createTasksBatch, getTask, updateTaskStatus } from "./tasks.ts";
|
|
18
|
+
import type { Plan } from "./types.ts";
|
|
19
|
+
|
|
20
|
+
let db: Database;
|
|
21
|
+
|
|
22
|
+
beforeEach(() => {
|
|
23
|
+
db = new Database(":memory:");
|
|
24
|
+
db.exec("PRAGMA foreign_keys = ON");
|
|
25
|
+
runMigrations(db);
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
function makePlan(overrides: Partial<Parameters<typeof createPlan>[1]> = {}): Plan {
|
|
29
|
+
return createPlan(db, {
|
|
30
|
+
id: crypto.randomUUID(),
|
|
31
|
+
slug: "test-plan",
|
|
32
|
+
title: "Test",
|
|
33
|
+
status: "draft",
|
|
34
|
+
priority: 2,
|
|
35
|
+
approvedAt: null,
|
|
36
|
+
completedAt: null,
|
|
37
|
+
sessionId: null,
|
|
38
|
+
overview: "test",
|
|
39
|
+
approach: null,
|
|
40
|
+
complexity: 3,
|
|
41
|
+
createdBy: "test",
|
|
42
|
+
updatedBy: "test",
|
|
43
|
+
sourceSessionId: null,
|
|
44
|
+
sourceMessageId: null,
|
|
45
|
+
category: null,
|
|
46
|
+
metadata: {},
|
|
47
|
+
archivedAt: null,
|
|
48
|
+
...overrides,
|
|
49
|
+
});
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
describe("v6: original_plan_data (write-once)", () => {
|
|
53
|
+
test("plan has original_plan_data after creation", () => {
|
|
54
|
+
const plan = makePlan();
|
|
55
|
+
const fetched = getPlan(db, plan.id);
|
|
56
|
+
|
|
57
|
+
expect(fetched).not.toBeNull();
|
|
58
|
+
const opd = fetched?.originalPlanData;
|
|
59
|
+
expect(opd).not.toBeNull();
|
|
60
|
+
|
|
61
|
+
const data = JSON.parse(opd as string);
|
|
62
|
+
expect(data.id).toBe(plan.id);
|
|
63
|
+
expect(data.slug).toBe("test-plan");
|
|
64
|
+
expect(data.title).toBe("Test");
|
|
65
|
+
expect(data.overview).toBe("test");
|
|
66
|
+
expect(data.createdBy).toBe("test");
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
test("original_plan_data is NOT overwritten on status update", () => {
|
|
70
|
+
const plan = makePlan();
|
|
71
|
+
const beforeRaw = getPlan(db, plan.id);
|
|
72
|
+
expect(beforeRaw).not.toBeNull();
|
|
73
|
+
const before = beforeRaw as Plan;
|
|
74
|
+
const originalData = before.originalPlanData;
|
|
75
|
+
|
|
76
|
+
// Update status — should NOT touch original_plan_data
|
|
77
|
+
updatePlanStatus(db, plan.id, "approved", { updatedBy: "admin" });
|
|
78
|
+
|
|
79
|
+
const afterRaw = getPlan(db, plan.id);
|
|
80
|
+
expect(afterRaw).not.toBeNull();
|
|
81
|
+
const after = afterRaw as Plan;
|
|
82
|
+
expect(after.originalPlanData).toBe(originalData);
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
test("original_plan_data is NOT overwritten on approve", () => {
|
|
86
|
+
const plan = makePlan();
|
|
87
|
+
const beforeRaw = getPlan(db, plan.id);
|
|
88
|
+
expect(beforeRaw).not.toBeNull();
|
|
89
|
+
const before = beforeRaw as Plan;
|
|
90
|
+
const originalData = before.originalPlanData;
|
|
91
|
+
|
|
92
|
+
approvePlan(db, plan.id, { updatedBy: "admin" });
|
|
93
|
+
|
|
94
|
+
const afterRaw = getPlan(db, plan.id);
|
|
95
|
+
expect(afterRaw).not.toBeNull();
|
|
96
|
+
const after = afterRaw as Plan;
|
|
97
|
+
expect(after.originalPlanData).toBe(originalData);
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
test("task has original_plan_data after creation", () => {
|
|
101
|
+
const plan = makePlan();
|
|
102
|
+
const tasks = createTasksBatch(db, plan.id, [
|
|
103
|
+
{
|
|
104
|
+
orderIndex: 0,
|
|
105
|
+
description: "test task",
|
|
106
|
+
agent: "js-smith",
|
|
107
|
+
files: ["src/index.ts"],
|
|
108
|
+
complexity: 2,
|
|
109
|
+
dependencies: [],
|
|
110
|
+
createdBy: "foreman",
|
|
111
|
+
updatedBy: "foreman",
|
|
112
|
+
sourceSessionId: null,
|
|
113
|
+
sourceMessageId: null,
|
|
114
|
+
reviewedBy: null,
|
|
115
|
+
tokensUsed: null,
|
|
116
|
+
durationMs: null,
|
|
117
|
+
artifacts: [],
|
|
118
|
+
metadata: {},
|
|
119
|
+
},
|
|
120
|
+
]);
|
|
121
|
+
|
|
122
|
+
const taskOpd = tasks[0]?.originalPlanData;
|
|
123
|
+
expect(taskOpd).not.toBeNull();
|
|
124
|
+
const data = JSON.parse(taskOpd as string);
|
|
125
|
+
expect(data.description).toBe("test task");
|
|
126
|
+
expect(data.agent).toBe("js-smith");
|
|
127
|
+
expect(data.files).toEqual(["src/index.ts"]);
|
|
128
|
+
});
|
|
129
|
+
|
|
130
|
+
test("task original_plan_data is NOT overwritten on status update", () => {
|
|
131
|
+
const plan = makePlan();
|
|
132
|
+
const tasks = createTasksBatch(db, plan.id, [
|
|
133
|
+
{
|
|
134
|
+
orderIndex: 0,
|
|
135
|
+
description: "test task",
|
|
136
|
+
agent: "js-smith",
|
|
137
|
+
files: [],
|
|
138
|
+
complexity: 1,
|
|
139
|
+
dependencies: [],
|
|
140
|
+
createdBy: "test",
|
|
141
|
+
updatedBy: "test",
|
|
142
|
+
sourceSessionId: null,
|
|
143
|
+
sourceMessageId: null,
|
|
144
|
+
reviewedBy: null,
|
|
145
|
+
tokensUsed: null,
|
|
146
|
+
durationMs: null,
|
|
147
|
+
artifacts: [],
|
|
148
|
+
metadata: {},
|
|
149
|
+
},
|
|
150
|
+
]);
|
|
151
|
+
|
|
152
|
+
const originalData = tasks[0]?.originalPlanData;
|
|
153
|
+
|
|
154
|
+
// Update task status — should NOT touch original_plan_data
|
|
155
|
+
const t0id = tasks[0]?.id as string;
|
|
156
|
+
updateTaskStatus(db, t0id, "running");
|
|
157
|
+
|
|
158
|
+
const updated = getTask(db, t0id);
|
|
159
|
+
|
|
160
|
+
expect(updated?.originalPlanData).toBe(originalData);
|
|
161
|
+
});
|
|
162
|
+
});
|
|
163
|
+
|
|
164
|
+
describe("v8: created_by_agent (write-once)", () => {
|
|
165
|
+
test("plan has created_by_agent after creation", () => {
|
|
166
|
+
const plan = makePlan({ createdByAgent: "foreman" });
|
|
167
|
+
const fetched = getPlan(db, plan.id);
|
|
168
|
+
|
|
169
|
+
expect(fetched).not.toBeNull();
|
|
170
|
+
expect(fetched?.createdByAgent).toBe("foreman");
|
|
171
|
+
});
|
|
172
|
+
|
|
173
|
+
test("created_by_agent defaults to null when not provided", () => {
|
|
174
|
+
const plan = makePlan(); // no createdByAgent
|
|
175
|
+
const fetched = getPlan(db, plan.id);
|
|
176
|
+
|
|
177
|
+
expect(fetched?.createdByAgent).toBeNull();
|
|
178
|
+
});
|
|
179
|
+
});
|
|
180
|
+
|
|
181
|
+
describe("v8: executed_by_agent/session", () => {
|
|
182
|
+
test("executed_by_agent and executed_by_session are null on creation", () => {
|
|
183
|
+
const plan = makePlan();
|
|
184
|
+
const fetched = getPlan(db, plan.id);
|
|
185
|
+
|
|
186
|
+
expect(fetched?.executedByAgent).toBeNull();
|
|
187
|
+
expect(fetched?.executedBySession).toBeNull();
|
|
188
|
+
});
|
|
189
|
+
|
|
190
|
+
test("can be set via direct SQL update", () => {
|
|
191
|
+
const plan = makePlan();
|
|
192
|
+
|
|
193
|
+
db.query("UPDATE plans SET executed_by_agent = ?, executed_by_session = ? WHERE id = ?").run(
|
|
194
|
+
"craftsman",
|
|
195
|
+
"ses_123",
|
|
196
|
+
plan.id,
|
|
197
|
+
);
|
|
198
|
+
|
|
199
|
+
const fetched = getPlan(db, plan.id);
|
|
200
|
+
expect(fetched?.executedByAgent).toBe("craftsman");
|
|
201
|
+
expect(fetched?.executedBySession).toBe("ses_123");
|
|
202
|
+
});
|
|
203
|
+
|
|
204
|
+
test("updatePlanStatus sets executed_by_agent on first executing transition", () => {
|
|
205
|
+
const plan = makePlan();
|
|
206
|
+
// createPlan returns the spread input which may have undefined for optional fields
|
|
207
|
+
// The DB stores null, but the TS object preserves the input shape
|
|
208
|
+
expect(plan.executedByAgent ?? null).toBeNull();
|
|
209
|
+
|
|
210
|
+
updatePlanStatus(db, plan.id, "executing", {
|
|
211
|
+
executedByAgent: "craftsman",
|
|
212
|
+
executedBySession: "ses_exec_1",
|
|
213
|
+
});
|
|
214
|
+
|
|
215
|
+
const fetched = getPlan(db, plan.id);
|
|
216
|
+
expect(fetched?.executedByAgent).toBe("craftsman");
|
|
217
|
+
expect(fetched?.executedBySession).toBe("ses_exec_1");
|
|
218
|
+
});
|
|
219
|
+
|
|
220
|
+
test("updatePlanStatus does NOT overwrite executed_by_agent on second executing", () => {
|
|
221
|
+
const plan = makePlan();
|
|
222
|
+
|
|
223
|
+
// First executing
|
|
224
|
+
updatePlanStatus(db, plan.id, "executing", {
|
|
225
|
+
executedByAgent: "craftsman",
|
|
226
|
+
executedBySession: "ses_first",
|
|
227
|
+
});
|
|
228
|
+
|
|
229
|
+
// Second executing with different agent — should NOT overwrite
|
|
230
|
+
updatePlanStatus(db, plan.id, "executing", {
|
|
231
|
+
executedByAgent: "go-smith",
|
|
232
|
+
executedBySession: "ses_second",
|
|
233
|
+
});
|
|
234
|
+
|
|
235
|
+
const fetched = getPlan(db, plan.id);
|
|
236
|
+
expect(fetched?.executedByAgent).toBe("craftsman");
|
|
237
|
+
expect(fetched?.executedBySession).toBe("ses_first");
|
|
238
|
+
});
|
|
239
|
+
});
|
|
240
|
+
|
|
241
|
+
// ─── Critical 2: v9 migration — plan_progress excludes archived ─────────────
|
|
242
|
+
|
|
243
|
+
describe("v9: plan_progress view fix", () => {
|
|
244
|
+
test("DB with schema_version=5 → runMigrations → schema_version=15", () => {
|
|
245
|
+
// Fresh DB already runs all migrations up to v15
|
|
246
|
+
const row = db.query("SELECT MAX(version) as version FROM schema_version").get() as {
|
|
247
|
+
version: number;
|
|
248
|
+
};
|
|
249
|
+
expect(row.version).toBe(15);
|
|
250
|
+
});
|
|
251
|
+
|
|
252
|
+
test("plan_progress excludes archived plans", () => {
|
|
253
|
+
const active = createPlan(db, {
|
|
254
|
+
id: crypto.randomUUID(),
|
|
255
|
+
slug: "active",
|
|
256
|
+
title: "Active",
|
|
257
|
+
status: "draft",
|
|
258
|
+
priority: 2,
|
|
259
|
+
approvedAt: null,
|
|
260
|
+
completedAt: null,
|
|
261
|
+
sessionId: null,
|
|
262
|
+
overview: "test",
|
|
263
|
+
approach: null,
|
|
264
|
+
complexity: 3,
|
|
265
|
+
createdBy: "test",
|
|
266
|
+
updatedBy: "test",
|
|
267
|
+
sourceSessionId: null,
|
|
268
|
+
sourceMessageId: null,
|
|
269
|
+
category: null,
|
|
270
|
+
metadata: {},
|
|
271
|
+
archivedAt: null,
|
|
272
|
+
});
|
|
273
|
+
const archived = createPlan(db, {
|
|
274
|
+
id: crypto.randomUUID(),
|
|
275
|
+
slug: "archived",
|
|
276
|
+
title: "Archived",
|
|
277
|
+
status: "draft",
|
|
278
|
+
priority: 2,
|
|
279
|
+
approvedAt: null,
|
|
280
|
+
completedAt: null,
|
|
281
|
+
sessionId: null,
|
|
282
|
+
overview: "test",
|
|
283
|
+
approach: null,
|
|
284
|
+
complexity: 3,
|
|
285
|
+
createdBy: "test",
|
|
286
|
+
updatedBy: "test",
|
|
287
|
+
sourceSessionId: null,
|
|
288
|
+
sourceMessageId: null,
|
|
289
|
+
category: null,
|
|
290
|
+
metadata: {},
|
|
291
|
+
archivedAt: null,
|
|
292
|
+
});
|
|
293
|
+
|
|
294
|
+
// Archive second plan
|
|
295
|
+
db.query("UPDATE plans SET archived_at = ? WHERE id = ?").run(Date.now(), archived.id);
|
|
296
|
+
|
|
297
|
+
const progress = getPlanProgress(db);
|
|
298
|
+
expect(progress).toHaveLength(1);
|
|
299
|
+
expect(progress[0]?.planId).toBe(active.id);
|
|
300
|
+
});
|
|
301
|
+
});
|
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ndomo DB — Migration runner.
|
|
3
|
+
*
|
|
4
|
+
* Reads MIGRATIONS from schema.ts, compares against schema_version,
|
|
5
|
+
* and applies pending migrations in a transaction.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import type { Database } from "bun:sqlite";
|
|
9
|
+
import { MIGRATIONS } from "./schema.ts";
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Add a column to a table if it doesn't already exist.
|
|
13
|
+
* Uses PRAGMA table_info to check — the only reliable idempotent pattern
|
|
14
|
+
* for SQLite 3.45 which lacks ALTER TABLE ADD COLUMN IF NOT EXISTS.
|
|
15
|
+
*/
|
|
16
|
+
function addColumnIfMissing(db: Database, table: string, column: string, type: string): void {
|
|
17
|
+
const cols = db.query(`PRAGMA table_info(${table})`).all() as Array<{ name: string }>;
|
|
18
|
+
if (!cols.some((c) => c.name === column)) {
|
|
19
|
+
db.exec(`ALTER TABLE ${table} ADD COLUMN ${column} ${type}`);
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* v15: backfill — rename finding keys inside analyses.findings_json.
|
|
25
|
+
*
|
|
26
|
+
* - description → observation
|
|
27
|
+
* - recommendation → proposedAction
|
|
28
|
+
*
|
|
29
|
+
* Idempotent: rows already renamed are skipped (presence of `observation`
|
|
30
|
+
* key signals the rename has already happened for that finding). Returns
|
|
31
|
+
* the number of findings renamed.
|
|
32
|
+
*
|
|
33
|
+
* Pure data migration — no DDL. Safe to call repeatedly.
|
|
34
|
+
*/
|
|
35
|
+
export function backfillAnalysisFindings(db: Database): number {
|
|
36
|
+
const rows = db
|
|
37
|
+
.query("SELECT id, findings_json FROM analyses")
|
|
38
|
+
.all() as Array<{ id: string; findings_json: string }>;
|
|
39
|
+
|
|
40
|
+
let renamed = 0;
|
|
41
|
+
const txn = db.transaction(() => {
|
|
42
|
+
for (const row of rows) {
|
|
43
|
+
let findings: unknown;
|
|
44
|
+
try {
|
|
45
|
+
findings = JSON.parse(row.findings_json);
|
|
46
|
+
} catch {
|
|
47
|
+
// Skip malformed JSON — leave row untouched
|
|
48
|
+
continue;
|
|
49
|
+
}
|
|
50
|
+
if (!Array.isArray(findings) || findings.length === 0) continue;
|
|
51
|
+
|
|
52
|
+
let mutated = false;
|
|
53
|
+
const next = findings.map((f: unknown) => {
|
|
54
|
+
if (f === null || typeof f !== "object") return f;
|
|
55
|
+
const obj = f as Record<string, unknown>;
|
|
56
|
+
// Skip if already renamed
|
|
57
|
+
if ("observation" in obj) return f;
|
|
58
|
+
|
|
59
|
+
const renamed_finding: Record<string, unknown> = { ...obj };
|
|
60
|
+
if ("description" in renamed_finding) {
|
|
61
|
+
renamed_finding.observation = renamed_finding.description;
|
|
62
|
+
delete renamed_finding.description;
|
|
63
|
+
mutated = true;
|
|
64
|
+
}
|
|
65
|
+
if ("recommendation" in renamed_finding) {
|
|
66
|
+
renamed_finding.proposedAction = renamed_finding.recommendation;
|
|
67
|
+
delete renamed_finding.recommendation;
|
|
68
|
+
mutated = true;
|
|
69
|
+
}
|
|
70
|
+
return renamed_finding;
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
if (mutated) {
|
|
74
|
+
renamed += next.length;
|
|
75
|
+
db.query("UPDATE analyses SET findings_json = ? WHERE id = ?").run(
|
|
76
|
+
JSON.stringify(next),
|
|
77
|
+
row.id,
|
|
78
|
+
);
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
});
|
|
82
|
+
txn();
|
|
83
|
+
return renamed;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
export function runMigrations(db: Database): void {
|
|
87
|
+
// Ensure schema_version table exists first
|
|
88
|
+
db.exec(
|
|
89
|
+
"CREATE TABLE IF NOT EXISTS schema_version (version INTEGER PRIMARY KEY, applied_at INTEGER NOT NULL, description TEXT);",
|
|
90
|
+
);
|
|
91
|
+
|
|
92
|
+
// Get current version
|
|
93
|
+
const row = db
|
|
94
|
+
.query<{ version: number }, []>("SELECT MAX(version) as version FROM schema_version")
|
|
95
|
+
.get();
|
|
96
|
+
const current = row?.version ?? 0;
|
|
97
|
+
|
|
98
|
+
// Apply pending migrations
|
|
99
|
+
for (const m of MIGRATIONS) {
|
|
100
|
+
if (m.version > current) {
|
|
101
|
+
const txn = db.transaction(() => {
|
|
102
|
+
// v5: add archived_at columns BEFORE running SQL that creates indexes
|
|
103
|
+
// on those columns. SQLite 3.45 lacks ADD COLUMN IF NOT EXISTS, so we
|
|
104
|
+
// check PRAGMA table_info first (addColumnIfMissing helper).
|
|
105
|
+
if (m.version === 5) {
|
|
106
|
+
addColumnIfMissing(db, "plans", "archived_at", "INTEGER");
|
|
107
|
+
addColumnIfMissing(db, "plan_tasks", "archived_at", "INTEGER");
|
|
108
|
+
addColumnIfMissing(db, "sessions", "archived_at", "INTEGER");
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
// v6: write-once audit trail — original_plan_data on plans + plan_tasks
|
|
112
|
+
if (m.version === 6) {
|
|
113
|
+
addColumnIfMissing(db, "plans", "original_plan_data", "TEXT");
|
|
114
|
+
addColumnIfMissing(db, "plan_tasks", "original_plan_data", "TEXT");
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
// v8: agent execution tracking on plans
|
|
118
|
+
if (m.version === 8) {
|
|
119
|
+
addColumnIfMissing(db, "plans", "created_by_agent", "TEXT");
|
|
120
|
+
addColumnIfMissing(db, "plans", "executed_by_agent", "TEXT");
|
|
121
|
+
addColumnIfMissing(db, "plans", "executed_by_session", "TEXT");
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
// v15: backfill finding keys (description→observation, recommendation→proposedAction)
|
|
125
|
+
// Data-only migration — runs in same transaction so a failure rolls back the
|
|
126
|
+
// schema_version insert (which would otherwise leave an inconsistent state).
|
|
127
|
+
if (m.version === 15) {
|
|
128
|
+
backfillAnalysisFindings(db);
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
// Execute SQL only if it contains actual statements (not just comments)
|
|
132
|
+
const hasStatements = m.sql.split("\n").some((line) => {
|
|
133
|
+
const trimmed = line.trim();
|
|
134
|
+
return trimmed.length > 0 && !trimmed.startsWith("--");
|
|
135
|
+
});
|
|
136
|
+
if (hasStatements) {
|
|
137
|
+
db.exec(m.sql);
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
db.query(
|
|
141
|
+
"INSERT INTO schema_version (version, applied_at, description) VALUES (?, ?, ?)",
|
|
142
|
+
).run(m.version, Date.now(), m.description);
|
|
143
|
+
});
|
|
144
|
+
txn();
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
}
|
|
@@ -0,0 +1,180 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tests for plan-archive serialization — verifies original_plan_data inclusion.
|
|
3
|
+
*
|
|
4
|
+
* Uses in-memory SQLite via bun:sqlite. Each test gets a fresh DB
|
|
5
|
+
* with the full schema applied by runMigrations.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { Database } from "bun:sqlite";
|
|
9
|
+
import { beforeEach, describe, expect, test } from "bun:test";
|
|
10
|
+
import { runMigrations } from "./migrations.ts";
|
|
11
|
+
import { serializePlanToMarkdown } from "./plan-archive.ts";
|
|
12
|
+
import { createPlan } from "./plans.ts";
|
|
13
|
+
import type { Plan } from "./types.ts";
|
|
14
|
+
|
|
15
|
+
let db: Database;
|
|
16
|
+
|
|
17
|
+
beforeEach(() => {
|
|
18
|
+
db = new Database(":memory:");
|
|
19
|
+
db.exec("PRAGMA foreign_keys = ON");
|
|
20
|
+
runMigrations(db);
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
function makePlan(overrides: Partial<Parameters<typeof createPlan>[1]> = {}): Plan {
|
|
24
|
+
return createPlan(db, {
|
|
25
|
+
id: crypto.randomUUID(),
|
|
26
|
+
slug: "test-plan",
|
|
27
|
+
title: "Test",
|
|
28
|
+
status: "draft",
|
|
29
|
+
priority: 2,
|
|
30
|
+
approvedAt: null,
|
|
31
|
+
completedAt: null,
|
|
32
|
+
sessionId: null,
|
|
33
|
+
overview: "test overview",
|
|
34
|
+
approach: null,
|
|
35
|
+
complexity: 3,
|
|
36
|
+
createdBy: "test",
|
|
37
|
+
updatedBy: "test",
|
|
38
|
+
sourceSessionId: null,
|
|
39
|
+
sourceMessageId: null,
|
|
40
|
+
category: null,
|
|
41
|
+
metadata: {},
|
|
42
|
+
archivedAt: null,
|
|
43
|
+
...overrides,
|
|
44
|
+
});
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
describe("serializePlanToMarkdown", () => {
|
|
48
|
+
test("includes original_plan_data section when present", () => {
|
|
49
|
+
const plan = makePlan();
|
|
50
|
+
// originalPlanData is set by createPlan
|
|
51
|
+
const opd = plan.originalPlanData;
|
|
52
|
+
expect(opd).not.toBeNull();
|
|
53
|
+
|
|
54
|
+
const md = serializePlanToMarkdown(plan, [], [], Date.now());
|
|
55
|
+
|
|
56
|
+
expect(md).toContain("## Original Plan Data (write-once)");
|
|
57
|
+
expect(md).toContain("```json");
|
|
58
|
+
expect(md).toContain(opd as string);
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
test("omits original_plan_data section when null", () => {
|
|
62
|
+
// Construct a raw Plan object without going through createPlan
|
|
63
|
+
// (createPlan always sets originalPlanData)
|
|
64
|
+
const plan: Plan = {
|
|
65
|
+
id: crypto.randomUUID(),
|
|
66
|
+
slug: "no-opd",
|
|
67
|
+
title: "No OPD",
|
|
68
|
+
status: "draft",
|
|
69
|
+
priority: 2,
|
|
70
|
+
createdAt: Date.now(),
|
|
71
|
+
updatedAt: Date.now(),
|
|
72
|
+
approvedAt: null,
|
|
73
|
+
completedAt: null,
|
|
74
|
+
sessionId: null,
|
|
75
|
+
overview: "test",
|
|
76
|
+
approach: null,
|
|
77
|
+
complexity: 3,
|
|
78
|
+
createdBy: "test",
|
|
79
|
+
updatedBy: "test",
|
|
80
|
+
sourceSessionId: null,
|
|
81
|
+
sourceMessageId: null,
|
|
82
|
+
category: null,
|
|
83
|
+
metadata: {},
|
|
84
|
+
archivedAt: null,
|
|
85
|
+
originalPlanData: null,
|
|
86
|
+
};
|
|
87
|
+
|
|
88
|
+
const md = serializePlanToMarkdown(plan, [], [], Date.now());
|
|
89
|
+
|
|
90
|
+
expect(md).not.toContain("## Original Plan Data (write-once)");
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
test("includes agent trail section when createdByAgent is set", () => {
|
|
94
|
+
const plan = makePlan({ createdByAgent: "foreman" });
|
|
95
|
+
|
|
96
|
+
const md = serializePlanToMarkdown(plan, [], [], Date.now());
|
|
97
|
+
|
|
98
|
+
expect(md).toContain("## Agent Trail");
|
|
99
|
+
expect(md).toContain("**Created by agent:** foreman");
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
test("includes executed_by_agent when set", () => {
|
|
103
|
+
const plan = makePlan({
|
|
104
|
+
createdByAgent: "foreman",
|
|
105
|
+
executedByAgent: "craftsman",
|
|
106
|
+
executedBySession: "ses_123",
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
const md = serializePlanToMarkdown(plan, [], [], Date.now());
|
|
110
|
+
|
|
111
|
+
expect(md).toContain("**Executed by agent:** craftsman");
|
|
112
|
+
expect(md).toContain("**Executed by session:** ses_123");
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
test("JSON with triple backticks does not break markdown", () => {
|
|
116
|
+
// Construct plan with originalPlanData containing triple backticks
|
|
117
|
+
const plan: Plan = {
|
|
118
|
+
id: crypto.randomUUID(),
|
|
119
|
+
slug: "backtick-test",
|
|
120
|
+
title: "Backtick Test",
|
|
121
|
+
status: "draft",
|
|
122
|
+
priority: 2,
|
|
123
|
+
createdAt: Date.now(),
|
|
124
|
+
updatedAt: Date.now(),
|
|
125
|
+
approvedAt: null,
|
|
126
|
+
completedAt: null,
|
|
127
|
+
sessionId: null,
|
|
128
|
+
overview: "test",
|
|
129
|
+
approach: null,
|
|
130
|
+
complexity: 3,
|
|
131
|
+
createdBy: "test",
|
|
132
|
+
updatedBy: "test",
|
|
133
|
+
sourceSessionId: null,
|
|
134
|
+
sourceMessageId: null,
|
|
135
|
+
category: null,
|
|
136
|
+
metadata: {},
|
|
137
|
+
archivedAt: null,
|
|
138
|
+
originalPlanData: '{"code":"```js\\nconsole.log(1)\\n```"}',
|
|
139
|
+
};
|
|
140
|
+
|
|
141
|
+
const md = serializePlanToMarkdown(plan, [], [], Date.now());
|
|
142
|
+
|
|
143
|
+
// Should contain the sanitized version
|
|
144
|
+
expect(md).toContain("## Original Plan Data (write-once)");
|
|
145
|
+
// The triple backticks should be escaped so inner code blocks don't break markdown
|
|
146
|
+
expect(md).not.toContain("```js\nconsole.log(1)\n```");
|
|
147
|
+
// Verify the escaped version is present instead
|
|
148
|
+
expect(md).toContain("\\`\\`\\`js");
|
|
149
|
+
});
|
|
150
|
+
|
|
151
|
+
test("JSON without triple backticks is unmodified", () => {
|
|
152
|
+
const plan: Plan = {
|
|
153
|
+
id: crypto.randomUUID(),
|
|
154
|
+
slug: "normal-test",
|
|
155
|
+
title: "Normal Test",
|
|
156
|
+
status: "draft",
|
|
157
|
+
priority: 2,
|
|
158
|
+
createdAt: Date.now(),
|
|
159
|
+
updatedAt: Date.now(),
|
|
160
|
+
approvedAt: null,
|
|
161
|
+
completedAt: null,
|
|
162
|
+
sessionId: null,
|
|
163
|
+
overview: "test",
|
|
164
|
+
approach: null,
|
|
165
|
+
complexity: 3,
|
|
166
|
+
createdBy: "test",
|
|
167
|
+
updatedBy: "test",
|
|
168
|
+
sourceSessionId: null,
|
|
169
|
+
sourceMessageId: null,
|
|
170
|
+
category: null,
|
|
171
|
+
metadata: {},
|
|
172
|
+
archivedAt: null,
|
|
173
|
+
originalPlanData: '{"slug":"test","title":"Test"}',
|
|
174
|
+
};
|
|
175
|
+
|
|
176
|
+
const md = serializePlanToMarkdown(plan, [], [], Date.now());
|
|
177
|
+
|
|
178
|
+
expect(md).toContain('{"slug":"test","title":"Test"}');
|
|
179
|
+
});
|
|
180
|
+
});
|