@deksden-com/dd-flow-cli 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.
Files changed (35) hide show
  1. package/README.md +274 -0
  2. package/dist/cli/help.js +308 -0
  3. package/dist/cli/run-cli.js +945 -0
  4. package/dist/cli.js +4 -0
  5. package/dist/domain/contracts.js +57 -0
  6. package/dist/domain/entity-ids.js +47 -0
  7. package/dist/domain/flow-contract.js +233 -0
  8. package/dist/domain/validation.js +91 -0
  9. package/dist/protocol/local-files.js +141 -0
  10. package/dist/runtime/context.js +11 -0
  11. package/dist/schemas/code-stage-report.schema.json +181 -0
  12. package/dist/schemas/flow-run-index.schema.json +129 -0
  13. package/dist/schemas/mb-upgrade-review-data.schema.json +813 -0
  14. package/dist/schemas/memorybank-permissions-preflight.schema.json +154 -0
  15. package/dist/schemas/merge-stage-report.schema.json +135 -0
  16. package/dist/services/audit.js +19 -0
  17. package/dist/services/cleanup.js +310 -0
  18. package/dist/services/config.js +143 -0
  19. package/dist/services/dashboard.js +436 -0
  20. package/dist/services/hooks.js +929 -0
  21. package/dist/services/lanes.js +327 -0
  22. package/dist/services/memory-permissions.js +344 -0
  23. package/dist/services/merge-queue.js +333 -0
  24. package/dist/services/plans.js +149 -0
  25. package/dist/services/projects.js +286 -0
  26. package/dist/services/protocols.js +606 -0
  27. package/dist/services/runs.js +359 -0
  28. package/dist/services/schema-validation.js +185 -0
  29. package/dist/services/sessions.js +365 -0
  30. package/dist/services/worktrees.js +204 -0
  31. package/dist/shared/errors.js +14 -0
  32. package/dist/shared/json.js +17 -0
  33. package/dist/storage/database.js +325 -0
  34. package/dist/storage/paths.js +56 -0
  35. package/package.json +44 -0
