ultra-builder-pro-cli 0.20.0 → 0.21.1
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/.ultra-template/docs/research/README.md +26 -5
- package/.ultra-template/specs/discovery.md +1 -1
- package/.ultra-template/tasks/tasks.json +1 -1
- package/CHANGELOG.md +66 -0
- package/README.md +54 -24
- package/adapters/_shared/codex-assets.cjs +69 -11
- package/adapters/_shared/interaction-contract.cjs +36 -3
- package/adapters/_shared/runtime-assets.cjs +41 -1
- package/adapters/claude.js +7 -5
- package/adapters/codex.js +1 -1
- package/adapters/kimi.js +2 -8
- package/adapters/opencode.js +30 -28
- package/bin/orchestrator.js +64 -24
- package/docs/AGENT-CONTEXT.md +74 -43
- package/docs/ARCHITECTURE.md +63 -31
- package/docs/ARTIFACT-AUTHORITY.md +234 -0
- package/docs/DECISIONS.md +10 -4
- package/docs/LEGACY-CLI-CROSSWALK.md +6 -4
- package/docs/PLUGIN-ISOLATION-CONTRACT.md +4 -3
- package/docs/ROADMAP.md +19 -5
- package/docs/RUNTIME-COMPAT-MATRIX.md +24 -13
- package/docs/STATE-DB-ACCESS-POLICY.md +11 -9
- package/docs/WORKFLOW-LIFECYCLE.md +70 -39
- package/hooks/README.md +6 -5
- package/hooks/active_task_context.py +97 -12
- package/hooks/context_spine.py +16 -4
- package/hooks/health_check.py +5 -4
- package/hooks/pre_stop_check.py +5 -2
- package/hooks/runtime_paths.py +396 -0
- package/hooks/subagent_tracker.py +5 -2
- package/hooks/workflow_checkpoint.py +16 -3
- package/hooks/workflow_context.py +11 -3
- package/hooks/workflow_resume.py +7 -4
- package/mcp-server/breadcrumb.cjs +20 -2
- package/mcp-server/lib/archive-journal.cjs +1026 -111
- package/mcp-server/lib/archive-mutation-worker.py +365 -0
- package/mcp-server/lib/artifact-registry.cjs +1126 -0
- package/mcp-server/lib/baseline-workflow.cjs +9 -4
- package/mcp-server/lib/change-packet.cjs +818 -0
- package/mcp-server/lib/change-workflow.cjs +932 -176
- package/mcp-server/lib/context-paths.cjs +111 -0
- package/mcp-server/lib/context-spine.cjs +511 -45
- package/mcp-server/lib/decision-dialogue.cjs +345 -3
- package/mcp-server/lib/delivery-transaction.cjs +471 -0
- package/mcp-server/lib/doctor-backup-worker.cjs +328 -0
- package/mcp-server/lib/doctor.cjs +373 -11
- package/mcp-server/lib/git-bootstrap.cjs +459 -47
- package/mcp-server/lib/init-project.cjs +190 -95
- package/mcp-server/lib/plan-publication-worker.cjs +830 -0
- package/mcp-server/lib/plan-store.cjs +700 -5
- package/mcp-server/lib/project-breadcrumb.cjs +23 -4
- package/mcp-server/lib/projector.cjs +174 -40
- package/mcp-server/lib/runtime-paths.cjs +1687 -0
- package/mcp-server/lib/safe-project-file.cjs +385 -0
- package/mcp-server/lib/spec-learning.cjs +28 -12
- package/mcp-server/lib/state-db.cjs +263 -9
- package/mcp-server/lib/state-ops.cjs +248 -49
- package/mcp-server/lib/telemetry.cjs +12 -3
- package/mcp-server/lib/workflow-state.cjs +610 -58
- package/mcp-server/server.cjs +278 -33
- package/orchestrator/daemon.cjs +2 -10
- package/orchestrator/parallel-orchestrator.cjs +314 -50
- package/orchestrator/planner/plan-builder.cjs +1 -1
- package/orchestrator/recovery.cjs +125 -7
- package/orchestrator/session-close-journal-worker.cjs +5 -0
- package/orchestrator/session-close-journal.cjs +1014 -0
- package/orchestrator/session-runner.cjs +679 -69
- package/orchestrator/worktree-manager.cjs +186 -35
- package/package.json +2 -3
- package/skills/cc-collab/SKILL.md +4 -1
- package/skills/codex-collab/SKILL.md +5 -2
- package/skills/ultra-change/SKILL.md +26 -8
- package/skills/ultra-deliver/SKILL.md +23 -11
- package/skills/ultra-dev/SKILL.md +33 -6
- package/skills/ultra-doctor/SKILL.md +6 -2
- package/skills/ultra-init/SKILL.md +4 -2
- package/skills/ultra-plan/SKILL.md +49 -16
- package/skills/ultra-research/SKILL.md +30 -27
- package/skills/ultra-review/SKILL.md +9 -5
- package/skills/ultra-status/SKILL.md +9 -4
- package/skills/ultra-test/SKILL.md +10 -5
- package/skills/ultra-think/SKILL.md +6 -2
- package/skills/ultra-think/references/decision-dialogue.md +17 -6
- package/skills/ultra-verify/SKILL.md +4 -1
- package/spec/cli-protocol.md +18 -7
- package/spec/mcp-tools.yaml +502 -44
- package/spec/schemas/change-delta.v1.schema.json +153 -0
- package/spec/schemas/documentation-reconciliation.v1.schema.json +106 -0
- package/spec/schemas/mcp-tools.schema.json +1 -1
- package/spec/schemas/state-db.sql +50 -13
- package/spec/schemas/tasks.v4.5.schema.json +1 -1
- package/ultra-tools/cli.cjs +3 -2
- package/ultra-tools/commands/db.cjs +76 -25
- package/ultra-tools/commands/migrate.cjs +995 -80
- package/ultra-tools/commands/session.cjs +34 -8
- package/ultra-tools/commands/status.cjs +32 -6
- package/ultra-tools/commands/system.cjs +129 -22
- package/commands/learn.md +0 -10
- package/orchestrator/skill-miner.cjs +0 -133
- package/output-styles/coding-vibes.md +0 -67
- package/output-styles/structural-thinking.md +0 -61
- package/skills/learn/SKILL.md +0 -58
|
@@ -7,6 +7,7 @@ const Database = require('better-sqlite3');
|
|
|
7
7
|
|
|
8
8
|
const { EXPECTED_VERSION, REQUIRED_TABLES } = require('./state-db.cjs');
|
|
9
9
|
const { readBreadcrumb } = require('./context-spine.cjs');
|
|
10
|
+
const runtimePaths = require('./runtime-paths.cjs');
|
|
10
11
|
|
|
11
12
|
function blockedBreadcrumb(code, {
|
|
12
13
|
allowedTransitions = [],
|
|
@@ -26,6 +27,7 @@ function blockedBreadcrumb(code, {
|
|
|
26
27
|
context_manifest_hash: null,
|
|
27
28
|
git_head: null,
|
|
28
29
|
baseline: null,
|
|
30
|
+
accepted_intent: [],
|
|
29
31
|
};
|
|
30
32
|
}
|
|
31
33
|
|
|
@@ -43,6 +45,12 @@ function doctorBreadcrumb(code) {
|
|
|
43
45
|
});
|
|
44
46
|
}
|
|
45
47
|
|
|
48
|
+
function compactText(value, maximum = 240) {
|
|
49
|
+
const normalized = String(value || '').replace(/\s+/g, ' ').trim();
|
|
50
|
+
if (normalized.length <= maximum) return normalized;
|
|
51
|
+
return `${normalized.slice(0, maximum - 3).trimEnd()}...`;
|
|
52
|
+
}
|
|
53
|
+
|
|
46
54
|
function latestSchemaVersion(db) {
|
|
47
55
|
return db.prepare(
|
|
48
56
|
'SELECT version FROM schema_version ORDER BY applied_at DESC, rowid DESC LIMIT 1',
|
|
@@ -59,7 +67,7 @@ function readProjectBreadcrumb(rootDir) {
|
|
|
59
67
|
const root = path.resolve(rootDir || process.cwd());
|
|
60
68
|
const ultraDir = path.join(root, '.ultra');
|
|
61
69
|
if (!fs.existsSync(ultraDir)) return null;
|
|
62
|
-
const dbPath =
|
|
70
|
+
const dbPath = runtimePaths.locateStateDb(root);
|
|
63
71
|
if (!fs.existsSync(dbPath)) {
|
|
64
72
|
return blockedBreadcrumb('STATE_DB_MISSING', {
|
|
65
73
|
allowedTransitions: ['ultra-init', 'ultra-status'],
|
|
@@ -131,8 +139,17 @@ function renderProjectBreadcrumb(rootDir, breadcrumb) {
|
|
|
131
139
|
lines.push(
|
|
132
140
|
`Decision thread: ${breadcrumb.decision.thread_id} (${breadcrumb.decision.status}/${breadcrumb.decision.mode})`,
|
|
133
141
|
current
|
|
134
|
-
? `Decision: ${current.id} — ${current.question}`
|
|
135
|
-
:
|
|
142
|
+
? `Decision: ${current.id} — ${compactText(current.question)}`
|
|
143
|
+
: (breadcrumb.decision.status === 'checkpoint_ready'
|
|
144
|
+
? 'Decision: checkpoint confirmation required'
|
|
145
|
+
: 'Decision: normalized intent recorded; application and read-back remain'),
|
|
146
|
+
);
|
|
147
|
+
}
|
|
148
|
+
for (const accepted of (breadcrumb.accepted_intent || []).slice(0, 3)) {
|
|
149
|
+
const value = accepted.decision
|
|
150
|
+
|| (accepted.consequences ? `Deferred: ${accepted.consequences}` : 'Normalized state recorded');
|
|
151
|
+
lines.push(
|
|
152
|
+
`Accepted intent: ${accepted.decision_id} [${accepted.authority || accepted.status}] ${compactText(value)}`,
|
|
136
153
|
);
|
|
137
154
|
}
|
|
138
155
|
lines.push(`Allowed transitions: ${(breadcrumb.allowed_transitions || []).join(', ') || 'none'}`);
|
|
@@ -144,7 +161,9 @@ function renderProjectBreadcrumb(rootDir, breadcrumb) {
|
|
|
144
161
|
`Context: ${breadcrumb.context_manifest_path} sha256=${breadcrumb.context_manifest_hash || 'unknown'}`,
|
|
145
162
|
);
|
|
146
163
|
}
|
|
147
|
-
lines.push(
|
|
164
|
+
lines.push(
|
|
165
|
+
'Lifecycle authority: .ultra/.runtime/state.db; digest-bound files carry semantic and evidence content; projections remain non-authoritative.',
|
|
166
|
+
);
|
|
148
167
|
return lines.join('\n');
|
|
149
168
|
}
|
|
150
169
|
|
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
'use strict';
|
|
2
2
|
|
|
3
|
-
// Projector: regenerates the read-only file views from .ultra/state.db.
|
|
3
|
+
// Projector: regenerates the read-only file views from .ultra/.runtime/state.db.
|
|
4
4
|
//
|
|
5
|
-
// state.db
|
|
6
|
-
//
|
|
5
|
+
// state.db owns lifecycle and artifact bindings (D32); registered digest-bound
|
|
6
|
+
// files own semantic/evidence bodies. tasks.json and contexts/task-*.md are
|
|
7
|
+
// complete projections — humans may read them, never write them. The projector
|
|
7
8
|
// is trigger-based: state-ops calls projectAll() after each successful
|
|
8
9
|
// write transaction. Projection output passes the v4.5 schemas under
|
|
9
10
|
// spec/schemas/ — see mcp-server/tests/projector.test.cjs for the
|
|
@@ -11,9 +12,14 @@
|
|
|
11
12
|
|
|
12
13
|
const fs = require('node:fs');
|
|
13
14
|
const path = require('node:path');
|
|
15
|
+
const crypto = require('node:crypto');
|
|
16
|
+
|
|
17
|
+
const ops = require('./state-ops.cjs');
|
|
18
|
+
const artifacts = require('./artifact-registry.cjs');
|
|
19
|
+
const contextPaths = require('./context-paths.cjs');
|
|
14
20
|
|
|
15
21
|
const SCHEMA_VERSION = '4.5';
|
|
16
|
-
const SOURCE_TAG = '.ultra/state.db';
|
|
22
|
+
const SOURCE_TAG = '.ultra/.runtime/state.db';
|
|
17
23
|
|
|
18
24
|
// Frozen SELECTs — values bind through @placeholders.
|
|
19
25
|
const TASK_PROJECTION_COLUMNS = 'id, title, type, priority, complexity, estimated_days, status, deps, files_modified, session_id, stale, tag, trace_to, outcome, slice_kind, public_seam, verification_command, acceptance_json, context_refs_json, docs_impact_json, ownership_json, context_file, completion_commit, created_at, updated_at';
|
|
@@ -84,7 +90,7 @@ function projectTasks(db, { tasksJson } = {}, opts = {}) {
|
|
|
84
90
|
return { path: target, count: rows.length };
|
|
85
91
|
}
|
|
86
92
|
|
|
87
|
-
function buildContextDoc(taskRow
|
|
93
|
+
function buildContextDoc(taskRow) {
|
|
88
94
|
const headerLines = [
|
|
89
95
|
'---',
|
|
90
96
|
`task_id: ${taskRow.id}`,
|
|
@@ -97,31 +103,20 @@ function buildContextDoc(taskRow, existingBody) {
|
|
|
97
103
|
if (taskRow.completion_commit) headerLines.push(`completion_commit: ${taskRow.completion_commit}`);
|
|
98
104
|
if (taskRow.stale) headerLines.push('stale: true');
|
|
99
105
|
headerLines.push(`schema_version: ${SCHEMA_VERSION}`);
|
|
106
|
+
headerLines.push(`generated_by: ${contextPaths.GENERATED_BY}`);
|
|
100
107
|
headerLines.push(`generated_at: ${taskRow.updated_at || new Date().toISOString()}`);
|
|
101
108
|
headerLines.push('---', '');
|
|
102
109
|
|
|
103
|
-
|
|
104
|
-
// v4.4 contexts carried a second generated status banner in the Markdown
|
|
105
|
-
// body. Once frontmatter is authoritative, retaining that line creates two
|
|
106
|
-
// state surfaces that can disagree. Remove only the legacy generated line;
|
|
107
|
-
// all task-authored body content remains intact.
|
|
108
|
-
body = body.replace(/^>\s*\*\*Status\*\*:.*(?:\r?\n|$)(?:\r?\n)?/mi, '');
|
|
109
|
-
// Strip any previous STALE banner so re-projection after reset is clean.
|
|
110
|
-
body = body.replace(/^> ⚠️\s*STALE[^\n]*\n(?:>[^\n]*\n)*\n?/m, '');
|
|
111
|
-
body = body.replace(
|
|
112
|
-
new RegExp(`${CONTRACT_START}[\\s\\S]*?${CONTRACT_END}\\n*`, 'm'),
|
|
113
|
-
'',
|
|
114
|
-
);
|
|
110
|
+
const body = [];
|
|
115
111
|
if (taskRow.stale) {
|
|
116
|
-
|
|
112
|
+
body.push([
|
|
117
113
|
`> ⚠️ STALE since ${taskRow.updated_at || new Date().toISOString()}`,
|
|
118
114
|
'> Specification evidence may have changed. Recompile the task context before continuing.',
|
|
119
115
|
'',
|
|
120
|
-
].join('\n');
|
|
121
|
-
body = banner + body;
|
|
116
|
+
].join('\n'));
|
|
122
117
|
}
|
|
123
|
-
body
|
|
124
|
-
return headerLines.join('\n') + body;
|
|
118
|
+
body.push(buildTaskContract(taskRow));
|
|
119
|
+
return headerLines.join('\n') + body.join('');
|
|
125
120
|
}
|
|
126
121
|
|
|
127
122
|
function buildTaskContract(task) {
|
|
@@ -167,43 +162,180 @@ function escapeYaml(str) {
|
|
|
167
162
|
return str;
|
|
168
163
|
}
|
|
169
164
|
|
|
170
|
-
function
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
165
|
+
function legacyContextParseError(message) {
|
|
166
|
+
const error = new Error(message);
|
|
167
|
+
error.code = 'LEGACY_CONTEXT_PARSE_ERROR';
|
|
168
|
+
return error;
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
function extractAuthoredContextBytes(contents) {
|
|
172
|
+
const original = Buffer.isBuffer(contents) ? contents : Buffer.from(contents);
|
|
173
|
+
let body = original.toString('utf8');
|
|
174
|
+
const frontmatter = /^(?:\uFEFF)?---(?:\r\n|\n)[\s\S]*?(?:\r\n|\n)---(?:(?:\r\n|\n)|$)/.exec(body);
|
|
175
|
+
if (frontmatter) body = body.slice(frontmatter[0].length);
|
|
176
|
+
const staleBanner = /^> ⚠️ STALE[^\r\n]*(?:(?:\r\n|\n)>[^\r\n]*)*(?:(?:\r\n|\n){1,2}|$)/.exec(body);
|
|
177
|
+
if (staleBanner) body = body.slice(staleBanner[0].length);
|
|
178
|
+
|
|
179
|
+
let cursor = 0;
|
|
180
|
+
while (true) {
|
|
181
|
+
const start = body.indexOf(CONTRACT_START, cursor);
|
|
182
|
+
if (start === -1) break;
|
|
183
|
+
const markerEnd = body.indexOf(CONTRACT_END, start + CONTRACT_START.length);
|
|
184
|
+
if (markerEnd === -1) {
|
|
185
|
+
throw legacyContextParseError(
|
|
186
|
+
'legacy task context has an unterminated generated execution contract',
|
|
187
|
+
);
|
|
188
|
+
}
|
|
189
|
+
let end = markerEnd + CONTRACT_END.length;
|
|
190
|
+
if (body.startsWith('\r\n', end)) end += 2;
|
|
191
|
+
else if (body.startsWith('\n', end)) end += 1;
|
|
192
|
+
const removeFrom = body.slice(0, start).trim() === '' ? 0 : start;
|
|
193
|
+
body = body.slice(0, removeFrom) + body.slice(end);
|
|
194
|
+
cursor = removeFrom;
|
|
195
|
+
}
|
|
196
|
+
return body.trim() === '' ? null : Buffer.from(body, 'utf8');
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
function promoteAuthoredContext(db, taskId, target, rootDir) {
|
|
200
|
+
if (!fs.existsSync(target.file)) return null;
|
|
201
|
+
const stat = fs.lstatSync(target.file);
|
|
202
|
+
if (!stat.isFile() || stat.isSymbolicLink()) {
|
|
203
|
+
throw new contextPaths.ContextPathError(
|
|
204
|
+
`task context must be a regular file: ${target.relative}`,
|
|
205
|
+
);
|
|
206
|
+
}
|
|
207
|
+
const sourceBytes = fs.readFileSync(target.file);
|
|
208
|
+
const bytes = extractAuthoredContextBytes(sourceBytes);
|
|
209
|
+
if (!bytes) return null;
|
|
210
|
+
const digest = crypto.createHash('sha256').update(bytes).digest('hex');
|
|
211
|
+
const sourceDigest = crypto.createHash('sha256').update(sourceBytes).digest('hex');
|
|
212
|
+
const safeTaskId = String(taskId).replace(/[^a-zA-Z0-9_-]+/g, '-');
|
|
213
|
+
const relative = `.ultra/docs/migration/legacy-context/${safeTaskId}-${digest.slice(0, 12)}.md`;
|
|
214
|
+
const file = path.resolve(rootDir, ...relative.split('/'));
|
|
215
|
+
const existed = fs.existsSync(file);
|
|
216
|
+
if (existed) {
|
|
217
|
+
const existingDigest = crypto.createHash('sha256').update(fs.readFileSync(file)).digest('hex');
|
|
218
|
+
if (existingDigest !== digest) {
|
|
219
|
+
throw new Error(`legacy context promotion digest conflict: ${relative}`);
|
|
220
|
+
}
|
|
221
|
+
} else {
|
|
222
|
+
writeAtomic(file, bytes);
|
|
223
|
+
}
|
|
224
|
+
try {
|
|
225
|
+
const recorded = artifacts.recordArtifact(db, {
|
|
226
|
+
owner_type: 'task',
|
|
227
|
+
owner_id: taskId,
|
|
228
|
+
kind: 'legacy_context_findings',
|
|
229
|
+
path: relative,
|
|
230
|
+
content_digest: digest,
|
|
231
|
+
source_refs: [{
|
|
232
|
+
type: 'external',
|
|
233
|
+
id: `legacy-context:${target.relative}`,
|
|
234
|
+
relation: 'imported_from',
|
|
235
|
+
}],
|
|
236
|
+
consumer_refs: [{ type: 'task', id: taskId, relation: 'consumed_by' }],
|
|
237
|
+
provenance: {
|
|
238
|
+
writer: 'ultra-projector',
|
|
239
|
+
operation: 'legacy-context-promotion',
|
|
240
|
+
},
|
|
241
|
+
metadata: {
|
|
242
|
+
original_path: target.relative,
|
|
243
|
+
original_digest: sourceDigest,
|
|
244
|
+
authored_body_digest: digest,
|
|
245
|
+
},
|
|
246
|
+
}, { rootDir });
|
|
247
|
+
return { path: relative, digest, artifact_id: recorded.artifact.id };
|
|
248
|
+
} catch (error) {
|
|
249
|
+
if (!existed) fs.rmSync(file, { force: true });
|
|
250
|
+
throw error;
|
|
251
|
+
}
|
|
177
252
|
}
|
|
178
253
|
|
|
179
254
|
function projectContext(db, taskId, { contextsDir } = {}, opts = {}) {
|
|
180
255
|
const row = db.prepare(READ_TASK_FOR_PROJECTION_SQL).get({ id: taskId });
|
|
181
256
|
if (!row) return null;
|
|
182
257
|
const projection = rowToProjection(row);
|
|
183
|
-
const
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
258
|
+
const rootDir = path.resolve(opts.rootDir || '.');
|
|
259
|
+
if (contextsDir) {
|
|
260
|
+
const supplied = path.resolve(contextsDir);
|
|
261
|
+
const canonical = defaultPaths(rootDir).contextsDir;
|
|
262
|
+
if (supplied !== canonical) {
|
|
263
|
+
throw new contextPaths.ContextPathError(
|
|
264
|
+
`custom context projection roots are not allowed: ${contextsDir}`,
|
|
265
|
+
);
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
const target = contextPaths.resolveContextPath(rootDir, projection.context_file, { taskId });
|
|
269
|
+
const promoted = promoteAuthoredContext(db, taskId, target, rootDir);
|
|
270
|
+
const next = buildContextDoc(projection);
|
|
271
|
+
writeAtomic(target.file, next);
|
|
272
|
+
return { path: target.file, task_id: taskId, promoted };
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
function listContextFiles(directory) {
|
|
276
|
+
if (!fs.existsSync(directory)) return [];
|
|
277
|
+
const output = [];
|
|
278
|
+
for (const entry of fs.readdirSync(directory, { withFileTypes: true })) {
|
|
279
|
+
const file = path.join(directory, entry.name);
|
|
280
|
+
if (entry.isSymbolicLink()) continue;
|
|
281
|
+
if (entry.isDirectory()) output.push(...listContextFiles(file));
|
|
282
|
+
else if (entry.isFile()) output.push(file);
|
|
283
|
+
}
|
|
284
|
+
return output;
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
function pruneGeneratedGhosts(db, rootDir, expected) {
|
|
288
|
+
const contextsDir = defaultPaths(rootDir).contextsDir;
|
|
289
|
+
const pruned = [];
|
|
290
|
+
for (const file of listContextFiles(contextsDir)) {
|
|
291
|
+
const relative = path.relative(rootDir, file).split(path.sep).join('/');
|
|
292
|
+
if (expected.has(relative) || path.basename(file) === 'TEMPLATE.md') continue;
|
|
293
|
+
const contents = fs.readFileSync(file);
|
|
294
|
+
if (!contextPaths.isGeneratedContextContents(contents)) continue;
|
|
295
|
+
const staged = `${file}.prune-${process.pid}-${Date.now()}`;
|
|
296
|
+
fs.renameSync(file, staged);
|
|
297
|
+
try {
|
|
298
|
+
ops.appendEvent(db, {
|
|
299
|
+
type: 'projection_pruned',
|
|
300
|
+
payload: { path: relative, reason: 'task_missing' },
|
|
301
|
+
});
|
|
302
|
+
fs.rmSync(staged, { force: true });
|
|
303
|
+
pruned.push(relative);
|
|
304
|
+
} catch (error) {
|
|
305
|
+
fs.renameSync(staged, file);
|
|
306
|
+
throw error;
|
|
307
|
+
}
|
|
308
|
+
}
|
|
309
|
+
return pruned.sort();
|
|
191
310
|
}
|
|
192
311
|
|
|
193
312
|
function projectAll(db, { rootDir = '.', tasksJson, contextsDir } = {}) {
|
|
194
|
-
const
|
|
313
|
+
const resolvedRoot = path.resolve(rootDir);
|
|
314
|
+
const paths = defaultPaths(resolvedRoot);
|
|
195
315
|
const tasksTarget = tasksJson || paths.tasksJson;
|
|
196
316
|
const contextsTarget = contextsDir || paths.contextsDir;
|
|
317
|
+
if (path.resolve(contextsTarget) !== paths.contextsDir) {
|
|
318
|
+
throw new contextPaths.ContextPathError(
|
|
319
|
+
`custom context projection roots are not allowed: ${contextsTarget}`,
|
|
320
|
+
);
|
|
321
|
+
}
|
|
197
322
|
|
|
323
|
+
const rows = db.prepare("SELECT id, context_file FROM tasks").all();
|
|
324
|
+
const expected = new Set(rows.map((row) => (
|
|
325
|
+
contextPaths.resolveContextPath(resolvedRoot, row.context_file, { taskId: row.id }).relative
|
|
326
|
+
)));
|
|
198
327
|
const tasksResult = projectTasks(db, { tasksJson: tasksTarget });
|
|
199
328
|
const contextResults = [];
|
|
200
|
-
const rows = db.prepare("SELECT id FROM tasks").all();
|
|
201
329
|
for (const r of rows) {
|
|
202
|
-
contextResults.push(projectContext(
|
|
330
|
+
contextResults.push(projectContext(
|
|
331
|
+
db, r.id, { contextsDir: contextsTarget }, { rootDir: resolvedRoot },
|
|
332
|
+
));
|
|
203
333
|
}
|
|
334
|
+
const pruned = pruneGeneratedGhosts(db, resolvedRoot, expected);
|
|
204
335
|
return {
|
|
205
336
|
tasks_json: tasksResult,
|
|
206
337
|
contexts: contextResults.filter(Boolean),
|
|
338
|
+
pruned,
|
|
207
339
|
};
|
|
208
340
|
}
|
|
209
341
|
|
|
@@ -214,4 +346,6 @@ module.exports = {
|
|
|
214
346
|
projectContext,
|
|
215
347
|
projectAll,
|
|
216
348
|
rowToProjection,
|
|
349
|
+
extractAuthoredContextBytes,
|
|
350
|
+
promoteAuthoredContext,
|
|
217
351
|
};
|