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/cli/index.js
CHANGED
|
@@ -14204,22 +14204,26 @@ async function readLedgerEvents(directory) {
|
|
|
14204
14204
|
return [];
|
|
14205
14205
|
}
|
|
14206
14206
|
}
|
|
14207
|
-
async function initLedger(directory, planId, initialPlanHash) {
|
|
14207
|
+
async function initLedger(directory, planId, initialPlanHash, initialPlan) {
|
|
14208
14208
|
const ledgerPath = getLedgerPath(directory);
|
|
14209
14209
|
const planJsonPath = getPlanJsonPath(directory);
|
|
14210
14210
|
if (fs.existsSync(ledgerPath)) {
|
|
14211
14211
|
throw new Error("Ledger already initialized. Use appendLedgerEvent to add events.");
|
|
14212
14212
|
}
|
|
14213
14213
|
let planHashAfter = initialPlanHash ?? "";
|
|
14214
|
+
let embeddedPlan = initialPlan;
|
|
14214
14215
|
if (!initialPlanHash) {
|
|
14215
14216
|
try {
|
|
14216
14217
|
if (fs.existsSync(planJsonPath)) {
|
|
14217
14218
|
const content = fs.readFileSync(planJsonPath, "utf8");
|
|
14218
14219
|
const plan = JSON.parse(content);
|
|
14219
14220
|
planHashAfter = computePlanHash(plan);
|
|
14221
|
+
if (!embeddedPlan)
|
|
14222
|
+
embeddedPlan = plan;
|
|
14220
14223
|
}
|
|
14221
14224
|
} catch {}
|
|
14222
14225
|
}
|
|
14226
|
+
const payload = embeddedPlan ? { plan: embeddedPlan, payload_hash: planHashAfter } : undefined;
|
|
14223
14227
|
const event = {
|
|
14224
14228
|
seq: 1,
|
|
14225
14229
|
timestamp: new Date().toISOString(),
|
|
@@ -14228,7 +14232,8 @@ async function initLedger(directory, planId, initialPlanHash) {
|
|
|
14228
14232
|
source: "initLedger",
|
|
14229
14233
|
plan_hash_before: "",
|
|
14230
14234
|
plan_hash_after: planHashAfter,
|
|
14231
|
-
schema_version: LEDGER_SCHEMA_VERSION
|
|
14235
|
+
schema_version: LEDGER_SCHEMA_VERSION,
|
|
14236
|
+
...payload ? { payload } : {}
|
|
14232
14237
|
};
|
|
14233
14238
|
fs.mkdirSync(path2.join(directory, ".swarm"), { recursive: true });
|
|
14234
14239
|
const tempPath = `${ledgerPath}.tmp.${Date.now()}.${Math.floor(Math.random() * 1e9)}`;
|
|
@@ -14315,7 +14320,7 @@ async function takeSnapshotEvent(directory, plan, options) {
|
|
|
14315
14320
|
payload: snapshotPayload
|
|
14316
14321
|
}, { planHashAfter: options?.planHashAfter });
|
|
14317
14322
|
}
|
|
14318
|
-
async function replayFromLedger(directory,
|
|
14323
|
+
async function replayFromLedger(directory, _options) {
|
|
14319
14324
|
const events = await readLedgerEvents(directory);
|
|
14320
14325
|
if (events.length === 0) {
|
|
14321
14326
|
return null;
|
|
@@ -14338,6 +14343,20 @@ async function replayFromLedger(directory, options) {
|
|
|
14338
14343
|
return plan2;
|
|
14339
14344
|
}
|
|
14340
14345
|
}
|
|
14346
|
+
const createdEvent = relevantEvents.find((e) => e.event_type === "plan_created");
|
|
14347
|
+
if (createdEvent?.payload && typeof createdEvent.payload === "object" && "plan" in createdEvent.payload) {
|
|
14348
|
+
const parseResult = PlanSchema.safeParse(createdEvent.payload.plan);
|
|
14349
|
+
if (parseResult.success) {
|
|
14350
|
+
let plan2 = parseResult.data;
|
|
14351
|
+
const eventsAfterCreated = relevantEvents.filter((e) => e.seq > createdEvent.seq);
|
|
14352
|
+
for (const event of eventsAfterCreated) {
|
|
14353
|
+
if (plan2 === null)
|
|
14354
|
+
return null;
|
|
14355
|
+
plan2 = applyEventToPlan(plan2, event);
|
|
14356
|
+
}
|
|
14357
|
+
return plan2;
|
|
14358
|
+
}
|
|
14359
|
+
}
|
|
14341
14360
|
const planJsonPath = getPlanJsonPath(directory);
|
|
14342
14361
|
if (!fs.existsSync(planJsonPath)) {
|
|
14343
14362
|
return null;
|
|
@@ -14360,6 +14379,11 @@ async function replayFromLedger(directory, options) {
|
|
|
14360
14379
|
function applyEventToPlan(plan, event) {
|
|
14361
14380
|
switch (event.event_type) {
|
|
14362
14381
|
case "plan_created":
|
|
14382
|
+
if (event.payload && typeof event.payload === "object" && "plan" in event.payload) {
|
|
14383
|
+
const parsed = PlanSchema.safeParse(event.payload.plan);
|
|
14384
|
+
if (parsed.success)
|
|
14385
|
+
return parsed.data;
|
|
14386
|
+
}
|
|
14363
14387
|
return plan;
|
|
14364
14388
|
case "task_status_changed":
|
|
14365
14389
|
if (event.task_id && event.to_status) {
|
|
@@ -14448,7 +14472,13 @@ var init_ledger = __esm(() => {
|
|
|
14448
14472
|
});
|
|
14449
14473
|
|
|
14450
14474
|
// src/plan/manager.ts
|
|
14451
|
-
import {
|
|
14475
|
+
import {
|
|
14476
|
+
copyFileSync,
|
|
14477
|
+
existsSync as existsSync2,
|
|
14478
|
+
readdirSync,
|
|
14479
|
+
renameSync as renameSync2,
|
|
14480
|
+
unlinkSync
|
|
14481
|
+
} from "fs";
|
|
14452
14482
|
import * as fsPromises from "fs/promises";
|
|
14453
14483
|
import * as path3 from "path";
|
|
14454
14484
|
async function loadPlanJsonOnly(directory) {
|
|
@@ -14700,35 +14730,53 @@ async function loadPlan(directory) {
|
|
|
14700
14730
|
return migrated;
|
|
14701
14731
|
}
|
|
14702
14732
|
if (await ledgerExists(directory)) {
|
|
14703
|
-
const
|
|
14704
|
-
|
|
14705
|
-
|
|
14706
|
-
|
|
14707
|
-
|
|
14733
|
+
const resolvedDir = path3.resolve(directory);
|
|
14734
|
+
const existingMutex = recoveryMutexes.get(resolvedDir);
|
|
14735
|
+
if (existingMutex) {
|
|
14736
|
+
await existingMutex;
|
|
14737
|
+
const postRecoveryPlan = await loadPlanJsonOnly(directory);
|
|
14738
|
+
if (postRecoveryPlan)
|
|
14739
|
+
return postRecoveryPlan;
|
|
14740
|
+
}
|
|
14741
|
+
let resolveRecovery;
|
|
14742
|
+
const mutex = new Promise((r) => {
|
|
14743
|
+
resolveRecovery = r;
|
|
14744
|
+
});
|
|
14745
|
+
recoveryMutexes.set(resolvedDir, mutex);
|
|
14708
14746
|
try {
|
|
14709
|
-
const
|
|
14710
|
-
if (
|
|
14711
|
-
|
|
14712
|
-
return
|
|
14713
|
-
}
|
|
14714
|
-
|
|
14715
|
-
|
|
14716
|
-
|
|
14717
|
-
|
|
14718
|
-
|
|
14719
|
-
await savePlan(directory, approved.plan);
|
|
14720
|
-
try {
|
|
14721
|
-
await takeSnapshotEvent(directory, approved.plan, {
|
|
14722
|
-
source: "recovery_from_approved_snapshot",
|
|
14723
|
-
approvalMetadata: approved.approval
|
|
14724
|
-
});
|
|
14725
|
-
} catch (healError) {
|
|
14726
|
-
warn(`[loadPlan] Recovery-heal snapshot append failed: ${healError instanceof Error ? healError.message : String(healError)}. Next loadPlan may re-enter recovery path.`);
|
|
14747
|
+
const rebuilt = await replayFromLedger(directory);
|
|
14748
|
+
if (rebuilt) {
|
|
14749
|
+
await savePlan(directory, rebuilt);
|
|
14750
|
+
return rebuilt;
|
|
14751
|
+
}
|
|
14752
|
+
try {
|
|
14753
|
+
const anchorEvents = await readLedgerEvents(directory);
|
|
14754
|
+
if (anchorEvents.length === 0) {
|
|
14755
|
+
warn("[loadPlan] Ledger present but no events readable \u2014 refusing approved-snapshot recovery (cannot verify plan identity).");
|
|
14756
|
+
return null;
|
|
14727
14757
|
}
|
|
14728
|
-
|
|
14758
|
+
const expectedPlanId = anchorEvents[0].plan_id;
|
|
14759
|
+
const approved = await loadLastApprovedPlan(directory, expectedPlanId);
|
|
14760
|
+
if (approved) {
|
|
14761
|
+
const approvedPhase = approved.approval && typeof approved.approval === "object" && "phase" in approved.approval ? approved.approval.phase : undefined;
|
|
14762
|
+
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.`);
|
|
14763
|
+
await savePlan(directory, approved.plan);
|
|
14764
|
+
try {
|
|
14765
|
+
await takeSnapshotEvent(directory, approved.plan, {
|
|
14766
|
+
source: "recovery_from_approved_snapshot",
|
|
14767
|
+
approvalMetadata: approved.approval
|
|
14768
|
+
});
|
|
14769
|
+
} catch (healError) {
|
|
14770
|
+
warn(`[loadPlan] Recovery-heal snapshot append failed: ${healError instanceof Error ? healError.message : String(healError)}. Next loadPlan may re-enter recovery path.`);
|
|
14771
|
+
}
|
|
14772
|
+
return approved.plan;
|
|
14773
|
+
}
|
|
14774
|
+
} catch (recoveryError) {
|
|
14775
|
+
warn(`[loadPlan] Approved-snapshot recovery failed: ${recoveryError instanceof Error ? recoveryError.message : String(recoveryError)}`);
|
|
14729
14776
|
}
|
|
14730
|
-
}
|
|
14731
|
-
|
|
14777
|
+
} finally {
|
|
14778
|
+
resolveRecovery();
|
|
14779
|
+
recoveryMutexes.delete(resolvedDir);
|
|
14732
14780
|
}
|
|
14733
14781
|
}
|
|
14734
14782
|
return null;
|
|
@@ -14777,7 +14825,7 @@ async function savePlan(directory, plan, options) {
|
|
|
14777
14825
|
const planId = `${validated.swarm}-${validated.title}`.replace(/[^a-zA-Z0-9-_]/g, "_");
|
|
14778
14826
|
const planHashForInit = computePlanHash(validated);
|
|
14779
14827
|
if (!await ledgerExists(directory)) {
|
|
14780
|
-
await initLedger(directory, planId, planHashForInit);
|
|
14828
|
+
await initLedger(directory, planId, planHashForInit, validated);
|
|
14781
14829
|
} else {
|
|
14782
14830
|
const existingEvents = await readLedgerEvents(directory);
|
|
14783
14831
|
if (existingEvents.length > 0 && existingEvents[0].plan_id !== planId) {
|
|
@@ -14796,7 +14844,7 @@ async function savePlan(directory, plan, options) {
|
|
|
14796
14844
|
let initSucceeded = false;
|
|
14797
14845
|
if (backupExists) {
|
|
14798
14846
|
try {
|
|
14799
|
-
await initLedger(directory, planId, planHashForInit);
|
|
14847
|
+
await initLedger(directory, planId, planHashForInit, validated);
|
|
14800
14848
|
initSucceeded = true;
|
|
14801
14849
|
} catch (initErr) {
|
|
14802
14850
|
const errorMessage = String(initErr);
|
|
@@ -14838,6 +14886,19 @@ async function savePlan(directory, plan, options) {
|
|
|
14838
14886
|
unlinkSync(oldLedgerBackupPath);
|
|
14839
14887
|
} catch {}
|
|
14840
14888
|
}
|
|
14889
|
+
const MAX_ARCHIVED_SIBLINGS = 5;
|
|
14890
|
+
try {
|
|
14891
|
+
const allFiles = readdirSync(swarmDir2);
|
|
14892
|
+
const archivedSiblings = allFiles.filter((f) => f.startsWith("plan-ledger.archived-") && f.endsWith(".jsonl")).sort();
|
|
14893
|
+
if (archivedSiblings.length > MAX_ARCHIVED_SIBLINGS) {
|
|
14894
|
+
const toRemove = archivedSiblings.slice(0, archivedSiblings.length - MAX_ARCHIVED_SIBLINGS);
|
|
14895
|
+
for (const file2 of toRemove) {
|
|
14896
|
+
try {
|
|
14897
|
+
unlinkSync(path3.join(swarmDir2, file2));
|
|
14898
|
+
} catch {}
|
|
14899
|
+
}
|
|
14900
|
+
}
|
|
14901
|
+
} catch {}
|
|
14841
14902
|
}
|
|
14842
14903
|
}
|
|
14843
14904
|
const currentHash = computeCurrentPlanHash(directory);
|
|
@@ -14887,7 +14948,7 @@ async function savePlan(directory, plan, options) {
|
|
|
14887
14948
|
}
|
|
14888
14949
|
} catch (error49) {
|
|
14889
14950
|
if (error49 instanceof LedgerStaleWriterError) {
|
|
14890
|
-
throw new
|
|
14951
|
+
throw new PlanConcurrentModificationError(`Concurrent plan modification detected after retries: ${error49.message}. Please retry the operation.`);
|
|
14891
14952
|
}
|
|
14892
14953
|
throw error49;
|
|
14893
14954
|
}
|
|
@@ -14897,7 +14958,11 @@ async function savePlan(directory, plan, options) {
|
|
|
14897
14958
|
if (latestSeq > 0 && latestSeq % SNAPSHOT_INTERVAL === 0) {
|
|
14898
14959
|
await takeSnapshotEvent(directory, validated, {
|
|
14899
14960
|
planHashAfter: hashAfter
|
|
14900
|
-
}).catch(() => {
|
|
14961
|
+
}).catch((err) => {
|
|
14962
|
+
if (process.env.DEBUG_SWARM) {
|
|
14963
|
+
warn(`[savePlan] Periodic snapshot write failed (non-fatal): ${err instanceof Error ? err.message : String(err)}`);
|
|
14964
|
+
}
|
|
14965
|
+
});
|
|
14901
14966
|
}
|
|
14902
14967
|
const swarmDir = path3.resolve(directory, ".swarm");
|
|
14903
14968
|
const planPath = path3.join(swarmDir, "plan.json");
|
|
@@ -14910,19 +14975,23 @@ async function savePlan(directory, plan, options) {
|
|
|
14910
14975
|
unlinkSync(tempPath);
|
|
14911
14976
|
} catch {}
|
|
14912
14977
|
}
|
|
14913
|
-
const contentHash = computePlanContentHash(validated);
|
|
14914
|
-
const markdown = derivePlanMarkdown(validated);
|
|
14915
|
-
const markdownWithHash = `<!-- PLAN_HASH: ${contentHash} -->
|
|
14916
|
-
${markdown}`;
|
|
14917
|
-
const mdPath = path3.join(swarmDir, "plan.md");
|
|
14918
|
-
const mdTempPath = path3.join(swarmDir, `plan.md.tmp.${Date.now()}.${Math.floor(Math.random() * 1e9)}`);
|
|
14919
14978
|
try {
|
|
14920
|
-
|
|
14921
|
-
|
|
14922
|
-
|
|
14979
|
+
const contentHash = computePlanContentHash(validated);
|
|
14980
|
+
const markdown = derivePlanMarkdown(validated);
|
|
14981
|
+
const markdownWithHash = `<!-- PLAN_HASH: ${contentHash} -->
|
|
14982
|
+
${markdown}`;
|
|
14983
|
+
const mdPath = path3.join(swarmDir, "plan.md");
|
|
14984
|
+
const mdTempPath = path3.join(swarmDir, `plan.md.tmp.${Date.now()}.${Math.floor(Math.random() * 1e9)}`);
|
|
14923
14985
|
try {
|
|
14924
|
-
|
|
14925
|
-
|
|
14986
|
+
await Bun.write(mdTempPath, markdownWithHash);
|
|
14987
|
+
renameSync2(mdTempPath, mdPath);
|
|
14988
|
+
} finally {
|
|
14989
|
+
try {
|
|
14990
|
+
unlinkSync(mdTempPath);
|
|
14991
|
+
} catch {}
|
|
14992
|
+
}
|
|
14993
|
+
} catch (mdError) {
|
|
14994
|
+
warn(`[savePlan] plan.md write failed (non-fatal, plan.json is authoritative): ${mdError instanceof Error ? mdError.message : String(mdError)}`);
|
|
14926
14995
|
}
|
|
14927
14996
|
try {
|
|
14928
14997
|
const markerPath = path3.join(swarmDir, ".plan-write-marker");
|
|
@@ -15232,14 +15301,21 @@ function migrateLegacyPlan(planContent, swarmId) {
|
|
|
15232
15301
|
};
|
|
15233
15302
|
return plan;
|
|
15234
15303
|
}
|
|
15235
|
-
var startupLedgerCheckedWorkspaces;
|
|
15304
|
+
var PlanConcurrentModificationError, startupLedgerCheckedWorkspaces, recoveryMutexes;
|
|
15236
15305
|
var init_manager = __esm(() => {
|
|
15237
15306
|
init_plan_schema();
|
|
15238
15307
|
init_utils2();
|
|
15239
15308
|
init_utils();
|
|
15240
15309
|
init_spec_hash();
|
|
15241
15310
|
init_ledger();
|
|
15311
|
+
PlanConcurrentModificationError = class PlanConcurrentModificationError extends Error {
|
|
15312
|
+
constructor(message) {
|
|
15313
|
+
super(message);
|
|
15314
|
+
this.name = "PlanConcurrentModificationError";
|
|
15315
|
+
}
|
|
15316
|
+
};
|
|
15242
15317
|
startupLedgerCheckedWorkspaces = new Set;
|
|
15318
|
+
recoveryMutexes = new Map;
|
|
15243
15319
|
});
|
|
15244
15320
|
|
|
15245
15321
|
// src/config/evidence-schema.ts
|
|
@@ -15492,44 +15568,51 @@ var init_evidence_schema = __esm(() => {
|
|
|
15492
15568
|
});
|
|
15493
15569
|
});
|
|
15494
15570
|
|
|
15495
|
-
// src/
|
|
15496
|
-
|
|
15497
|
-
import * as fs3 from "fs/promises";
|
|
15498
|
-
import * as path5 from "path";
|
|
15499
|
-
function isValidEvidenceType(type) {
|
|
15500
|
-
return VALID_EVIDENCE_TYPES.includes(type);
|
|
15501
|
-
}
|
|
15502
|
-
function sanitizeTaskId(taskId) {
|
|
15571
|
+
// src/validation/task-id.ts
|
|
15572
|
+
function checkUnsafeChars(taskId) {
|
|
15503
15573
|
if (!taskId || taskId.length === 0) {
|
|
15504
|
-
|
|
15574
|
+
return "Invalid task ID: empty string";
|
|
15505
15575
|
}
|
|
15506
15576
|
if (/\0/.test(taskId)) {
|
|
15507
|
-
|
|
15577
|
+
return "Invalid task ID: contains null bytes";
|
|
15508
15578
|
}
|
|
15509
15579
|
for (let i = 0;i < taskId.length; i++) {
|
|
15510
15580
|
if (taskId.charCodeAt(i) < 32) {
|
|
15511
|
-
|
|
15581
|
+
return "Invalid task ID: contains control characters";
|
|
15512
15582
|
}
|
|
15513
15583
|
}
|
|
15514
|
-
if (taskId.includes("..") || taskId.includes("
|
|
15515
|
-
|
|
15516
|
-
}
|
|
15517
|
-
if (TASK_ID_REGEX.test(taskId)) {
|
|
15518
|
-
return taskId;
|
|
15519
|
-
}
|
|
15520
|
-
if (RETRO_TASK_ID_REGEX.test(taskId)) {
|
|
15521
|
-
return taskId;
|
|
15584
|
+
if (taskId.includes("..") || taskId.includes("/") || taskId.includes("\\")) {
|
|
15585
|
+
return "Invalid task ID: path traversal detected";
|
|
15522
15586
|
}
|
|
15523
|
-
|
|
15524
|
-
|
|
15587
|
+
return;
|
|
15588
|
+
}
|
|
15589
|
+
function sanitizeTaskId(taskId) {
|
|
15590
|
+
const unsafeMsg = checkUnsafeChars(taskId);
|
|
15591
|
+
if (unsafeMsg) {
|
|
15592
|
+
throw new Error(unsafeMsg);
|
|
15525
15593
|
}
|
|
15526
|
-
if (GENERAL_TASK_ID_REGEX.test(taskId)) {
|
|
15594
|
+
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)) {
|
|
15527
15595
|
return taskId;
|
|
15528
15596
|
}
|
|
15529
15597
|
throw new Error(`Invalid task ID: must be alphanumeric (ASCII) with optional hyphens, underscores, or dots, got "${taskId}"`);
|
|
15530
15598
|
}
|
|
15599
|
+
var STRICT_TASK_ID_PATTERN, RETRO_TASK_ID_REGEX, INTERNAL_TOOL_ID_REGEX, GENERAL_TASK_ID_REGEX;
|
|
15600
|
+
var init_task_id = __esm(() => {
|
|
15601
|
+
STRICT_TASK_ID_PATTERN = /^\d+\.\d+(\.\d+)*$/;
|
|
15602
|
+
RETRO_TASK_ID_REGEX = /^retro-\d+$/;
|
|
15603
|
+
INTERNAL_TOOL_ID_REGEX = /^(?:sast_scan|quality_budget|syntax_check|placeholder_scan|sbom_generate|build|secretscan)$/;
|
|
15604
|
+
GENERAL_TASK_ID_REGEX = /^[a-zA-Z0-9][a-zA-Z0-9._-]*$/;
|
|
15605
|
+
});
|
|
15606
|
+
|
|
15607
|
+
// src/evidence/manager.ts
|
|
15608
|
+
import { mkdirSync as mkdirSync2, readdirSync as readdirSync2, rmSync, statSync as statSync2 } from "fs";
|
|
15609
|
+
import * as fs3 from "fs/promises";
|
|
15610
|
+
import * as path5 from "path";
|
|
15611
|
+
function isValidEvidenceType(type) {
|
|
15612
|
+
return VALID_EVIDENCE_TYPES.includes(type);
|
|
15613
|
+
}
|
|
15531
15614
|
async function saveEvidence(directory, taskId, evidence) {
|
|
15532
|
-
const sanitizedTaskId =
|
|
15615
|
+
const sanitizedTaskId = sanitizeTaskId2(taskId);
|
|
15533
15616
|
const relativePath = path5.join("evidence", sanitizedTaskId, "evidence.json");
|
|
15534
15617
|
const evidencePath = validateSwarmPath(directory, relativePath);
|
|
15535
15618
|
const evidenceDir = path5.dirname(evidencePath);
|
|
@@ -15560,9 +15643,14 @@ async function saveEvidence(directory, taskId, evidence) {
|
|
|
15560
15643
|
updated_at: now
|
|
15561
15644
|
};
|
|
15562
15645
|
}
|
|
15646
|
+
const MAX_BUNDLE_ENTRIES = 100;
|
|
15647
|
+
let entries = [...bundle.entries, evidence];
|
|
15648
|
+
if (entries.length > MAX_BUNDLE_ENTRIES) {
|
|
15649
|
+
entries = entries.slice(entries.length - MAX_BUNDLE_ENTRIES);
|
|
15650
|
+
}
|
|
15563
15651
|
const updatedBundle = {
|
|
15564
15652
|
...bundle,
|
|
15565
|
-
entries
|
|
15653
|
+
entries,
|
|
15566
15654
|
updated_at: new Date().toISOString()
|
|
15567
15655
|
};
|
|
15568
15656
|
const bundleJson = JSON.stringify(updatedBundle);
|
|
@@ -15607,7 +15695,7 @@ function wrapFlatRetrospective(flatEntry, taskId) {
|
|
|
15607
15695
|
};
|
|
15608
15696
|
}
|
|
15609
15697
|
async function loadEvidence(directory, taskId) {
|
|
15610
|
-
const sanitizedTaskId =
|
|
15698
|
+
const sanitizedTaskId = sanitizeTaskId2(taskId);
|
|
15611
15699
|
const relativePath = path5.join("evidence", sanitizedTaskId, "evidence.json");
|
|
15612
15700
|
const evidencePath = validateSwarmPath(directory, relativePath);
|
|
15613
15701
|
const content = await readSwarmFileAsync(directory, relativePath);
|
|
@@ -15661,7 +15749,7 @@ async function listEvidenceTaskIds(directory) {
|
|
|
15661
15749
|
}
|
|
15662
15750
|
let entries;
|
|
15663
15751
|
try {
|
|
15664
|
-
entries =
|
|
15752
|
+
entries = readdirSync2(evidenceBasePath);
|
|
15665
15753
|
} catch {
|
|
15666
15754
|
return [];
|
|
15667
15755
|
}
|
|
@@ -15673,7 +15761,7 @@ async function listEvidenceTaskIds(directory) {
|
|
|
15673
15761
|
if (!stats.isDirectory()) {
|
|
15674
15762
|
continue;
|
|
15675
15763
|
}
|
|
15676
|
-
|
|
15764
|
+
sanitizeTaskId2(entry);
|
|
15677
15765
|
taskIds.push(entry);
|
|
15678
15766
|
} catch (error49) {
|
|
15679
15767
|
if (error49 instanceof Error && !error49.message.startsWith("Invalid task ID")) {
|
|
@@ -15684,7 +15772,7 @@ async function listEvidenceTaskIds(directory) {
|
|
|
15684
15772
|
return taskIds.sort();
|
|
15685
15773
|
}
|
|
15686
15774
|
async function deleteEvidence(directory, taskId) {
|
|
15687
|
-
const sanitizedTaskId =
|
|
15775
|
+
const sanitizedTaskId = sanitizeTaskId2(taskId);
|
|
15688
15776
|
const relativePath = path5.join("evidence", sanitizedTaskId);
|
|
15689
15777
|
const evidenceDir = validateSwarmPath(directory, relativePath);
|
|
15690
15778
|
try {
|
|
@@ -15746,12 +15834,13 @@ async function archiveEvidence(directory, maxAgeDays, maxBundles) {
|
|
|
15746
15834
|
}
|
|
15747
15835
|
return archived;
|
|
15748
15836
|
}
|
|
15749
|
-
var VALID_EVIDENCE_TYPES,
|
|
15837
|
+
var VALID_EVIDENCE_TYPES, sanitizeTaskId2, LEGACY_TASK_COMPLEXITY_MAP;
|
|
15750
15838
|
var init_manager2 = __esm(() => {
|
|
15751
15839
|
init_zod();
|
|
15752
15840
|
init_evidence_schema();
|
|
15753
15841
|
init_utils2();
|
|
15754
15842
|
init_utils();
|
|
15843
|
+
init_task_id();
|
|
15755
15844
|
VALID_EVIDENCE_TYPES = [
|
|
15756
15845
|
"review",
|
|
15757
15846
|
"test",
|
|
@@ -15767,10 +15856,7 @@ var init_manager2 = __esm(() => {
|
|
|
15767
15856
|
"quality_budget",
|
|
15768
15857
|
"secretscan"
|
|
15769
15858
|
];
|
|
15770
|
-
|
|
15771
|
-
RETRO_TASK_ID_REGEX = /^retro-\d+$/;
|
|
15772
|
-
INTERNAL_TOOL_ID_REGEX = /^(?:sast_scan|quality_budget|syntax_check|placeholder_scan|sbom_generate|build|secretscan)$/;
|
|
15773
|
-
GENERAL_TASK_ID_REGEX = /^[a-zA-Z0-9][a-zA-Z0-9._-]*$/;
|
|
15859
|
+
sanitizeTaskId2 = sanitizeTaskId;
|
|
15774
15860
|
LEGACY_TASK_COMPLEXITY_MAP = {
|
|
15775
15861
|
low: "simple",
|
|
15776
15862
|
medium: "moderate",
|
|
@@ -33569,11 +33655,14 @@ async function executeWriteRetro(args, directory) {
|
|
|
33569
33655
|
try {
|
|
33570
33656
|
const allTaskIds = await listEvidenceTaskIds(directory);
|
|
33571
33657
|
const phaseTaskIds = allTaskIds.filter((id) => id.startsWith(`${phase}.`));
|
|
33658
|
+
const sessionStart = args.metadata && typeof args.metadata.session_start === "string" ? args.metadata.session_start : undefined;
|
|
33572
33659
|
for (const phaseTaskId of phaseTaskIds) {
|
|
33573
33660
|
const result = await loadEvidence(directory, phaseTaskId);
|
|
33574
33661
|
if (result.status !== "found")
|
|
33575
33662
|
continue;
|
|
33576
33663
|
const bundle = result.bundle;
|
|
33664
|
+
if (sessionStart && bundle.updated_at < sessionStart)
|
|
33665
|
+
continue;
|
|
33577
33666
|
for (const entry of bundle.entries) {
|
|
33578
33667
|
const e = entry;
|
|
33579
33668
|
if (e.type === "review" && e.verdict === "fail") {
|
|
@@ -33765,6 +33854,18 @@ async function handleCloseCommand(directory, args) {
|
|
|
33765
33854
|
}
|
|
33766
33855
|
}
|
|
33767
33856
|
}
|
|
33857
|
+
let sessionStart;
|
|
33858
|
+
{
|
|
33859
|
+
let earliest = Infinity;
|
|
33860
|
+
for (const [, session] of swarmState.agentSessions) {
|
|
33861
|
+
if (session.lastAgentEventTime > 0 && session.lastAgentEventTime < earliest) {
|
|
33862
|
+
earliest = session.lastAgentEventTime;
|
|
33863
|
+
}
|
|
33864
|
+
}
|
|
33865
|
+
if (earliest < Infinity) {
|
|
33866
|
+
sessionStart = new Date(earliest).toISOString();
|
|
33867
|
+
}
|
|
33868
|
+
}
|
|
33768
33869
|
const wrotePhaseRetro = closedPhases.length > 0;
|
|
33769
33870
|
if (!wrotePhaseRetro && !planExists) {
|
|
33770
33871
|
try {
|
|
@@ -33780,7 +33881,10 @@ async function handleCloseCommand(directory, args) {
|
|
|
33780
33881
|
test_failures: 0,
|
|
33781
33882
|
security_findings: 0,
|
|
33782
33883
|
integration_issues: 0,
|
|
33783
|
-
metadata: {
|
|
33884
|
+
metadata: {
|
|
33885
|
+
session_scope: "plan_free",
|
|
33886
|
+
...sessionStart ? { session_start: sessionStart } : {}
|
|
33887
|
+
}
|
|
33784
33888
|
}, directory);
|
|
33785
33889
|
try {
|
|
33786
33890
|
const parsed = JSON.parse(sessionRetroResult);
|
|
@@ -33837,7 +33941,8 @@ async function handleCloseCommand(directory, args) {
|
|
|
33837
33941
|
}
|
|
33838
33942
|
}
|
|
33839
33943
|
const timestamp = new Date().toISOString().replace(/[:.]/g, "-");
|
|
33840
|
-
const
|
|
33944
|
+
const suffix = Math.random().toString(36).slice(2, 8);
|
|
33945
|
+
const archiveDir = path11.join(swarmDir, "archive", `swarm-${timestamp}-${suffix}`);
|
|
33841
33946
|
let archiveResult = "";
|
|
33842
33947
|
let archivedFileCount = 0;
|
|
33843
33948
|
const archivedActiveStateFiles = new Set;
|
|
@@ -34101,11 +34206,23 @@ async function handleCloseCommand(directory, args) {
|
|
|
34101
34206
|
if (pruneErrors.length > 0) {
|
|
34102
34207
|
warnings.push(`Could not prune ${pruneErrors.length} branch(es) (unmerged or checked out): ${pruneErrors.join(", ")}`);
|
|
34103
34208
|
}
|
|
34104
|
-
const
|
|
34209
|
+
const retroWarnings = warnings.filter((w) => w.includes("Retrospective write") || w.includes("retrospective write") || w.includes("Session retrospective"));
|
|
34210
|
+
const otherWarnings = warnings.filter((w) => !w.includes("Retrospective write") && !w.includes("retrospective write") && !w.includes("Session retrospective"));
|
|
34211
|
+
let warningMsg = "";
|
|
34212
|
+
if (retroWarnings.length > 0) {
|
|
34213
|
+
warningMsg += `
|
|
34214
|
+
|
|
34215
|
+
**\u26A0 Retrospective evidence incomplete:**
|
|
34216
|
+
${retroWarnings.map((w) => `- ${w}`).join(`
|
|
34217
|
+
`)}`;
|
|
34218
|
+
}
|
|
34219
|
+
if (otherWarnings.length > 0) {
|
|
34220
|
+
warningMsg += `
|
|
34105
34221
|
|
|
34106
34222
|
**Warnings:**
|
|
34107
|
-
${
|
|
34108
|
-
`)}
|
|
34223
|
+
${otherWarnings.map((w) => `- ${w}`).join(`
|
|
34224
|
+
`)}`;
|
|
34225
|
+
}
|
|
34109
34226
|
if (planAlreadyDone) {
|
|
34110
34227
|
return `\u2705 Session finalized. Plan was already in a terminal state \u2014 cleanup and archive applied.
|
|
34111
34228
|
|
|
@@ -34777,7 +34894,7 @@ async function handleDarkMatterCommand(directory, args) {
|
|
|
34777
34894
|
|
|
34778
34895
|
// src/services/diagnose-service.ts
|
|
34779
34896
|
import * as child_process4 from "child_process";
|
|
34780
|
-
import { existsSync as existsSync6, readdirSync as
|
|
34897
|
+
import { existsSync as existsSync6, readdirSync as readdirSync3, readFileSync as readFileSync5, statSync as statSync3 } from "fs";
|
|
34781
34898
|
import path15 from "path";
|
|
34782
34899
|
import { fileURLToPath } from "url";
|
|
34783
34900
|
init_manager2();
|
|
@@ -34983,7 +35100,7 @@ async function checkPlanSync(directory, plan) {
|
|
|
34983
35100
|
}
|
|
34984
35101
|
async function checkConfigBackups(directory) {
|
|
34985
35102
|
try {
|
|
34986
|
-
const files =
|
|
35103
|
+
const files = readdirSync3(directory);
|
|
34987
35104
|
const backupCount = files.filter((f) => /\.opencode-swarm\.yaml\.bak/.test(f)).length;
|
|
34988
35105
|
if (backupCount <= 5) {
|
|
34989
35106
|
return {
|
|
@@ -35449,7 +35566,7 @@ async function getDiagnoseData(directory) {
|
|
|
35449
35566
|
checks5.push(await checkCurator(directory));
|
|
35450
35567
|
try {
|
|
35451
35568
|
const evidenceDir = path15.join(directory, ".swarm", "evidence");
|
|
35452
|
-
const snapshotFiles = existsSync6(evidenceDir) ?
|
|
35569
|
+
const snapshotFiles = existsSync6(evidenceDir) ? readdirSync3(evidenceDir).filter((f) => f.startsWith("agent-tools-") && f.endsWith(".json")) : [];
|
|
35453
35570
|
if (snapshotFiles.length > 0) {
|
|
35454
35571
|
const latest = snapshotFiles.sort().pop();
|
|
35455
35572
|
checks5.push({
|
|
@@ -36,18 +36,8 @@ export declare function isQualityBudgetEvidence(evidence: Evidence): evidence is
|
|
|
36
36
|
* Type guard for secretscan evidence
|
|
37
37
|
*/
|
|
38
38
|
export declare function isSecretscanEvidence(evidence: Evidence): evidence is SecretscanEvidence;
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
* Accepts four formats:
|
|
42
|
-
* 1. Canonical N.M or N.M.P numeric format (matches TASK_ID_REGEX)
|
|
43
|
-
* 2. Retrospective format: retro-<number> (matches RETRO_TASK_ID_REGEX)
|
|
44
|
-
* 3. Internal automated-tool format: specific tool IDs (sast_scan, quality_budget, etc.)
|
|
45
|
-
* 4. General safe alphanumeric IDs: ASCII letter/digit start, body of letters/digits/dots/hyphens/underscores
|
|
46
|
-
* Rejects: empty string, null bytes, control characters, path traversal (..), spaces, and any
|
|
47
|
-
* character outside the ASCII alphanumeric + [._-] set.
|
|
48
|
-
* @throws Error with descriptive message on failure
|
|
49
|
-
*/
|
|
50
|
-
export declare function sanitizeTaskId(taskId: string): string;
|
|
39
|
+
import { sanitizeTaskId as _sanitizeTaskId } from '../validation/task-id';
|
|
40
|
+
export declare const sanitizeTaskId: typeof _sanitizeTaskId;
|
|
51
41
|
/**
|
|
52
42
|
* Save evidence to a task's evidence bundle.
|
|
53
43
|
* Creates new bundle if doesn't exist, appends to existing.
|
package/dist/gate-evidence.d.ts
CHANGED
|
@@ -24,15 +24,8 @@ export interface TaskEvidence {
|
|
|
24
24
|
export declare const DEFAULT_REQUIRED_GATES: string[];
|
|
25
25
|
/**
|
|
26
26
|
* Canonical task-id validation helper.
|
|
27
|
-
*
|
|
28
|
-
*
|
|
29
|
-
*
|
|
30
|
-
* Validates:
|
|
31
|
-
* - Non-empty string
|
|
32
|
-
* - Matches N.M or N.M.P numeric pattern (e.g., "1.1", "1.2.3")
|
|
33
|
-
* - No path traversal (..)
|
|
34
|
-
* - No path separators (/, \)
|
|
35
|
-
* - No null bytes
|
|
27
|
+
* Delegates to the shared strict validator (#452 item 2).
|
|
28
|
+
* Re-exported for backward compatibility with existing callers.
|
|
36
29
|
*/
|
|
37
30
|
export declare function isValidTaskId(taskId: string): boolean;
|
|
38
31
|
/**
|