opencode-swarm 6.62.0 → 6.63.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/dist/cli/index.js +204 -87
- package/dist/evidence/manager.d.ts +2 -12
- package/dist/gate-evidence.d.ts +2 -9
- package/dist/index.js +275 -149
- package/dist/plan/ledger.d.ts +2 -2
- package/dist/plan/manager.d.ts +8 -0
- package/dist/validation/task-id.d.ts +43 -0
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -15788,22 +15788,26 @@ async function readLedgerEvents(directory) {
|
|
|
15788
15788
|
return [];
|
|
15789
15789
|
}
|
|
15790
15790
|
}
|
|
15791
|
-
async function initLedger(directory, planId, initialPlanHash) {
|
|
15791
|
+
async function initLedger(directory, planId, initialPlanHash, initialPlan) {
|
|
15792
15792
|
const ledgerPath = getLedgerPath(directory);
|
|
15793
15793
|
const planJsonPath = getPlanJsonPath(directory);
|
|
15794
15794
|
if (fs3.existsSync(ledgerPath)) {
|
|
15795
15795
|
throw new Error("Ledger already initialized. Use appendLedgerEvent to add events.");
|
|
15796
15796
|
}
|
|
15797
15797
|
let planHashAfter = initialPlanHash ?? "";
|
|
15798
|
+
let embeddedPlan = initialPlan;
|
|
15798
15799
|
if (!initialPlanHash) {
|
|
15799
15800
|
try {
|
|
15800
15801
|
if (fs3.existsSync(planJsonPath)) {
|
|
15801
15802
|
const content = fs3.readFileSync(planJsonPath, "utf8");
|
|
15802
15803
|
const plan = JSON.parse(content);
|
|
15803
15804
|
planHashAfter = computePlanHash(plan);
|
|
15805
|
+
if (!embeddedPlan)
|
|
15806
|
+
embeddedPlan = plan;
|
|
15804
15807
|
}
|
|
15805
15808
|
} catch {}
|
|
15806
15809
|
}
|
|
15810
|
+
const payload = embeddedPlan ? { plan: embeddedPlan, payload_hash: planHashAfter } : undefined;
|
|
15807
15811
|
const event = {
|
|
15808
15812
|
seq: 1,
|
|
15809
15813
|
timestamp: new Date().toISOString(),
|
|
@@ -15812,7 +15816,8 @@ async function initLedger(directory, planId, initialPlanHash) {
|
|
|
15812
15816
|
source: "initLedger",
|
|
15813
15817
|
plan_hash_before: "",
|
|
15814
15818
|
plan_hash_after: planHashAfter,
|
|
15815
|
-
schema_version: LEDGER_SCHEMA_VERSION
|
|
15819
|
+
schema_version: LEDGER_SCHEMA_VERSION,
|
|
15820
|
+
...payload ? { payload } : {}
|
|
15816
15821
|
};
|
|
15817
15822
|
fs3.mkdirSync(path3.join(directory, ".swarm"), { recursive: true });
|
|
15818
15823
|
const tempPath = `${ledgerPath}.tmp.${Date.now()}.${Math.floor(Math.random() * 1e9)}`;
|
|
@@ -15899,7 +15904,7 @@ async function takeSnapshotEvent(directory, plan, options) {
|
|
|
15899
15904
|
payload: snapshotPayload
|
|
15900
15905
|
}, { planHashAfter: options?.planHashAfter });
|
|
15901
15906
|
}
|
|
15902
|
-
async function replayFromLedger(directory,
|
|
15907
|
+
async function replayFromLedger(directory, _options) {
|
|
15903
15908
|
const events = await readLedgerEvents(directory);
|
|
15904
15909
|
if (events.length === 0) {
|
|
15905
15910
|
return null;
|
|
@@ -15922,6 +15927,20 @@ async function replayFromLedger(directory, options) {
|
|
|
15922
15927
|
return plan2;
|
|
15923
15928
|
}
|
|
15924
15929
|
}
|
|
15930
|
+
const createdEvent = relevantEvents.find((e) => e.event_type === "plan_created");
|
|
15931
|
+
if (createdEvent?.payload && typeof createdEvent.payload === "object" && "plan" in createdEvent.payload) {
|
|
15932
|
+
const parseResult = PlanSchema.safeParse(createdEvent.payload.plan);
|
|
15933
|
+
if (parseResult.success) {
|
|
15934
|
+
let plan2 = parseResult.data;
|
|
15935
|
+
const eventsAfterCreated = relevantEvents.filter((e) => e.seq > createdEvent.seq);
|
|
15936
|
+
for (const event of eventsAfterCreated) {
|
|
15937
|
+
if (plan2 === null)
|
|
15938
|
+
return null;
|
|
15939
|
+
plan2 = applyEventToPlan(plan2, event);
|
|
15940
|
+
}
|
|
15941
|
+
return plan2;
|
|
15942
|
+
}
|
|
15943
|
+
}
|
|
15925
15944
|
const planJsonPath = getPlanJsonPath(directory);
|
|
15926
15945
|
if (!fs3.existsSync(planJsonPath)) {
|
|
15927
15946
|
return null;
|
|
@@ -15944,6 +15963,11 @@ async function replayFromLedger(directory, options) {
|
|
|
15944
15963
|
function applyEventToPlan(plan, event) {
|
|
15945
15964
|
switch (event.event_type) {
|
|
15946
15965
|
case "plan_created":
|
|
15966
|
+
if (event.payload && typeof event.payload === "object" && "plan" in event.payload) {
|
|
15967
|
+
const parsed = PlanSchema.safeParse(event.payload.plan);
|
|
15968
|
+
if (parsed.success)
|
|
15969
|
+
return parsed.data;
|
|
15970
|
+
}
|
|
15947
15971
|
return plan;
|
|
15948
15972
|
case "task_status_changed":
|
|
15949
15973
|
if (event.task_id && event.to_status) {
|
|
@@ -16032,7 +16056,13 @@ var init_ledger = __esm(() => {
|
|
|
16032
16056
|
});
|
|
16033
16057
|
|
|
16034
16058
|
// src/plan/manager.ts
|
|
16035
|
-
import {
|
|
16059
|
+
import {
|
|
16060
|
+
copyFileSync,
|
|
16061
|
+
existsSync as existsSync3,
|
|
16062
|
+
readdirSync,
|
|
16063
|
+
renameSync as renameSync2,
|
|
16064
|
+
unlinkSync
|
|
16065
|
+
} from "fs";
|
|
16036
16066
|
import * as fsPromises from "fs/promises";
|
|
16037
16067
|
import * as path4 from "path";
|
|
16038
16068
|
async function loadPlanJsonOnly(directory) {
|
|
@@ -16284,35 +16314,53 @@ async function loadPlan(directory) {
|
|
|
16284
16314
|
return migrated;
|
|
16285
16315
|
}
|
|
16286
16316
|
if (await ledgerExists(directory)) {
|
|
16287
|
-
const
|
|
16288
|
-
|
|
16289
|
-
|
|
16290
|
-
|
|
16291
|
-
|
|
16317
|
+
const resolvedDir = path4.resolve(directory);
|
|
16318
|
+
const existingMutex = recoveryMutexes.get(resolvedDir);
|
|
16319
|
+
if (existingMutex) {
|
|
16320
|
+
await existingMutex;
|
|
16321
|
+
const postRecoveryPlan = await loadPlanJsonOnly(directory);
|
|
16322
|
+
if (postRecoveryPlan)
|
|
16323
|
+
return postRecoveryPlan;
|
|
16324
|
+
}
|
|
16325
|
+
let resolveRecovery;
|
|
16326
|
+
const mutex = new Promise((r) => {
|
|
16327
|
+
resolveRecovery = r;
|
|
16328
|
+
});
|
|
16329
|
+
recoveryMutexes.set(resolvedDir, mutex);
|
|
16292
16330
|
try {
|
|
16293
|
-
const
|
|
16294
|
-
if (
|
|
16295
|
-
|
|
16296
|
-
return
|
|
16331
|
+
const rebuilt = await replayFromLedger(directory);
|
|
16332
|
+
if (rebuilt) {
|
|
16333
|
+
await savePlan(directory, rebuilt);
|
|
16334
|
+
return rebuilt;
|
|
16297
16335
|
}
|
|
16298
|
-
|
|
16299
|
-
|
|
16300
|
-
|
|
16301
|
-
|
|
16302
|
-
|
|
16303
|
-
|
|
16304
|
-
|
|
16305
|
-
|
|
16306
|
-
|
|
16307
|
-
|
|
16308
|
-
});
|
|
16309
|
-
|
|
16310
|
-
|
|
16336
|
+
try {
|
|
16337
|
+
const anchorEvents = await readLedgerEvents(directory);
|
|
16338
|
+
if (anchorEvents.length === 0) {
|
|
16339
|
+
warn("[loadPlan] Ledger present but no events readable \u2014 refusing approved-snapshot recovery (cannot verify plan identity).");
|
|
16340
|
+
return null;
|
|
16341
|
+
}
|
|
16342
|
+
const expectedPlanId = anchorEvents[0].plan_id;
|
|
16343
|
+
const approved = await loadLastApprovedPlan(directory, expectedPlanId);
|
|
16344
|
+
if (approved) {
|
|
16345
|
+
const approvedPhase = approved.approval && typeof approved.approval === "object" && "phase" in approved.approval ? approved.approval.phase : undefined;
|
|
16346
|
+
warn(`[loadPlan] Ledger replay returned no plan \u2014 recovered from critic-approved snapshot seq=${approved.seq} timestamp=${approved.timestamp} (approval phase=${approvedPhase ?? "unknown"}). This may roll the plan back to an earlier phase \u2014 verify before continuing.`);
|
|
16347
|
+
await savePlan(directory, approved.plan);
|
|
16348
|
+
try {
|
|
16349
|
+
await takeSnapshotEvent(directory, approved.plan, {
|
|
16350
|
+
source: "recovery_from_approved_snapshot",
|
|
16351
|
+
approvalMetadata: approved.approval
|
|
16352
|
+
});
|
|
16353
|
+
} catch (healError) {
|
|
16354
|
+
warn(`[loadPlan] Recovery-heal snapshot append failed: ${healError instanceof Error ? healError.message : String(healError)}. Next loadPlan may re-enter recovery path.`);
|
|
16355
|
+
}
|
|
16356
|
+
return approved.plan;
|
|
16311
16357
|
}
|
|
16312
|
-
|
|
16358
|
+
} catch (recoveryError) {
|
|
16359
|
+
warn(`[loadPlan] Approved-snapshot recovery failed: ${recoveryError instanceof Error ? recoveryError.message : String(recoveryError)}`);
|
|
16313
16360
|
}
|
|
16314
|
-
}
|
|
16315
|
-
|
|
16361
|
+
} finally {
|
|
16362
|
+
resolveRecovery();
|
|
16363
|
+
recoveryMutexes.delete(resolvedDir);
|
|
16316
16364
|
}
|
|
16317
16365
|
}
|
|
16318
16366
|
return null;
|
|
@@ -16361,7 +16409,7 @@ async function savePlan(directory, plan, options) {
|
|
|
16361
16409
|
const planId = `${validated.swarm}-${validated.title}`.replace(/[^a-zA-Z0-9-_]/g, "_");
|
|
16362
16410
|
const planHashForInit = computePlanHash(validated);
|
|
16363
16411
|
if (!await ledgerExists(directory)) {
|
|
16364
|
-
await initLedger(directory, planId, planHashForInit);
|
|
16412
|
+
await initLedger(directory, planId, planHashForInit, validated);
|
|
16365
16413
|
} else {
|
|
16366
16414
|
const existingEvents = await readLedgerEvents(directory);
|
|
16367
16415
|
if (existingEvents.length > 0 && existingEvents[0].plan_id !== planId) {
|
|
@@ -16380,7 +16428,7 @@ async function savePlan(directory, plan, options) {
|
|
|
16380
16428
|
let initSucceeded = false;
|
|
16381
16429
|
if (backupExists) {
|
|
16382
16430
|
try {
|
|
16383
|
-
await initLedger(directory, planId, planHashForInit);
|
|
16431
|
+
await initLedger(directory, planId, planHashForInit, validated);
|
|
16384
16432
|
initSucceeded = true;
|
|
16385
16433
|
} catch (initErr) {
|
|
16386
16434
|
const errorMessage = String(initErr);
|
|
@@ -16422,6 +16470,19 @@ async function savePlan(directory, plan, options) {
|
|
|
16422
16470
|
unlinkSync(oldLedgerBackupPath);
|
|
16423
16471
|
} catch {}
|
|
16424
16472
|
}
|
|
16473
|
+
const MAX_ARCHIVED_SIBLINGS = 5;
|
|
16474
|
+
try {
|
|
16475
|
+
const allFiles = readdirSync(swarmDir2);
|
|
16476
|
+
const archivedSiblings = allFiles.filter((f) => f.startsWith("plan-ledger.archived-") && f.endsWith(".jsonl")).sort();
|
|
16477
|
+
if (archivedSiblings.length > MAX_ARCHIVED_SIBLINGS) {
|
|
16478
|
+
const toRemove = archivedSiblings.slice(0, archivedSiblings.length - MAX_ARCHIVED_SIBLINGS);
|
|
16479
|
+
for (const file2 of toRemove) {
|
|
16480
|
+
try {
|
|
16481
|
+
unlinkSync(path4.join(swarmDir2, file2));
|
|
16482
|
+
} catch {}
|
|
16483
|
+
}
|
|
16484
|
+
}
|
|
16485
|
+
} catch {}
|
|
16425
16486
|
}
|
|
16426
16487
|
}
|
|
16427
16488
|
const currentHash = computeCurrentPlanHash(directory);
|
|
@@ -16471,7 +16532,7 @@ async function savePlan(directory, plan, options) {
|
|
|
16471
16532
|
}
|
|
16472
16533
|
} catch (error49) {
|
|
16473
16534
|
if (error49 instanceof LedgerStaleWriterError) {
|
|
16474
|
-
throw new
|
|
16535
|
+
throw new PlanConcurrentModificationError(`Concurrent plan modification detected after retries: ${error49.message}. Please retry the operation.`);
|
|
16475
16536
|
}
|
|
16476
16537
|
throw error49;
|
|
16477
16538
|
}
|
|
@@ -16481,7 +16542,11 @@ async function savePlan(directory, plan, options) {
|
|
|
16481
16542
|
if (latestSeq > 0 && latestSeq % SNAPSHOT_INTERVAL === 0) {
|
|
16482
16543
|
await takeSnapshotEvent(directory, validated, {
|
|
16483
16544
|
planHashAfter: hashAfter
|
|
16484
|
-
}).catch(() => {
|
|
16545
|
+
}).catch((err2) => {
|
|
16546
|
+
if (process.env.DEBUG_SWARM) {
|
|
16547
|
+
warn(`[savePlan] Periodic snapshot write failed (non-fatal): ${err2 instanceof Error ? err2.message : String(err2)}`);
|
|
16548
|
+
}
|
|
16549
|
+
});
|
|
16485
16550
|
}
|
|
16486
16551
|
const swarmDir = path4.resolve(directory, ".swarm");
|
|
16487
16552
|
const planPath = path4.join(swarmDir, "plan.json");
|
|
@@ -16494,19 +16559,23 @@ async function savePlan(directory, plan, options) {
|
|
|
16494
16559
|
unlinkSync(tempPath);
|
|
16495
16560
|
} catch {}
|
|
16496
16561
|
}
|
|
16497
|
-
const contentHash = computePlanContentHash(validated);
|
|
16498
|
-
const markdown = derivePlanMarkdown(validated);
|
|
16499
|
-
const markdownWithHash = `<!-- PLAN_HASH: ${contentHash} -->
|
|
16500
|
-
${markdown}`;
|
|
16501
|
-
const mdPath = path4.join(swarmDir, "plan.md");
|
|
16502
|
-
const mdTempPath = path4.join(swarmDir, `plan.md.tmp.${Date.now()}.${Math.floor(Math.random() * 1e9)}`);
|
|
16503
16562
|
try {
|
|
16504
|
-
|
|
16505
|
-
|
|
16506
|
-
|
|
16563
|
+
const contentHash = computePlanContentHash(validated);
|
|
16564
|
+
const markdown = derivePlanMarkdown(validated);
|
|
16565
|
+
const markdownWithHash = `<!-- PLAN_HASH: ${contentHash} -->
|
|
16566
|
+
${markdown}`;
|
|
16567
|
+
const mdPath = path4.join(swarmDir, "plan.md");
|
|
16568
|
+
const mdTempPath = path4.join(swarmDir, `plan.md.tmp.${Date.now()}.${Math.floor(Math.random() * 1e9)}`);
|
|
16507
16569
|
try {
|
|
16508
|
-
|
|
16509
|
-
|
|
16570
|
+
await Bun.write(mdTempPath, markdownWithHash);
|
|
16571
|
+
renameSync2(mdTempPath, mdPath);
|
|
16572
|
+
} finally {
|
|
16573
|
+
try {
|
|
16574
|
+
unlinkSync(mdTempPath);
|
|
16575
|
+
} catch {}
|
|
16576
|
+
}
|
|
16577
|
+
} catch (mdError) {
|
|
16578
|
+
warn(`[savePlan] plan.md write failed (non-fatal, plan.json is authoritative): ${mdError instanceof Error ? mdError.message : String(mdError)}`);
|
|
16510
16579
|
}
|
|
16511
16580
|
try {
|
|
16512
16581
|
const markerPath = path4.join(swarmDir, ".plan-write-marker");
|
|
@@ -16551,11 +16620,6 @@ ${markdown}`;
|
|
|
16551
16620
|
return targetPlan;
|
|
16552
16621
|
}
|
|
16553
16622
|
async function updateTaskStatus(directory, taskId, status) {
|
|
16554
|
-
const plan = await loadPlan(directory);
|
|
16555
|
-
if (plan === null) {
|
|
16556
|
-
throw new Error(`Plan not found in directory: ${directory}`);
|
|
16557
|
-
}
|
|
16558
|
-
let taskFound = false;
|
|
16559
16623
|
const derivePhaseStatusFromTasks = (tasks) => {
|
|
16560
16624
|
if (tasks.length > 0 && tasks.every((task) => task.status === "completed")) {
|
|
16561
16625
|
return "complete";
|
|
@@ -16568,26 +16632,44 @@ async function updateTaskStatus(directory, taskId, status) {
|
|
|
16568
16632
|
}
|
|
16569
16633
|
return "pending";
|
|
16570
16634
|
};
|
|
16571
|
-
const
|
|
16572
|
-
|
|
16573
|
-
|
|
16574
|
-
|
|
16575
|
-
|
|
16576
|
-
|
|
16577
|
-
|
|
16635
|
+
const MAX_OUTER_RETRIES = 1;
|
|
16636
|
+
for (let attempt = 0;attempt <= MAX_OUTER_RETRIES; attempt++) {
|
|
16637
|
+
const plan = await loadPlan(directory);
|
|
16638
|
+
if (plan === null) {
|
|
16639
|
+
throw new Error(`Plan not found in directory: ${directory}`);
|
|
16640
|
+
}
|
|
16641
|
+
let taskFound = false;
|
|
16642
|
+
const updatedPhases = plan.phases.map((phase) => {
|
|
16643
|
+
const updatedTasks = phase.tasks.map((task) => {
|
|
16644
|
+
if (task.id === taskId) {
|
|
16645
|
+
taskFound = true;
|
|
16646
|
+
return { ...task, status };
|
|
16647
|
+
}
|
|
16648
|
+
return task;
|
|
16649
|
+
});
|
|
16650
|
+
return {
|
|
16651
|
+
...phase,
|
|
16652
|
+
status: derivePhaseStatusFromTasks(updatedTasks),
|
|
16653
|
+
tasks: updatedTasks
|
|
16654
|
+
};
|
|
16578
16655
|
});
|
|
16579
|
-
|
|
16580
|
-
|
|
16581
|
-
|
|
16582
|
-
|
|
16583
|
-
|
|
16584
|
-
|
|
16585
|
-
|
|
16586
|
-
|
|
16656
|
+
if (!taskFound) {
|
|
16657
|
+
throw new Error(`Task not found: ${taskId}`);
|
|
16658
|
+
}
|
|
16659
|
+
const updatedPlan = { ...plan, phases: updatedPhases };
|
|
16660
|
+
try {
|
|
16661
|
+
await savePlan(directory, updatedPlan, {
|
|
16662
|
+
preserveCompletedStatuses: true
|
|
16663
|
+
});
|
|
16664
|
+
return updatedPlan;
|
|
16665
|
+
} catch (error49) {
|
|
16666
|
+
if (error49 instanceof PlanConcurrentModificationError && attempt < MAX_OUTER_RETRIES) {
|
|
16667
|
+
continue;
|
|
16668
|
+
}
|
|
16669
|
+
throw error49;
|
|
16670
|
+
}
|
|
16587
16671
|
}
|
|
16588
|
-
|
|
16589
|
-
await savePlan(directory, updatedPlan, { preserveCompletedStatuses: true });
|
|
16590
|
-
return updatedPlan;
|
|
16672
|
+
throw new Error("updateTaskStatus: unexpected loop exit");
|
|
16591
16673
|
}
|
|
16592
16674
|
function derivePlanMarkdown(plan) {
|
|
16593
16675
|
const statusMap = {
|
|
@@ -16855,57 +16937,90 @@ function migrateLegacyPlan(planContent, swarmId) {
|
|
|
16855
16937
|
};
|
|
16856
16938
|
return plan;
|
|
16857
16939
|
}
|
|
16858
|
-
var startupLedgerCheckedWorkspaces;
|
|
16940
|
+
var PlanConcurrentModificationError, startupLedgerCheckedWorkspaces, recoveryMutexes;
|
|
16859
16941
|
var init_manager = __esm(() => {
|
|
16860
16942
|
init_plan_schema();
|
|
16861
16943
|
init_utils2();
|
|
16862
16944
|
init_utils();
|
|
16863
16945
|
init_spec_hash();
|
|
16864
16946
|
init_ledger();
|
|
16947
|
+
PlanConcurrentModificationError = class PlanConcurrentModificationError extends Error {
|
|
16948
|
+
constructor(message) {
|
|
16949
|
+
super(message);
|
|
16950
|
+
this.name = "PlanConcurrentModificationError";
|
|
16951
|
+
}
|
|
16952
|
+
};
|
|
16865
16953
|
startupLedgerCheckedWorkspaces = new Set;
|
|
16954
|
+
recoveryMutexes = new Map;
|
|
16866
16955
|
});
|
|
16867
16956
|
|
|
16868
|
-
// src/
|
|
16869
|
-
|
|
16870
|
-
import * as fs4 from "fs/promises";
|
|
16871
|
-
import * as path5 from "path";
|
|
16872
|
-
function isValidEvidenceType(type) {
|
|
16873
|
-
return VALID_EVIDENCE_TYPES.includes(type);
|
|
16874
|
-
}
|
|
16875
|
-
function isSecretscanEvidence(evidence) {
|
|
16876
|
-
return evidence.type === "secretscan";
|
|
16877
|
-
}
|
|
16878
|
-
function sanitizeTaskId(taskId) {
|
|
16957
|
+
// src/validation/task-id.ts
|
|
16958
|
+
function checkUnsafeChars(taskId) {
|
|
16879
16959
|
if (!taskId || taskId.length === 0) {
|
|
16880
|
-
|
|
16960
|
+
return "Invalid task ID: empty string";
|
|
16881
16961
|
}
|
|
16882
16962
|
if (/\0/.test(taskId)) {
|
|
16883
|
-
|
|
16963
|
+
return "Invalid task ID: contains null bytes";
|
|
16884
16964
|
}
|
|
16885
16965
|
for (let i2 = 0;i2 < taskId.length; i2++) {
|
|
16886
16966
|
if (taskId.charCodeAt(i2) < 32) {
|
|
16887
|
-
|
|
16967
|
+
return "Invalid task ID: contains control characters";
|
|
16888
16968
|
}
|
|
16889
16969
|
}
|
|
16890
|
-
if (taskId.includes("..") || taskId.includes("
|
|
16891
|
-
|
|
16892
|
-
}
|
|
16893
|
-
if (TASK_ID_REGEX.test(taskId)) {
|
|
16894
|
-
return taskId;
|
|
16970
|
+
if (taskId.includes("..") || taskId.includes("/") || taskId.includes("\\")) {
|
|
16971
|
+
return "Invalid task ID: path traversal detected";
|
|
16895
16972
|
}
|
|
16896
|
-
|
|
16897
|
-
|
|
16973
|
+
return;
|
|
16974
|
+
}
|
|
16975
|
+
function isStrictTaskId(taskId) {
|
|
16976
|
+
if (!taskId)
|
|
16977
|
+
return false;
|
|
16978
|
+
const unsafeMsg = checkUnsafeChars(taskId);
|
|
16979
|
+
if (unsafeMsg)
|
|
16980
|
+
return false;
|
|
16981
|
+
return STRICT_TASK_ID_PATTERN.test(taskId);
|
|
16982
|
+
}
|
|
16983
|
+
function assertStrictTaskId(taskId) {
|
|
16984
|
+
if (!isStrictTaskId(taskId)) {
|
|
16985
|
+
throw new Error(`Invalid taskId: "${taskId}". Must match N.M or N.M.P (e.g. "1.1", "1.2.3").`);
|
|
16898
16986
|
}
|
|
16899
|
-
|
|
16900
|
-
|
|
16987
|
+
}
|
|
16988
|
+
function sanitizeTaskId(taskId) {
|
|
16989
|
+
const unsafeMsg = checkUnsafeChars(taskId);
|
|
16990
|
+
if (unsafeMsg) {
|
|
16991
|
+
throw new Error(unsafeMsg);
|
|
16901
16992
|
}
|
|
16902
|
-
if (GENERAL_TASK_ID_REGEX.test(taskId)) {
|
|
16993
|
+
if (STRICT_TASK_ID_PATTERN.test(taskId) || RETRO_TASK_ID_REGEX.test(taskId) || INTERNAL_TOOL_ID_REGEX.test(taskId) || GENERAL_TASK_ID_REGEX.test(taskId)) {
|
|
16903
16994
|
return taskId;
|
|
16904
16995
|
}
|
|
16905
16996
|
throw new Error(`Invalid task ID: must be alphanumeric (ASCII) with optional hyphens, underscores, or dots, got "${taskId}"`);
|
|
16906
16997
|
}
|
|
16998
|
+
function validateTaskIdFormat(taskId) {
|
|
16999
|
+
if (!STRICT_TASK_ID_PATTERN.test(taskId)) {
|
|
17000
|
+
return `Invalid taskId "${taskId}". Must match pattern N.M or N.M.P (e.g., "1.1", "1.2.3")`;
|
|
17001
|
+
}
|
|
17002
|
+
return;
|
|
17003
|
+
}
|
|
17004
|
+
var STRICT_TASK_ID_PATTERN, RETRO_TASK_ID_REGEX, INTERNAL_TOOL_ID_REGEX, GENERAL_TASK_ID_REGEX;
|
|
17005
|
+
var init_task_id = __esm(() => {
|
|
17006
|
+
STRICT_TASK_ID_PATTERN = /^\d+\.\d+(\.\d+)*$/;
|
|
17007
|
+
RETRO_TASK_ID_REGEX = /^retro-\d+$/;
|
|
17008
|
+
INTERNAL_TOOL_ID_REGEX = /^(?:sast_scan|quality_budget|syntax_check|placeholder_scan|sbom_generate|build|secretscan)$/;
|
|
17009
|
+
GENERAL_TASK_ID_REGEX = /^[a-zA-Z0-9][a-zA-Z0-9._-]*$/;
|
|
17010
|
+
});
|
|
17011
|
+
|
|
17012
|
+
// src/evidence/manager.ts
|
|
17013
|
+
import { mkdirSync as mkdirSync2, readdirSync as readdirSync2, rmSync, statSync as statSync2 } from "fs";
|
|
17014
|
+
import * as fs4 from "fs/promises";
|
|
17015
|
+
import * as path5 from "path";
|
|
17016
|
+
function isValidEvidenceType(type) {
|
|
17017
|
+
return VALID_EVIDENCE_TYPES.includes(type);
|
|
17018
|
+
}
|
|
17019
|
+
function isSecretscanEvidence(evidence) {
|
|
17020
|
+
return evidence.type === "secretscan";
|
|
17021
|
+
}
|
|
16907
17022
|
async function saveEvidence(directory, taskId, evidence) {
|
|
16908
|
-
const sanitizedTaskId =
|
|
17023
|
+
const sanitizedTaskId = sanitizeTaskId2(taskId);
|
|
16909
17024
|
const relativePath = path5.join("evidence", sanitizedTaskId, "evidence.json");
|
|
16910
17025
|
const evidencePath = validateSwarmPath(directory, relativePath);
|
|
16911
17026
|
const evidenceDir = path5.dirname(evidencePath);
|
|
@@ -16936,9 +17051,14 @@ async function saveEvidence(directory, taskId, evidence) {
|
|
|
16936
17051
|
updated_at: now
|
|
16937
17052
|
};
|
|
16938
17053
|
}
|
|
17054
|
+
const MAX_BUNDLE_ENTRIES = 100;
|
|
17055
|
+
let entries = [...bundle.entries, evidence];
|
|
17056
|
+
if (entries.length > MAX_BUNDLE_ENTRIES) {
|
|
17057
|
+
entries = entries.slice(entries.length - MAX_BUNDLE_ENTRIES);
|
|
17058
|
+
}
|
|
16939
17059
|
const updatedBundle = {
|
|
16940
17060
|
...bundle,
|
|
16941
|
-
entries
|
|
17061
|
+
entries,
|
|
16942
17062
|
updated_at: new Date().toISOString()
|
|
16943
17063
|
};
|
|
16944
17064
|
const bundleJson = JSON.stringify(updatedBundle);
|
|
@@ -16983,7 +17103,7 @@ function wrapFlatRetrospective(flatEntry, taskId) {
|
|
|
16983
17103
|
};
|
|
16984
17104
|
}
|
|
16985
17105
|
async function loadEvidence(directory, taskId) {
|
|
16986
|
-
const sanitizedTaskId =
|
|
17106
|
+
const sanitizedTaskId = sanitizeTaskId2(taskId);
|
|
16987
17107
|
const relativePath = path5.join("evidence", sanitizedTaskId, "evidence.json");
|
|
16988
17108
|
const evidencePath = validateSwarmPath(directory, relativePath);
|
|
16989
17109
|
const content = await readSwarmFileAsync(directory, relativePath);
|
|
@@ -17037,7 +17157,7 @@ async function listEvidenceTaskIds(directory) {
|
|
|
17037
17157
|
}
|
|
17038
17158
|
let entries;
|
|
17039
17159
|
try {
|
|
17040
|
-
entries =
|
|
17160
|
+
entries = readdirSync2(evidenceBasePath);
|
|
17041
17161
|
} catch {
|
|
17042
17162
|
return [];
|
|
17043
17163
|
}
|
|
@@ -17049,7 +17169,7 @@ async function listEvidenceTaskIds(directory) {
|
|
|
17049
17169
|
if (!stats.isDirectory()) {
|
|
17050
17170
|
continue;
|
|
17051
17171
|
}
|
|
17052
|
-
|
|
17172
|
+
sanitizeTaskId2(entry);
|
|
17053
17173
|
taskIds.push(entry);
|
|
17054
17174
|
} catch (error49) {
|
|
17055
17175
|
if (error49 instanceof Error && !error49.message.startsWith("Invalid task ID")) {
|
|
@@ -17060,7 +17180,7 @@ async function listEvidenceTaskIds(directory) {
|
|
|
17060
17180
|
return taskIds.sort();
|
|
17061
17181
|
}
|
|
17062
17182
|
async function deleteEvidence(directory, taskId) {
|
|
17063
|
-
const sanitizedTaskId =
|
|
17183
|
+
const sanitizedTaskId = sanitizeTaskId2(taskId);
|
|
17064
17184
|
const relativePath = path5.join("evidence", sanitizedTaskId);
|
|
17065
17185
|
const evidenceDir = validateSwarmPath(directory, relativePath);
|
|
17066
17186
|
try {
|
|
@@ -17122,12 +17242,13 @@ async function archiveEvidence(directory, maxAgeDays, maxBundles) {
|
|
|
17122
17242
|
}
|
|
17123
17243
|
return archived;
|
|
17124
17244
|
}
|
|
17125
|
-
var VALID_EVIDENCE_TYPES,
|
|
17245
|
+
var VALID_EVIDENCE_TYPES, sanitizeTaskId2, LEGACY_TASK_COMPLEXITY_MAP;
|
|
17126
17246
|
var init_manager2 = __esm(() => {
|
|
17127
17247
|
init_zod();
|
|
17128
17248
|
init_evidence_schema();
|
|
17129
17249
|
init_utils2();
|
|
17130
17250
|
init_utils();
|
|
17251
|
+
init_task_id();
|
|
17131
17252
|
VALID_EVIDENCE_TYPES = [
|
|
17132
17253
|
"review",
|
|
17133
17254
|
"test",
|
|
@@ -17143,10 +17264,7 @@ var init_manager2 = __esm(() => {
|
|
|
17143
17264
|
"quality_budget",
|
|
17144
17265
|
"secretscan"
|
|
17145
17266
|
];
|
|
17146
|
-
|
|
17147
|
-
RETRO_TASK_ID_REGEX = /^retro-\d+$/;
|
|
17148
|
-
INTERNAL_TOOL_ID_REGEX = /^(?:sast_scan|quality_budget|syntax_check|placeholder_scan|sbom_generate|build|secretscan)$/;
|
|
17149
|
-
GENERAL_TASK_ID_REGEX = /^[a-zA-Z0-9][a-zA-Z0-9._-]*$/;
|
|
17267
|
+
sanitizeTaskId2 = sanitizeTaskId;
|
|
17150
17268
|
LEGACY_TASK_COMPLEXITY_MAP = {
|
|
17151
17269
|
low: "simple",
|
|
17152
17270
|
medium: "moderate",
|
|
@@ -40852,22 +40970,10 @@ __export(exports_gate_evidence, {
|
|
|
40852
40970
|
import { mkdirSync as mkdirSync12, readFileSync as readFileSync18, renameSync as renameSync10, unlinkSync as unlinkSync5 } from "fs";
|
|
40853
40971
|
import * as path39 from "path";
|
|
40854
40972
|
function isValidTaskId2(taskId) {
|
|
40855
|
-
|
|
40856
|
-
return false;
|
|
40857
|
-
if (taskId.includes(".."))
|
|
40858
|
-
return false;
|
|
40859
|
-
if (taskId.includes("/"))
|
|
40860
|
-
return false;
|
|
40861
|
-
if (taskId.includes("\\"))
|
|
40862
|
-
return false;
|
|
40863
|
-
if (taskId.includes("\x00"))
|
|
40864
|
-
return false;
|
|
40865
|
-
return TASK_ID_PATTERN.test(taskId);
|
|
40973
|
+
return isStrictTaskId(taskId);
|
|
40866
40974
|
}
|
|
40867
40975
|
function assertValidTaskId(taskId) {
|
|
40868
|
-
|
|
40869
|
-
throw new Error(`Invalid taskId: "${taskId}". Must match N.M or N.M.P (e.g. "1.1", "1.2.3").`);
|
|
40870
|
-
}
|
|
40976
|
+
assertStrictTaskId(taskId);
|
|
40871
40977
|
}
|
|
40872
40978
|
function deriveRequiredGates(agentType) {
|
|
40873
40979
|
switch (agentType) {
|
|
@@ -40900,6 +41006,7 @@ function getEvidenceDir(directory) {
|
|
|
40900
41006
|
return path39.join(directory, ".swarm", "evidence");
|
|
40901
41007
|
}
|
|
40902
41008
|
function getEvidencePath(directory, taskId) {
|
|
41009
|
+
assertValidTaskId(taskId);
|
|
40903
41010
|
return path39.join(getEvidenceDir(directory), `${taskId}.json`);
|
|
40904
41011
|
}
|
|
40905
41012
|
function readExisting(evidencePath) {
|
|
@@ -40987,11 +41094,11 @@ async function hasPassedAllGates(directory, taskId) {
|
|
|
40987
41094
|
return false;
|
|
40988
41095
|
return evidence.required_gates.every((gate) => evidence.gates[gate] != null);
|
|
40989
41096
|
}
|
|
40990
|
-
var DEFAULT_REQUIRED_GATES
|
|
41097
|
+
var DEFAULT_REQUIRED_GATES;
|
|
40991
41098
|
var init_gate_evidence = __esm(() => {
|
|
40992
41099
|
init_telemetry();
|
|
41100
|
+
init_task_id();
|
|
40993
41101
|
DEFAULT_REQUIRED_GATES = ["reviewer", "test_engineer"];
|
|
40994
|
-
TASK_ID_PATTERN = /^\d+\.\d+(\.\d+)*$/;
|
|
40995
41102
|
});
|
|
40996
41103
|
|
|
40997
41104
|
// src/hooks/review-receipt.ts
|
|
@@ -47288,11 +47395,14 @@ async function executeWriteRetro(args2, directory) {
|
|
|
47288
47395
|
try {
|
|
47289
47396
|
const allTaskIds = await listEvidenceTaskIds(directory);
|
|
47290
47397
|
const phaseTaskIds = allTaskIds.filter((id) => id.startsWith(`${phase}.`));
|
|
47398
|
+
const sessionStart = args2.metadata && typeof args2.metadata.session_start === "string" ? args2.metadata.session_start : undefined;
|
|
47291
47399
|
for (const phaseTaskId of phaseTaskIds) {
|
|
47292
47400
|
const result = await loadEvidence(directory, phaseTaskId);
|
|
47293
47401
|
if (result.status !== "found")
|
|
47294
47402
|
continue;
|
|
47295
47403
|
const bundle = result.bundle;
|
|
47404
|
+
if (sessionStart && bundle.updated_at < sessionStart)
|
|
47405
|
+
continue;
|
|
47296
47406
|
for (const entry of bundle.entries) {
|
|
47297
47407
|
const e = entry;
|
|
47298
47408
|
if (e.type === "review" && e.verdict === "fail") {
|
|
@@ -47484,6 +47594,18 @@ async function handleCloseCommand(directory, args2) {
|
|
|
47484
47594
|
}
|
|
47485
47595
|
}
|
|
47486
47596
|
}
|
|
47597
|
+
let sessionStart;
|
|
47598
|
+
{
|
|
47599
|
+
let earliest = Infinity;
|
|
47600
|
+
for (const [, session] of swarmState.agentSessions) {
|
|
47601
|
+
if (session.lastAgentEventTime > 0 && session.lastAgentEventTime < earliest) {
|
|
47602
|
+
earliest = session.lastAgentEventTime;
|
|
47603
|
+
}
|
|
47604
|
+
}
|
|
47605
|
+
if (earliest < Infinity) {
|
|
47606
|
+
sessionStart = new Date(earliest).toISOString();
|
|
47607
|
+
}
|
|
47608
|
+
}
|
|
47487
47609
|
const wrotePhaseRetro = closedPhases.length > 0;
|
|
47488
47610
|
if (!wrotePhaseRetro && !planExists) {
|
|
47489
47611
|
try {
|
|
@@ -47499,7 +47621,10 @@ async function handleCloseCommand(directory, args2) {
|
|
|
47499
47621
|
test_failures: 0,
|
|
47500
47622
|
security_findings: 0,
|
|
47501
47623
|
integration_issues: 0,
|
|
47502
|
-
metadata: {
|
|
47624
|
+
metadata: {
|
|
47625
|
+
session_scope: "plan_free",
|
|
47626
|
+
...sessionStart ? { session_start: sessionStart } : {}
|
|
47627
|
+
}
|
|
47503
47628
|
}, directory);
|
|
47504
47629
|
try {
|
|
47505
47630
|
const parsed = JSON.parse(sessionRetroResult);
|
|
@@ -47556,7 +47681,8 @@ async function handleCloseCommand(directory, args2) {
|
|
|
47556
47681
|
}
|
|
47557
47682
|
}
|
|
47558
47683
|
const timestamp = new Date().toISOString().replace(/[:.]/g, "-");
|
|
47559
|
-
const
|
|
47684
|
+
const suffix = Math.random().toString(36).slice(2, 8);
|
|
47685
|
+
const archiveDir = path14.join(swarmDir, "archive", `swarm-${timestamp}-${suffix}`);
|
|
47560
47686
|
let archiveResult = "";
|
|
47561
47687
|
let archivedFileCount = 0;
|
|
47562
47688
|
const archivedActiveStateFiles = new Set;
|
|
@@ -47820,11 +47946,23 @@ async function handleCloseCommand(directory, args2) {
|
|
|
47820
47946
|
if (pruneErrors.length > 0) {
|
|
47821
47947
|
warnings.push(`Could not prune ${pruneErrors.length} branch(es) (unmerged or checked out): ${pruneErrors.join(", ")}`);
|
|
47822
47948
|
}
|
|
47823
|
-
const
|
|
47949
|
+
const retroWarnings = warnings.filter((w) => w.includes("Retrospective write") || w.includes("retrospective write") || w.includes("Session retrospective"));
|
|
47950
|
+
const otherWarnings = warnings.filter((w) => !w.includes("Retrospective write") && !w.includes("retrospective write") && !w.includes("Session retrospective"));
|
|
47951
|
+
let warningMsg = "";
|
|
47952
|
+
if (retroWarnings.length > 0) {
|
|
47953
|
+
warningMsg += `
|
|
47954
|
+
|
|
47955
|
+
**\u26A0 Retrospective evidence incomplete:**
|
|
47956
|
+
${retroWarnings.map((w) => `- ${w}`).join(`
|
|
47957
|
+
`)}`;
|
|
47958
|
+
}
|
|
47959
|
+
if (otherWarnings.length > 0) {
|
|
47960
|
+
warningMsg += `
|
|
47824
47961
|
|
|
47825
47962
|
**Warnings:**
|
|
47826
|
-
${
|
|
47827
|
-
`)}
|
|
47963
|
+
${otherWarnings.map((w) => `- ${w}`).join(`
|
|
47964
|
+
`)}`;
|
|
47965
|
+
}
|
|
47828
47966
|
if (planAlreadyDone) {
|
|
47829
47967
|
return `\u2705 Session finalized. Plan was already in a terminal state \u2014 cleanup and archive applied.
|
|
47830
47968
|
|
|
@@ -49074,7 +49212,7 @@ init_manager2();
|
|
|
49074
49212
|
init_utils2();
|
|
49075
49213
|
init_manager();
|
|
49076
49214
|
import * as child_process4 from "child_process";
|
|
49077
|
-
import { existsSync as existsSync8, readdirSync as
|
|
49215
|
+
import { existsSync as existsSync8, readdirSync as readdirSync3, readFileSync as readFileSync5, statSync as statSync4 } from "fs";
|
|
49078
49216
|
import path19 from "path";
|
|
49079
49217
|
import { fileURLToPath } from "url";
|
|
49080
49218
|
function validateTaskDag(plan) {
|
|
@@ -49277,7 +49415,7 @@ async function checkPlanSync(directory, plan) {
|
|
|
49277
49415
|
}
|
|
49278
49416
|
async function checkConfigBackups(directory) {
|
|
49279
49417
|
try {
|
|
49280
|
-
const files =
|
|
49418
|
+
const files = readdirSync3(directory);
|
|
49281
49419
|
const backupCount = files.filter((f) => /\.opencode-swarm\.yaml\.bak/.test(f)).length;
|
|
49282
49420
|
if (backupCount <= 5) {
|
|
49283
49421
|
return {
|
|
@@ -49743,7 +49881,7 @@ async function getDiagnoseData(directory) {
|
|
|
49743
49881
|
checks5.push(await checkCurator(directory));
|
|
49744
49882
|
try {
|
|
49745
49883
|
const evidenceDir = path19.join(directory, ".swarm", "evidence");
|
|
49746
|
-
const snapshotFiles = existsSync8(evidenceDir) ?
|
|
49884
|
+
const snapshotFiles = existsSync8(evidenceDir) ? readdirSync3(evidenceDir).filter((f) => f.startsWith("agent-tools-") && f.endsWith(".json")) : [];
|
|
49747
49885
|
if (snapshotFiles.length > 0) {
|
|
49748
49886
|
const latest = snapshotFiles.sort().pop();
|
|
49749
49887
|
checks5.push({
|
|
@@ -51975,7 +52113,7 @@ async function handleResetSessionCommand(directory, _args) {
|
|
|
51975
52113
|
// src/summaries/manager.ts
|
|
51976
52114
|
init_utils2();
|
|
51977
52115
|
init_utils();
|
|
51978
|
-
import { mkdirSync as mkdirSync9, readdirSync as
|
|
52116
|
+
import { mkdirSync as mkdirSync9, readdirSync as readdirSync9, renameSync as renameSync8, rmSync as rmSync3, statSync as statSync7 } from "fs";
|
|
51979
52117
|
import * as path31 from "path";
|
|
51980
52118
|
var SUMMARY_ID_REGEX = /^S\d+$/;
|
|
51981
52119
|
function sanitizeSummaryId(id) {
|
|
@@ -64697,24 +64835,14 @@ var build_check = createSwarmTool({
|
|
|
64697
64835
|
// src/tools/check-gate-status.ts
|
|
64698
64836
|
init_dist();
|
|
64699
64837
|
init_manager2();
|
|
64838
|
+
init_task_id();
|
|
64700
64839
|
init_create_tool();
|
|
64701
64840
|
init_resolve_working_directory();
|
|
64702
64841
|
import * as fs42 from "fs";
|
|
64703
64842
|
import * as path54 from "path";
|
|
64704
64843
|
var EVIDENCE_DIR = ".swarm/evidence";
|
|
64705
|
-
var TASK_ID_PATTERN2 = /^\d+\.\d+(\.\d+)*$/;
|
|
64706
64844
|
function isValidTaskId3(taskId) {
|
|
64707
|
-
|
|
64708
|
-
return false;
|
|
64709
|
-
if (taskId.includes(".."))
|
|
64710
|
-
return false;
|
|
64711
|
-
if (taskId.includes("/"))
|
|
64712
|
-
return false;
|
|
64713
|
-
if (taskId.includes("\\"))
|
|
64714
|
-
return false;
|
|
64715
|
-
if (taskId.includes("\x00"))
|
|
64716
|
-
return false;
|
|
64717
|
-
return TASK_ID_PATTERN2.test(taskId);
|
|
64845
|
+
return isStrictTaskId(taskId);
|
|
64718
64846
|
}
|
|
64719
64847
|
function isPathWithinSwarm(filePath, workspaceRoot) {
|
|
64720
64848
|
const normalizedWorkspace = path54.resolve(workspaceRoot);
|
|
@@ -66182,15 +66310,12 @@ var curator_analyze = createSwarmTool({
|
|
|
66182
66310
|
// src/tools/declare-scope.ts
|
|
66183
66311
|
init_tool();
|
|
66184
66312
|
init_state();
|
|
66313
|
+
init_task_id();
|
|
66185
66314
|
init_create_tool();
|
|
66186
66315
|
import * as fs46 from "fs";
|
|
66187
66316
|
import * as path58 from "path";
|
|
66188
|
-
function
|
|
66189
|
-
|
|
66190
|
-
if (!taskIdPattern.test(taskId)) {
|
|
66191
|
-
return `Invalid taskId "${taskId}". Must match pattern N.M or N.M.P (e.g., "1.1", "1.2.3")`;
|
|
66192
|
-
}
|
|
66193
|
-
return;
|
|
66317
|
+
function validateTaskIdFormat2(taskId) {
|
|
66318
|
+
return validateTaskIdFormat(taskId);
|
|
66194
66319
|
}
|
|
66195
66320
|
function validateFiles(files) {
|
|
66196
66321
|
const errors5 = [];
|
|
@@ -66208,7 +66333,7 @@ function validateFiles(files) {
|
|
|
66208
66333
|
return errors5;
|
|
66209
66334
|
}
|
|
66210
66335
|
async function executeDeclareScope(args2, fallbackDir) {
|
|
66211
|
-
const taskIdError =
|
|
66336
|
+
const taskIdError = validateTaskIdFormat2(args2.taskId);
|
|
66212
66337
|
if (taskIdError) {
|
|
66213
66338
|
return {
|
|
66214
66339
|
success: false,
|
|
@@ -76367,6 +76492,7 @@ async function validateDiffScope(taskId, directory) {
|
|
|
76367
76492
|
init_manager();
|
|
76368
76493
|
init_state();
|
|
76369
76494
|
init_telemetry();
|
|
76495
|
+
init_task_id();
|
|
76370
76496
|
init_create_tool();
|
|
76371
76497
|
var VALID_STATUSES2 = [
|
|
76372
76498
|
"pending",
|
|
@@ -76381,8 +76507,8 @@ function validateStatus(status) {
|
|
|
76381
76507
|
return;
|
|
76382
76508
|
}
|
|
76383
76509
|
function validateTaskId(taskId) {
|
|
76384
|
-
const
|
|
76385
|
-
if (
|
|
76510
|
+
const result = validateTaskIdFormat(taskId);
|
|
76511
|
+
if (result) {
|
|
76386
76512
|
return `Invalid task_id "${taskId}". Must match pattern N.M or N.M.P (e.g., "1.1", "1.2.3")`;
|
|
76387
76513
|
}
|
|
76388
76514
|
return;
|