ultra-builder-pro-cli 0.24.2 → 0.25.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/CHANGELOG.md +42 -0
- package/adapters/claude.js +104 -0
- package/adapters/codex.js +363 -40
- package/adapters/grok.js +1254 -58
- package/adapters/kimi.js +682 -13
- package/adapters/opencode.js +132 -11
- package/docs/AGENT-CONTEXT.md +14 -0
- package/docs/ARTIFACT-AUTHORITY.md +13 -0
- package/docs/COMMIT-HASH-BACKFILL.md +25 -18
- package/docs/RUNTIME-COMPAT-MATRIX.md +18 -2
- package/mcp-server/lib/artifact-registry.cjs +181 -51
- package/mcp-server/lib/baseline-workflow.cjs +160 -23
- package/mcp-server/lib/change-workflow.cjs +21 -16
- package/mcp-server/lib/context-envelope.cjs +124 -51
- package/mcp-server/lib/decision-records.cjs +164 -78
- package/mcp-server/lib/kernel-change-workflow.cjs +2029 -0
- package/mcp-server/lib/legacy-change-workflow.cjs +294 -71
- package/mcp-server/lib/managed-file-write.cjs +56 -0
- package/mcp-server/lib/prd-parser.cjs +2 -2
- package/mcp-server/lib/stage-checkpoints.cjs +163 -9
- package/mcp-server/lib/state-db.cjs +186 -8
- package/mcp-server/lib/state-ops.cjs +214 -31
- package/mcp-server/lib/task-contract.cjs +404 -0
- package/mcp-server/lib/task-expander.cjs +2 -2
- package/mcp-server/lib/task-ledger.cjs +175 -62
- package/mcp-server/lib/ultra-facade.cjs +1786 -169
- package/mcp-server/lib/worker-packet.cjs +2 -2
- package/mcp-server/server.cjs +23 -4
- package/orchestrator/parallel-orchestrator.cjs +114 -27
- package/orchestrator/session-runner.cjs +25 -136
- package/package.json +1 -1
- package/skills/ultra-change/SKILL.md +6 -1
- package/skills/ultra-dev/SKILL.md +9 -4
- package/skills/ultra-plan/SKILL.md +13 -4
- package/skills/ultra-plan/references/semantic-preflight.md +6 -4
- package/spec/mcp-tools.yaml +114 -9
- package/spec/schemas/context-file.v4.5.schema.json +2 -2
- package/spec/schemas/state-db.sql +10 -6
- package/spec/schemas/task-ledger.v2.schema.json +23 -8
- package/spec/schemas/tasks.v4.5.schema.json +34 -12
|
@@ -4,12 +4,25 @@ const crypto = require('node:crypto');
|
|
|
4
4
|
const fs = require('node:fs');
|
|
5
5
|
const path = require('node:path');
|
|
6
6
|
const util = require('node:util');
|
|
7
|
+
const Ajv = require('ajv/dist/2020');
|
|
8
|
+
const addFormats = require('ajv-formats');
|
|
7
9
|
|
|
8
10
|
const canonical = require('./canonical-json.cjs');
|
|
9
11
|
const decisionRecords = require('./decision-records.cjs');
|
|
12
|
+
const {
|
|
13
|
+
restoreManagedFile,
|
|
14
|
+
snapshotManagedFile,
|
|
15
|
+
writeManagedFile,
|
|
16
|
+
} = require('./managed-file-write.cjs');
|
|
10
17
|
const ops = require('./state-ops.cjs');
|
|
11
18
|
const { readStableProjectFile } = require('./safe-project-file.cjs');
|
|
12
19
|
const stageCheckpoints = require('./stage-checkpoints.cjs');
|
|
20
|
+
const taskContract = require('./task-contract.cjs');
|
|
21
|
+
const taskLedgerSchema = require('../../spec/schemas/task-ledger.v2.schema.json');
|
|
22
|
+
|
|
23
|
+
const taskLedgerAjv = new Ajv({ allErrors: true, strict: false });
|
|
24
|
+
addFormats(taskLedgerAjv);
|
|
25
|
+
const validateTaskLedgerSchema = taskLedgerAjv.compile(taskLedgerSchema);
|
|
13
26
|
|
|
14
27
|
const LEDGER_KIND = 'ultra-team-task-ledger';
|
|
15
28
|
const LEDGER_SCHEMA_VERSION = '2.0';
|
|
@@ -17,6 +30,7 @@ const LEGACY_LEDGER_SCHEMA_VERSION = '1.0';
|
|
|
17
30
|
const LEDGER_RELATIVE_PATH = '.ultra/tasks/tasks.json';
|
|
18
31
|
const HASH_PATTERN = /^[0-9a-f]{64}$/;
|
|
19
32
|
const MAX_ANCESTORS = 64;
|
|
33
|
+
const PUBLICATION_ROLLBACK = Symbol('task-ledger-publication-rollback');
|
|
20
34
|
const DURABLE_BASELINE_FIELDS = Object.freeze([
|
|
21
35
|
'id',
|
|
22
36
|
'project_name',
|
|
@@ -48,29 +62,7 @@ const LEGACY_DURABLE_BASELINE_FIELDS = Object.freeze(
|
|
|
48
62
|
field === 'research_checkpoint_id' ? 'research_run_id' : field
|
|
49
63
|
)),
|
|
50
64
|
);
|
|
51
|
-
const DURABLE_TASK_FIELDS =
|
|
52
|
-
'id',
|
|
53
|
-
'title',
|
|
54
|
-
'type',
|
|
55
|
-
'priority',
|
|
56
|
-
'complexity',
|
|
57
|
-
'estimated_days',
|
|
58
|
-
'status',
|
|
59
|
-
'deps',
|
|
60
|
-
'files_modified',
|
|
61
|
-
'stale',
|
|
62
|
-
'trace_to',
|
|
63
|
-
'outcome',
|
|
64
|
-
'slice_kind',
|
|
65
|
-
'public_seam',
|
|
66
|
-
'verification_command',
|
|
67
|
-
'acceptance',
|
|
68
|
-
'context_refs',
|
|
69
|
-
'docs_impact',
|
|
70
|
-
'ownership',
|
|
71
|
-
'change_id',
|
|
72
|
-
'parent_id',
|
|
73
|
-
]);
|
|
65
|
+
const DURABLE_TASK_FIELDS = taskContract.DURABLE_TASK_FIELDS;
|
|
74
66
|
const DURABLE_CHANGE_FIELDS = Object.freeze([
|
|
75
67
|
'id',
|
|
76
68
|
'title',
|
|
@@ -84,6 +76,7 @@ const DURABLE_CHANGE_FIELDS = Object.freeze([
|
|
|
84
76
|
'classification',
|
|
85
77
|
'research_disposition',
|
|
86
78
|
'base_commit',
|
|
79
|
+
'supersedes_id',
|
|
87
80
|
'artifact_root',
|
|
88
81
|
'closed_at',
|
|
89
82
|
]);
|
|
@@ -241,6 +234,7 @@ function portableChange(row) {
|
|
|
241
234
|
['classification', parseJson(row.classification_json, {})],
|
|
242
235
|
['research_disposition', parseJson(row.research_disposition_json, {})],
|
|
243
236
|
['base_commit', row.base_commit],
|
|
237
|
+
['supersedes_id', row.supersedes_id],
|
|
244
238
|
['artifact_root', row.artifact_root],
|
|
245
239
|
['closed_at', row.closed_at],
|
|
246
240
|
]);
|
|
@@ -331,6 +325,20 @@ function durableStatus(task, previous) {
|
|
|
331
325
|
return 'pending';
|
|
332
326
|
}
|
|
333
327
|
|
|
328
|
+
function assertValidTaskContract(entry) {
|
|
329
|
+
try {
|
|
330
|
+
taskContract.validateDurableTask(entry, { metadata: false });
|
|
331
|
+
} catch (error) {
|
|
332
|
+
if (!(error instanceof taskContract.TaskContractError)) throw error;
|
|
333
|
+
throw new TaskLedgerError(
|
|
334
|
+
'TASK_LEDGER_INVALID',
|
|
335
|
+
`task ledger entry ${entry?.id || '(unknown)'} violates the Task Contract: ${error.message}`,
|
|
336
|
+
error.details,
|
|
337
|
+
);
|
|
338
|
+
}
|
|
339
|
+
return entry;
|
|
340
|
+
}
|
|
341
|
+
|
|
334
342
|
function durableTask(task, previous = null) {
|
|
335
343
|
const value = compactObject([
|
|
336
344
|
['id', task.id],
|
|
@@ -340,29 +348,33 @@ function durableTask(task, previous = null) {
|
|
|
340
348
|
['complexity', task.complexity],
|
|
341
349
|
['estimated_days', task.estimated_days],
|
|
342
350
|
['status', durableStatus(task, previous)],
|
|
343
|
-
['deps',
|
|
344
|
-
['files_modified',
|
|
345
|
-
['stale',
|
|
351
|
+
['deps', task.deps == null ? [] : task.deps],
|
|
352
|
+
['files_modified', task.files_modified == null ? [] : task.files_modified],
|
|
353
|
+
['stale', task.stale == null ? false : task.stale],
|
|
346
354
|
['trace_to', task.trace_to],
|
|
347
355
|
['outcome', task.outcome],
|
|
348
356
|
['slice_kind', task.slice_kind],
|
|
349
357
|
['public_seam', task.public_seam],
|
|
350
358
|
['verification_command', task.verification_command],
|
|
351
|
-
['acceptance',
|
|
352
|
-
['context_refs',
|
|
353
|
-
['docs_impact', task.docs_impact
|
|
354
|
-
|
|
359
|
+
['acceptance', task.acceptance == null ? [] : task.acceptance],
|
|
360
|
+
['context_refs', task.context_refs == null ? [] : task.context_refs],
|
|
361
|
+
['docs_impact', task.docs_impact == null
|
|
362
|
+
? { status: 'unknown', files: [], rationale: null }
|
|
363
|
+
: task.docs_impact],
|
|
364
|
+
['ownership', task.ownership == null ? {} : task.ownership],
|
|
355
365
|
['change_id', task.change_id],
|
|
356
366
|
['parent_id', task.parent_id],
|
|
357
367
|
]);
|
|
358
368
|
const digest = sha256(value);
|
|
359
|
-
|
|
360
|
-
return {
|
|
369
|
+
const entry = {
|
|
361
370
|
...value,
|
|
362
371
|
revision: previous ? Number(previous.revision || 0) + 1 : 1,
|
|
363
372
|
parent_digest: previous?.digest || null,
|
|
364
373
|
digest,
|
|
365
374
|
};
|
|
375
|
+
assertValidTaskContract(entry);
|
|
376
|
+
if (previous && previous.digest === digest) return previous;
|
|
377
|
+
return entry;
|
|
366
378
|
}
|
|
367
379
|
|
|
368
380
|
function statePayload({
|
|
@@ -385,7 +397,9 @@ function validateTask(entry) {
|
|
|
385
397
|
if (!entry || typeof entry !== 'object' || Array.isArray(entry)) {
|
|
386
398
|
throw new TaskLedgerError('TASK_LEDGER_INVALID', 'every task ledger entry must be an object');
|
|
387
399
|
}
|
|
388
|
-
for (const field of [
|
|
400
|
+
for (const field of [
|
|
401
|
+
'id', 'title', 'type', 'priority', 'status', 'revision', 'digest',
|
|
402
|
+
]) {
|
|
389
403
|
if (entry[field] === undefined || entry[field] === null || entry[field] === '') {
|
|
390
404
|
throw new TaskLedgerError(
|
|
391
405
|
'TASK_LEDGER_INVALID',
|
|
@@ -393,6 +407,19 @@ function validateTask(entry) {
|
|
|
393
407
|
);
|
|
394
408
|
}
|
|
395
409
|
}
|
|
410
|
+
if (!Object.prototype.hasOwnProperty.call(entry, 'parent_digest')) {
|
|
411
|
+
throw new TaskLedgerError(
|
|
412
|
+
'TASK_LEDGER_INVALID',
|
|
413
|
+
`task ledger entry ${entry.id || '(unknown)'} is missing parent_digest`,
|
|
414
|
+
);
|
|
415
|
+
}
|
|
416
|
+
assertValidTaskContract(entry);
|
|
417
|
+
if (!Number.isInteger(entry.revision) || entry.revision < 1) {
|
|
418
|
+
throw new TaskLedgerError(
|
|
419
|
+
'TASK_LEDGER_INVALID',
|
|
420
|
+
`task ledger entry ${entry.id} has an invalid revision`,
|
|
421
|
+
);
|
|
422
|
+
}
|
|
396
423
|
if (!HASH_PATTERN.test(entry.digest) || entry.digest !== sha256(
|
|
397
424
|
Object.fromEntries(DURABLE_TASK_FIELDS
|
|
398
425
|
.filter((field) => entry[field] !== undefined)
|
|
@@ -411,6 +438,35 @@ function validateTask(entry) {
|
|
|
411
438
|
}
|
|
412
439
|
}
|
|
413
440
|
|
|
441
|
+
function validateLegacyTask(entry) {
|
|
442
|
+
if (!entry || typeof entry !== 'object' || Array.isArray(entry)) {
|
|
443
|
+
throw new TaskLedgerError('TASK_LEDGER_INVALID', 'every legacy task entry must be an object');
|
|
444
|
+
}
|
|
445
|
+
for (const field of ['id', 'title', 'type', 'priority', 'status', 'revision', 'digest']) {
|
|
446
|
+
if (entry[field] === undefined || entry[field] === null || entry[field] === '') {
|
|
447
|
+
throw new TaskLedgerError(
|
|
448
|
+
'TASK_LEDGER_INVALID',
|
|
449
|
+
`legacy task entry ${entry.id || '(unknown)'} is missing ${field}`,
|
|
450
|
+
);
|
|
451
|
+
}
|
|
452
|
+
}
|
|
453
|
+
const value = Object.fromEntries(DURABLE_TASK_FIELDS
|
|
454
|
+
.filter((field) => entry[field] !== undefined)
|
|
455
|
+
.map((field) => [field, entry[field]]));
|
|
456
|
+
if (!HASH_PATTERN.test(entry.digest) || entry.digest !== sha256(value)) {
|
|
457
|
+
throw new TaskLedgerError(
|
|
458
|
+
'TASK_LEDGER_DIGEST_MISMATCH',
|
|
459
|
+
`legacy task entry ${entry.id} does not match its digest`,
|
|
460
|
+
);
|
|
461
|
+
}
|
|
462
|
+
if (entry.parent_digest != null && !HASH_PATTERN.test(entry.parent_digest)) {
|
|
463
|
+
throw new TaskLedgerError(
|
|
464
|
+
'TASK_LEDGER_INVALID',
|
|
465
|
+
`legacy task entry ${entry.id} has an invalid parent_digest`,
|
|
466
|
+
);
|
|
467
|
+
}
|
|
468
|
+
}
|
|
469
|
+
|
|
414
470
|
function validateBaseline(entry) {
|
|
415
471
|
if (entry == null) return;
|
|
416
472
|
if (typeof entry !== 'object' || Array.isArray(entry)) {
|
|
@@ -571,7 +627,7 @@ function validateLegacyLedger(document, file) {
|
|
|
571
627
|
return false;
|
|
572
628
|
}
|
|
573
629
|
validateLegacyBaseline(document.baseline);
|
|
574
|
-
document.tasks.forEach(
|
|
630
|
+
document.tasks.forEach(validateLegacyTask);
|
|
575
631
|
document.changes.forEach(validateChange);
|
|
576
632
|
const expected = sha256({
|
|
577
633
|
baseline: document.baseline || null,
|
|
@@ -615,6 +671,16 @@ function validateLedger(document, file = '(memory)') {
|
|
|
615
671
|
`${file} is not an Ultra team task ledger v${LEDGER_SCHEMA_VERSION}`,
|
|
616
672
|
);
|
|
617
673
|
}
|
|
674
|
+
if (!validateTaskLedgerSchema(document)) {
|
|
675
|
+
const first = validateTaskLedgerSchema.errors?.[0];
|
|
676
|
+
const location = first?.instancePath || '(root)';
|
|
677
|
+
throw new TaskLedgerError(
|
|
678
|
+
'TASK_LEDGER_INVALID',
|
|
679
|
+
`${file} violates the exact v${LEDGER_SCHEMA_VERSION} schema at ${location}: `
|
|
680
|
+
+ `${first?.message || 'schema validation failed'}`,
|
|
681
|
+
{ errors: validateTaskLedgerSchema.errors || [] },
|
|
682
|
+
);
|
|
683
|
+
}
|
|
618
684
|
if (document.ancestors.length > MAX_ANCESTORS
|
|
619
685
|
|| document.ancestors.some((digest) => !HASH_PATTERN.test(digest))) {
|
|
620
686
|
throw new TaskLedgerError(
|
|
@@ -703,22 +769,12 @@ function readTaskLedger(rootDir, { optional = true } = {}) {
|
|
|
703
769
|
}
|
|
704
770
|
|
|
705
771
|
function writeLedgerAtomic(rootDir, document) {
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
}
|
|
713
|
-
}
|
|
714
|
-
const temp = `${file}.tmp-${process.pid}-${Date.now()}`;
|
|
715
|
-
try {
|
|
716
|
-
fs.writeFileSync(temp, `${JSON.stringify(document, null, 2)}\n`, { flag: 'wx' });
|
|
717
|
-
fs.renameSync(temp, file);
|
|
718
|
-
} finally {
|
|
719
|
-
try { fs.rmSync(temp, { force: true }); } catch { /* best effort */ }
|
|
720
|
-
}
|
|
721
|
-
return file;
|
|
772
|
+
writeManagedFile(
|
|
773
|
+
rootDir,
|
|
774
|
+
LEDGER_RELATIVE_PATH,
|
|
775
|
+
`${JSON.stringify(document, null, 2)}\n`,
|
|
776
|
+
);
|
|
777
|
+
return ledgerPath(rootDir);
|
|
722
778
|
}
|
|
723
779
|
|
|
724
780
|
function migrateLegacyProjection(db, rootDir, originalError) {
|
|
@@ -998,19 +1054,56 @@ function publishTaskLedger(db, {
|
|
|
998
1054
|
tasks,
|
|
999
1055
|
};
|
|
1000
1056
|
validateLedger(document);
|
|
1001
|
-
const
|
|
1002
|
-
|
|
1003
|
-
|
|
1004
|
-
|
|
1005
|
-
|
|
1006
|
-
|
|
1007
|
-
|
|
1057
|
+
const snapshot = snapshotManagedFile(rootDir, LEDGER_RELATIVE_PATH);
|
|
1058
|
+
let file;
|
|
1059
|
+
try {
|
|
1060
|
+
file = writeLedgerAtomic(rootDir, document);
|
|
1061
|
+
recordLedgerEvent(db, 'task_ledger_published', document, {
|
|
1062
|
+
reason,
|
|
1063
|
+
migrated_legacy_projection: prior.migrated,
|
|
1064
|
+
legacy_backup_path: prior.backup,
|
|
1065
|
+
});
|
|
1066
|
+
} catch (error) {
|
|
1067
|
+
try {
|
|
1068
|
+
restoreManagedFile(rootDir, snapshot);
|
|
1069
|
+
} catch (rollbackError) {
|
|
1070
|
+
throw new TaskLedgerError(
|
|
1071
|
+
'STATE_CORRUPT',
|
|
1072
|
+
'task ledger publication failed and the previous ledger bytes could not be restored',
|
|
1073
|
+
{
|
|
1074
|
+
cause: error.code || error.message,
|
|
1075
|
+
rollback: rollbackError.code || rollbackError.message,
|
|
1076
|
+
path: LEDGER_RELATIVE_PATH,
|
|
1077
|
+
},
|
|
1078
|
+
);
|
|
1079
|
+
}
|
|
1080
|
+
throw error;
|
|
1081
|
+
}
|
|
1082
|
+
const result = {
|
|
1008
1083
|
changed: true,
|
|
1009
1084
|
path: file,
|
|
1010
1085
|
ledger: document,
|
|
1011
1086
|
migrated_legacy_projection: prior.migrated,
|
|
1012
1087
|
legacy_backup_path: prior.backup,
|
|
1013
1088
|
};
|
|
1089
|
+
Object.defineProperty(result, PUBLICATION_ROLLBACK, {
|
|
1090
|
+
configurable: false,
|
|
1091
|
+
enumerable: false,
|
|
1092
|
+
writable: false,
|
|
1093
|
+
value: { rootDir: path.resolve(rootDir), snapshot },
|
|
1094
|
+
});
|
|
1095
|
+
return result;
|
|
1096
|
+
}
|
|
1097
|
+
|
|
1098
|
+
function rollbackTaskLedgerPublication(publication) {
|
|
1099
|
+
const rollback = publication?.[PUBLICATION_ROLLBACK];
|
|
1100
|
+
if (!rollback) return { restored: false, reason: 'NO_PUBLICATION_ROLLBACK' };
|
|
1101
|
+
restoreManagedFile(rollback.rootDir, rollback.snapshot);
|
|
1102
|
+
return {
|
|
1103
|
+
restored: true,
|
|
1104
|
+
path: rollback.snapshot.path,
|
|
1105
|
+
previous_existed: rollback.snapshot.existed,
|
|
1106
|
+
};
|
|
1014
1107
|
}
|
|
1015
1108
|
|
|
1016
1109
|
function latestImportedCheckpoint(db) {
|
|
@@ -1247,8 +1340,8 @@ function insertChange(db, change) {
|
|
|
1247
1340
|
`INSERT INTO changes
|
|
1248
1341
|
(id, title, kind, status, intent, docs_impact_json, provider_refs_json,
|
|
1249
1342
|
baseline_bypass_json, contract_json, classification_json,
|
|
1250
|
-
research_disposition_json, base_commit, artifact_root, closed_at)
|
|
1251
|
-
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
|
|
1343
|
+
research_disposition_json, base_commit, supersedes_id, artifact_root, closed_at)
|
|
1344
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
|
|
1252
1345
|
).run(
|
|
1253
1346
|
change.id,
|
|
1254
1347
|
change.title,
|
|
@@ -1262,6 +1355,7 @@ function insertChange(db, change) {
|
|
|
1262
1355
|
JSON.stringify(change.classification || {}),
|
|
1263
1356
|
JSON.stringify(change.research_disposition || {}),
|
|
1264
1357
|
change.base_commit || null,
|
|
1358
|
+
change.supersedes_id || null,
|
|
1265
1359
|
change.artifact_root,
|
|
1266
1360
|
change.closed_at || null,
|
|
1267
1361
|
);
|
|
@@ -1273,7 +1367,7 @@ function updateChangeFromLedger(db, change) {
|
|
|
1273
1367
|
title = ?, kind = ?, status = ?, intent = ?, docs_impact_json = ?,
|
|
1274
1368
|
provider_refs_json = ?, baseline_bypass_json = ?, contract_json = ?,
|
|
1275
1369
|
classification_json = ?, research_disposition_json = ?, base_commit = ?,
|
|
1276
|
-
artifact_root = ?, closed_at = ?, updated_at = ?
|
|
1370
|
+
supersedes_id = ?, artifact_root = ?, closed_at = ?, updated_at = ?
|
|
1277
1371
|
WHERE id = ?`,
|
|
1278
1372
|
).run(
|
|
1279
1373
|
change.title,
|
|
@@ -1287,6 +1381,7 @@ function updateChangeFromLedger(db, change) {
|
|
|
1287
1381
|
JSON.stringify(change.classification || {}),
|
|
1288
1382
|
JSON.stringify(change.research_disposition || {}),
|
|
1289
1383
|
change.base_commit || null,
|
|
1384
|
+
change.supersedes_id || null,
|
|
1290
1385
|
change.artifact_root,
|
|
1291
1386
|
change.closed_at || null,
|
|
1292
1387
|
new Date().toISOString(),
|
|
@@ -1317,7 +1412,9 @@ function insertTaskFromLedger(db, task) {
|
|
|
1317
1412
|
change_id: task.change_id || null,
|
|
1318
1413
|
parent_id: null,
|
|
1319
1414
|
});
|
|
1320
|
-
if (task.status !== created.status
|
|
1415
|
+
if (task.status !== created.status || Boolean(task.stale) !== created.stale) {
|
|
1416
|
+
updateTaskFromLedger(db, task);
|
|
1417
|
+
}
|
|
1321
1418
|
return ops.readTask(db, task.id);
|
|
1322
1419
|
}
|
|
1323
1420
|
|
|
@@ -1652,6 +1749,21 @@ function importTaskLedger(db, {
|
|
|
1652
1749
|
parentUpdates.push([incoming.id, incoming.parent_id || null]);
|
|
1653
1750
|
continue;
|
|
1654
1751
|
}
|
|
1752
|
+
const runningSession = db.prepare(
|
|
1753
|
+
"SELECT sid FROM sessions WHERE task_id = ? AND status = 'running' LIMIT 1",
|
|
1754
|
+
).get(incoming.id);
|
|
1755
|
+
if (current.session_id || runningSession) {
|
|
1756
|
+
throw new TaskLedgerError(
|
|
1757
|
+
'TASK_LEDGER_ACTIVE_TASK_CONFLICT',
|
|
1758
|
+
`task ${incoming.id} changed in Git while a local session still owns it`,
|
|
1759
|
+
{
|
|
1760
|
+
task_id: incoming.id,
|
|
1761
|
+
local_status: current.status,
|
|
1762
|
+
task_session_id: current.session_id || null,
|
|
1763
|
+
running_session_id: runningSession?.sid || null,
|
|
1764
|
+
},
|
|
1765
|
+
);
|
|
1766
|
+
}
|
|
1655
1767
|
if (current.status === 'in_progress' && last && incoming.digest !== last) {
|
|
1656
1768
|
throw new TaskLedgerError(
|
|
1657
1769
|
'TASK_LEDGER_ACTIVE_TASK_CONFLICT',
|
|
@@ -1717,7 +1829,7 @@ function importTaskLedger(db, {
|
|
|
1717
1829
|
{ task_id: id, session_count: sessionCount },
|
|
1718
1830
|
);
|
|
1719
1831
|
}
|
|
1720
|
-
|
|
1832
|
+
ops.deleteTask(db, id, { rootDir });
|
|
1721
1833
|
deleted += 1;
|
|
1722
1834
|
}
|
|
1723
1835
|
for (const [id, parentId] of parentUpdates) {
|
|
@@ -1846,6 +1958,7 @@ module.exports = {
|
|
|
1846
1958
|
ledgerPath,
|
|
1847
1959
|
publishTaskLedger,
|
|
1848
1960
|
readTaskLedger,
|
|
1961
|
+
rollbackTaskLedgerPublication,
|
|
1849
1962
|
syncTaskLedger,
|
|
1850
1963
|
validateLedger,
|
|
1851
1964
|
};
|