package/dist/cli.js ADDED
@@ -0,0 +1,4 @@
1
+ #!/usr/bin/env node
2
+ import { runCli } from "./cli/run-cli.js";
3
+ const exitCode = await runCli(process.argv.slice(2));
4
+ process.exitCode = exitCode;
@@ -0,0 +1,57 @@
1
+ import { canTransition as canFlowTransition, defaultFlowContract, isAllowedStage as isContractStage } from "./flow-contract.js";
2
+ export const schemaVersion = "0.1.0";
3
+ export const allowedPlanKinds = [
4
+ "analysis",
5
+ "decision",
6
+ "implementation",
7
+ "documentation",
8
+ "check",
9
+ "verification",
10
+ "review",
11
+ "scenario",
12
+ "evidence",
13
+ "operations"
14
+ ];
15
+ export const allowedPlanStatuses = ["pending", "in_progress", "done", "blocked", "skipped"];
16
+ export const allowedStages = [
17
+ "registered",
18
+ "prime",
19
+ "plan",
20
+ "implementation",
21
+ "readiness",
22
+ "ready_for_merge",
23
+ "queued_for_merge",
24
+ "integration",
25
+ "cancelled",
26
+ "closed",
27
+ "blocked",
28
+ "waiting_for_user"
29
+ ];
30
+ export const mergeQueueStatuses = ["ready", "claimed", "merged", "failed", "requeued", "cancelled"];
31
+ export const mergeSessionStatuses = ["active", "stopping", "stopped", "expired"];
32
+ export const bootstrapStatuses = ["not_required", "pending", "running", "succeeded", "failed", "skipped"];
33
+ export const flowKinds = ["planning", "implementation", "merge_worker", "merge_job", "memory_flow", "research_no_protocol"];
34
+ export const flowSessionStatuses = ["pending", "active", "waiting_user", "blocked", "stopping", "stopped", "closed"];
35
+ export const continuationPolicies = ["none", "go_router", "implementation_plan", "merge_queue", "merge_job", "memory_flow"];
36
+ export const cmuxModes = ["off", "auto", "required"];
37
+ export function isActiveProtocolStatus(status) {
38
+ return !["closed", "closed_local", "cancelled"].includes(status);
39
+ }
40
+ export function isActiveQueueStatus(status) {
41
+ return ["ready", "claimed", "requeued"].includes(status);
42
+ }
43
+ export function isQueueHistoryStatus(status) {
44
+ return !isActiveQueueStatus(status);
45
+ }
46
+ export function isAllowedStage(value, contract = defaultFlowContract) {
47
+ return isContractStage(value, contract);
48
+ }
49
+ /**
50
+ * Mechanical v1 transition graph for protocol stages.
51
+ *
52
+ * @docs .memory-bank/spec/system/v1-observable-state.md
53
+ * @scenario .memory-bank/scenarios/SCN-002-transition-audit.md
54
+ */
55
+ export function canTransition(from, to, contract = defaultFlowContract) {
56
+ return canFlowTransition(from, to, contract);
57
+ }
@@ -0,0 +1,47 @@
1
+ import path from "node:path";
2
+ import { AppError } from "../shared/errors.js";
3
+ const fullIdPattern = /^([A-Z]{2,4})-(\d{3})-([a-z0-9]+(?:-[a-z0-9]+)*)$/;
4
+ const shortIdPattern = /^([A-Z]{2,4})-\d{3}$/;
5
+ export function parseFullEntityId(id) {
6
+ const match = fullIdPattern.exec(id);
7
+ if (!match?.[1] || !match[2] || !match[3]) {
8
+ throw new AppError("validation", `Invalid typed entity id: ${id}`, 2, {
9
+ expected: "TYPE-NNN-slug"
10
+ });
11
+ }
12
+ return {
13
+ type: match[1],
14
+ shortId: `${match[1]}-${match[2]}`,
15
+ slug: match[3],
16
+ fullId: id
17
+ };
18
+ }
19
+ export function isFullEntityId(id) {
20
+ return fullIdPattern.test(id);
21
+ }
22
+ export function isShortEntityId(id) {
23
+ return shortIdPattern.test(id);
24
+ }
25
+ export function slugFromRoot(root) {
26
+ const basename = path.basename(root);
27
+ const slug = basename
28
+ .normalize("NFKD")
29
+ .replace(/[\u0300-\u036f]/g, "")
30
+ .toLowerCase()
31
+ .replace(/[^a-z0-9]+/g, "-")
32
+ .replace(/^-+|-+$/g, "")
33
+ .replace(/-{2,}/g, "-");
34
+ return slug || "project";
35
+ }
36
+ export function formatFullId(type, sequence, slug) {
37
+ if (!/^[A-Z]{2,4}$/.test(type)) {
38
+ throw new AppError("validation", `Invalid entity type: ${type}`, 2);
39
+ }
40
+ if (sequence < 1 || sequence > 999) {
41
+ throw new AppError("validation", `Entity sequence is out of range: ${sequence}`, 2);
42
+ }
43
+ return `${type}-${String(sequence).padStart(3, "0")}-${slug}`;
44
+ }
45
+ export function shortIdForFullId(id) {
46
+ return parseFullEntityId(id).shortId;
47
+ }
@@ -0,0 +1,233 @@
1
+ import fs from "node:fs";
2
+ import path from "node:path";
3
+ import { parse as parseYaml } from "yaml";
4
+ import { AppError } from "../shared/errors.js";
5
+ import { parseJsonObject } from "../shared/json.js";
6
+ export const defaultFlowContract = {
7
+ id: "dd-flow-canonical-2026-05",
8
+ version: 1,
9
+ stages: {
10
+ registered: { terminal: false },
11
+ prime: { terminal: false },
12
+ plan: { terminal: false },
13
+ implementation: { terminal: false },
14
+ readiness: { terminal: false },
15
+ ready_for_merge: { terminal: false },
16
+ queued_for_merge: { terminal: false },
17
+ integration: { terminal: false },
18
+ cancelled: { terminal: true },
19
+ closed: { terminal: true },
20
+ blocked: { terminal: false },
21
+ waiting_for_user: { terminal: false }
22
+ },
23
+ transitions: {
24
+ registered: ["prime", "blocked", "waiting_for_user"],
25
+ prime: ["plan", "blocked", "waiting_for_user"],
26
+ plan: ["implementation", "blocked", "waiting_for_user"],
27
+ implementation: ["readiness", "blocked", "waiting_for_user"],
28
+ readiness: ["ready_for_merge", "blocked", "waiting_for_user"],
29
+ ready_for_merge: ["queued_for_merge", "blocked", "waiting_for_user"],
30
+ queued_for_merge: ["integration", "blocked", "waiting_for_user"],
31
+ integration: ["closed", "blocked", "waiting_for_user"],
32
+ cancelled: [],
33
+ closed: [],
34
+ blocked: ["prime", "plan", "implementation", "readiness", "ready_for_merge", "closed"],
35
+ waiting_for_user: ["prime", "plan", "implementation", "readiness", "blocked", "closed"]
36
+ },
37
+ readiness: {
38
+ command: "ready-for-merge",
39
+ allowed_from: ["readiness", "ready_for_merge"],
40
+ target_stage: "ready_for_merge"
41
+ },
42
+ merge_queue: {
43
+ complete: {
44
+ target_stage: "closed",
45
+ next_action: "none"
46
+ }
47
+ },
48
+ defaults: {
49
+ registered_next_action: "run_prime_or_feature_plan"
50
+ },
51
+ legacy_aliases: {
52
+ g0: "prime",
53
+ m1: "readiness",
54
+ m2: "integration"
55
+ }
56
+ };
57
+ export function loadProjectFlowContract(projectRoot) {
58
+ const contractPath = findProjectFlowContractPath(projectRoot);
59
+ if (!contractPath) {
60
+ return defaultFlowContract;
61
+ }
62
+ return normalizeFlowContract(readFlowContractObject(contractPath), contractPath);
63
+ }
64
+ export function flowContractForState(state) {
65
+ if (!state.flow_contract) {
66
+ return defaultFlowContract;
67
+ }
68
+ if (typeof state.flow_contract !== "object" || Array.isArray(state.flow_contract)) {
69
+ return defaultFlowContract;
70
+ }
71
+ return normalizeFlowContract(state.flow_contract, "state.flow_contract");
72
+ }
73
+ export function normalizeStage(value, contract) {
74
+ return contract.legacy_aliases[value] ?? value;
75
+ }
76
+ export function isAllowedStage(value, contract = defaultFlowContract) {
77
+ return Object.prototype.hasOwnProperty.call(contract.stages, normalizeStage(value, contract));
78
+ }
79
+ export function requireContractStage(value, contract = defaultFlowContract) {
80
+ const stage = normalizeStage(value, contract);
81
+ if (!Object.prototype.hasOwnProperty.call(contract.stages, stage)) {
82
+ throw new AppError("validation", `Unknown protocol stage: ${value}`, 2, {
83
+ stage: value,
84
+ flow_contract_id: contract.id,
85
+ allowed: Object.keys(contract.stages)
86
+ });
87
+ }
88
+ return stage;
89
+ }
90
+ export function canTransition(from, to, contract = defaultFlowContract) {
91
+ const normalizedFrom = requireContractStage(from, contract);
92
+ const normalizedTo = requireContractStage(to, contract);
93
+ if (normalizedTo === "cancelled") {
94
+ return !contract.stages[normalizedFrom]?.terminal;
95
+ }
96
+ return contract.transitions[normalizedFrom]?.includes(normalizedTo) ?? false;
97
+ }
98
+ function normalizeFlowContract(value, label) {
99
+ const id = requireString(value.id, `${label}.id`);
100
+ const version = requireNumber(value.version, `${label}.version`);
101
+ const stages = normalizeStages(value.stages, `${label}.stages`);
102
+ const legacyAliases = normalizeStringMap(value.legacy_aliases ?? {}, `${label}.legacy_aliases`);
103
+ const transitions = normalizeTransitions(value.transitions, `${label}.transitions`, stages, legacyAliases);
104
+ const readiness = normalizeReadiness(value.readiness, `${label}.readiness`, stages, legacyAliases);
105
+ const mergeQueue = normalizeMergeQueue(value.merge_queue, `${label}.merge_queue`, stages, legacyAliases);
106
+ const defaults = normalizeDefaults(value.defaults, `${label}.defaults`);
107
+ return { id, version, stages, transitions, readiness, merge_queue: mergeQueue, defaults, legacy_aliases: legacyAliases };
108
+ }
109
+ function findProjectFlowContractPath(projectRoot) {
110
+ const basePath = path.join(projectRoot, ".memory-bank", "dd-flow", "flow-contract");
111
+ for (const extension of [".json", ".yaml", ".yml"]) {
112
+ const candidate = `${basePath}${extension}`;
113
+ if (fs.existsSync(candidate)) {
114
+ return candidate;
115
+ }
116
+ }
117
+ return undefined;
118
+ }
119
+ function readFlowContractObject(file) {
120
+ const raw = fs.readFileSync(file, "utf8");
121
+ if (file.endsWith(".json")) {
122
+ return parseJsonObject(raw, file);
123
+ }
124
+ const value = parseYaml(raw);
125
+ if (!value || typeof value !== "object" || Array.isArray(value)) {
126
+ throw new AppError("validation", `${file} must contain an object`, 2);
127
+ }
128
+ return value;
129
+ }
130
+ function normalizeStages(value, label) {
131
+ if (!value || typeof value !== "object" || Array.isArray(value)) {
132
+ throw new AppError("validation", `${label} must be an object`, 2);
133
+ }
134
+ const stages = {};
135
+ for (const [stage, config] of Object.entries(value)) {
136
+ if (!config || typeof config !== "object" || Array.isArray(config)) {
137
+ throw new AppError("validation", `${label}.${stage} must be an object`, 2);
138
+ }
139
+ const terminal = config.terminal;
140
+ stages[stage] = { terminal: typeof terminal === "boolean" ? terminal : false };
141
+ }
142
+ if (!stages.registered || !stages.closed || !stages.cancelled) {
143
+ throw new AppError("validation", `${label} must include registered, closed, and cancelled stages`, 2);
144
+ }
145
+ return stages;
146
+ }
147
+ function normalizeTransitions(value, label, stages, aliases) {
148
+ if (!value || typeof value !== "object" || Array.isArray(value)) {
149
+ throw new AppError("validation", `${label} must be an object`, 2);
150
+ }
151
+ const transitions = {};
152
+ for (const [from, tos] of Object.entries(value)) {
153
+ const normalizedFrom = aliases[from] ?? from;
154
+ requireKnownStage(normalizedFrom, stages, `${label}.${from}`);
155
+ if (!Array.isArray(tos) || tos.some((to) => typeof to !== "string")) {
156
+ throw new AppError("validation", `${label}.${from} must be a string array`, 2);
157
+ }
158
+ transitions[normalizedFrom] = tos.map((to) => {
159
+ const normalizedTo = aliases[to] ?? to;
160
+ requireKnownStage(normalizedTo, stages, `${label}.${from}`);
161
+ return normalizedTo;
162
+ });
163
+ }
164
+ return transitions;
165
+ }
166
+ function normalizeReadiness(value, label, stages, aliases) {
167
+ const object = requireObject(value, label);
168
+ const command = requireString(object.command, `${label}.command`);
169
+ if (command !== "ready-for-merge") {
170
+ throw new AppError("validation", `${label}.command must be ready-for-merge`, 2);
171
+ }
172
+ if (!Array.isArray(object.allowed_from) || object.allowed_from.some((stage) => typeof stage !== "string")) {
173
+ throw new AppError("validation", `${label}.allowed_from must be a string array`, 2);
174
+ }
175
+ const allowedFrom = object.allowed_from.map((stage) => normalizeKnownStage(stage, stages, aliases, `${label}.allowed_from`));
176
+ const targetStage = normalizeKnownStage(requireString(object.target_stage, `${label}.target_stage`), stages, aliases, label);
177
+ return { command, allowed_from: allowedFrom, target_stage: targetStage };
178
+ }
179
+ function normalizeMergeQueue(value, label, stages, aliases) {
180
+ const object = requireObject(value, label);
181
+ const complete = requireObject(object.complete, `${label}.complete`);
182
+ return {
183
+ complete: {
184
+ target_stage: normalizeKnownStage(requireString(complete.target_stage, `${label}.complete.target_stage`), stages, aliases, label),
185
+ next_action: requireString(complete.next_action, `${label}.complete.next_action`)
186
+ }
187
+ };
188
+ }
189
+ function normalizeDefaults(value, label) {
190
+ const object = requireObject(value, label);
191
+ return { registered_next_action: requireString(object.registered_next_action, `${label}.registered_next_action`) };
192
+ }
193
+ function normalizeStringMap(value, label) {
194
+ if (!value || typeof value !== "object" || Array.isArray(value)) {
195
+ throw new AppError("validation", `${label} must be an object`, 2);
196
+ }
197
+ const output = {};
198
+ for (const [key, mapValue] of Object.entries(value)) {
199
+ if (typeof mapValue !== "string" || mapValue.length === 0) {
200
+ throw new AppError("validation", `${label}.${key} must be a non-empty string`, 2);
201
+ }
202
+ output[key] = mapValue;
203
+ }
204
+ return output;
205
+ }
206
+ function requireObject(value, label) {
207
+ if (!value || typeof value !== "object" || Array.isArray(value)) {
208
+ throw new AppError("validation", `${label} must be an object`, 2);
209
+ }
210
+ return value;
211
+ }
212
+ function requireString(value, label) {
213
+ if (typeof value !== "string" || value.length === 0) {
214
+ throw new AppError("validation", `${label} must be a non-empty string`, 2);
215
+ }
216
+ return value;
217
+ }
218
+ function requireNumber(value, label) {
219
+ if (typeof value !== "number" || !Number.isInteger(value) || value <= 0) {
220
+ throw new AppError("validation", `${label} must be a positive integer`, 2);
221
+ }
222
+ return value;
223
+ }
224
+ function normalizeKnownStage(value, stages, aliases, label) {
225
+ const normalized = aliases[value] ?? value;
226
+ requireKnownStage(normalized, stages, label);
227
+ return normalized;
228
+ }
229
+ function requireKnownStage(value, stages, label) {
230
+ if (!Object.prototype.hasOwnProperty.call(stages, value)) {
231
+ throw new AppError("validation", `${label} references unknown stage: ${value}`, 2);
232
+ }
233
+ }
@@ -0,0 +1,91 @@
1
+ import { AppError } from "../shared/errors.js";
2
+ import { allowedPlanKinds, allowedPlanStatuses, isAllowedStage } from "./contracts.js";
3
+ import { normalizeStage } from "./flow-contract.js";
4
+ export function requireStage(value, contract) {
5
+ if (!isAllowedStage(value, contract)) {
6
+ throw new AppError("validation", `Unknown protocol stage: ${value}`, 2);
7
+ }
8
+ return contract ? normalizeStage(value, contract) : value;
9
+ }
10
+ export function validatePlan(input, expectedProtocolId) {
11
+ const plan = input;
12
+ if (!plan || typeof plan !== "object" || Array.isArray(plan)) {
13
+ throw new AppError("validation", "Plan must be a JSON object", 2);
14
+ }
15
+ requireString(plan.schema_version, "schema_version");
16
+ requireString(plan.plan_id, "plan_id");
17
+ requireString(plan.protocol_id, "protocol_id");
18
+ requireString(plan.title, "title");
19
+ if (expectedProtocolId && plan.protocol_id !== expectedProtocolId) {
20
+ throw new AppError("validation", "Plan protocol_id does not match command protocol id", 2, {
21
+ expectedProtocolId,
22
+ actualProtocolId: plan.protocol_id
23
+ });
24
+ }
25
+ if (!Array.isArray(plan.items)) {
26
+ throw new AppError("validation", "Plan items must be an array", 2);
27
+ }
28
+ const ids = new Set();
29
+ const items = plan.items.map((item, index) => validatePlanItem(item, index));
30
+ for (const item of items) {
31
+ if (ids.has(item.id)) {
32
+ throw new AppError("validation", `Duplicate plan item id: ${item.id}`, 2);
33
+ }
34
+ ids.add(item.id);
35
+ }
36
+ for (const item of items) {
37
+ for (const dep of item.depends_on) {
38
+ if (!ids.has(dep)) {
39
+ throw new AppError("validation", `Plan item ${item.id} depends on missing item ${dep}`, 2);
40
+ }
41
+ }
42
+ }
43
+ return { ...plan, items };
44
+ }
45
+ function validatePlanItem(input, index) {
46
+ const item = input;
47
+ if (!item || typeof item !== "object" || Array.isArray(item)) {
48
+ throw new AppError("validation", `Plan item ${index} must be an object`, 2);
49
+ }
50
+ const id = requireString(item.id, `items[${index}].id`);
51
+ const kind = requireString(item.kind, `items[${index}].kind`);
52
+ if (!allowedPlanKinds.includes(kind)) {
53
+ throw new AppError("validation", `Invalid plan item kind for ${id}: ${kind}`, 2);
54
+ }
55
+ const status = requireString(item.status, `items[${index}].status`);
56
+ if (!allowedPlanStatuses.includes(status)) {
57
+ throw new AppError("validation", `Invalid plan item status for ${id}: ${status}`, 2);
58
+ }
59
+ if (!Array.isArray(item.depends_on) || item.depends_on.some((dep) => typeof dep !== "string")) {
60
+ throw new AppError("validation", `Plan item ${id} depends_on must be a string array`, 2);
61
+ }
62
+ if (!Array.isArray(item.evidence) || item.evidence.some((entry) => typeof entry !== "string")) {
63
+ throw new AppError("validation", `Plan item ${id} evidence must be a string array`, 2);
64
+ }
65
+ return {
66
+ id,
67
+ kind: kind,
68
+ title: requireString(item.title, `items[${index}].title`),
69
+ status: status,
70
+ depends_on: item.depends_on,
71
+ stage: requireString(item.stage, `items[${index}].stage`),
72
+ required: requireBoolean(item.required, `items[${index}].required`),
73
+ owner: requireString(item.owner, `items[${index}].owner`),
74
+ summary: typeof item.summary === "string" ? item.summary : "",
75
+ evidence: item.evidence,
76
+ ...(typeof item.block_reason === "string" ? { block_reason: item.block_reason } : {}),
77
+ ...(typeof item.user_required === "boolean" ? { user_required: item.user_required } : {})
78
+ };
79
+ }
80
+ function requireString(value, label) {
81
+ if (typeof value !== "string" || value.length === 0) {
82
+ throw new AppError("validation", `${label} must be a non-empty string`, 2);
83
+ }
84
+ return value;
85
+ }
86
+ function requireBoolean(value, label) {
87
+ if (typeof value !== "boolean") {
88
+ throw new AppError("validation", `${label} must be a boolean`, 2);
89
+ }
90
+ return value;
91
+ }
@@ -0,0 +1,141 @@
1
+ import fs from "node:fs";
2
+ import path from "node:path";
3
+ import { schemaVersion } from "../domain/contracts.js";
4
+ import { loadProjectFlowContract } from "../domain/flow-contract.js";
5
+ import { ensureDir, planJsonPath, protocolDir, stateJsonPath } from "../storage/paths.js";
6
+ export function ensureProtocolFiles(input) {
7
+ const storageRoot = input.storageRoot ?? input.projectRoot;
8
+ const dir = protocolDir(storageRoot, input.protocolId);
9
+ ensureDir(dir);
10
+ const summaryPath = path.join(dir, "summary.md");
11
+ if (!fs.existsSync(summaryPath)) {
12
+ fs.writeFileSync(summaryPath, [
13
+ "---",
14
+ `file: '.memory-bank/protocol/${input.protocolId}/summary.md'`,
15
+ `description: 'Protocol trace for ${input.protocolId}.'`,
16
+ `purpose: 'Read for the active dd-flow protocol trace, current stage, evidence, and closure state.'`,
17
+ "version: '0.1.0'",
18
+ `date: '${input.now.slice(0, 10)}'`,
19
+ "status: 'ACTIVE'",
20
+ "---",
21
+ "",
22
+ `# ${input.protocolId}`,
23
+ "",
24
+ `Registered by handshake id \`${input.handshakeId}\`.`,
25
+ ""
26
+ ].join("\n"));
27
+ }
28
+ const statePath = stateJsonPath(storageRoot, input.protocolId);
29
+ const existing = readStateIfExists(statePath);
30
+ const flowContract = existing?.flow_contract ?? loadProjectFlowContract(input.projectRoot);
31
+ const state = existing ??
32
+ {
33
+ schema_version: schemaVersion,
34
+ protocol_id: input.protocolId,
35
+ project_root: input.projectRoot,
36
+ status: "registered",
37
+ stage: "registered",
38
+ next_action: flowContract.defaults.registered_next_action,
39
+ route: {
40
+ planning: "feature_plan",
41
+ git: "feature_worktree",
42
+ delivery: "local",
43
+ ci: "none"
44
+ },
45
+ workspace: {
46
+ integration_branch: null,
47
+ feature_branch: null,
48
+ worktree_path: null
49
+ },
50
+ plan: {
51
+ plan_id: null,
52
+ total: 0,
53
+ done: 0,
54
+ blocked: 0
55
+ },
56
+ blockers: [],
57
+ active_def: [],
58
+ flow_contract: flowContract,
59
+ updated_at: input.now
60
+ };
61
+ writeState(statePath, { ...state, flow_contract: flowContract, updated_at: input.now });
62
+ return { ...state, flow_contract: flowContract, updated_at: input.now };
63
+ }
64
+ export function ensureRuntimeProtocolFiles(input) {
65
+ ensureDir(input.runtimeDir);
66
+ const metadataPath = path.join(input.runtimeDir, "metadata.json");
67
+ if (!fs.existsSync(metadataPath)) {
68
+ fs.writeFileSync(metadataPath, `${JSON.stringify({ protocol_id: input.protocolId, handshake_id: input.handshakeId, project_root: input.projectRoot }, null, 2)}\n`);
69
+ }
70
+ const statePath = path.join(input.runtimeDir, "state.json");
71
+ const existing = readStateIfExists(statePath);
72
+ const flowContract = existing?.flow_contract ?? loadProjectFlowContract(input.projectRoot);
73
+ const state = existing ??
74
+ {
75
+ schema_version: schemaVersion,
76
+ protocol_id: input.protocolId,
77
+ project_root: input.projectRoot,
78
+ status: "registered",
79
+ stage: "registered",
80
+ next_action: flowContract.defaults.registered_next_action,
81
+ route: {
82
+ planning: "feature_plan",
83
+ git: "feature_worktree",
84
+ delivery: "local",
85
+ ci: "none"
86
+ },
87
+ workspace: {
88
+ integration_branch: null,
89
+ feature_branch: null,
90
+ worktree_path: null
91
+ },
92
+ plan: {
93
+ plan_id: null,
94
+ total: 0,
95
+ done: 0,
96
+ blocked: 0
97
+ },
98
+ blockers: [],
99
+ active_def: [],
100
+ flow_contract: flowContract,
101
+ updated_at: input.now
102
+ };
103
+ const workspace = input.workspacePath ? { ...state.workspace, worktree_path: input.workspacePath } : state.workspace;
104
+ const nextState = { ...state, workspace, flow_contract: flowContract, updated_at: input.now };
105
+ writeState(statePath, nextState);
106
+ return nextState;
107
+ }
108
+ export function readState(projectRoot, protocolId) {
109
+ return readStateFile(stateJsonPath(projectRoot, protocolId), protocolId);
110
+ }
111
+ export function readStateFile(file, protocolId) {
112
+ const state = readStateIfExists(file);
113
+ if (!state) {
114
+ throw new Error(`Missing state.json for protocol ${protocolId}`);
115
+ }
116
+ return state;
117
+ }
118
+ export function writeState(file, state) {
119
+ fs.writeFileSync(file, `${JSON.stringify(state, null, 2)}\n`);
120
+ }
121
+ export function readPlan(projectRoot, protocolId) {
122
+ return readPlanFile(planJsonPath(projectRoot, protocolId));
123
+ }
124
+ export function readPlanFile(file) {
125
+ if (!fs.existsSync(file)) {
126
+ return undefined;
127
+ }
128
+ return JSON.parse(fs.readFileSync(file, "utf8"));
129
+ }
130
+ export function writePlan(projectRoot, protocolId, plan) {
131
+ writePlanFile(planJsonPath(projectRoot, protocolId), plan);
132
+ }
133
+ export function writePlanFile(file, plan) {
134
+ fs.writeFileSync(file, `${JSON.stringify(plan, null, 2)}\n`);
135
+ }
136
+ function readStateIfExists(file) {
137
+ if (!fs.existsSync(file)) {
138
+ return undefined;
139
+ }
140
+ return JSON.parse(fs.readFileSync(file, "utf8"));
141
+ }
@@ -0,0 +1,11 @@
1
+ import { getDatabase } from "../storage/database.js";
2
+ import { resolveDdFlowHome } from "../storage/paths.js";
3
+ export function createContext(env) {
4
+ const ddFlowHome = resolveDdFlowHome(env);
5
+ return {
6
+ ddFlowHome,
7
+ db: getDatabase(ddFlowHome),
8
+ env,
9
+ now: () => new Date().toISOString()
10
+ };
11
+ }