@deksden-com/dd-flow-cli 0.6.0 → 0.8.0-beta.135
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/CHANGELOG.md +636 -0
- package/README.md +13 -1
- package/dist/build-info.json +10 -10
- package/dist/cli/help.js +108 -18
- package/dist/cli/run-cli.js +626 -49
- package/dist/domain/flow-contract.js +11 -0
- package/dist/domain/stage-catalog.js +22 -0
- package/dist/runtime/context.js +8 -2
- package/dist/schemas/code-review-decision.schema.json +26 -0
- package/dist/schemas/code-review-result.schema.json +14 -0
- package/dist/schemas/code-stage-report.schema.json +7 -2
- package/dist/schemas/code-verification.schema.json +14 -0
- package/dist/schemas/code-work-batch.schema.json +24 -0
- package/dist/schemas/code-work-result.schema.json +16 -0
- package/dist/schemas/engine-manifest.schema.json +22 -0
- package/dist/schemas/flow-contract.schema.json +6 -3
- package/dist/schemas/flow-run.schema.json +16 -122
- package/dist/schemas/mb-upgrade-migration-report.schema.json +3 -1
- package/dist/schemas/merge-stage-report-legacy-0.4.2.schema.json +24 -0
- package/dist/schemas/plan-aspect-map.schema.json +22 -0
- package/dist/schemas/plan-review-decision.schema.json +14 -0
- package/dist/schemas/plan-review-result.schema.json +42 -0
- package/dist/schemas/protocol-plan.schema.json +15 -182
- package/dist/schemas/run-engine-binding.schema.json +37 -0
- package/dist/schemas/stage-finish-input.schema.json +16 -2
- package/dist/schemas/stage-prompt.schema.json +4 -4
- package/dist/schemas/stage-report.schema.json +8 -7
- package/dist/schemas/vnext-protocol-plan.schema.json +37 -0
- package/dist/schemas/vnext-protocolize-result.schema.json +29 -0
- package/dist/schemas/vnext-specify.schema.json +45 -0
- package/dist/services/branch-context.js +1 -1
- package/dist/services/canon.js +15 -1
- package/dist/services/cleanup.js +8 -8
- package/dist/services/cli-operation-classifier.js +60 -8
- package/dist/services/code-checks.js +244 -0
- package/dist/services/compatibility-preflight.js +1 -1
- package/dist/services/config.js +7 -1
- package/dist/services/dashboard.js +14 -14
- package/dist/services/engines.js +408 -30
- package/dist/services/eval-snapshots.js +404 -0
- package/dist/services/hooks.js +775 -23
- package/dist/services/ids.js +16 -6
- package/dist/services/lanes.js +1 -5
- package/dist/services/merge-queue.js +53 -5
- package/dist/services/merge-worker.js +5 -6
- package/dist/services/migrations.js +307 -44
- package/dist/services/plan-runtime.js +5 -5
- package/dist/services/plans.js +5 -3
- package/dist/services/projects.js +4 -4
- package/dist/services/prompts.js +1 -1
- package/dist/services/protocols.js +31 -10
- package/dist/services/run-engine-bindings.js +157 -0
- package/dist/services/run-projection.js +49 -13
- package/dist/services/runs.js +525 -58
- package/dist/services/schema-validation.js +116 -2
- package/dist/services/sessions.js +51 -12
- package/dist/services/stage-blocker.js +57 -0
- package/dist/services/stage-context.js +90 -0
- package/dist/services/stage-lifecycle.js +288 -77
- package/dist/services/stage-pause.js +175 -0
- package/dist/services/stage-report-renderer.js +65 -0
- package/dist/services/status.js +8 -3
- package/dist/services/usage.js +526 -18
- package/dist/services/vnext-code-review.js +308 -0
- package/dist/services/vnext-code.js +616 -0
- package/dist/services/vnext-contracts.js +1 -0
- package/dist/services/vnext-execution-profile.js +27 -0
- package/dist/services/vnext-fanout.js +79 -0
- package/dist/services/vnext-plan-review.js +499 -0
- package/dist/services/vnext-plan.js +576 -0
- package/dist/services/vnext-protocolize.js +542 -0
- package/dist/services/vnext-specify.js +595 -0
- package/dist/services/vnext-workspace-policy.js +87 -0
- package/dist/services/work-registry.js +499 -0
- package/dist/services/worktrees.js +58 -37
- package/dist/storage/database.js +292 -42
- package/dist/storage/paths.js +47 -1
- package/package.json +12 -12
|
@@ -37,53 +37,67 @@ export function createWorktreeRecord(context, input) {
|
|
|
37
37
|
worktree_path: existing.worktree_path
|
|
38
38
|
});
|
|
39
39
|
}
|
|
40
|
+
const created = createManagedWorktree(context, { projectRoot: protocol.project_root, branch: input.branch, base: input.base, path: input.path });
|
|
41
|
+
recordManagedWorktree(context, { protocolId: protocol.id, projectRoot: protocol.project_root, branch: input.branch, base: input.base, created });
|
|
42
|
+
return { ok: true, worktree: activeWorktreeRecord(context, protocol.project_id, protocol.id) };
|
|
43
|
+
}
|
|
44
|
+
/** Creates the checkout before a PRT exists. PROTOCOLIZE materializes the PRT only inside this checkout. */
|
|
45
|
+
export function createManagedWorktree(context, input) {
|
|
46
|
+
const projectRoot = resolveProjectRoot(input.projectRoot);
|
|
40
47
|
const worktrunk = ensureManagedWorktrunk(context);
|
|
41
|
-
|
|
48
|
+
// Worktrunk owns creation; the CLI supplies its project-scoped service path
|
|
49
|
+
// as a one-shot config override rather than relying on a developer's user config.
|
|
50
|
+
const command = [worktrunk.bin, "-C", projectRoot, "--config-set", `worktree-path = ${JSON.stringify(path.resolve(input.path))}`, "switch", "--create", input.branch, "--base", input.base, "--no-cd", "--format=json", "-y"];
|
|
42
51
|
const result = runExternal(command);
|
|
43
|
-
if (result.exit_code !== 0)
|
|
52
|
+
if (result.exit_code !== 0)
|
|
44
53
|
throw new AppError("worktrunk_command_unavailable", "Worktrunk switch failed", 1, { command: sanitizeCommand(command), result });
|
|
45
|
-
}
|
|
46
54
|
const response = parseWorktrunkResponse(result.stdout, command);
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
55
|
+
if (isTrackedProjectPath(projectRoot, response.workspace_path))
|
|
56
|
+
throw new AppError("worktree_active", "Worktrunk returned a workspace inside tracked project files", 1, { path: response.workspace_path });
|
|
57
|
+
return { worktreePath: response.workspace_path, worktrunk, command, result, response };
|
|
58
|
+
}
|
|
59
|
+
/** Attaches an already-created managed checkout to a newly materialized PRT. */
|
|
60
|
+
export function recordManagedWorktree(context, input) {
|
|
61
|
+
const protocol = scopedProtocol(context, input.projectRoot, input.protocolId);
|
|
62
|
+
const existing = activeWorktreeRecord(context, protocol.project_id, protocol.id);
|
|
63
|
+
if (existing)
|
|
64
|
+
throw new AppError("worktree_already_exists", "Protocol already has an active worktree record", 1, { protocol_id: protocol.id, worktree_path: existing.worktree_path });
|
|
51
65
|
const now = context.now();
|
|
52
66
|
const state = readProtocolRuntimeState(context, protocol).state;
|
|
53
67
|
context.db.run(`INSERT INTO worktree_records
|
|
54
68
|
(protocol_id, project_id, integration_branch, feature_branch, base_ref, worktree_path,
|
|
55
69
|
worktrunk_metadata_json, bootstrap_status, status, last_command_result_json, created_at, updated_at, closed_at)
|
|
56
|
-
VALUES (?, ?, ?, ?, ?, ?, ?,
|
|
57
|
-
protocol.id,
|
|
58
|
-
protocol.project_id,
|
|
59
|
-
typeof state.workspace.integration_branch === "string" ? state.workspace.integration_branch : null,
|
|
60
|
-
input.branch,
|
|
61
|
-
input.base,
|
|
62
|
-
worktreePath,
|
|
63
|
-
JSON.stringify({ bin: worktrunk.bin, version: worktrunk.version, command, response }),
|
|
64
|
-
JSON.stringify(result),
|
|
65
|
-
now,
|
|
66
|
-
now
|
|
67
|
-
]);
|
|
70
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, 'active', ?, ?, ?, NULL)`, [protocol.id, protocol.project_id, typeof state.workspace.integration_branch === "string" ? state.workspace.integration_branch : null, input.branch, input.base, input.created.worktreePath, JSON.stringify({ bin: input.created.worktrunk.bin, version: input.created.worktrunk.version, command: input.created.command, response: input.created.response }), input.bootstrap?.status ?? "pending", JSON.stringify(input.bootstrap ?? input.created.result), now, now]);
|
|
68
71
|
persistProtocolState(context, protocol, {
|
|
69
72
|
...state,
|
|
70
|
-
workspace: {
|
|
71
|
-
...state.workspace,
|
|
72
|
-
feature_branch: input.branch,
|
|
73
|
-
worktree_path: worktreePath,
|
|
74
|
-
base_commit: input.base,
|
|
75
|
-
worktrunk: { bin: worktrunk.bin, version: worktrunk.version, response },
|
|
76
|
-
bootstrap: { status: typeof response.bootstrap_status === "string" ? response.bootstrap_status : "pending" }
|
|
77
|
-
},
|
|
73
|
+
workspace: { ...state.workspace, feature_branch: input.branch, worktree_path: input.created.worktreePath, base_commit: input.base, worktrunk: { bin: input.created.worktrunk.bin, version: input.created.worktrunk.version, response: input.created.response }, bootstrap: input.bootstrap ?? { status: typeof input.created.response.bootstrap_status === "string" ? input.created.response.bootstrap_status : "pending" } },
|
|
78
74
|
updated_at: now
|
|
79
75
|
});
|
|
80
|
-
appendAudit(context, {
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
76
|
+
appendAudit(context, { protocolId: protocol.id, projectId: protocol.project_id, eventType: "worktree.created", payload: { protocol_id: protocol.id, feature_branch: input.branch, base: input.base, path: input.created.worktreePath } });
|
|
77
|
+
}
|
|
78
|
+
/** Bootstrap a just-created RUN workspace before a durable PRT exists. */
|
|
79
|
+
export function bootstrapManagedWorktree(context, input) {
|
|
80
|
+
const projectRoot = resolveProjectRoot(input.projectRoot);
|
|
81
|
+
const worktreePath = path.resolve(input.worktreePath);
|
|
82
|
+
const includeFile = path.join(projectRoot, ".worktreeinclude");
|
|
83
|
+
const configFile = path.join(projectRoot, ".config", "wt.toml");
|
|
84
|
+
if (!fs.existsSync(includeFile) || !fs.existsSync(configFile)) {
|
|
85
|
+
throw new AppError("bootstrap_unavailable", "Project-owned Worktrunk bootstrap files are required", 1, { include_file: includeFile, config_file: configFile });
|
|
86
|
+
}
|
|
87
|
+
const worktrunk = ensureManagedWorktrunk(context);
|
|
88
|
+
const commands = [
|
|
89
|
+
[worktrunk.bin, "-C", worktreePath, "step", "copy-ignored", "--require-include"],
|
|
90
|
+
[worktrunk.bin, "-C", worktreePath, "hook", "pre-start", "--yes"]
|
|
91
|
+
];
|
|
92
|
+
const results = [];
|
|
93
|
+
for (const command of commands) {
|
|
94
|
+
const result = runExternal(command);
|
|
95
|
+
results.push({ command: sanitizeCommand(command), ...result });
|
|
96
|
+
if (result.exit_code !== 0) {
|
|
97
|
+
throw new AppError("bootstrap_failed", "Project-owned Worktrunk bootstrap failed", 1, { command: sanitizeCommand(command), result, results });
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
return { status: "succeeded", commands: results };
|
|
87
101
|
}
|
|
88
102
|
export function getWorktreeStatus(context, input) {
|
|
89
103
|
const protocol = scopedProtocol(context, input.projectRoot, input.protocolId);
|
|
@@ -222,13 +236,20 @@ function parseWorktrunkResponse(stdout, command) {
|
|
|
222
236
|
throw new AppError("worktrunk_invalid_response", "Worktrunk JSON response must be an object", 1, { command: sanitizeCommand(command) });
|
|
223
237
|
}
|
|
224
238
|
const response = parsed;
|
|
225
|
-
|
|
226
|
-
|
|
239
|
+
// `wt switch --format=json` returns `path` in the pinned 0.72 release.
|
|
240
|
+
// Normalize provider output once; the rest of dd-flow uses workspace_path.
|
|
241
|
+
const workspacePath = typeof response.workspace_path === "string"
|
|
242
|
+
? response.workspace_path
|
|
243
|
+
: typeof response.path === "string"
|
|
244
|
+
? response.path
|
|
245
|
+
: null;
|
|
246
|
+
if (!workspacePath || !path.isAbsolute(workspacePath)) {
|
|
247
|
+
throw new AppError("worktrunk_invalid_response", "Worktrunk response must contain an absolute path or workspace_path", 1, {
|
|
227
248
|
command: sanitizeCommand(command),
|
|
228
249
|
response
|
|
229
250
|
});
|
|
230
251
|
}
|
|
231
|
-
return { ...response, workspace_path: path.resolve(
|
|
252
|
+
return { ...response, workspace_path: path.resolve(workspacePath) };
|
|
232
253
|
}
|
|
233
254
|
function markBootstrapFailed(context, protocol, record, failure) {
|
|
234
255
|
const now = context.now();
|
package/dist/storage/database.js
CHANGED
|
@@ -5,27 +5,117 @@ import { ensureDir } from "./paths.js";
|
|
|
5
5
|
import { AppError } from "../shared/errors.js";
|
|
6
6
|
const require = createRequire(import.meta.url);
|
|
7
7
|
const { DatabaseSync } = require("node:sqlite");
|
|
8
|
-
|
|
9
|
-
|
|
8
|
+
/** Creates the single Work/Session authority used by a fresh vNext beta runtime. */
|
|
9
|
+
export function ensureVnextWorkStorage(db) {
|
|
10
|
+
const legacy = db.get("SELECT name FROM sqlite_master WHERE type = 'table' AND name IN ('vnext_works', 'vnext_agent_turns', 'flow_agent_turns') LIMIT 1");
|
|
11
|
+
if (legacy)
|
|
12
|
+
throw new AppError("beta_storage_recreate_required", "This beta requires a fresh DD_FLOW_HOME; the previous Work runtime storage is not compatible", 1);
|
|
13
|
+
const existing = db.get("SELECT sql FROM sqlite_master WHERE type = 'table' AND name = 'works'");
|
|
14
|
+
if (existing && (!/task TEXT NOT NULL/.test(existing.sql) || /launch_token|adapter_session_id/.test(existing.sql))) {
|
|
15
|
+
throw new AppError("beta_storage_recreate_required", "This beta requires a fresh DD_FLOW_HOME; the existing works schema is not compatible", 1);
|
|
16
|
+
}
|
|
17
|
+
db.exec(`
|
|
18
|
+
CREATE TABLE IF NOT EXISTS works (
|
|
19
|
+
work_id TEXT PRIMARY KEY,
|
|
20
|
+
project_id TEXT NOT NULL,
|
|
21
|
+
run_id TEXT NOT NULL,
|
|
22
|
+
parent_work_id TEXT,
|
|
23
|
+
task TEXT NOT NULL,
|
|
24
|
+
launch_policy TEXT NOT NULL DEFAULT 'reuse_allowed',
|
|
25
|
+
result_schema TEXT,
|
|
26
|
+
payload_json TEXT,
|
|
27
|
+
depends_on_json TEXT NOT NULL DEFAULT '[]',
|
|
28
|
+
status TEXT NOT NULL,
|
|
29
|
+
result TEXT,
|
|
30
|
+
created_at TEXT NOT NULL,
|
|
31
|
+
started_at TEXT,
|
|
32
|
+
updated_at TEXT NOT NULL,
|
|
33
|
+
completed_at TEXT,
|
|
34
|
+
FOREIGN KEY(project_id) REFERENCES projects(id),
|
|
35
|
+
FOREIGN KEY(project_id, run_id) REFERENCES runs(project_id, id),
|
|
36
|
+
FOREIGN KEY(parent_work_id) REFERENCES works(work_id)
|
|
37
|
+
);
|
|
38
|
+
CREATE INDEX IF NOT EXISTS idx_works_run ON works(project_id, run_id, updated_at DESC);
|
|
39
|
+
CREATE TABLE IF NOT EXISTS work_sessions (
|
|
40
|
+
id TEXT PRIMARY KEY,
|
|
41
|
+
work_id TEXT NOT NULL,
|
|
42
|
+
session_id TEXT NOT NULL,
|
|
43
|
+
hook_event_id TEXT,
|
|
44
|
+
status TEXT NOT NULL,
|
|
45
|
+
prompt_path TEXT NOT NULL,
|
|
46
|
+
result_path TEXT,
|
|
47
|
+
created_at TEXT NOT NULL,
|
|
48
|
+
updated_at TEXT NOT NULL,
|
|
49
|
+
completed_at TEXT,
|
|
50
|
+
FOREIGN KEY(work_id) REFERENCES works(work_id)
|
|
51
|
+
);
|
|
52
|
+
CREATE INDEX IF NOT EXISTS idx_work_sessions_work ON work_sessions(work_id, created_at DESC);
|
|
53
|
+
CREATE UNIQUE INDEX IF NOT EXISTS idx_work_sessions_open ON work_sessions(work_id) WHERE status = 'running';
|
|
54
|
+
CREATE TABLE IF NOT EXISTS check_receipts (
|
|
55
|
+
id TEXT PRIMARY KEY,
|
|
56
|
+
project_id TEXT NOT NULL,
|
|
57
|
+
run_id TEXT NOT NULL,
|
|
58
|
+
work_id TEXT,
|
|
59
|
+
scope TEXT NOT NULL,
|
|
60
|
+
declaration_id TEXT NOT NULL,
|
|
61
|
+
command TEXT NOT NULL,
|
|
62
|
+
status TEXT NOT NULL,
|
|
63
|
+
exit_code INTEGER,
|
|
64
|
+
stdout_path TEXT NOT NULL,
|
|
65
|
+
stderr_path TEXT NOT NULL,
|
|
66
|
+
receipt_path TEXT NOT NULL,
|
|
67
|
+
workspace_fingerprint TEXT NOT NULL,
|
|
68
|
+
artifacts_json TEXT NOT NULL,
|
|
69
|
+
started_at TEXT NOT NULL,
|
|
70
|
+
finished_at TEXT NOT NULL,
|
|
71
|
+
FOREIGN KEY(work_id) REFERENCES works(work_id)
|
|
72
|
+
);
|
|
73
|
+
CREATE INDEX IF NOT EXISTS idx_check_receipts_run ON check_receipts(project_id, run_id, started_at);
|
|
74
|
+
`);
|
|
75
|
+
}
|
|
76
|
+
export function getDatabase(ddFlowHome, mode = "initialize") {
|
|
10
77
|
const dbPath = path.join(ddFlowHome, "db.sqlite");
|
|
11
|
-
const
|
|
12
|
-
|
|
13
|
-
|
|
78
|
+
const exists = fs.existsSync(dbPath);
|
|
79
|
+
if (mode === "read_existing" && !exists) {
|
|
80
|
+
return emptyReadOnlyDatabase(dbPath);
|
|
81
|
+
}
|
|
82
|
+
if (!exists || mode === "migrate")
|
|
83
|
+
ensureDir(ddFlowHome);
|
|
84
|
+
const db = mode === "read_existing" ? new DatabaseSync(dbPath, { readOnly: true }) : new DatabaseSync(dbPath);
|
|
85
|
+
configureDatabase(db, mode);
|
|
86
|
+
// Internal additive schema changes must be available to every write command.
|
|
87
|
+
// Higher-level Memory Bank migrations remain explicit in services/migrations.
|
|
88
|
+
if (mode !== "read_existing")
|
|
89
|
+
migrate(db);
|
|
90
|
+
if (mode === "read_existing")
|
|
91
|
+
db.exec("PRAGMA query_only = ON");
|
|
14
92
|
db.exec("PRAGMA foreign_keys = ON");
|
|
15
93
|
return {
|
|
16
94
|
path: dbPath,
|
|
95
|
+
writable: mode !== "read_existing",
|
|
96
|
+
close: () => db.close?.(),
|
|
17
97
|
exec: (sql) => db.exec(sql),
|
|
18
98
|
run: (sql, params = []) => db.prepare(sql).run(...params),
|
|
19
99
|
get: (sql, params = []) => db.prepare(sql).get(...params),
|
|
20
100
|
all: (sql, params = []) => db.prepare(sql).all(...params)
|
|
21
101
|
};
|
|
22
102
|
}
|
|
23
|
-
function
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
103
|
+
function emptyReadOnlyDatabase(dbPath) {
|
|
104
|
+
return {
|
|
105
|
+
path: dbPath,
|
|
106
|
+
writable: false,
|
|
107
|
+
exec: () => { throw new AppError("read_only_storage", "Read-only access cannot write absent dd-flow storage", 1, { path: dbPath }); },
|
|
108
|
+
run: () => { throw new AppError("read_only_storage", "Read-only access cannot write absent dd-flow storage", 1, { path: dbPath }); },
|
|
109
|
+
get: () => undefined,
|
|
110
|
+
all: () => []
|
|
111
|
+
};
|
|
112
|
+
}
|
|
113
|
+
function configureDatabase(db, mode) {
|
|
114
|
+
db.exec("PRAGMA busy_timeout = 4000");
|
|
115
|
+
if (mode !== "read_existing") {
|
|
116
|
+
db.exec("PRAGMA journal_mode = WAL");
|
|
117
|
+
db.exec("PRAGMA synchronous = NORMAL");
|
|
118
|
+
}
|
|
29
119
|
}
|
|
30
120
|
function migrate(db) {
|
|
31
121
|
db.exec(`
|
|
@@ -248,9 +338,18 @@ function migrate(db) {
|
|
|
248
338
|
updated_at TEXT NOT NULL
|
|
249
339
|
);
|
|
250
340
|
|
|
251
|
-
CREATE TABLE IF NOT EXISTS
|
|
341
|
+
CREATE TABLE IF NOT EXISTS sessions (
|
|
252
342
|
session_id TEXT NOT NULL,
|
|
253
343
|
project_id TEXT NOT NULL,
|
|
344
|
+
harness TEXT NOT NULL DEFAULT 'codex-desktop',
|
|
345
|
+
provider_session_id TEXT,
|
|
346
|
+
agent_id TEXT,
|
|
347
|
+
parent_id TEXT,
|
|
348
|
+
provider TEXT,
|
|
349
|
+
model TEXT,
|
|
350
|
+
reasoning TEXT,
|
|
351
|
+
mode TEXT,
|
|
352
|
+
agent_type TEXT,
|
|
254
353
|
project_root TEXT NOT NULL,
|
|
255
354
|
flow_kind TEXT NOT NULL,
|
|
256
355
|
status TEXT NOT NULL,
|
|
@@ -275,7 +374,7 @@ function migrate(db) {
|
|
|
275
374
|
FOREIGN KEY(project_id) REFERENCES projects(id)
|
|
276
375
|
);
|
|
277
376
|
|
|
278
|
-
CREATE TABLE IF NOT EXISTS
|
|
377
|
+
CREATE TABLE IF NOT EXISTS runs (
|
|
279
378
|
id TEXT NOT NULL,
|
|
280
379
|
short_id TEXT NOT NULL,
|
|
281
380
|
slug TEXT NOT NULL,
|
|
@@ -292,6 +391,7 @@ function migrate(db) {
|
|
|
292
391
|
run_dir TEXT NOT NULL,
|
|
293
392
|
run_index_path TEXT NOT NULL,
|
|
294
393
|
run_home_path TEXT,
|
|
394
|
+
run_root TEXT,
|
|
295
395
|
layout_version TEXT,
|
|
296
396
|
artifact_root_kind TEXT,
|
|
297
397
|
index_json TEXT NOT NULL,
|
|
@@ -303,10 +403,76 @@ function migrate(db) {
|
|
|
303
403
|
);
|
|
304
404
|
|
|
305
405
|
CREATE INDEX IF NOT EXISTS idx_flow_runs_project_updated
|
|
306
|
-
ON
|
|
406
|
+
ON runs(project_id, updated_at DESC);
|
|
307
407
|
|
|
308
408
|
CREATE INDEX IF NOT EXISTS idx_flow_runs_short_id
|
|
309
|
-
ON
|
|
409
|
+
ON runs(short_id);
|
|
410
|
+
|
|
411
|
+
CREATE TABLE IF NOT EXISTS works (
|
|
412
|
+
work_id TEXT PRIMARY KEY,
|
|
413
|
+
project_id TEXT NOT NULL,
|
|
414
|
+
run_id TEXT NOT NULL,
|
|
415
|
+
parent_work_id TEXT,
|
|
416
|
+
task TEXT NOT NULL,
|
|
417
|
+
launch_policy TEXT NOT NULL DEFAULT 'reuse_allowed',
|
|
418
|
+
result_schema TEXT,
|
|
419
|
+
payload_json TEXT,
|
|
420
|
+
depends_on_json TEXT NOT NULL DEFAULT '[]',
|
|
421
|
+
status TEXT NOT NULL,
|
|
422
|
+
result TEXT,
|
|
423
|
+
created_at TEXT NOT NULL,
|
|
424
|
+
started_at TEXT,
|
|
425
|
+
updated_at TEXT NOT NULL,
|
|
426
|
+
completed_at TEXT,
|
|
427
|
+
FOREIGN KEY(project_id) REFERENCES projects(id),
|
|
428
|
+
FOREIGN KEY(project_id, run_id) REFERENCES runs(project_id, id),
|
|
429
|
+
FOREIGN KEY(parent_work_id) REFERENCES works(work_id)
|
|
430
|
+
);
|
|
431
|
+
|
|
432
|
+
CREATE INDEX IF NOT EXISTS idx_works_run
|
|
433
|
+
ON works(project_id, run_id, updated_at DESC);
|
|
434
|
+
|
|
435
|
+
CREATE TABLE IF NOT EXISTS work_sessions (
|
|
436
|
+
id TEXT PRIMARY KEY,
|
|
437
|
+
work_id TEXT NOT NULL,
|
|
438
|
+
session_id TEXT NOT NULL,
|
|
439
|
+
hook_event_id TEXT,
|
|
440
|
+
status TEXT NOT NULL,
|
|
441
|
+
prompt_path TEXT NOT NULL,
|
|
442
|
+
result_path TEXT,
|
|
443
|
+
created_at TEXT NOT NULL,
|
|
444
|
+
updated_at TEXT NOT NULL,
|
|
445
|
+
completed_at TEXT,
|
|
446
|
+
FOREIGN KEY(work_id) REFERENCES works(work_id)
|
|
447
|
+
);
|
|
448
|
+
|
|
449
|
+
CREATE INDEX IF NOT EXISTS idx_work_sessions_work
|
|
450
|
+
ON work_sessions(work_id, created_at DESC);
|
|
451
|
+
CREATE UNIQUE INDEX IF NOT EXISTS idx_work_sessions_open
|
|
452
|
+
ON work_sessions(work_id) WHERE status = 'running';
|
|
453
|
+
|
|
454
|
+
CREATE TABLE IF NOT EXISTS check_receipts (
|
|
455
|
+
id TEXT PRIMARY KEY,
|
|
456
|
+
project_id TEXT NOT NULL,
|
|
457
|
+
run_id TEXT NOT NULL,
|
|
458
|
+
work_id TEXT,
|
|
459
|
+
scope TEXT NOT NULL,
|
|
460
|
+
declaration_id TEXT NOT NULL,
|
|
461
|
+
command TEXT NOT NULL,
|
|
462
|
+
status TEXT NOT NULL,
|
|
463
|
+
exit_code INTEGER,
|
|
464
|
+
stdout_path TEXT NOT NULL,
|
|
465
|
+
stderr_path TEXT NOT NULL,
|
|
466
|
+
receipt_path TEXT NOT NULL,
|
|
467
|
+
workspace_fingerprint TEXT NOT NULL,
|
|
468
|
+
artifacts_json TEXT NOT NULL,
|
|
469
|
+
started_at TEXT NOT NULL,
|
|
470
|
+
finished_at TEXT NOT NULL,
|
|
471
|
+
FOREIGN KEY(work_id) REFERENCES works(work_id)
|
|
472
|
+
);
|
|
473
|
+
|
|
474
|
+
CREATE INDEX IF NOT EXISTS idx_check_receipts_run
|
|
475
|
+
ON check_receipts(project_id, run_id, started_at);
|
|
310
476
|
|
|
311
477
|
CREATE TABLE IF NOT EXISTS flow_run_flag_mutations (
|
|
312
478
|
project_id TEXT NOT NULL,
|
|
@@ -320,7 +486,7 @@ function migrate(db) {
|
|
|
320
486
|
FOREIGN KEY(project_id) REFERENCES projects(id)
|
|
321
487
|
);
|
|
322
488
|
|
|
323
|
-
CREATE TABLE IF NOT EXISTS
|
|
489
|
+
CREATE TABLE IF NOT EXISTS usage (
|
|
324
490
|
id TEXT PRIMARY KEY,
|
|
325
491
|
project_id TEXT NOT NULL,
|
|
326
492
|
run_id TEXT NOT NULL,
|
|
@@ -338,6 +504,10 @@ function migrate(db) {
|
|
|
338
504
|
output_tokens INTEGER,
|
|
339
505
|
reasoning_output_tokens INTEGER,
|
|
340
506
|
source_kind TEXT NOT NULL,
|
|
507
|
+
source_locator TEXT,
|
|
508
|
+
source_sha256 TEXT,
|
|
509
|
+
source_size_bytes INTEGER,
|
|
510
|
+
source_mtime_ms INTEGER,
|
|
341
511
|
token_event_at TEXT,
|
|
342
512
|
turn_id TEXT,
|
|
343
513
|
turn_attribution TEXT NOT NULL,
|
|
@@ -349,14 +519,55 @@ function migrate(db) {
|
|
|
349
519
|
);
|
|
350
520
|
|
|
351
521
|
CREATE INDEX IF NOT EXISTS idx_flow_run_usage_snapshots_run
|
|
352
|
-
ON
|
|
522
|
+
ON usage(project_id, run_id, session_id, observed_at, id);
|
|
353
523
|
|
|
354
|
-
CREATE TABLE IF NOT EXISTS
|
|
524
|
+
CREATE TABLE IF NOT EXISTS harness_usage_snapshots (
|
|
525
|
+
id TEXT PRIMARY KEY,
|
|
526
|
+
project_id TEXT NOT NULL,
|
|
527
|
+
harness TEXT NOT NULL,
|
|
528
|
+
provider_session_id TEXT NOT NULL,
|
|
529
|
+
daemon_id TEXT,
|
|
530
|
+
observed_at TEXT NOT NULL,
|
|
531
|
+
total_tokens INTEGER NOT NULL,
|
|
532
|
+
input_tokens INTEGER,
|
|
533
|
+
cache_read_input_tokens INTEGER,
|
|
534
|
+
cache_write_input_tokens INTEGER,
|
|
535
|
+
output_tokens INTEGER,
|
|
536
|
+
reasoning_output_tokens INTEGER,
|
|
537
|
+
request_count INTEGER,
|
|
538
|
+
error_count INTEGER,
|
|
539
|
+
tool_calls INTEGER,
|
|
540
|
+
tool_failures INTEGER,
|
|
541
|
+
tool_by_name_json TEXT,
|
|
542
|
+
source_kind TEXT NOT NULL DEFAULT 'zcode_session_usage_v1',
|
|
543
|
+
usage_scope TEXT NOT NULL DEFAULT 'physical_session',
|
|
544
|
+
completeness TEXT NOT NULL DEFAULT 'complete',
|
|
545
|
+
created_at TEXT NOT NULL,
|
|
546
|
+
UNIQUE(project_id, harness, provider_session_id, observed_at),
|
|
547
|
+
FOREIGN KEY(project_id) REFERENCES projects(id)
|
|
548
|
+
);
|
|
549
|
+
|
|
550
|
+
CREATE INDEX IF NOT EXISTS idx_harness_usage_provider
|
|
551
|
+
ON harness_usage_snapshots(project_id, harness, provider_session_id, observed_at, id);
|
|
552
|
+
|
|
553
|
+
CREATE TABLE IF NOT EXISTS hook_events (
|
|
355
554
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
356
555
|
project_id TEXT NOT NULL,
|
|
357
556
|
protocol_id TEXT,
|
|
557
|
+
harness TEXT NOT NULL DEFAULT 'codex-desktop',
|
|
558
|
+
provider_session_id TEXT,
|
|
559
|
+
parent_session_id TEXT,
|
|
560
|
+
daemon_id TEXT,
|
|
358
561
|
session_id TEXT,
|
|
562
|
+
agent_id TEXT,
|
|
359
563
|
turn_id TEXT,
|
|
564
|
+
transcript_path TEXT,
|
|
565
|
+
provider TEXT,
|
|
566
|
+
model TEXT,
|
|
567
|
+
reasoning TEXT,
|
|
568
|
+
mode TEXT,
|
|
569
|
+
agent_type TEXT,
|
|
570
|
+
cwd TEXT,
|
|
360
571
|
event_key TEXT,
|
|
361
572
|
event_name TEXT NOT NULL,
|
|
362
573
|
tool_name TEXT,
|
|
@@ -439,32 +650,71 @@ function migrate(db) {
|
|
|
439
650
|
ensureColumn(db, "merge_queue", "completed_at", "ALTER TABLE merge_queue ADD COLUMN completed_at TEXT");
|
|
440
651
|
ensureColumn(db, "codex_session_bindings", "continuation_count", "ALTER TABLE codex_session_bindings ADD COLUMN continuation_count INTEGER NOT NULL DEFAULT 0");
|
|
441
652
|
ensureColumn(db, "codex_session_bindings", "last_action_hash", "ALTER TABLE codex_session_bindings ADD COLUMN last_action_hash TEXT");
|
|
442
|
-
ensureColumn(db, "
|
|
443
|
-
ensureColumn(db, "
|
|
444
|
-
ensureColumn(db, "
|
|
445
|
-
ensureColumn(db, "
|
|
446
|
-
ensureColumn(db, "
|
|
447
|
-
ensureColumn(db, "
|
|
448
|
-
ensureColumn(db, "
|
|
449
|
-
ensureColumn(db, "
|
|
450
|
-
ensureColumn(db, "
|
|
451
|
-
ensureColumn(db, "
|
|
452
|
-
ensureColumn(db, "
|
|
453
|
-
ensureColumn(db, "
|
|
454
|
-
ensureColumn(db, "
|
|
455
|
-
db
|
|
456
|
-
ensureColumn(db, "
|
|
457
|
-
ensureColumn(db, "
|
|
458
|
-
ensureColumn(db, "
|
|
459
|
-
ensureColumn(db, "
|
|
653
|
+
ensureColumn(db, "sessions", "current_stage", "ALTER TABLE sessions ADD COLUMN current_stage TEXT");
|
|
654
|
+
ensureColumn(db, "sessions", "next_action", "ALTER TABLE sessions ADD COLUMN next_action TEXT");
|
|
655
|
+
ensureColumn(db, "sessions", "metadata_json", "ALTER TABLE sessions ADD COLUMN metadata_json TEXT NOT NULL DEFAULT '{}'");
|
|
656
|
+
ensureColumn(db, "sessions", "run_id", "ALTER TABLE sessions ADD COLUMN run_id TEXT");
|
|
657
|
+
ensureColumn(db, "sessions", "provider_session_id", "ALTER TABLE sessions ADD COLUMN provider_session_id TEXT");
|
|
658
|
+
ensureColumn(db, "sessions", "harness", "ALTER TABLE sessions ADD COLUMN harness TEXT NOT NULL DEFAULT 'codex-desktop'");
|
|
659
|
+
ensureColumn(db, "sessions", "agent_id", "ALTER TABLE sessions ADD COLUMN agent_id TEXT");
|
|
660
|
+
ensureColumn(db, "sessions", "provider", "ALTER TABLE sessions ADD COLUMN provider TEXT");
|
|
661
|
+
ensureColumn(db, "sessions", "parent_id", "ALTER TABLE sessions ADD COLUMN parent_id TEXT");
|
|
662
|
+
ensureColumn(db, "sessions", "model", "ALTER TABLE sessions ADD COLUMN model TEXT");
|
|
663
|
+
ensureColumn(db, "sessions", "reasoning", "ALTER TABLE sessions ADD COLUMN reasoning TEXT");
|
|
664
|
+
ensureColumn(db, "sessions", "mode", "ALTER TABLE sessions ADD COLUMN mode TEXT");
|
|
665
|
+
ensureColumn(db, "sessions", "agent_type", "ALTER TABLE sessions ADD COLUMN agent_type TEXT");
|
|
666
|
+
ensureColumn(db, "sessions", "parent_session_id", "ALTER TABLE sessions ADD COLUMN parent_session_id TEXT");
|
|
667
|
+
ensureColumn(db, "sessions", "role", "ALTER TABLE sessions ADD COLUMN role TEXT");
|
|
668
|
+
ensureColumn(db, "sessions", "aspect_id", "ALTER TABLE sessions ADD COLUMN aspect_id TEXT");
|
|
669
|
+
ensureColumn(db, "sessions", "plan_item_id", "ALTER TABLE sessions ADD COLUMN plan_item_id TEXT");
|
|
670
|
+
ensureColumn(db, "sessions", "session_kind", "ALTER TABLE sessions ADD COLUMN session_kind TEXT");
|
|
671
|
+
ensureColumn(db, "sessions", "coverage_units_json", "ALTER TABLE sessions ADD COLUMN coverage_units_json TEXT NOT NULL DEFAULT '[]'");
|
|
672
|
+
ensureColumn(db, "usage", "cache_read_input_tokens", "ALTER TABLE usage ADD COLUMN cache_read_input_tokens INTEGER");
|
|
673
|
+
ensureColumn(db, "usage", "cache_write_input_tokens", "ALTER TABLE usage ADD COLUMN cache_write_input_tokens INTEGER");
|
|
674
|
+
ensureColumn(db, "usage", "uncached_input_tokens", "ALTER TABLE usage ADD COLUMN uncached_input_tokens INTEGER");
|
|
675
|
+
ensureColumn(db, "usage", "source_locator", "ALTER TABLE usage ADD COLUMN source_locator TEXT");
|
|
676
|
+
ensureColumn(db, "usage", "source_sha256", "ALTER TABLE usage ADD COLUMN source_sha256 TEXT");
|
|
677
|
+
ensureColumn(db, "usage", "source_size_bytes", "ALTER TABLE usage ADD COLUMN source_size_bytes INTEGER");
|
|
678
|
+
ensureColumn(db, "usage", "source_mtime_ms", "ALTER TABLE usage ADD COLUMN source_mtime_ms INTEGER");
|
|
679
|
+
db.prepare("UPDATE usage SET cache_read_input_tokens = cached_input_tokens WHERE cache_read_input_tokens IS NULL AND cached_input_tokens IS NOT NULL").run();
|
|
680
|
+
ensureColumn(db, "runs", "run_home_path", "ALTER TABLE runs ADD COLUMN run_home_path TEXT");
|
|
681
|
+
ensureColumn(db, "runs", "run_root", "ALTER TABLE runs ADD COLUMN run_root TEXT");
|
|
682
|
+
ensureColumn(db, "runs", "layout_version", "ALTER TABLE runs ADD COLUMN layout_version TEXT");
|
|
683
|
+
ensureColumn(db, "runs", "artifact_root_kind", "ALTER TABLE runs ADD COLUMN artifact_root_kind TEXT");
|
|
684
|
+
ensureColumn(db, "works", "payload_json", "ALTER TABLE works ADD COLUMN payload_json TEXT");
|
|
685
|
+
ensureColumn(db, "check_receipts", "declaration_id", "ALTER TABLE check_receipts ADD COLUMN declaration_id TEXT NOT NULL DEFAULT 'CHK-LEGACY'");
|
|
686
|
+
ensureColumn(db, "check_receipts", "workspace_fingerprint", "ALTER TABLE check_receipts ADD COLUMN workspace_fingerprint TEXT NOT NULL DEFAULT ''");
|
|
687
|
+
ensureColumn(db, "check_receipts", "artifacts_json", "ALTER TABLE check_receipts ADD COLUMN artifacts_json TEXT NOT NULL DEFAULT '[]'");
|
|
688
|
+
ensureColumn(db, "hook_events", "event_key", "ALTER TABLE hook_events ADD COLUMN event_key TEXT");
|
|
689
|
+
ensureColumn(db, "hook_events", "match_key", "ALTER TABLE hook_events ADD COLUMN match_key TEXT");
|
|
690
|
+
ensureColumn(db, "hook_events", "claimed_at", "ALTER TABLE hook_events ADD COLUMN claimed_at TEXT");
|
|
691
|
+
ensureColumn(db, "hook_events", "agent_id", "ALTER TABLE hook_events ADD COLUMN agent_id TEXT");
|
|
692
|
+
ensureColumn(db, "hook_events", "transcript_path", "ALTER TABLE hook_events ADD COLUMN transcript_path TEXT");
|
|
693
|
+
ensureColumn(db, "hook_events", "provider", "ALTER TABLE hook_events ADD COLUMN provider TEXT");
|
|
694
|
+
ensureColumn(db, "hook_events", "model", "ALTER TABLE hook_events ADD COLUMN model TEXT");
|
|
695
|
+
ensureColumn(db, "hook_events", "reasoning", "ALTER TABLE hook_events ADD COLUMN reasoning TEXT");
|
|
696
|
+
ensureColumn(db, "hook_events", "mode", "ALTER TABLE hook_events ADD COLUMN mode TEXT");
|
|
697
|
+
ensureColumn(db, "hook_events", "agent_type", "ALTER TABLE hook_events ADD COLUMN agent_type TEXT");
|
|
698
|
+
ensureColumn(db, "hook_events", "harness", "ALTER TABLE hook_events ADD COLUMN harness TEXT NOT NULL DEFAULT 'codex-desktop'");
|
|
699
|
+
ensureColumn(db, "hook_events", "provider_session_id", "ALTER TABLE hook_events ADD COLUMN provider_session_id TEXT");
|
|
700
|
+
ensureColumn(db, "hook_events", "parent_session_id", "ALTER TABLE hook_events ADD COLUMN parent_session_id TEXT");
|
|
701
|
+
ensureColumn(db, "hook_events", "daemon_id", "ALTER TABLE hook_events ADD COLUMN daemon_id TEXT");
|
|
702
|
+
ensureColumn(db, "hook_events", "cwd", "ALTER TABLE hook_events ADD COLUMN cwd TEXT");
|
|
703
|
+
ensureColumn(db, "harness_usage_snapshots", "tool_calls", "ALTER TABLE harness_usage_snapshots ADD COLUMN tool_calls INTEGER");
|
|
704
|
+
ensureColumn(db, "harness_usage_snapshots", "tool_failures", "ALTER TABLE harness_usage_snapshots ADD COLUMN tool_failures INTEGER");
|
|
705
|
+
ensureColumn(db, "harness_usage_snapshots", "tool_by_name_json", "ALTER TABLE harness_usage_snapshots ADD COLUMN tool_by_name_json TEXT");
|
|
706
|
+
ensureColumn(db, "harness_usage_snapshots", "source_kind", "ALTER TABLE harness_usage_snapshots ADD COLUMN source_kind TEXT NOT NULL DEFAULT 'zcode_session_usage_v1'");
|
|
707
|
+
ensureColumn(db, "harness_usage_snapshots", "usage_scope", "ALTER TABLE harness_usage_snapshots ADD COLUMN usage_scope TEXT NOT NULL DEFAULT 'physical_session'");
|
|
708
|
+
ensureColumn(db, "harness_usage_snapshots", "completeness", "ALTER TABLE harness_usage_snapshots ADD COLUMN completeness TEXT NOT NULL DEFAULT 'complete'");
|
|
460
709
|
ensureColumn(db, "flow_jobs", "updated_at", "ALTER TABLE flow_jobs ADD COLUMN updated_at TEXT NOT NULL DEFAULT ''");
|
|
461
|
-
db.exec("CREATE UNIQUE INDEX IF NOT EXISTS idx_codex_hook_events_event_key ON
|
|
710
|
+
db.exec("CREATE UNIQUE INDEX IF NOT EXISTS idx_codex_hook_events_event_key ON hook_events(project_id, event_key) WHERE event_key IS NOT NULL");
|
|
711
|
+
db.exec("CREATE INDEX IF NOT EXISTS idx_codex_hook_events_bootstrap_claim ON hook_events(project_id, session_id, match_key, status, created_at DESC)");
|
|
462
712
|
ensureColumn(db, "lane_waiters", "expires_at", "ALTER TABLE lane_waiters ADD COLUMN expires_at TEXT");
|
|
463
713
|
db.exec(`
|
|
464
714
|
CREATE INDEX IF NOT EXISTS idx_flow_runs_project_updated
|
|
465
|
-
ON
|
|
715
|
+
ON runs(project_id, updated_at DESC);
|
|
466
716
|
CREATE INDEX IF NOT EXISTS idx_flow_runs_short_id
|
|
467
|
-
ON
|
|
717
|
+
ON runs(short_id);
|
|
468
718
|
`);
|
|
469
719
|
}
|
|
470
720
|
function migrateProjectScopedIdentity(db) {
|
|
@@ -479,7 +729,7 @@ function migrateProjectScopedIdentity(db) {
|
|
|
479
729
|
|
|
480
730
|
ALTER TABLE merge_queue RENAME TO merge_queue_legacy_global_identity;
|
|
481
731
|
ALTER TABLE worktree_records RENAME TO worktree_records_legacy_global_identity;
|
|
482
|
-
ALTER TABLE
|
|
732
|
+
ALTER TABLE runs RENAME TO flow_runs_legacy_global_identity;
|
|
483
733
|
ALTER TABLE protocols RENAME TO protocols_legacy_global_identity;
|
|
484
734
|
|
|
485
735
|
CREATE TABLE protocols (
|
|
@@ -560,7 +810,7 @@ function migrateProjectScopedIdentity(db) {
|
|
|
560
810
|
status, last_command_result_json, created_at, updated_at, closed_at
|
|
561
811
|
FROM worktree_records_legacy_global_identity;
|
|
562
812
|
|
|
563
|
-
CREATE TABLE
|
|
813
|
+
CREATE TABLE runs (
|
|
564
814
|
id TEXT NOT NULL,
|
|
565
815
|
short_id TEXT NOT NULL,
|
|
566
816
|
slug TEXT NOT NULL,
|
|
@@ -586,7 +836,7 @@ function migrateProjectScopedIdentity(db) {
|
|
|
586
836
|
PRIMARY KEY(project_id, id),
|
|
587
837
|
FOREIGN KEY(project_id) REFERENCES projects(id)
|
|
588
838
|
);
|
|
589
|
-
INSERT INTO
|
|
839
|
+
INSERT INTO runs
|
|
590
840
|
(id, short_id, slug, project_id, project_root, workspace_root, flow_kind,
|
|
591
841
|
subject_type, subject_id, status, verdict, next_action, runtime_path,
|
|
592
842
|
run_dir, run_index_path, run_home_path, layout_version,
|
package/dist/storage/paths.js
CHANGED
|
@@ -3,18 +3,64 @@ import os from "node:os";
|
|
|
3
3
|
import path from "node:path";
|
|
4
4
|
import { AppError } from "../shared/errors.js";
|
|
5
5
|
export function resolveDdFlowHome(env) {
|
|
6
|
+
// Keep the operator spelling stable in generated commands; containment checks
|
|
7
|
+
// canonicalize paths separately when filesystem identity matters.
|
|
6
8
|
return path.resolve(env.DD_FLOW_HOME ?? path.join(os.homedir(), ".dd-flow"));
|
|
7
9
|
}
|
|
8
10
|
export function ensureDir(dir) {
|
|
9
11
|
fs.mkdirSync(dir, { recursive: true });
|
|
10
12
|
}
|
|
11
13
|
export function resolveProjectRoot(root) {
|
|
12
|
-
const absolute =
|
|
14
|
+
const absolute = canonicalPath(root, false);
|
|
13
15
|
if (!fs.existsSync(absolute)) {
|
|
14
16
|
throw new AppError("not_found", `Project root does not exist: ${absolute}`, 1);
|
|
15
17
|
}
|
|
16
18
|
return fs.realpathSync(absolute);
|
|
17
19
|
}
|
|
20
|
+
/** Resolve an existing path to its filesystem identity, or normalize a future path. */
|
|
21
|
+
export function canonicalPath(value, mustExist = true) {
|
|
22
|
+
const absolute = path.resolve(value);
|
|
23
|
+
if (fs.existsSync(absolute))
|
|
24
|
+
return fs.realpathSync(absolute);
|
|
25
|
+
if (mustExist)
|
|
26
|
+
throw new AppError("not_found", `Path does not exist: ${absolute}`, 1, { path: absolute });
|
|
27
|
+
const parent = path.dirname(absolute);
|
|
28
|
+
if (!fs.existsSync(parent))
|
|
29
|
+
return absolute;
|
|
30
|
+
return path.join(fs.realpathSync(parent), path.basename(absolute));
|
|
31
|
+
}
|
|
32
|
+
export function assertPathWithin(root, candidate, label = "path") {
|
|
33
|
+
const canonicalRoot = canonicalPath(root);
|
|
34
|
+
const canonicalCandidate = canonicalPath(candidate, false);
|
|
35
|
+
const relative = path.relative(canonicalRoot, canonicalCandidate);
|
|
36
|
+
if (relative === "" || (!relative.startsWith("..") && !path.isAbsolute(relative)))
|
|
37
|
+
return canonicalCandidate;
|
|
38
|
+
throw new AppError("validation", `${label} must stay inside ${canonicalRoot}`, 2, { root: canonicalRoot, [label]: canonicalCandidate });
|
|
39
|
+
}
|
|
40
|
+
export function runReference(runId, relativePath) {
|
|
41
|
+
const normalized = relativePath.replaceAll(path.sep, "/").replace(/^\/+/, "");
|
|
42
|
+
if (!normalized || normalized.split("/").includes(".."))
|
|
43
|
+
throw new AppError("validation", "RUN reference must be a safe relative path", 2, { relative_path: relativePath });
|
|
44
|
+
return `run://${runId}/${normalized}`;
|
|
45
|
+
}
|
|
46
|
+
export function resolveRunReference(value, runId, runRoot) {
|
|
47
|
+
const prefix = `run://${runId}/`;
|
|
48
|
+
if (!value.startsWith(prefix))
|
|
49
|
+
return value;
|
|
50
|
+
return assertPathWithin(runRoot, path.join(runRoot, value.slice(prefix.length)), "run_reference");
|
|
51
|
+
}
|
|
52
|
+
export function resolveRunReferences(value, runId, runRoot) {
|
|
53
|
+
return value.replace(new RegExp(`run://${escapeRegExp(runId)}/([^\\s)]+)`, "g"), (reference) => resolveRunReference(reference, runId, runRoot));
|
|
54
|
+
}
|
|
55
|
+
export function writeJsonAtomic(file, value) {
|
|
56
|
+
ensureDir(path.dirname(file));
|
|
57
|
+
const temporary = `${file}.tmp-${process.pid}-${Date.now()}`;
|
|
58
|
+
fs.writeFileSync(temporary, `${JSON.stringify(value, null, 2)}\n`);
|
|
59
|
+
fs.renameSync(temporary, file);
|
|
60
|
+
}
|
|
61
|
+
function escapeRegExp(value) {
|
|
62
|
+
return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
63
|
+
}
|
|
18
64
|
export function protocolDir(projectRoot, protocolId) {
|
|
19
65
|
return path.join(projectRoot, ".memory-bank", "protocol", protocolId);
|
|
20
66
|
}
|