opencode-swarm 7.33.0 → 7.33.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/README.md +1 -0
- package/dist/cli/index.js +1047 -516
- package/dist/evidence/gate-bridge.d.ts +15 -0
- package/dist/index.js +403 -155
- package/dist/plan/ledger.d.ts +9 -0
- package/dist/plan/manager.d.ts +28 -2
- package/dist/services/evidence-summary-service.d.ts +5 -0
- package/dist/telemetry.d.ts +1 -1
- package/dist/tools/save-plan.d.ts +12 -0
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -48,7 +48,7 @@ var package_default;
|
|
|
48
48
|
var init_package = __esm(() => {
|
|
49
49
|
package_default = {
|
|
50
50
|
name: "opencode-swarm",
|
|
51
|
-
version: "7.33.
|
|
51
|
+
version: "7.33.1",
|
|
52
52
|
description: "Architect-centric agentic swarm plugin for OpenCode - hub-and-spoke orchestration with SME consultation, code generation, and QA review",
|
|
53
53
|
main: "dist/index.js",
|
|
54
54
|
types: "dist/index.d.ts",
|
|
@@ -17370,6 +17370,32 @@ async function appendLedgerEvent(directory, eventInput, options) {
|
|
|
17370
17370
|
fs4.renameSync(tempPath, ledgerPath);
|
|
17371
17371
|
return event;
|
|
17372
17372
|
}
|
|
17373
|
+
async function takeSnapshotWithRetry(directory, plan, options) {
|
|
17374
|
+
const MAX_RETRIES = 3;
|
|
17375
|
+
const TOTAL_ATTEMPTS = 1 + MAX_RETRIES;
|
|
17376
|
+
const telemetrySource = options?.source ?? "save_plan_tool";
|
|
17377
|
+
const snapshotOptions = { planHashAfter: options?.planHashAfter };
|
|
17378
|
+
let lastError;
|
|
17379
|
+
for (let attempt = 1;attempt <= TOTAL_ATTEMPTS; attempt++) {
|
|
17380
|
+
try {
|
|
17381
|
+
await takeSnapshotEvent(directory, plan, snapshotOptions);
|
|
17382
|
+
return;
|
|
17383
|
+
} catch (err2) {
|
|
17384
|
+
lastError = err2 instanceof Error ? err2 : new Error(String(err2));
|
|
17385
|
+
if (attempt < TOTAL_ATTEMPTS) {
|
|
17386
|
+
await new Promise((r) => setTimeout(r, 10 * 2 ** (attempt - 1)));
|
|
17387
|
+
}
|
|
17388
|
+
}
|
|
17389
|
+
}
|
|
17390
|
+
console.warn(`[takeSnapshotWithRetry] Snapshot failed after ${MAX_RETRIES} retries (${TOTAL_ATTEMPTS} attempts): ${lastError.message}`);
|
|
17391
|
+
try {
|
|
17392
|
+
emit("snapshot_failed", {
|
|
17393
|
+
error: lastError.message,
|
|
17394
|
+
retries: MAX_RETRIES,
|
|
17395
|
+
source: telemetrySource
|
|
17396
|
+
});
|
|
17397
|
+
} catch {}
|
|
17398
|
+
}
|
|
17373
17399
|
async function takeSnapshotEvent(directory, plan, options) {
|
|
17374
17400
|
const payloadHash = computePlanHash(plan);
|
|
17375
17401
|
const snapshotPayload = {
|
|
@@ -17603,6 +17629,7 @@ async function loadLastApprovedPlan(directory, expectedPlanId) {
|
|
|
17603
17629
|
var LEDGER_SCHEMA_VERSION = "1.1.0", LEDGER_FILENAME = "plan-ledger.jsonl", PLAN_JSON_FILENAME = "plan.json", LedgerStaleWriterError;
|
|
17604
17630
|
var init_ledger = __esm(() => {
|
|
17605
17631
|
init_plan_schema();
|
|
17632
|
+
init_telemetry();
|
|
17606
17633
|
LedgerStaleWriterError = class LedgerStaleWriterError extends Error {
|
|
17607
17634
|
constructor(message) {
|
|
17608
17635
|
super(message);
|
|
@@ -17796,7 +17823,9 @@ async function loadPlan(directory) {
|
|
|
17796
17823
|
try {
|
|
17797
17824
|
const rebuilt = await replayFromLedger(directory);
|
|
17798
17825
|
if (rebuilt) {
|
|
17799
|
-
await rebuildPlan(directory, rebuilt
|
|
17826
|
+
await rebuildPlan(directory, rebuilt, {
|
|
17827
|
+
reason: "ledger_hash_mismatch_recovery"
|
|
17828
|
+
});
|
|
17800
17829
|
warn("[loadPlan] Rebuilt plan from ledger. Checkpoint available at .swarm/SWARM_PLAN.md if it exists.");
|
|
17801
17830
|
return rebuilt;
|
|
17802
17831
|
}
|
|
@@ -17804,7 +17833,9 @@ async function loadPlan(directory) {
|
|
|
17804
17833
|
try {
|
|
17805
17834
|
const approved = await loadLastApprovedPlan(directory, currentPlanId);
|
|
17806
17835
|
if (approved) {
|
|
17807
|
-
await rebuildPlan(directory, approved.plan
|
|
17836
|
+
await rebuildPlan(directory, approved.plan, {
|
|
17837
|
+
reason: "approved_snapshot_fallback"
|
|
17838
|
+
});
|
|
17808
17839
|
try {
|
|
17809
17840
|
await takeSnapshotEvent(directory, approved.plan, {
|
|
17810
17841
|
source: "recovery_from_approved_snapshot",
|
|
@@ -17881,7 +17912,9 @@ async function loadPlan(directory) {
|
|
|
17881
17912
|
} else if (catchFirstEvent !== null && rawPlanId !== null) {
|
|
17882
17913
|
const rebuilt = await replayFromLedger(directory);
|
|
17883
17914
|
if (rebuilt) {
|
|
17884
|
-
await rebuildPlan(directory, rebuilt
|
|
17915
|
+
await rebuildPlan(directory, rebuilt, {
|
|
17916
|
+
reason: "validation_failure_recovery"
|
|
17917
|
+
});
|
|
17885
17918
|
warn("[loadPlan] Rebuilt plan from ledger after validation failure. Projection was stale.");
|
|
17886
17919
|
return rebuilt;
|
|
17887
17920
|
}
|
|
@@ -18242,12 +18275,9 @@ async function savePlan(directory, plan, options) {
|
|
|
18242
18275
|
const SNAPSHOT_INTERVAL = 50;
|
|
18243
18276
|
const latestSeq = await getLatestLedgerSeq(directory);
|
|
18244
18277
|
if (latestSeq > 0 && latestSeq % SNAPSHOT_INTERVAL === 0) {
|
|
18245
|
-
await
|
|
18246
|
-
planHashAfter: hashAfter
|
|
18247
|
-
|
|
18248
|
-
if (process.env.DEBUG_SWARM) {
|
|
18249
|
-
warn(`[savePlan] Periodic snapshot write failed (non-fatal): ${err2 instanceof Error ? err2.message : String(err2)}`);
|
|
18250
|
-
}
|
|
18278
|
+
await takeSnapshotWithRetry(directory, validated, {
|
|
18279
|
+
planHashAfter: hashAfter,
|
|
18280
|
+
source: "savePlan_manager"
|
|
18251
18281
|
});
|
|
18252
18282
|
}
|
|
18253
18283
|
const swarmDir = path6.resolve(directory, ".swarm");
|
|
@@ -18261,6 +18291,17 @@ async function savePlan(directory, plan, options) {
|
|
|
18261
18291
|
unlinkSync(tempPath);
|
|
18262
18292
|
} catch {}
|
|
18263
18293
|
}
|
|
18294
|
+
try {
|
|
18295
|
+
const markerPath = path6.join(swarmDir, ".plan-write-marker");
|
|
18296
|
+
const inProgressMarker = JSON.stringify({
|
|
18297
|
+
source: "plan_manager",
|
|
18298
|
+
timestamp: new Date().toISOString(),
|
|
18299
|
+
phases_count: validated.phases.length,
|
|
18300
|
+
tasks_count: validated.phases.reduce((sum, p) => sum + p.tasks.length, 0),
|
|
18301
|
+
in_progress: true
|
|
18302
|
+
});
|
|
18303
|
+
await bunWrite(markerPath, inProgressMarker);
|
|
18304
|
+
} catch {}
|
|
18264
18305
|
try {
|
|
18265
18306
|
const contentHash = computePlanContentHash(validated);
|
|
18266
18307
|
const markdown = derivePlanMarkdown(validated);
|
|
@@ -18294,41 +18335,146 @@ ${markdown}`;
|
|
|
18294
18335
|
source: "plan_manager",
|
|
18295
18336
|
timestamp: new Date().toISOString(),
|
|
18296
18337
|
phases_count: validated.phases.length,
|
|
18297
|
-
tasks_count: tasksCount
|
|
18338
|
+
tasks_count: tasksCount,
|
|
18339
|
+
in_progress: false
|
|
18298
18340
|
});
|
|
18299
18341
|
await bunWrite(markerPath, marker);
|
|
18300
18342
|
} catch {}
|
|
18301
18343
|
}
|
|
18302
|
-
async function rebuildPlan(directory, plan) {
|
|
18344
|
+
async function rebuildPlan(directory, plan, options) {
|
|
18303
18345
|
const targetPlan = plan ?? await replayFromLedger(directory);
|
|
18304
18346
|
if (!targetPlan)
|
|
18305
18347
|
return null;
|
|
18306
18348
|
const swarmDir = path6.join(directory, ".swarm");
|
|
18307
18349
|
const planPath = path6.join(swarmDir, "plan.json");
|
|
18308
18350
|
const mdPath = path6.join(swarmDir, "plan.md");
|
|
18309
|
-
const tempPlanPath = path6.join(swarmDir, `plan.json.rebuild.${Date.now()}`);
|
|
18351
|
+
const tempPlanPath = path6.join(swarmDir, `plan.json.rebuild.${Date.now()}.${Math.floor(Math.random() * 1e9)}`);
|
|
18310
18352
|
await bunWrite(tempPlanPath, JSON.stringify(targetPlan, null, 2));
|
|
18311
18353
|
renameSync3(tempPlanPath, planPath);
|
|
18312
|
-
const contentHash = computePlanContentHash(targetPlan);
|
|
18313
|
-
const markdown = derivePlanMarkdown(targetPlan);
|
|
18314
|
-
const markdownWithHash = `<!-- PLAN_HASH: ${contentHash} -->
|
|
18315
|
-
${markdown}`;
|
|
18316
|
-
const tempMdPath = path6.join(swarmDir, `plan.md.rebuild.${Date.now()}`);
|
|
18317
|
-
await bunWrite(tempMdPath, markdownWithHash);
|
|
18318
|
-
renameSync3(tempMdPath, mdPath);
|
|
18319
18354
|
try {
|
|
18320
18355
|
const markerPath = path6.join(swarmDir, ".plan-write-marker");
|
|
18321
|
-
const
|
|
18322
|
-
const marker = JSON.stringify({
|
|
18356
|
+
const inProgressMarker = JSON.stringify({
|
|
18323
18357
|
source: "plan_manager",
|
|
18324
18358
|
timestamp: new Date().toISOString(),
|
|
18325
18359
|
phases_count: targetPlan.phases.length,
|
|
18326
|
-
tasks_count:
|
|
18360
|
+
tasks_count: targetPlan.phases.reduce((sum, phase) => sum + phase.tasks.length, 0),
|
|
18361
|
+
in_progress: true
|
|
18327
18362
|
});
|
|
18328
|
-
await bunWrite(markerPath,
|
|
18363
|
+
await bunWrite(markerPath, inProgressMarker);
|
|
18364
|
+
} catch {}
|
|
18365
|
+
try {
|
|
18366
|
+
const contentHash = computePlanContentHash(targetPlan);
|
|
18367
|
+
const markdown = derivePlanMarkdown(targetPlan);
|
|
18368
|
+
const markdownWithHash = `<!-- PLAN_HASH: ${contentHash} -->
|
|
18369
|
+
${markdown}`;
|
|
18370
|
+
const tempMdPath = path6.join(swarmDir, `plan.md.rebuild.${Date.now()}.${Math.floor(Math.random() * 1e9)}`);
|
|
18371
|
+
await bunWrite(tempMdPath, markdownWithHash);
|
|
18372
|
+
renameSync3(tempMdPath, mdPath);
|
|
18373
|
+
} finally {
|
|
18374
|
+
try {
|
|
18375
|
+
const markerPath = path6.join(swarmDir, ".plan-write-marker");
|
|
18376
|
+
const tasksCount = targetPlan.phases.reduce((sum, phase) => sum + phase.tasks.length, 0);
|
|
18377
|
+
const marker = JSON.stringify({
|
|
18378
|
+
source: "plan_manager",
|
|
18379
|
+
timestamp: new Date().toISOString(),
|
|
18380
|
+
phases_count: targetPlan.phases.length,
|
|
18381
|
+
tasks_count: tasksCount,
|
|
18382
|
+
in_progress: false
|
|
18383
|
+
});
|
|
18384
|
+
await bunWrite(markerPath, marker);
|
|
18385
|
+
} catch {}
|
|
18386
|
+
}
|
|
18387
|
+
try {
|
|
18388
|
+
const planId = derivePlanId(targetPlan);
|
|
18389
|
+
const planHashAfter = computePlanHash(targetPlan);
|
|
18390
|
+
await appendLedgerEvent(directory, {
|
|
18391
|
+
event_type: "plan_rebuilt",
|
|
18392
|
+
source: "rebuildPlan",
|
|
18393
|
+
plan_id: planId,
|
|
18394
|
+
payload: {
|
|
18395
|
+
reason: options?.reason ?? "ledger_replay_recovery",
|
|
18396
|
+
phases_count: targetPlan.phases.length,
|
|
18397
|
+
tasks_count: targetPlan.phases.reduce((sum, p) => sum + p.tasks.length, 0)
|
|
18398
|
+
}
|
|
18399
|
+
}, { planHashAfter });
|
|
18329
18400
|
} catch {}
|
|
18330
18401
|
return targetPlan;
|
|
18331
18402
|
}
|
|
18403
|
+
async function closePlanTerminalState(directory, plan, options) {
|
|
18404
|
+
const planId = derivePlanId(plan);
|
|
18405
|
+
const validated = PlanSchema.parse(plan);
|
|
18406
|
+
const hashAfter = computePlanHash(validated);
|
|
18407
|
+
for (const taskId of options.closedTaskIds) {
|
|
18408
|
+
let taskPhaseId;
|
|
18409
|
+
for (const phase of validated.phases) {
|
|
18410
|
+
if (phase.tasks.some((t) => t.id === taskId)) {
|
|
18411
|
+
taskPhaseId = phase.id;
|
|
18412
|
+
break;
|
|
18413
|
+
}
|
|
18414
|
+
}
|
|
18415
|
+
const fromStatus = options.originalStatuses?.get(taskId) ?? "in_progress";
|
|
18416
|
+
await appendLedgerEvent(directory, {
|
|
18417
|
+
plan_id: planId,
|
|
18418
|
+
event_type: "task_status_changed",
|
|
18419
|
+
task_id: taskId,
|
|
18420
|
+
phase_id: taskPhaseId,
|
|
18421
|
+
from_status: fromStatus,
|
|
18422
|
+
to_status: "closed",
|
|
18423
|
+
source: "close_terminal"
|
|
18424
|
+
}, { planHashAfter: hashAfter });
|
|
18425
|
+
}
|
|
18426
|
+
for (const phaseId of options.closedPhaseIds) {
|
|
18427
|
+
await appendLedgerEvent(directory, {
|
|
18428
|
+
plan_id: planId,
|
|
18429
|
+
event_type: "phase_completed",
|
|
18430
|
+
phase_id: phaseId,
|
|
18431
|
+
source: "close_terminal"
|
|
18432
|
+
}, { planHashAfter: hashAfter });
|
|
18433
|
+
}
|
|
18434
|
+
await takeSnapshotEvent(directory, validated, {
|
|
18435
|
+
planHashAfter: hashAfter,
|
|
18436
|
+
source: "close_terminal"
|
|
18437
|
+
});
|
|
18438
|
+
const swarmDir = path6.join(directory, ".swarm");
|
|
18439
|
+
const planPath = path6.join(swarmDir, "plan.json");
|
|
18440
|
+
const tempPlanPath = path6.join(swarmDir, `plan.json.close.${Date.now()}.${Math.floor(Math.random() * 1e9)}`);
|
|
18441
|
+
await bunWrite(tempPlanPath, JSON.stringify(validated, null, 2));
|
|
18442
|
+
renameSync3(tempPlanPath, planPath);
|
|
18443
|
+
try {
|
|
18444
|
+
const markerPath = path6.join(swarmDir, ".plan-write-marker");
|
|
18445
|
+
const inProgressMarker = JSON.stringify({
|
|
18446
|
+
source: "plan_manager_close",
|
|
18447
|
+
timestamp: new Date().toISOString(),
|
|
18448
|
+
phases_count: validated.phases.length,
|
|
18449
|
+
tasks_count: validated.phases.reduce((sum, phase) => sum + phase.tasks.length, 0),
|
|
18450
|
+
in_progress: true
|
|
18451
|
+
});
|
|
18452
|
+
await bunWrite(markerPath, inProgressMarker);
|
|
18453
|
+
} catch {}
|
|
18454
|
+
try {
|
|
18455
|
+
const mdPath = path6.join(swarmDir, "plan.md");
|
|
18456
|
+
const contentHash = computePlanContentHash(validated);
|
|
18457
|
+
const markdown = derivePlanMarkdown(validated);
|
|
18458
|
+
const markdownWithHash = `<!-- PLAN_HASH: ${contentHash} -->
|
|
18459
|
+
${markdown}`;
|
|
18460
|
+
const mdTempPath = path6.join(swarmDir, `plan.md.close.${Date.now()}.${Math.floor(Math.random() * 1e9)}`);
|
|
18461
|
+
await bunWrite(mdTempPath, markdownWithHash);
|
|
18462
|
+
renameSync3(mdTempPath, mdPath);
|
|
18463
|
+
} finally {
|
|
18464
|
+
try {
|
|
18465
|
+
const markerPath = path6.join(swarmDir, ".plan-write-marker");
|
|
18466
|
+
const tasksCount = validated.phases.reduce((sum, phase) => sum + phase.tasks.length, 0);
|
|
18467
|
+
const marker = JSON.stringify({
|
|
18468
|
+
source: "plan_manager_close",
|
|
18469
|
+
timestamp: new Date().toISOString(),
|
|
18470
|
+
phases_count: validated.phases.length,
|
|
18471
|
+
tasks_count: tasksCount,
|
|
18472
|
+
in_progress: false
|
|
18473
|
+
});
|
|
18474
|
+
await bunWrite(markerPath, marker);
|
|
18475
|
+
} catch {}
|
|
18476
|
+
}
|
|
18477
|
+
}
|
|
18332
18478
|
async function updateTaskStatus(directory, taskId, status) {
|
|
18333
18479
|
const derivePhaseStatusFromTasks = (tasks) => {
|
|
18334
18480
|
if (tasks.length > 0 && tasks.every((task) => task.status === "completed")) {
|
|
@@ -58844,6 +58990,12 @@ async function handleCloseCommand(directory, args2, options = {}) {
|
|
|
58844
58990
|
}
|
|
58845
58991
|
}
|
|
58846
58992
|
if (planExists) {
|
|
58993
|
+
const originalStatuses = new Map;
|
|
58994
|
+
for (const phase of planData.phases ?? []) {
|
|
58995
|
+
for (const task of phase.tasks ?? []) {
|
|
58996
|
+
originalStatuses.set(task.id, task.status);
|
|
58997
|
+
}
|
|
58998
|
+
}
|
|
58847
58999
|
const guaranteeResult = guaranteeAllPlansComplete(planData);
|
|
58848
59000
|
for (const phaseId of guaranteeResult.closedPhaseIds) {
|
|
58849
59001
|
if (!closedPhases.includes(phaseId)) {
|
|
@@ -58857,11 +59009,15 @@ async function handleCloseCommand(directory, args2, options = {}) {
|
|
|
58857
59009
|
}
|
|
58858
59010
|
if (!planAlreadyDone || guaranteeResult.closedPhaseIds.length > 0 || guaranteeResult.closedTaskIds.length > 0) {
|
|
58859
59011
|
try {
|
|
58860
|
-
await
|
|
59012
|
+
await closePlanTerminalState(directory, planData, {
|
|
59013
|
+
closedPhaseIds: guaranteeResult.closedPhaseIds,
|
|
59014
|
+
closedTaskIds: guaranteeResult.closedTaskIds,
|
|
59015
|
+
originalStatuses
|
|
59016
|
+
});
|
|
58861
59017
|
} catch (error93) {
|
|
58862
59018
|
const msg = error93 instanceof Error ? error93.message : String(error93);
|
|
58863
|
-
warnings.push(`Failed to persist terminal plan
|
|
58864
|
-
console.warn("[close-command] Failed to write plan
|
|
59019
|
+
warnings.push(`Failed to persist terminal plan state: ${msg}`);
|
|
59020
|
+
console.warn("[close-command] Failed to write terminal plan state:", error93);
|
|
58865
59021
|
}
|
|
58866
59022
|
}
|
|
58867
59023
|
}
|
|
@@ -59149,6 +59305,7 @@ var init_close = __esm(() => {
|
|
|
59149
59305
|
init_knowledge_curator();
|
|
59150
59306
|
init_knowledge_store();
|
|
59151
59307
|
init_utils2();
|
|
59308
|
+
init_manager();
|
|
59152
59309
|
init_scope_persistence();
|
|
59153
59310
|
init_skill_improver();
|
|
59154
59311
|
init_state();
|
|
@@ -59891,6 +60048,120 @@ function getPluginCachePaths() {
|
|
|
59891
60048
|
}
|
|
59892
60049
|
var init_cache_paths = () => {};
|
|
59893
60050
|
|
|
60051
|
+
// src/evidence/gate-bridge.ts
|
|
60052
|
+
async function readDurableGateEvidence(directory, taskId) {
|
|
60053
|
+
try {
|
|
60054
|
+
return await readTaskEvidence(directory, taskId);
|
|
60055
|
+
} catch {
|
|
60056
|
+
return null;
|
|
60057
|
+
}
|
|
60058
|
+
}
|
|
60059
|
+
function getDurableGateEvidenceStatus(evidence) {
|
|
60060
|
+
if (!evidence?.gates || typeof evidence.gates !== "object") {
|
|
60061
|
+
return {
|
|
60062
|
+
isComplete: false,
|
|
60063
|
+
missingGates: [],
|
|
60064
|
+
evidenceExists: evidence != null,
|
|
60065
|
+
invalid: false
|
|
60066
|
+
};
|
|
60067
|
+
}
|
|
60068
|
+
if (!Array.isArray(evidence.required_gates) || evidence.required_gates.length === 0) {
|
|
60069
|
+
return {
|
|
60070
|
+
isComplete: false,
|
|
60071
|
+
missingGates: ["required_gates"],
|
|
60072
|
+
evidenceExists: true,
|
|
60073
|
+
invalid: false
|
|
60074
|
+
};
|
|
60075
|
+
}
|
|
60076
|
+
const missingGates = evidence.required_gates.filter((gate) => evidence.gates[gate] == null);
|
|
60077
|
+
return {
|
|
60078
|
+
isComplete: missingGates.length === 0,
|
|
60079
|
+
missingGates,
|
|
60080
|
+
evidenceExists: true,
|
|
60081
|
+
invalid: false
|
|
60082
|
+
};
|
|
60083
|
+
}
|
|
60084
|
+
async function getDurableGateEvidenceStatusForTask(directory, taskId) {
|
|
60085
|
+
if (!isValidTaskId(taskId)) {
|
|
60086
|
+
return {
|
|
60087
|
+
isComplete: false,
|
|
60088
|
+
missingGates: [],
|
|
60089
|
+
evidenceExists: false,
|
|
60090
|
+
invalid: false
|
|
60091
|
+
};
|
|
60092
|
+
}
|
|
60093
|
+
try {
|
|
60094
|
+
return getDurableGateEvidenceStatus(readTaskEvidenceRaw(directory, taskId));
|
|
60095
|
+
} catch {
|
|
60096
|
+
return {
|
|
60097
|
+
isComplete: false,
|
|
60098
|
+
missingGates: ["invalid_gate_evidence"],
|
|
60099
|
+
evidenceExists: true,
|
|
60100
|
+
invalid: true
|
|
60101
|
+
};
|
|
60102
|
+
}
|
|
60103
|
+
}
|
|
60104
|
+
function gateEvidenceToEntry(taskId, gate, type, evidence) {
|
|
60105
|
+
const gateRecord = evidence.gates[gate];
|
|
60106
|
+
if (!gateRecord) {
|
|
60107
|
+
return null;
|
|
60108
|
+
}
|
|
60109
|
+
const base = {
|
|
60110
|
+
task_id: taskId,
|
|
60111
|
+
timestamp: gateRecord.timestamp,
|
|
60112
|
+
agent: gateRecord.agent || gate,
|
|
60113
|
+
verdict: "pass",
|
|
60114
|
+
summary: `Gate evidence recorded by ${gate}`,
|
|
60115
|
+
metadata: { source: "durable_gate_evidence", gate }
|
|
60116
|
+
};
|
|
60117
|
+
if (type === "review") {
|
|
60118
|
+
return {
|
|
60119
|
+
...base,
|
|
60120
|
+
type,
|
|
60121
|
+
risk: "low",
|
|
60122
|
+
issues: []
|
|
60123
|
+
};
|
|
60124
|
+
}
|
|
60125
|
+
if (type === "approval") {
|
|
60126
|
+
return {
|
|
60127
|
+
...base,
|
|
60128
|
+
type
|
|
60129
|
+
};
|
|
60130
|
+
}
|
|
60131
|
+
return {
|
|
60132
|
+
...base,
|
|
60133
|
+
type,
|
|
60134
|
+
tests_passed: 0,
|
|
60135
|
+
tests_failed: 0,
|
|
60136
|
+
failures: []
|
|
60137
|
+
};
|
|
60138
|
+
}
|
|
60139
|
+
function mergeDurableGateEntriesFromEvidence(taskId, entries, evidence) {
|
|
60140
|
+
if (!evidence?.gates) {
|
|
60141
|
+
return entries;
|
|
60142
|
+
}
|
|
60143
|
+
const merged = [...entries];
|
|
60144
|
+
for (const gate of Object.keys(evidence.gates).sort()) {
|
|
60145
|
+
const type = GATE_EVIDENCE_TYPE_BY_GATE[gate] ?? "approval";
|
|
60146
|
+
if ((type === "review" || type === "test") && merged.some((entry2) => entry2.type === type)) {
|
|
60147
|
+
continue;
|
|
60148
|
+
}
|
|
60149
|
+
const entry = gateEvidenceToEntry(taskId, gate, type, evidence);
|
|
60150
|
+
if (entry) {
|
|
60151
|
+
merged.push(entry);
|
|
60152
|
+
}
|
|
60153
|
+
}
|
|
60154
|
+
return merged;
|
|
60155
|
+
}
|
|
60156
|
+
var GATE_EVIDENCE_TYPE_BY_GATE;
|
|
60157
|
+
var init_gate_bridge = __esm(() => {
|
|
60158
|
+
init_gate_evidence();
|
|
60159
|
+
GATE_EVIDENCE_TYPE_BY_GATE = {
|
|
60160
|
+
reviewer: "review",
|
|
60161
|
+
test_engineer: "test"
|
|
60162
|
+
};
|
|
60163
|
+
});
|
|
60164
|
+
|
|
59894
60165
|
// src/services/version-check.ts
|
|
59895
60166
|
import { existsSync as existsSync14, mkdirSync as mkdirSync10, readFileSync as readFileSync7, writeFileSync as writeFileSync4 } from "node:fs";
|
|
59896
60167
|
import { homedir as homedir5 } from "node:os";
|
|
@@ -60038,7 +60309,21 @@ async function checkEvidenceCompleteness(directory, plan) {
|
|
|
60038
60309
|
}
|
|
60039
60310
|
if (completedTaskIds.length > 0) {
|
|
60040
60311
|
const evidenceTaskIds = new Set(await listEvidenceTaskIds(directory));
|
|
60041
|
-
const missingEvidence =
|
|
60312
|
+
const missingEvidence = [];
|
|
60313
|
+
for (const id of completedTaskIds) {
|
|
60314
|
+
const gateStatus = await getDurableGateEvidenceStatusForTask(directory, id);
|
|
60315
|
+
if (gateStatus.isComplete) {
|
|
60316
|
+
continue;
|
|
60317
|
+
}
|
|
60318
|
+
if (gateStatus.evidenceExists && gateStatus.missingGates.length > 0) {
|
|
60319
|
+
missingEvidence.push(id);
|
|
60320
|
+
continue;
|
|
60321
|
+
}
|
|
60322
|
+
if (evidenceTaskIds.has(id)) {
|
|
60323
|
+
continue;
|
|
60324
|
+
}
|
|
60325
|
+
missingEvidence.push(id);
|
|
60326
|
+
}
|
|
60042
60327
|
if (missingEvidence.length === 0) {
|
|
60043
60328
|
return {
|
|
60044
60329
|
name: "Evidence",
|
|
@@ -60793,6 +61078,7 @@ var init_diagnose_service = __esm(() => {
|
|
|
60793
61078
|
init_package();
|
|
60794
61079
|
init_cache_paths();
|
|
60795
61080
|
init_loader();
|
|
61081
|
+
init_gate_bridge();
|
|
60796
61082
|
init_manager2();
|
|
60797
61083
|
init_utils2();
|
|
60798
61084
|
init_manager();
|
|
@@ -63359,8 +63645,7 @@ function getTaskStatus(task, bundle) {
|
|
|
63359
63645
|
}
|
|
63360
63646
|
return "pending";
|
|
63361
63647
|
}
|
|
63362
|
-
function
|
|
63363
|
-
const entries = _internals20.normalizeBundleEntries(bundle);
|
|
63648
|
+
function evidenceCompleteFromEntries(entries) {
|
|
63364
63649
|
if (entries.length === 0) {
|
|
63365
63650
|
return {
|
|
63366
63651
|
isComplete: false,
|
|
@@ -63379,6 +63664,9 @@ function isEvidenceComplete(bundle) {
|
|
|
63379
63664
|
missingEvidence: missing
|
|
63380
63665
|
};
|
|
63381
63666
|
}
|
|
63667
|
+
function isEvidenceComplete(bundle) {
|
|
63668
|
+
return evidenceCompleteFromEntries(_internals20.normalizeBundleEntries(bundle));
|
|
63669
|
+
}
|
|
63382
63670
|
function getTaskBlockers(task, summary, status) {
|
|
63383
63671
|
const blockers = [];
|
|
63384
63672
|
if (task?.blocked_reason) {
|
|
@@ -63395,11 +63683,19 @@ function getTaskBlockers(task, summary, status) {
|
|
|
63395
63683
|
async function buildTaskSummary(directory, task, taskId) {
|
|
63396
63684
|
const result = await loadEvidence(directory, taskId);
|
|
63397
63685
|
const bundle = result.status === "found" ? result.bundle : null;
|
|
63686
|
+
const gateEvidence = await readDurableGateEvidence(directory, taskId);
|
|
63398
63687
|
const phase = task?.phase ?? 0;
|
|
63399
63688
|
const status = _internals20.getTaskStatus(task, bundle);
|
|
63400
|
-
const
|
|
63689
|
+
const entries = mergeDurableGateEntriesFromEvidence(taskId, _internals20.normalizeBundleEntries(bundle), gateEvidence);
|
|
63690
|
+
let evidenceCheck = _internals20.evidenceCompleteFromEntries(entries);
|
|
63691
|
+
if (gateEvidence) {
|
|
63692
|
+
const gateStatus = getDurableGateEvidenceStatus(gateEvidence);
|
|
63693
|
+
evidenceCheck = gateStatus.isComplete ? { isComplete: true, missingEvidence: [] } : {
|
|
63694
|
+
isComplete: false,
|
|
63695
|
+
missingEvidence: gateStatus.missingGates.map((gate) => `gate:${gate}`)
|
|
63696
|
+
};
|
|
63697
|
+
}
|
|
63401
63698
|
const blockers = _internals20.getTaskBlockers(task, evidenceCheck, status);
|
|
63402
|
-
const entries = _internals20.normalizeBundleEntries(bundle);
|
|
63403
63699
|
const hasReview = entries.some((e) => e.type === "review");
|
|
63404
63700
|
const hasTest = entries.some((e) => e.type === "test");
|
|
63405
63701
|
const hasApproval = entries.some((e) => e.type === "approval");
|
|
@@ -63577,6 +63873,7 @@ function isAutoSummaryEnabled(automationConfig) {
|
|
|
63577
63873
|
}
|
|
63578
63874
|
var VALID_EVIDENCE_TYPES2, REQUIRED_EVIDENCE_TYPES, EVIDENCE_SUMMARY_VERSION = "1.0.0", _internals20;
|
|
63579
63875
|
var init_evidence_summary_service = __esm(() => {
|
|
63876
|
+
init_gate_bridge();
|
|
63580
63877
|
init_manager2();
|
|
63581
63878
|
init_manager();
|
|
63582
63879
|
init_utils();
|
|
@@ -63594,6 +63891,7 @@ var init_evidence_summary_service = __esm(() => {
|
|
|
63594
63891
|
isAutoSummaryEnabled,
|
|
63595
63892
|
normalizeBundleEntries,
|
|
63596
63893
|
getTaskStatus,
|
|
63894
|
+
evidenceCompleteFromEntries,
|
|
63597
63895
|
isEvidenceComplete,
|
|
63598
63896
|
getTaskBlockers,
|
|
63599
63897
|
buildTaskSummary,
|
|
@@ -71204,7 +71502,22 @@ async function runEvidenceCheck(dir) {
|
|
|
71204
71502
|
};
|
|
71205
71503
|
}
|
|
71206
71504
|
const evidenceTaskIds = new Set(await listEvidenceTaskIds(dir));
|
|
71207
|
-
const missingEvidence =
|
|
71505
|
+
const missingEvidence = [];
|
|
71506
|
+
for (const id of completedTaskIds) {
|
|
71507
|
+
const gateStatus = await getDurableGateEvidenceStatusForTask(dir, id);
|
|
71508
|
+
if (gateStatus.isComplete) {
|
|
71509
|
+
continue;
|
|
71510
|
+
}
|
|
71511
|
+
if (gateStatus.evidenceExists && gateStatus.missingGates.length > 0) {
|
|
71512
|
+
missingEvidence.push(id);
|
|
71513
|
+
continue;
|
|
71514
|
+
}
|
|
71515
|
+
if (evidenceTaskIds.has(id)) {
|
|
71516
|
+
continue;
|
|
71517
|
+
}
|
|
71518
|
+
missingEvidence.push(id);
|
|
71519
|
+
}
|
|
71520
|
+
const completedWithEvidence = completedTaskIds.length - missingEvidence.length;
|
|
71208
71521
|
if (missingEvidence.length > 0) {
|
|
71209
71522
|
return {
|
|
71210
71523
|
type: "evidence",
|
|
@@ -71212,7 +71525,7 @@ async function runEvidenceCheck(dir) {
|
|
|
71212
71525
|
message: `${missingEvidence.length} completed task(s) missing evidence`,
|
|
71213
71526
|
details: {
|
|
71214
71527
|
totalCompleted: completedTaskIds.length,
|
|
71215
|
-
totalWithEvidence:
|
|
71528
|
+
totalWithEvidence: completedWithEvidence,
|
|
71216
71529
|
missingTasks: missingEvidence.slice(0, 10),
|
|
71217
71530
|
missingCount: missingEvidence.length
|
|
71218
71531
|
},
|
|
@@ -71225,7 +71538,7 @@ async function runEvidenceCheck(dir) {
|
|
|
71225
71538
|
message: `All ${completedTaskIds.length} completed tasks have evidence`,
|
|
71226
71539
|
details: {
|
|
71227
71540
|
totalCompleted: completedTaskIds.length,
|
|
71228
|
-
totalWithEvidence:
|
|
71541
|
+
totalWithEvidence: completedWithEvidence
|
|
71229
71542
|
},
|
|
71230
71543
|
durationMs: Date.now() - startTime
|
|
71231
71544
|
};
|
|
@@ -71456,6 +71769,7 @@ async function handlePreflightCommand(directory, _args) {
|
|
|
71456
71769
|
}
|
|
71457
71770
|
var MIN_CHECK_TIMEOUT_MS = 5000, MAX_CHECK_TIMEOUT_MS = 300000, DEFAULT_CONFIG, _internals34;
|
|
71458
71771
|
var init_preflight_service = __esm(() => {
|
|
71772
|
+
init_gate_bridge();
|
|
71459
71773
|
init_manager2();
|
|
71460
71774
|
init_manager();
|
|
71461
71775
|
init_lint();
|
|
@@ -76401,121 +76715,14 @@ Do NOT share other agents' responses at this stage.
|
|
|
76401
76715
|
### MODE: DEEP_DIVE
|
|
76402
76716
|
Activates when: architect receives \`[MODE: DEEP_DIVE profile=X max_explorers=N output=X update_main=X allow_dirty=X] <scope>\` signal from the deep-dive command handler.
|
|
76403
76717
|
|
|
76404
|
-
Purpose:
|
|
76405
|
-
|
|
76406
|
-
|
|
76407
|
-
|
|
76408
|
-
|
|
76409
|
-
- \`profile\`: one of standard | security | ux | architecture | full (default: standard)
|
|
76410
|
-
- \`max_explorers\`: integer 1..8 (default: 6, or 8 for full profile)
|
|
76411
|
-
- \`output\`: markdown | json (default: markdown)
|
|
76412
|
-
- \`update_main\`: boolean (default: true) — whether to fetch/ff-only main before starting
|
|
76413
|
-
- \`allow_dirty\`: boolean (default: false) — whether to proceed with uncommitted changes
|
|
76414
|
-
|
|
76415
|
-
If the header is malformed or missing required fields, report the error and stop.
|
|
76416
|
-
|
|
76417
|
-
#### STEP 1 — REPO READINESS
|
|
76418
|
-
1. Check git working tree status. If dirty and \`allow_dirty\` is false, warn the user and ask whether to proceed. Do NOT proceed automatically.
|
|
76419
|
-
2. If \`update_main\` is true and tree is clean: check current branch. If not on \`main\`, report current branch to user and ASK FOR CONFIRMATION before switching. Only after explicit user approval: \`git fetch origin main && git checkout main && git merge --ff-only origin/main\`. If ff-only fails, warn the user and ask before proceeding.
|
|
76420
|
-
3. Record the current HEAD commit hash for the report.
|
|
76421
|
-
|
|
76422
|
-
#### STEP 2 — SCOPE RESOLUTION
|
|
76423
|
-
Use the following tools to map the audit scope:
|
|
76424
|
-
1. \`repo_map\` with action "build" to establish the code graph
|
|
76425
|
-
2. \`repo_map\` with action "localization" for the scope target
|
|
76426
|
-
3. \`symbols\` and \`batch_symbols\` on key files identified by localization
|
|
76427
|
-
4. \`imports\` to trace dependency boundaries
|
|
76428
|
-
5. \`doc_scan\` if documentation coverage is relevant
|
|
76429
|
-
6. \`knowledge_recall\` with query matching the scope domain
|
|
76430
|
-
|
|
76431
|
-
Produce a SCOPE MAP: list of files, modules, and interfaces within the audit boundary. Cap at 50 files total.
|
|
76432
|
-
|
|
76433
|
-
#### STEP 3 — EXPLORER MISSIONS (Parallel Waves)
|
|
76434
|
-
Dispatch explorer waves using parallel Task calls. Each wave contains up to \`max_explorers\` missions.
|
|
76435
|
-
|
|
76436
|
-
**File caps per mission:**
|
|
76437
|
-
- 8 files maximum per mission
|
|
76438
|
-
- ~3500 total lines across all files in a mission
|
|
76439
|
-
- Group files by import proximity (files that import each other go in the same mission)
|
|
76440
|
-
|
|
76441
|
-
**Profile-based lane selection — each profile activates specific lanes:**
|
|
76442
|
-
|
|
76443
|
-
| Lane | Template | standard | security | ux | architecture | full |
|
|
76444
|
-
|------|----------|----------|----------|----|-------------|------|
|
|
76445
|
-
| SCOPE_MAP | Map structure, exports, boundaries | ✓ | ✓ | ✓ | ✓ | ✓ |
|
|
76446
|
-
| WIRING_DATAFLOW | Trace data flow, API contracts, state propagation | ✓ | ✓ | | ✓ | ✓ |
|
|
76447
|
-
| RUNTIME_BEHAVIOR | Error handling, edge cases, lifecycle, async patterns | ✓ | | | ✓ | ✓ |
|
|
76448
|
-
| UX_FLOW | User-facing behavior, accessibility, responsiveness | | | ✓ | | ✓ |
|
|
76449
|
-
| SECURITY_TRUST | Auth boundaries, input validation, trust transitions | | ✓ | | | ✓ |
|
|
76450
|
-
| TEST_COVERAGE | Coverage gaps, flaky tests, missing assertions | ✓ | | | | ✓ |
|
|
76451
|
-
| PERFORMANCE_RELIABILITY | Resource leaks, N+1 queries, race conditions | | | | ✓ | ✓ |
|
|
76452
|
-
| DOCS_CONFIG_DEPLOYMENT | Config consistency, docs accuracy, deployment drift | | | | | ✓ |
|
|
76453
|
-
|
|
76454
|
-
Each explorer mission receives:
|
|
76455
|
-
- Lane template name and description
|
|
76456
|
-
- Assigned files (8 max, grouped by import proximity)
|
|
76457
|
-
- The scope map context from Step 2
|
|
76458
|
-
- Instruction: "You are performing a [LANE] audit. Report findings as candidate observations with severity (INFO/LOW/MEDIUM/HIGH/CRITICAL), location, and evidence."
|
|
76459
|
-
|
|
76460
|
-
Explorer missions are dispatched in parallel waves. Wait for ALL missions in a wave to complete before dispatching the next wave.
|
|
76461
|
-
|
|
76462
|
-
Explorers generate CANDIDATE FINDINGS only — they do NOT make verdicts. All findings are unverified until Step 5.
|
|
76463
|
-
|
|
76464
|
-
#### STEP 4 — NORMALIZE CANDIDATES
|
|
76465
|
-
1. Collect all candidate findings from all explorer missions.
|
|
76466
|
-
2. Deduplicate: merge findings that reference the same location and issue.
|
|
76467
|
-
3. Assign DD-C001 through DD-CNNN identifiers to unique findings.
|
|
76468
|
-
4. Cap at 10 findings per shard (see Step 5 for sharding).
|
|
76469
|
-
5. Sort by severity (CRITICAL → HIGH → MEDIUM → LOW → INFO).
|
|
76470
|
-
|
|
76471
|
-
#### STEP 5 — ALWAYS 2 PARALLEL REVIEWERS
|
|
76472
|
-
Split the verified candidates into 2 shards of ≤10 candidates each. Dispatch 2 parallel \`{{AGENT_PREFIX}}reviewer\` calls.
|
|
76473
|
-
|
|
76474
|
-
Each reviewer receives:
|
|
76475
|
-
- Their shard of candidates (up to 10)
|
|
76476
|
-
- The scope map context
|
|
76477
|
-
- The original scope description
|
|
76478
|
-
- Instruction: "Verify or reject each candidate finding. For each: verdict (VERIFIED / REJECTED / NEEDS_MORE_EVIDENCE), confidence (0-1), and brief reasoning."
|
|
76479
|
-
|
|
76480
|
-
Reviewers MUST NOT suggest fixes — they verify findings only.
|
|
76481
|
-
|
|
76482
|
-
#### STEP 5b — REVIEWER MERGE/DEDUP
|
|
76483
|
-
After both reviewers return, perform a lightweight sync pass:
|
|
76484
|
-
1. Cross-reference findings between reviewers — flag correlations
|
|
76485
|
-
2. Deduplicate any findings both reviewers verified independently
|
|
76486
|
-
3. For NEEDS_MORE_EVIDENCE findings: if the other reviewer verified a related finding, merge
|
|
76487
|
-
4. Produce a unified findings list with verified/rejected status
|
|
76488
|
-
|
|
76489
|
-
#### STEP 6 — CRITIC CHALLENGE (HIGH/CRITICAL only)
|
|
76490
|
-
For verified findings rated HIGH or CRITICAL, dispatch sequential critic passes:
|
|
76491
|
-
|
|
76492
|
-
**Pass 1 — False-positive / root-cause challenge:**
|
|
76493
|
-
- \`{{AGENT_PREFIX}}critic\` receives each HIGH/CRITICAL finding
|
|
76494
|
-
- Challenge: "Is this a false positive? Is the root cause correctly identified? Provide verdict: SURVIVES / DOWNGRADE / REJECT"
|
|
76495
|
-
- Only findings that SURVIVE proceed to Pass 2
|
|
76496
|
-
|
|
76497
|
-
**Pass 2 — Impact / severity challenge:**
|
|
76498
|
-
- \`{{AGENT_PREFIX}}critic\` receives surviving findings
|
|
76499
|
-
- Challenge: "Is the severity correctly rated? Could this be lower impact than claimed? Provide verdict: SURVIVES / DOWNGRADE / REJECT"
|
|
76500
|
-
- Final severity is the critic's assessed severity
|
|
76501
|
-
|
|
76502
|
-
CRITICAL: Do NOT challenge MEDIUM/LOW/INFO findings. Only HIGH and CRITICAL go through critic review.
|
|
76503
|
-
|
|
76504
|
-
#### STEP 7 — FINAL REPORT
|
|
76505
|
-
Assemble and present the audit report:
|
|
76506
|
-
|
|
76507
|
-
1. **Wiring Map**: Visual summary of the scope's module structure and data flow
|
|
76508
|
-
2. **Functionality Assessment**: High-level summary of what the scope does and how well
|
|
76509
|
-
3. **Verified Findings Table**: DD-ID, severity, location, description, evidence
|
|
76510
|
-
4. **Rejected Candidates**: Brief list with rejection reasons
|
|
76511
|
-
5. **Enhancements**: Non-blocking improvement suggestions
|
|
76512
|
-
6. **Recommended Implementation Phases**: If findings suggest follow-up work, outline phases
|
|
76513
|
-
7. **JSON Block** (when output=json): Structured machine-readable findings
|
|
76514
|
-
|
|
76515
|
-
IMPORTANT CONSTRAINTS for MODE: DEEP_DIVE:
|
|
76516
|
-
- Do NOT mutate source code under any circumstances
|
|
76718
|
+
Purpose: Read-only deep audit of the specified codebase scope using parallel explorer waves, always 2 parallel reviewers, and sequential critic challenge. This mode does NOT mutate source code, does NOT delegate to coder, and does NOT call declare_scope.
|
|
76719
|
+
|
|
76720
|
+
ACTION: Load skill \`file:.opencode/skills/deep-dive/SKILL.md\` immediately and follow its protocol.
|
|
76721
|
+
|
|
76722
|
+
HARD CONSTRAINTS (apply regardless of skill load success):
|
|
76517
76723
|
- Do NOT delegate to coder
|
|
76518
76724
|
- Do NOT call declare_scope
|
|
76725
|
+
- Do NOT mutate source code
|
|
76519
76726
|
- Do NOT create or modify any files outside .swarm/
|
|
76520
76727
|
- No final finding may appear in the report without reviewer verification
|
|
76521
76728
|
- Explorers generate candidate findings only — reviewers verify or reject
|
|
@@ -84996,6 +85203,10 @@ class PlanSyncWorker {
|
|
|
84996
85203
|
const planMtimeMs = Math.floor(planStats.mtimeMs);
|
|
84997
85204
|
const markerContent = fs37.readFileSync(markerPath, "utf8");
|
|
84998
85205
|
const marker = JSON.parse(markerContent);
|
|
85206
|
+
if (marker.in_progress === true) {
|
|
85207
|
+
log("[PlanSyncWorker] Skipping unauthorized-write check - plan write in progress");
|
|
85208
|
+
return;
|
|
85209
|
+
}
|
|
84999
85210
|
const markerTimestampMs = new Date(marker.timestamp).getTime();
|
|
85000
85211
|
if (planMtimeMs > markerTimestampMs + 5000) {
|
|
85001
85212
|
log("[PlanSyncWorker] WARNING: plan.json may have been written outside save_plan/savePlan - unauthorized direct write suspected", { planMtimeMs, markerTimestampMs });
|
|
@@ -112516,6 +112727,9 @@ init_ledger();
|
|
|
112516
112727
|
init_manager();
|
|
112517
112728
|
init_state();
|
|
112518
112729
|
init_create_tool();
|
|
112730
|
+
function executionProfilesEqual(a, b) {
|
|
112731
|
+
return a.parallelization_enabled === b.parallelization_enabled && a.max_concurrent_tasks === b.max_concurrent_tasks && a.council_parallel === b.council_parallel && a.locked === b.locked;
|
|
112732
|
+
}
|
|
112519
112733
|
function detectPlaceholderContent(args2) {
|
|
112520
112734
|
const issues = [];
|
|
112521
112735
|
const placeholderPattern = /^\[\w[\w\s]*\]$/;
|
|
@@ -112680,18 +112894,51 @@ async function executeSavePlan(args2, fallbackDir) {
|
|
|
112680
112894
|
}
|
|
112681
112895
|
}
|
|
112682
112896
|
}
|
|
112683
|
-
if (
|
|
112684
|
-
|
|
112897
|
+
if (args2.confirm_identity_change !== true) {
|
|
112898
|
+
const existingId = derivePlanId(existing);
|
|
112899
|
+
const incomingId = derivePlanId({
|
|
112900
|
+
swarm: args2.swarm_id,
|
|
112901
|
+
title: args2.title
|
|
112902
|
+
});
|
|
112903
|
+
if (existingId !== incomingId) {
|
|
112685
112904
|
return {
|
|
112686
112905
|
success: false,
|
|
112687
|
-
message: "
|
|
112906
|
+
message: "PLAN_IDENTITY_MISMATCH: The incoming plan identity does not match the existing plan. " + "To overwrite with a new identity, set confirm_identity_change: true.",
|
|
112688
112907
|
errors: [
|
|
112689
|
-
|
|
112908
|
+
`Existing plan identity: ${existingId} (swarm: "${existing.swarm}", title: "${existing.title}")`,
|
|
112909
|
+
`Incoming plan identity: ${incomingId} (swarm: "${args2.swarm_id}", title: "${args2.title}")`
|
|
112690
112910
|
],
|
|
112691
|
-
recovery_guidance: "
|
|
112911
|
+
recovery_guidance: "Verify the title and swarm_id match the intended plan. " + "If the identity change is intentional, retry with confirm_identity_change: true. " + "Never write .swarm/plan.json or .swarm/plan.md directly."
|
|
112692
112912
|
};
|
|
112693
112913
|
}
|
|
112694
|
-
|
|
112914
|
+
}
|
|
112915
|
+
if (existing.execution_profile?.locked) {
|
|
112916
|
+
if (args2.execution_profile !== undefined && !args2.reset_statuses) {
|
|
112917
|
+
const requestedProfile = ExecutionProfileSchema.safeParse({
|
|
112918
|
+
...existing.execution_profile,
|
|
112919
|
+
...args2.execution_profile
|
|
112920
|
+
});
|
|
112921
|
+
if (!requestedProfile.success) {
|
|
112922
|
+
return {
|
|
112923
|
+
success: false,
|
|
112924
|
+
message: "Invalid execution_profile: schema validation failed",
|
|
112925
|
+
errors: requestedProfile.error.issues.map((i2) => `${i2.path.join(".")}: ${i2.message}`),
|
|
112926
|
+
recovery_guidance: "Check execution_profile fields: parallelization_enabled (boolean), " + "max_concurrent_tasks (integer 1-64), council_parallel (boolean), locked (boolean)."
|
|
112927
|
+
};
|
|
112928
|
+
}
|
|
112929
|
+
if (executionProfilesEqual(existing.execution_profile, requestedProfile.data)) {
|
|
112930
|
+
preservedExecutionProfile = existing.execution_profile;
|
|
112931
|
+
} else {
|
|
112932
|
+
return {
|
|
112933
|
+
success: false,
|
|
112934
|
+
message: "EXECUTION_PROFILE_LOCKED: The execution_profile for this plan is locked and cannot be changed.",
|
|
112935
|
+
errors: [
|
|
112936
|
+
"execution_profile.locked is true — to change the profile you must first unlock it via a separate plan revision that explicitly sets locked: false, or reset the plan with reset_statuses."
|
|
112937
|
+
],
|
|
112938
|
+
recovery_guidance: "Remove the execution_profile field from this save_plan call to preserve the locked profile, " + "or use reset_statuses: true to start fresh (this clears the lock). " + "Never modify execution_profile directly in plan.json."
|
|
112939
|
+
};
|
|
112940
|
+
}
|
|
112941
|
+
} else if (!args2.reset_statuses) {
|
|
112695
112942
|
preservedExecutionProfile = existing.execution_profile;
|
|
112696
112943
|
}
|
|
112697
112944
|
} else {
|
|
@@ -112863,7 +113110,7 @@ async function executeSavePlan(args2, fallbackDir) {
|
|
|
112863
113110
|
});
|
|
112864
113111
|
const savedPlan = await loadPlanJsonOnly(dir);
|
|
112865
113112
|
if (savedPlan) {
|
|
112866
|
-
await
|
|
113113
|
+
await takeSnapshotWithRetry(dir, savedPlan);
|
|
112867
113114
|
}
|
|
112868
113115
|
if (resolvedProfile !== undefined && savedPlan) {
|
|
112869
113116
|
const planId = derivePlanId(plan);
|
|
@@ -112961,6 +113208,7 @@ var save_plan = createSwarmTool({
|
|
|
112961
113208
|
removed_task_ids: exports_external.array(exports_external.string()).optional().describe("Task IDs that are present in the prior plan but intentionally being " + "removed by this save. Every task missing from `phases` MUST be enumerated " + "here, otherwise save_plan rejects with PLAN_TASK_REMOVAL_NOT_ACKNOWLEDGED. " + "Tasks not yet finished (status pending/in_progress/blocked) MUST NOT be " + "removed without explicit user confirmation."),
|
|
112962
113209
|
removal_reason: exports_external.string().optional().describe("Required when removed_task_ids is non-empty. Human-readable reason recorded " + "on each task_removed ledger event."),
|
|
112963
113210
|
confirm_destructive_reset: exports_external.boolean().optional().describe("Required when reset_statuses is true AND at least one task is missing from " + "the new plan. Set true to acknowledge that the destructive reset drops " + "unfinished work. When set together with reset_statuses, save_plan auto-" + "populates removed_task_ids from the missing set."),
|
|
113211
|
+
confirm_identity_change: exports_external.boolean().optional().describe("When true, allows overwriting an existing plan that has a different " + "identity (swarm_id + title). Without this flag, save_plan rejects " + "with PLAN_IDENTITY_MISMATCH if the identity differs."),
|
|
112964
113212
|
execution_profile: exports_external.object({
|
|
112965
113213
|
parallelization_enabled: exports_external.boolean().optional().describe("When true, enables parallel task dispatch for this plan. Default false (serial)."),
|
|
112966
113214
|
max_concurrent_tasks: exports_external.number().int().min(1).max(64).optional().describe("Maximum tasks that may run concurrently when parallelization is enabled. Default 1."),
|