opencode-swarm 7.19.0 → 7.19.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/dist/cli/index.js +213 -32
- package/dist/config/plan-schema.d.ts +8 -0
- package/dist/hooks/guardrails.d.ts +21 -0
- package/dist/hooks/system-enhancer.d.ts +19 -0
- package/dist/index.js +960 -595
- package/dist/plan/ledger.d.ts +8 -3
- package/dist/plan/manager.d.ts +52 -0
- package/dist/services/status-service.d.ts +8 -0
- package/dist/tools/save-plan.d.ts +19 -0
- package/dist/types/events.d.ts +21 -1
- package/package.json +1 -1
package/dist/cli/index.js
CHANGED
|
@@ -34,7 +34,7 @@ var package_default;
|
|
|
34
34
|
var init_package = __esm(() => {
|
|
35
35
|
package_default = {
|
|
36
36
|
name: "opencode-swarm",
|
|
37
|
-
version: "7.19.
|
|
37
|
+
version: "7.19.1",
|
|
38
38
|
description: "Architect-centric agentic swarm plugin for OpenCode - hub-and-spoke orchestration with SME consultation, code generation, and QA review",
|
|
39
39
|
main: "dist/index.js",
|
|
40
40
|
types: "dist/index.d.ts",
|
|
@@ -14893,6 +14893,17 @@ function applyEventToPlan(plan, event) {
|
|
|
14893
14893
|
return plan;
|
|
14894
14894
|
case "task_added":
|
|
14895
14895
|
return plan;
|
|
14896
|
+
case "task_removed":
|
|
14897
|
+
if (event.task_id) {
|
|
14898
|
+
for (const phase of plan.phases) {
|
|
14899
|
+
const idx = phase.tasks.findIndex((t) => t.id === event.task_id);
|
|
14900
|
+
if (idx !== -1) {
|
|
14901
|
+
phase.tasks.splice(idx, 1);
|
|
14902
|
+
break;
|
|
14903
|
+
}
|
|
14904
|
+
}
|
|
14905
|
+
}
|
|
14906
|
+
return plan;
|
|
14896
14907
|
case "task_updated":
|
|
14897
14908
|
return plan;
|
|
14898
14909
|
case "plan_rebuilt":
|
|
@@ -15000,7 +15011,7 @@ async function loadLastApprovedPlan(directory, expectedPlanId) {
|
|
|
15000
15011
|
}
|
|
15001
15012
|
return null;
|
|
15002
15013
|
}
|
|
15003
|
-
var LEDGER_SCHEMA_VERSION = "1.
|
|
15014
|
+
var LEDGER_SCHEMA_VERSION = "1.1.0", LEDGER_FILENAME = "plan-ledger.jsonl", PLAN_JSON_FILENAME = "plan.json", LedgerStaleWriterError;
|
|
15004
15015
|
var init_ledger = __esm(() => {
|
|
15005
15016
|
init_plan_schema();
|
|
15006
15017
|
LedgerStaleWriterError = class LedgerStaleWriterError extends Error {
|
|
@@ -15290,7 +15301,13 @@ async function loadPlan(directory) {
|
|
|
15290
15301
|
const planMdContent2 = await readSwarmFileAsync(directory, "plan.md");
|
|
15291
15302
|
if (planMdContent2 !== null) {
|
|
15292
15303
|
const migrated = migrateLegacyPlan(planMdContent2);
|
|
15293
|
-
await
|
|
15304
|
+
const { removedCount } = await savePlanWithAutoAcknowledgedRemovals(directory, migrated, "load_plan_migration_from_md", "migrate legacy plan.md to plan.json");
|
|
15305
|
+
if (removedCount > 0) {
|
|
15306
|
+
migrated._midLoadRemovals = {
|
|
15307
|
+
count: removedCount,
|
|
15308
|
+
source: "load_plan_migration_from_md"
|
|
15309
|
+
};
|
|
15310
|
+
}
|
|
15294
15311
|
return migrated;
|
|
15295
15312
|
}
|
|
15296
15313
|
}
|
|
@@ -15319,7 +15336,13 @@ async function loadPlan(directory) {
|
|
|
15319
15336
|
try {
|
|
15320
15337
|
const rebuilt = await replayFromLedger(directory);
|
|
15321
15338
|
if (rebuilt) {
|
|
15322
|
-
await
|
|
15339
|
+
const { removedCount } = await savePlanWithAutoAcknowledgedRemovals(directory, rebuilt, "load_plan_rebuild_from_ledger", "rebuild plan from ledger replay");
|
|
15340
|
+
if (removedCount > 0) {
|
|
15341
|
+
rebuilt._midLoadRemovals = {
|
|
15342
|
+
count: removedCount,
|
|
15343
|
+
source: "load_plan_rebuild_from_ledger"
|
|
15344
|
+
};
|
|
15345
|
+
}
|
|
15323
15346
|
return rebuilt;
|
|
15324
15347
|
}
|
|
15325
15348
|
try {
|
|
@@ -15333,7 +15356,13 @@ async function loadPlan(directory) {
|
|
|
15333
15356
|
if (approved) {
|
|
15334
15357
|
const approvedPhase = approved.approval && typeof approved.approval === "object" && "phase" in approved.approval ? approved.approval.phase : undefined;
|
|
15335
15358
|
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.`);
|
|
15336
|
-
await
|
|
15359
|
+
const { removedCount: snapshotRemovedCount } = await savePlanWithAutoAcknowledgedRemovals(directory, approved.plan, "load_plan_recovery_from_approved_snapshot", "restore from critic-approved snapshot");
|
|
15360
|
+
if (snapshotRemovedCount > 0) {
|
|
15361
|
+
approved.plan._midLoadRemovals = {
|
|
15362
|
+
count: snapshotRemovedCount,
|
|
15363
|
+
source: "load_plan_recovery_from_approved_snapshot"
|
|
15364
|
+
};
|
|
15365
|
+
}
|
|
15337
15366
|
try {
|
|
15338
15367
|
await takeSnapshotEvent(directory, approved.plan, {
|
|
15339
15368
|
source: "recovery_from_approved_snapshot",
|
|
@@ -15354,6 +15383,28 @@ async function loadPlan(directory) {
|
|
|
15354
15383
|
}
|
|
15355
15384
|
return null;
|
|
15356
15385
|
}
|
|
15386
|
+
async function savePlanWithAutoAcknowledgedRemovals(directory, plan, source, reason, options) {
|
|
15387
|
+
const existing = await _internals3.loadPlanJsonOnly(directory);
|
|
15388
|
+
const newIds = new Set;
|
|
15389
|
+
for (const phase of plan.phases) {
|
|
15390
|
+
for (const task of phase.tasks)
|
|
15391
|
+
newIds.add(task.id);
|
|
15392
|
+
}
|
|
15393
|
+
const removedIds = [];
|
|
15394
|
+
if (existing) {
|
|
15395
|
+
for (const phase of existing.phases) {
|
|
15396
|
+
for (const task of phase.tasks) {
|
|
15397
|
+
if (!newIds.has(task.id))
|
|
15398
|
+
removedIds.push(task.id);
|
|
15399
|
+
}
|
|
15400
|
+
}
|
|
15401
|
+
}
|
|
15402
|
+
await savePlan(directory, plan, {
|
|
15403
|
+
...options ?? {},
|
|
15404
|
+
acknowledged_removals: { ids: removedIds, reason, source }
|
|
15405
|
+
});
|
|
15406
|
+
return { removedCount: removedIds.length };
|
|
15407
|
+
}
|
|
15357
15408
|
async function savePlan(directory, plan, options) {
|
|
15358
15409
|
if (directory === null || directory === undefined || typeof directory !== "string" || directory.trim().length === 0) {
|
|
15359
15410
|
throw new Error(`Invalid directory: directory must be a non-empty string`);
|
|
@@ -15490,6 +15541,73 @@ async function savePlan(directory, plan, options) {
|
|
|
15490
15541
|
oldTaskMap.set(task.id, { phase: task.phase, status: task.status });
|
|
15491
15542
|
}
|
|
15492
15543
|
}
|
|
15544
|
+
const newTaskIds = new Set;
|
|
15545
|
+
for (const phase of validated.phases) {
|
|
15546
|
+
for (const task of phase.tasks)
|
|
15547
|
+
newTaskIds.add(task.id);
|
|
15548
|
+
}
|
|
15549
|
+
const missingTasks = [];
|
|
15550
|
+
for (const [id, info] of oldTaskMap.entries()) {
|
|
15551
|
+
if (!newTaskIds.has(id)) {
|
|
15552
|
+
missingTasks.push({ id, phase: info.phase, status: info.status });
|
|
15553
|
+
}
|
|
15554
|
+
}
|
|
15555
|
+
const ack = options?.acknowledged_removals;
|
|
15556
|
+
if (missingTasks.length > 0) {
|
|
15557
|
+
if (!ack) {
|
|
15558
|
+
throw new PlanTaskRemovalNotAcknowledgedError(missingTasks);
|
|
15559
|
+
}
|
|
15560
|
+
if (typeof ack.reason !== "string" || ack.reason.trim().length === 0) {
|
|
15561
|
+
throw new Error("PLAN_ACKNOWLEDGED_REMOVAL_INVALID: acknowledged_removals.reason must be a non-empty string.");
|
|
15562
|
+
}
|
|
15563
|
+
if (typeof ack.source !== "string" || ack.source.trim().length === 0) {
|
|
15564
|
+
throw new Error("PLAN_ACKNOWLEDGED_REMOVAL_INVALID: acknowledged_removals.source must be a non-empty string.");
|
|
15565
|
+
}
|
|
15566
|
+
const ackSet = new Set(ack.ids);
|
|
15567
|
+
const missingIdsSet = new Set(missingTasks.map((t) => t.id));
|
|
15568
|
+
const unacked = missingTasks.filter((t) => !ackSet.has(t.id));
|
|
15569
|
+
if (unacked.length > 0) {
|
|
15570
|
+
throw new PlanTaskRemovalNotAcknowledgedError(unacked);
|
|
15571
|
+
}
|
|
15572
|
+
for (const id of ack.ids) {
|
|
15573
|
+
if (!missingIdsSet.has(id)) {
|
|
15574
|
+
throw new Error(`PLAN_ACKNOWLEDGED_REMOVAL_INVALID: acknowledged_removals contains "${id}" but that task is not missing from the plan.`);
|
|
15575
|
+
}
|
|
15576
|
+
}
|
|
15577
|
+
try {
|
|
15578
|
+
for (const missing of missingTasks) {
|
|
15579
|
+
const eventInput = {
|
|
15580
|
+
plan_id: derivePlanId(validated),
|
|
15581
|
+
event_type: "task_removed",
|
|
15582
|
+
task_id: missing.id,
|
|
15583
|
+
phase_id: missing.phase,
|
|
15584
|
+
from_status: missing.status,
|
|
15585
|
+
source: ack.source,
|
|
15586
|
+
payload: { reason: ack.reason, source: ack.source }
|
|
15587
|
+
};
|
|
15588
|
+
const capturedTaskId = missing.id;
|
|
15589
|
+
await retryCasWithBackoff(directory, eventInput, {
|
|
15590
|
+
expectedHash: currentHash,
|
|
15591
|
+
planHashAfter: hashAfter,
|
|
15592
|
+
verifyValid: async () => {
|
|
15593
|
+
const onDisk = await _internals3.loadPlanJsonOnly(directory);
|
|
15594
|
+
if (!onDisk)
|
|
15595
|
+
return true;
|
|
15596
|
+
for (const p of onDisk.phases) {
|
|
15597
|
+
if (p.tasks.some((x) => x.id === capturedTaskId))
|
|
15598
|
+
return true;
|
|
15599
|
+
}
|
|
15600
|
+
return false;
|
|
15601
|
+
}
|
|
15602
|
+
});
|
|
15603
|
+
}
|
|
15604
|
+
} catch (error49) {
|
|
15605
|
+
if (error49 instanceof LedgerStaleWriterError) {
|
|
15606
|
+
throw new PlanConcurrentModificationError(`Concurrent plan modification detected after retries: ${error49.message}. Please retry the operation.`);
|
|
15607
|
+
}
|
|
15608
|
+
throw error49;
|
|
15609
|
+
}
|
|
15610
|
+
}
|
|
15493
15611
|
try {
|
|
15494
15612
|
for (const phase of validated.phases) {
|
|
15495
15613
|
for (const task of phase.tasks) {
|
|
@@ -15890,7 +16008,7 @@ function migrateLegacyPlan(planContent, swarmId) {
|
|
|
15890
16008
|
};
|
|
15891
16009
|
return plan;
|
|
15892
16010
|
}
|
|
15893
|
-
var PlanConcurrentModificationError, startupLedgerCheckedWorkspaces, recoveryMutexes, _internals3, CAS_BACKOFF_START_MS = 5, CAS_BACKOFF_CAP_MS = 250, CAS_BACKOFF_JITTER = 0.25, CAS_MAX_RETRIES = 3;
|
|
16011
|
+
var PlanConcurrentModificationError, PlanTaskRemovalNotAcknowledgedError, startupLedgerCheckedWorkspaces, recoveryMutexes, _internals3, CAS_BACKOFF_START_MS = 5, CAS_BACKOFF_CAP_MS = 250, CAS_BACKOFF_JITTER = 0.25, CAS_MAX_RETRIES = 3;
|
|
15894
16012
|
var init_manager = __esm(() => {
|
|
15895
16013
|
init_plan_schema();
|
|
15896
16014
|
init_utils2();
|
|
@@ -15905,6 +16023,15 @@ var init_manager = __esm(() => {
|
|
|
15905
16023
|
this.name = "PlanConcurrentModificationError";
|
|
15906
16024
|
}
|
|
15907
16025
|
};
|
|
16026
|
+
PlanTaskRemovalNotAcknowledgedError = class PlanTaskRemovalNotAcknowledgedError extends Error {
|
|
16027
|
+
missingTasks;
|
|
16028
|
+
constructor(missingTasks) {
|
|
16029
|
+
const idList = missingTasks.map((t) => `${t.id}(${t.status})`).join(", ");
|
|
16030
|
+
super(`PLAN_TASK_REMOVAL_NOT_ACKNOWLEDGED: the following tasks were present in the prior plan but missing from the new save: ${idList}. Pass acknowledged_removals.ids covering all missing task IDs with a non-empty reason to proceed.`);
|
|
16031
|
+
this.name = "PlanTaskRemovalNotAcknowledgedError";
|
|
16032
|
+
this.missingTasks = missingTasks;
|
|
16033
|
+
}
|
|
16034
|
+
};
|
|
15908
16035
|
startupLedgerCheckedWorkspaces = new Set;
|
|
15909
16036
|
recoveryMutexes = new Map;
|
|
15910
16037
|
_internals3 = {
|
|
@@ -20821,7 +20948,7 @@ var init_model_limits = __esm(() => {
|
|
|
20821
20948
|
var init_normalize_tool_name = () => {};
|
|
20822
20949
|
|
|
20823
20950
|
// src/hooks/guardrails.ts
|
|
20824
|
-
var storedInputArgs, TRANSIENT_STATUS_CODES, toolCallsSinceLastWrite, noOpWarningIssued, consecutiveNoToolTurns, DC_SAFE_TARGETS, DC_FS_ROOTS, pathNormalizationCache, globMatcherCache;
|
|
20951
|
+
var SPEC_DRIFT_BLOCKED_TOOLS, storedInputArgs, TRANSIENT_STATUS_CODES, toolCallsSinceLastWrite, noOpWarningIssued, consecutiveNoToolTurns, DC_SAFE_TARGETS, DC_FS_ROOTS, pathNormalizationCache, globMatcherCache;
|
|
20825
20952
|
var init_guardrails = __esm(() => {
|
|
20826
20953
|
init_quick_lru();
|
|
20827
20954
|
init_agents2();
|
|
@@ -20840,6 +20967,13 @@ var init_guardrails = __esm(() => {
|
|
|
20840
20967
|
init_loop_detector();
|
|
20841
20968
|
init_model_limits();
|
|
20842
20969
|
init_normalize_tool_name();
|
|
20970
|
+
SPEC_DRIFT_BLOCKED_TOOLS = new Set([
|
|
20971
|
+
"save_plan",
|
|
20972
|
+
"update_task_status",
|
|
20973
|
+
"phase_complete",
|
|
20974
|
+
"lean_turbo_run_phase",
|
|
20975
|
+
"lean_turbo_acquire_locks"
|
|
20976
|
+
]);
|
|
20843
20977
|
storedInputArgs = new Map;
|
|
20844
20978
|
TRANSIENT_STATUS_CODES = new Set([408, 429, 500, 502, 503, 504, 529]);
|
|
20845
20979
|
toolCallsSinceLastWrite = new Map;
|
|
@@ -51158,6 +51292,25 @@ var init_context_budget_service = __esm(() => {
|
|
|
51158
51292
|
});
|
|
51159
51293
|
|
|
51160
51294
|
// src/services/status-service.ts
|
|
51295
|
+
import * as fsSync2 from "fs";
|
|
51296
|
+
import * as path46 from "path";
|
|
51297
|
+
function readSpecStalenessSnapshot(directory) {
|
|
51298
|
+
try {
|
|
51299
|
+
const p = path46.join(directory, ".swarm", "spec-staleness.json");
|
|
51300
|
+
if (!fsSync2.existsSync(p))
|
|
51301
|
+
return { stale: false };
|
|
51302
|
+
const raw = fsSync2.readFileSync(p, "utf-8");
|
|
51303
|
+
const parsed = JSON.parse(raw);
|
|
51304
|
+
return {
|
|
51305
|
+
stale: true,
|
|
51306
|
+
reason: typeof parsed?.reason === "string" ? parsed.reason : undefined,
|
|
51307
|
+
storedHash: typeof parsed?.specHash_plan === "string" ? parsed.specHash_plan : undefined,
|
|
51308
|
+
currentHash: typeof parsed?.specHash_current === "string" || parsed?.specHash_current === null ? parsed.specHash_current : undefined
|
|
51309
|
+
};
|
|
51310
|
+
} catch {
|
|
51311
|
+
return { stale: false };
|
|
51312
|
+
}
|
|
51313
|
+
}
|
|
51161
51314
|
async function getStatusData(directory, agents) {
|
|
51162
51315
|
const plan = await loadPlan(directory);
|
|
51163
51316
|
let status;
|
|
@@ -51223,6 +51376,16 @@ async function getStatusData(directory, agents) {
|
|
|
51223
51376
|
};
|
|
51224
51377
|
}
|
|
51225
51378
|
}
|
|
51379
|
+
const drift = readSpecStalenessSnapshot(directory);
|
|
51380
|
+
if (drift.stale) {
|
|
51381
|
+
status.specStale = true;
|
|
51382
|
+
status.specStaleReason = drift.reason;
|
|
51383
|
+
status.specStaleStoredHash = drift.storedHash;
|
|
51384
|
+
status.specStaleCurrentHash = drift.currentHash;
|
|
51385
|
+
} else if (plan && plan._specStale) {
|
|
51386
|
+
status.specStale = true;
|
|
51387
|
+
status.specStaleReason = plan._specStaleReason;
|
|
51388
|
+
}
|
|
51226
51389
|
return enrichWithLeanTurbo(status, directory);
|
|
51227
51390
|
}
|
|
51228
51391
|
function enrichWithLeanTurbo(status, directory) {
|
|
@@ -51289,6 +51452,12 @@ function formatStatusMarkdown(status) {
|
|
|
51289
51452
|
`**Tasks**: ${status.completedTasks}/${status.totalTasks} complete`,
|
|
51290
51453
|
`**Agents**: ${status.agentCount} registered`
|
|
51291
51454
|
];
|
|
51455
|
+
if (status.specStale) {
|
|
51456
|
+
const reason = status.specStaleReason ?? "spec.md changed since plan saved";
|
|
51457
|
+
const stored = status.specStaleStoredHash ?? "unknown";
|
|
51458
|
+
const current = status.specStaleCurrentHash ?? "(spec.md missing)";
|
|
51459
|
+
lines.push("", `**Spec drift detected**: ${reason} (stored: ${stored}, current: ${current})`, "Run `/swarm clarify` to update the spec or `/swarm acknowledge-spec-drift` to dismiss.");
|
|
51460
|
+
}
|
|
51292
51461
|
if (status.turboStrategy && status.turboStrategy !== "off") {
|
|
51293
51462
|
lines.push("");
|
|
51294
51463
|
if (status.turboStrategy === "lean") {
|
|
@@ -51338,6 +51507,18 @@ function formatStatusMarkdown(status) {
|
|
|
51338
51507
|
async function handleStatusCommand(directory, agents) {
|
|
51339
51508
|
const statusData = await getStatusData(directory, agents);
|
|
51340
51509
|
if (!statusData.hasPlan) {
|
|
51510
|
+
if (statusData.specStale) {
|
|
51511
|
+
const reason = statusData.specStaleReason ?? "spec.md changed since plan saved";
|
|
51512
|
+
const stored = statusData.specStaleStoredHash ?? "unknown";
|
|
51513
|
+
const current = statusData.specStaleCurrentHash ?? "(spec.md missing)";
|
|
51514
|
+
return [
|
|
51515
|
+
"No active swarm plan found.",
|
|
51516
|
+
"",
|
|
51517
|
+
`**Spec drift detected**: ${reason} (stored: ${stored}, current: ${current})`,
|
|
51518
|
+
"Run `/swarm clarify` to update the spec or `/swarm acknowledge-spec-drift` to dismiss."
|
|
51519
|
+
].join(`
|
|
51520
|
+
`);
|
|
51521
|
+
}
|
|
51341
51522
|
return "No active swarm plan found.";
|
|
51342
51523
|
}
|
|
51343
51524
|
return formatStatusMarkdown(statusData);
|
|
@@ -51641,7 +51822,7 @@ var init_write_retro2 = __esm(() => {
|
|
|
51641
51822
|
|
|
51642
51823
|
// src/commands/command-dispatch.ts
|
|
51643
51824
|
import fs28 from "fs";
|
|
51644
|
-
import
|
|
51825
|
+
import path47 from "path";
|
|
51645
51826
|
function normalizeSwarmCommandInput(command, argumentText) {
|
|
51646
51827
|
if (command !== "swarm" && !command.startsWith("swarm-")) {
|
|
51647
51828
|
return { isSwarmCommand: false, tokens: [] };
|
|
@@ -51677,9 +51858,9 @@ ${similar.map((cmd) => ` - /swarm ${cmd}`).join(`
|
|
|
51677
51858
|
`);
|
|
51678
51859
|
}
|
|
51679
51860
|
function maybeMarkFirstRun(directory) {
|
|
51680
|
-
const sentinelPath =
|
|
51861
|
+
const sentinelPath = path47.join(directory, ".swarm", ".first-run-complete");
|
|
51681
51862
|
try {
|
|
51682
|
-
const swarmDir =
|
|
51863
|
+
const swarmDir = path47.join(directory, ".swarm");
|
|
51683
51864
|
fs28.mkdirSync(swarmDir, { recursive: true });
|
|
51684
51865
|
fs28.writeFileSync(sentinelPath, `first-run-complete: ${new Date().toISOString()}
|
|
51685
51866
|
`, { flag: "wx" });
|
|
@@ -52347,24 +52528,24 @@ function validateAliases() {
|
|
|
52347
52528
|
}
|
|
52348
52529
|
aliasTargets.get(target).push(name);
|
|
52349
52530
|
const visited = new Set;
|
|
52350
|
-
const
|
|
52531
|
+
const path48 = [];
|
|
52351
52532
|
let current = target;
|
|
52352
52533
|
while (current) {
|
|
52353
52534
|
const currentEntry = COMMAND_REGISTRY[current];
|
|
52354
52535
|
if (!currentEntry)
|
|
52355
52536
|
break;
|
|
52356
52537
|
if (visited.has(current)) {
|
|
52357
|
-
const cycleStart =
|
|
52538
|
+
const cycleStart = path48.indexOf(current);
|
|
52358
52539
|
const fullChain = [
|
|
52359
52540
|
name,
|
|
52360
|
-
...
|
|
52541
|
+
...path48.slice(0, cycleStart > 0 ? cycleStart : path48.length),
|
|
52361
52542
|
current
|
|
52362
52543
|
].join(" \u2192 ");
|
|
52363
52544
|
errors5.push(`Circular alias detected: ${fullChain}`);
|
|
52364
52545
|
break;
|
|
52365
52546
|
}
|
|
52366
52547
|
visited.add(current);
|
|
52367
|
-
|
|
52548
|
+
path48.push(current);
|
|
52368
52549
|
current = currentEntry.aliasOf || "";
|
|
52369
52550
|
}
|
|
52370
52551
|
}
|
|
@@ -52875,53 +53056,53 @@ init_cache_paths();
|
|
|
52875
53056
|
init_constants();
|
|
52876
53057
|
import * as fs29 from "fs";
|
|
52877
53058
|
import * as os7 from "os";
|
|
52878
|
-
import * as
|
|
53059
|
+
import * as path48 from "path";
|
|
52879
53060
|
var { version: version4 } = package_default;
|
|
52880
53061
|
var CONFIG_DIR = getPluginConfigDir();
|
|
52881
|
-
var OPENCODE_CONFIG_PATH =
|
|
52882
|
-
var PLUGIN_CONFIG_PATH =
|
|
52883
|
-
var PROMPTS_DIR =
|
|
53062
|
+
var OPENCODE_CONFIG_PATH = path48.join(CONFIG_DIR, "opencode.json");
|
|
53063
|
+
var PLUGIN_CONFIG_PATH = path48.join(CONFIG_DIR, "opencode-swarm.json");
|
|
53064
|
+
var PROMPTS_DIR = path48.join(CONFIG_DIR, "opencode-swarm");
|
|
52884
53065
|
var OPENCODE_PLUGIN_CACHE_PATHS = getPluginCachePaths();
|
|
52885
53066
|
var OPENCODE_PLUGIN_LOCK_FILE_PATHS = getPluginLockFilePaths();
|
|
52886
53067
|
function isSafeCachePath(p) {
|
|
52887
|
-
const resolved =
|
|
52888
|
-
const home =
|
|
53068
|
+
const resolved = path48.resolve(p);
|
|
53069
|
+
const home = path48.resolve(os7.homedir());
|
|
52889
53070
|
if (resolved === "/" || resolved === home || resolved.length <= home.length) {
|
|
52890
53071
|
return false;
|
|
52891
53072
|
}
|
|
52892
|
-
const segments = resolved.split(
|
|
53073
|
+
const segments = resolved.split(path48.sep).filter((s) => s.length > 0);
|
|
52893
53074
|
if (segments.length < 4) {
|
|
52894
53075
|
return false;
|
|
52895
53076
|
}
|
|
52896
|
-
const leaf =
|
|
53077
|
+
const leaf = path48.basename(resolved);
|
|
52897
53078
|
if (leaf !== "opencode-swarm@latest" && leaf !== "opencode-swarm") {
|
|
52898
53079
|
return false;
|
|
52899
53080
|
}
|
|
52900
|
-
const parent =
|
|
53081
|
+
const parent = path48.basename(path48.dirname(resolved));
|
|
52901
53082
|
if (parent !== "packages" && parent !== "node_modules") {
|
|
52902
53083
|
return false;
|
|
52903
53084
|
}
|
|
52904
|
-
const grandparent =
|
|
53085
|
+
const grandparent = path48.basename(path48.dirname(path48.dirname(resolved)));
|
|
52905
53086
|
if (grandparent !== "opencode") {
|
|
52906
53087
|
return false;
|
|
52907
53088
|
}
|
|
52908
53089
|
return true;
|
|
52909
53090
|
}
|
|
52910
53091
|
function isSafeLockFilePath(p) {
|
|
52911
|
-
const resolved =
|
|
52912
|
-
const home =
|
|
53092
|
+
const resolved = path48.resolve(p);
|
|
53093
|
+
const home = path48.resolve(os7.homedir());
|
|
52913
53094
|
if (resolved === "/" || resolved === home || resolved.length <= home.length) {
|
|
52914
53095
|
return false;
|
|
52915
53096
|
}
|
|
52916
|
-
const segments = resolved.split(
|
|
53097
|
+
const segments = resolved.split(path48.sep).filter((s) => s.length > 0);
|
|
52917
53098
|
if (segments.length < 4) {
|
|
52918
53099
|
return false;
|
|
52919
53100
|
}
|
|
52920
|
-
const leaf =
|
|
53101
|
+
const leaf = path48.basename(resolved);
|
|
52921
53102
|
if (leaf !== "bun.lock" && leaf !== "bun.lockb" && leaf !== "package-lock.json") {
|
|
52922
53103
|
return false;
|
|
52923
53104
|
}
|
|
52924
|
-
const parent =
|
|
53105
|
+
const parent = path48.basename(path48.dirname(resolved));
|
|
52925
53106
|
if (parent !== "opencode") {
|
|
52926
53107
|
return false;
|
|
52927
53108
|
}
|
|
@@ -52947,8 +53128,8 @@ function saveJson(filepath, data) {
|
|
|
52947
53128
|
}
|
|
52948
53129
|
function writeProjectConfigIfMissing(cwd) {
|
|
52949
53130
|
try {
|
|
52950
|
-
const opencodeDir =
|
|
52951
|
-
const projectConfigPath =
|
|
53131
|
+
const opencodeDir = path48.join(cwd, ".opencode");
|
|
53132
|
+
const projectConfigPath = path48.join(opencodeDir, "opencode-swarm.json");
|
|
52952
53133
|
if (fs29.existsSync(projectConfigPath)) {
|
|
52953
53134
|
return;
|
|
52954
53135
|
}
|
|
@@ -52965,7 +53146,7 @@ async function install() {
|
|
|
52965
53146
|
`);
|
|
52966
53147
|
ensureDir(CONFIG_DIR);
|
|
52967
53148
|
ensureDir(PROMPTS_DIR);
|
|
52968
|
-
const LEGACY_CONFIG_PATH =
|
|
53149
|
+
const LEGACY_CONFIG_PATH = path48.join(CONFIG_DIR, "config.json");
|
|
52969
53150
|
let opencodeConfig = loadJson(OPENCODE_CONFIG_PATH);
|
|
52970
53151
|
if (!opencodeConfig) {
|
|
52971
53152
|
const legacyConfig = loadJson(LEGACY_CONFIG_PATH);
|
|
@@ -172,10 +172,18 @@ export type Plan = z.infer<typeof PlanSchema>;
|
|
|
172
172
|
/**
|
|
173
173
|
* Runtime plan with spec staleness tracking.
|
|
174
174
|
* Extends Plan with runtime-only fields that are not persisted.
|
|
175
|
+
*
|
|
176
|
+
* `_midLoadRemovals` is attached by loadPlan-recovery paths that auto-
|
|
177
|
+
* acknowledged task removals (issue #853) so the system-enhancer Layer A
|
|
178
|
+
* can disclose the count to the model without re-reading the ledger.
|
|
175
179
|
*/
|
|
176
180
|
export type RuntimePlan = Plan & {
|
|
177
181
|
_specStale?: boolean;
|
|
178
182
|
_specStaleReason?: string;
|
|
183
|
+
_midLoadRemovals?: {
|
|
184
|
+
count: number;
|
|
185
|
+
source: string;
|
|
186
|
+
};
|
|
179
187
|
};
|
|
180
188
|
/**
|
|
181
189
|
* Find the first phase that is in progress.
|
|
@@ -9,6 +9,27 @@
|
|
|
9
9
|
import * as path from 'node:path';
|
|
10
10
|
import { type AuthorityConfig, type GuardrailsConfig } from '../config/schema';
|
|
11
11
|
import { type FileZone } from '../context/zone-classifier';
|
|
12
|
+
/**
|
|
13
|
+
* Issue #853 Layer B: tools that are structurally blocked while
|
|
14
|
+
* `.swarm/spec-staleness.json` exists. Every blocked tool mutates plan
|
|
15
|
+
* state (save_plan, update_task_status, phase_complete) or proceeds with
|
|
16
|
+
* lean-turbo execution (lean_turbo_run_phase, lean_turbo_acquire_locks).
|
|
17
|
+
* The architect must run /swarm clarify or /swarm acknowledge-spec-drift
|
|
18
|
+
* before any of these will succeed.
|
|
19
|
+
*
|
|
20
|
+
* Read tools (get_approved_plan, lint_spec, set_qa_gates, convene_*,
|
|
21
|
+
* lean_turbo_plan_lanes, lean_turbo_runner_status, lean_turbo_review) are
|
|
22
|
+
* intentionally NOT blocked — drift surfacing should not block exploration.
|
|
23
|
+
*/
|
|
24
|
+
export declare const SPEC_DRIFT_BLOCKED_TOOLS: Set<string>;
|
|
25
|
+
/**
|
|
26
|
+
* Throw SPEC_DRIFT_BLOCK if the tool is on the block-list and the
|
|
27
|
+
* spec-staleness marker file exists. Layer B is structural (not a
|
|
28
|
+
* retryable error) — deterministic disk read every call, no cache, so
|
|
29
|
+
* /swarm acknowledge-spec-drift (which removes the marker) is reflected
|
|
30
|
+
* immediately on the next tool call.
|
|
31
|
+
*/
|
|
32
|
+
export declare function enforceSpecDriftGate(directory: string | undefined, toolName: string): void;
|
|
12
33
|
/**
|
|
13
34
|
* Retrieves stored input args for a given callID.
|
|
14
35
|
* Used by other hooks (e.g., delegation-gate) to access tool input args.
|
|
@@ -6,6 +6,25 @@
|
|
|
6
6
|
* Reads plan.md and injects phase context into the system prompt.
|
|
7
7
|
*/
|
|
8
8
|
import type { PluginConfig } from '../config';
|
|
9
|
+
/**
|
|
10
|
+
* Build the [spec-drift] advisory injected into the model's system prompt
|
|
11
|
+
* after every loadPlan whenever spec staleness is detected (issue #853
|
|
12
|
+
* Layer A). The text is appended to `output.system` and survives the
|
|
13
|
+
* single-system-message collapse at `experimental.chat.system.transform`.
|
|
14
|
+
*
|
|
15
|
+
* The "Do NOT proceed" line enumerates every tool in SPEC_DRIFT_BLOCKED_TOOLS
|
|
16
|
+
* so the architect knows exactly which calls will return SPEC_DRIFT_BLOCK
|
|
17
|
+
* from Layer B.
|
|
18
|
+
*/
|
|
19
|
+
export declare function buildSpecDriftAdvisory(args: {
|
|
20
|
+
reason: string;
|
|
21
|
+
currentHash: string | null;
|
|
22
|
+
storedHash: string;
|
|
23
|
+
midLoadRemovals?: {
|
|
24
|
+
count: number;
|
|
25
|
+
source: string;
|
|
26
|
+
};
|
|
27
|
+
}): string;
|
|
9
28
|
/**
|
|
10
29
|
* Build a retrospective injection string for the architect system message.
|
|
11
30
|
* Tier 1: direct phase-scoped lookup for same-plan previous phase.
|