opencode-swarm 7.19.0 → 7.19.2
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 +249 -35
- 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 +996 -598
- 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/tools/test-runner.d.ts +1 -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.2",
|
|
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;
|
|
@@ -48620,7 +48754,7 @@ function analyzeFailures(workingDir) {
|
|
|
48620
48754
|
} catch {}
|
|
48621
48755
|
return report;
|
|
48622
48756
|
}
|
|
48623
|
-
var MAX_OUTPUT_BYTES3 = 512000, MAX_COMMAND_LENGTH2 = 500, DEFAULT_TIMEOUT_MS = 60000, MAX_TIMEOUT_MS = 300000, MAX_SAFE_TEST_FILES = 50, POWERSHELL_METACHARACTERS, DISPATCH_FRAMEWORK_MAP, COMPOUND_TEST_EXTENSIONS, TEST_DIRECTORY_NAMES, SOURCE_EXTENSIONS, SKIP_DIRECTORIES, test_runner;
|
|
48757
|
+
var MAX_OUTPUT_BYTES3 = 512000, MAX_COMMAND_LENGTH2 = 500, DEFAULT_TIMEOUT_MS = 60000, MAX_TIMEOUT_MS = 300000, MAX_SAFE_TEST_FILES = 50, MAX_SAFE_SOURCE_FILES = 1, POWERSHELL_METACHARACTERS, DISPATCH_FRAMEWORK_MAP, COMPOUND_TEST_EXTENSIONS, TEST_DIRECTORY_NAMES, SOURCE_EXTENSIONS, SKIP_DIRECTORIES, test_runner;
|
|
48624
48758
|
var init_test_runner = __esm(() => {
|
|
48625
48759
|
init_zod();
|
|
48626
48760
|
init_discovery();
|
|
@@ -48807,8 +48941,8 @@ var init_test_runner = __esm(() => {
|
|
|
48807
48941
|
success: false,
|
|
48808
48942
|
framework: "none",
|
|
48809
48943
|
scope: "all",
|
|
48810
|
-
error: 'scope "all" is
|
|
48811
|
-
message: '
|
|
48944
|
+
error: 'scope "all" is blocked for agent use. Use scope "convention" with specific test files, or scope "graph" with exactly one source file.',
|
|
48945
|
+
message: 'The full test suite is blocked in agent context. Use scope "convention" with specific test files, or scope "graph" with exactly one source file. Example: { scope: "convention", files: ["src/tools/test-runner.ts"] }',
|
|
48812
48946
|
outcome: "error"
|
|
48813
48947
|
};
|
|
48814
48948
|
return JSON.stringify(errorResult, null, 2);
|
|
@@ -48889,6 +49023,17 @@ var init_test_runner = __esm(() => {
|
|
|
48889
49023
|
};
|
|
48890
49024
|
return JSON.stringify(errorResult, null, 2);
|
|
48891
49025
|
}
|
|
49026
|
+
if (sourceFiles.length > MAX_SAFE_SOURCE_FILES) {
|
|
49027
|
+
const errorResult = {
|
|
49028
|
+
success: false,
|
|
49029
|
+
framework,
|
|
49030
|
+
scope,
|
|
49031
|
+
error: `scope "convention" accepts at most ${MAX_SAFE_SOURCE_FILES} source file for discovery (got ${sourceFiles.length}). Treat this as SKIP without retry.`,
|
|
49032
|
+
message: `Too many source files for scope "convention" discovery (${sourceFiles.length} provided, limit is ${MAX_SAFE_SOURCE_FILES}). Call test_runner once per source file, or pass direct test file paths instead of source files.`,
|
|
49033
|
+
outcome: "scope_exceeded"
|
|
49034
|
+
};
|
|
49035
|
+
return JSON.stringify(errorResult, null, 2);
|
|
49036
|
+
}
|
|
48892
49037
|
testFiles = [
|
|
48893
49038
|
...directTestFiles,
|
|
48894
49039
|
...getTestFilesFromConvention(sourceFiles, workingDir)
|
|
@@ -48912,6 +49057,17 @@ var init_test_runner = __esm(() => {
|
|
|
48912
49057
|
};
|
|
48913
49058
|
return JSON.stringify(errorResult, null, 2);
|
|
48914
49059
|
}
|
|
49060
|
+
if (sourceFiles.length > MAX_SAFE_SOURCE_FILES) {
|
|
49061
|
+
const errorResult = {
|
|
49062
|
+
success: false,
|
|
49063
|
+
framework,
|
|
49064
|
+
scope,
|
|
49065
|
+
error: `scope "graph" accepts at most ${MAX_SAFE_SOURCE_FILES} source file (got ${sourceFiles.length}). Treat this as SKIP without retry.`,
|
|
49066
|
+
message: `Too many source files for scope "graph" (${sourceFiles.length} provided, limit is ${MAX_SAFE_SOURCE_FILES}). Call test_runner once per source file, or use scope "convention" with direct test file paths.`,
|
|
49067
|
+
outcome: "scope_exceeded"
|
|
49068
|
+
};
|
|
49069
|
+
return JSON.stringify(errorResult, null, 2);
|
|
49070
|
+
}
|
|
48915
49071
|
const graphTestFiles = await getTestFilesFromGraph(sourceFiles, workingDir);
|
|
48916
49072
|
if (graphTestFiles.length > 0) {
|
|
48917
49073
|
testFiles = graphTestFiles;
|
|
@@ -48939,6 +49095,17 @@ var init_test_runner = __esm(() => {
|
|
|
48939
49095
|
};
|
|
48940
49096
|
return JSON.stringify(errorResult, null, 2);
|
|
48941
49097
|
}
|
|
49098
|
+
if (sourceFiles.length > MAX_SAFE_SOURCE_FILES) {
|
|
49099
|
+
const errorResult = {
|
|
49100
|
+
success: false,
|
|
49101
|
+
framework,
|
|
49102
|
+
scope,
|
|
49103
|
+
error: `scope "impact" accepts at most ${MAX_SAFE_SOURCE_FILES} source file (got ${sourceFiles.length}). Treat this as SKIP without retry.`,
|
|
49104
|
+
message: `Too many source files for scope "impact" (${sourceFiles.length} provided, limit is ${MAX_SAFE_SOURCE_FILES}). Call test_runner once per source file, or use scope "convention" with direct test file paths.`,
|
|
49105
|
+
outcome: "scope_exceeded"
|
|
49106
|
+
};
|
|
49107
|
+
return JSON.stringify(errorResult, null, 2);
|
|
49108
|
+
}
|
|
48942
49109
|
try {
|
|
48943
49110
|
const impactResult = await analyzeImpact(sourceFiles, workingDir);
|
|
48944
49111
|
if (impactResult.impactedTests.length > 0) {
|
|
@@ -51158,6 +51325,25 @@ var init_context_budget_service = __esm(() => {
|
|
|
51158
51325
|
});
|
|
51159
51326
|
|
|
51160
51327
|
// src/services/status-service.ts
|
|
51328
|
+
import * as fsSync2 from "fs";
|
|
51329
|
+
import * as path46 from "path";
|
|
51330
|
+
function readSpecStalenessSnapshot(directory) {
|
|
51331
|
+
try {
|
|
51332
|
+
const p = path46.join(directory, ".swarm", "spec-staleness.json");
|
|
51333
|
+
if (!fsSync2.existsSync(p))
|
|
51334
|
+
return { stale: false };
|
|
51335
|
+
const raw = fsSync2.readFileSync(p, "utf-8");
|
|
51336
|
+
const parsed = JSON.parse(raw);
|
|
51337
|
+
return {
|
|
51338
|
+
stale: true,
|
|
51339
|
+
reason: typeof parsed?.reason === "string" ? parsed.reason : undefined,
|
|
51340
|
+
storedHash: typeof parsed?.specHash_plan === "string" ? parsed.specHash_plan : undefined,
|
|
51341
|
+
currentHash: typeof parsed?.specHash_current === "string" || parsed?.specHash_current === null ? parsed.specHash_current : undefined
|
|
51342
|
+
};
|
|
51343
|
+
} catch {
|
|
51344
|
+
return { stale: false };
|
|
51345
|
+
}
|
|
51346
|
+
}
|
|
51161
51347
|
async function getStatusData(directory, agents) {
|
|
51162
51348
|
const plan = await loadPlan(directory);
|
|
51163
51349
|
let status;
|
|
@@ -51223,6 +51409,16 @@ async function getStatusData(directory, agents) {
|
|
|
51223
51409
|
};
|
|
51224
51410
|
}
|
|
51225
51411
|
}
|
|
51412
|
+
const drift = readSpecStalenessSnapshot(directory);
|
|
51413
|
+
if (drift.stale) {
|
|
51414
|
+
status.specStale = true;
|
|
51415
|
+
status.specStaleReason = drift.reason;
|
|
51416
|
+
status.specStaleStoredHash = drift.storedHash;
|
|
51417
|
+
status.specStaleCurrentHash = drift.currentHash;
|
|
51418
|
+
} else if (plan && plan._specStale) {
|
|
51419
|
+
status.specStale = true;
|
|
51420
|
+
status.specStaleReason = plan._specStaleReason;
|
|
51421
|
+
}
|
|
51226
51422
|
return enrichWithLeanTurbo(status, directory);
|
|
51227
51423
|
}
|
|
51228
51424
|
function enrichWithLeanTurbo(status, directory) {
|
|
@@ -51289,6 +51485,12 @@ function formatStatusMarkdown(status) {
|
|
|
51289
51485
|
`**Tasks**: ${status.completedTasks}/${status.totalTasks} complete`,
|
|
51290
51486
|
`**Agents**: ${status.agentCount} registered`
|
|
51291
51487
|
];
|
|
51488
|
+
if (status.specStale) {
|
|
51489
|
+
const reason = status.specStaleReason ?? "spec.md changed since plan saved";
|
|
51490
|
+
const stored = status.specStaleStoredHash ?? "unknown";
|
|
51491
|
+
const current = status.specStaleCurrentHash ?? "(spec.md missing)";
|
|
51492
|
+
lines.push("", `**Spec drift detected**: ${reason} (stored: ${stored}, current: ${current})`, "Run `/swarm clarify` to update the spec or `/swarm acknowledge-spec-drift` to dismiss.");
|
|
51493
|
+
}
|
|
51292
51494
|
if (status.turboStrategy && status.turboStrategy !== "off") {
|
|
51293
51495
|
lines.push("");
|
|
51294
51496
|
if (status.turboStrategy === "lean") {
|
|
@@ -51338,6 +51540,18 @@ function formatStatusMarkdown(status) {
|
|
|
51338
51540
|
async function handleStatusCommand(directory, agents) {
|
|
51339
51541
|
const statusData = await getStatusData(directory, agents);
|
|
51340
51542
|
if (!statusData.hasPlan) {
|
|
51543
|
+
if (statusData.specStale) {
|
|
51544
|
+
const reason = statusData.specStaleReason ?? "spec.md changed since plan saved";
|
|
51545
|
+
const stored = statusData.specStaleStoredHash ?? "unknown";
|
|
51546
|
+
const current = statusData.specStaleCurrentHash ?? "(spec.md missing)";
|
|
51547
|
+
return [
|
|
51548
|
+
"No active swarm plan found.",
|
|
51549
|
+
"",
|
|
51550
|
+
`**Spec drift detected**: ${reason} (stored: ${stored}, current: ${current})`,
|
|
51551
|
+
"Run `/swarm clarify` to update the spec or `/swarm acknowledge-spec-drift` to dismiss."
|
|
51552
|
+
].join(`
|
|
51553
|
+
`);
|
|
51554
|
+
}
|
|
51341
51555
|
return "No active swarm plan found.";
|
|
51342
51556
|
}
|
|
51343
51557
|
return formatStatusMarkdown(statusData);
|
|
@@ -51641,7 +51855,7 @@ var init_write_retro2 = __esm(() => {
|
|
|
51641
51855
|
|
|
51642
51856
|
// src/commands/command-dispatch.ts
|
|
51643
51857
|
import fs28 from "fs";
|
|
51644
|
-
import
|
|
51858
|
+
import path47 from "path";
|
|
51645
51859
|
function normalizeSwarmCommandInput(command, argumentText) {
|
|
51646
51860
|
if (command !== "swarm" && !command.startsWith("swarm-")) {
|
|
51647
51861
|
return { isSwarmCommand: false, tokens: [] };
|
|
@@ -51677,9 +51891,9 @@ ${similar.map((cmd) => ` - /swarm ${cmd}`).join(`
|
|
|
51677
51891
|
`);
|
|
51678
51892
|
}
|
|
51679
51893
|
function maybeMarkFirstRun(directory) {
|
|
51680
|
-
const sentinelPath =
|
|
51894
|
+
const sentinelPath = path47.join(directory, ".swarm", ".first-run-complete");
|
|
51681
51895
|
try {
|
|
51682
|
-
const swarmDir =
|
|
51896
|
+
const swarmDir = path47.join(directory, ".swarm");
|
|
51683
51897
|
fs28.mkdirSync(swarmDir, { recursive: true });
|
|
51684
51898
|
fs28.writeFileSync(sentinelPath, `first-run-complete: ${new Date().toISOString()}
|
|
51685
51899
|
`, { flag: "wx" });
|
|
@@ -52347,24 +52561,24 @@ function validateAliases() {
|
|
|
52347
52561
|
}
|
|
52348
52562
|
aliasTargets.get(target).push(name);
|
|
52349
52563
|
const visited = new Set;
|
|
52350
|
-
const
|
|
52564
|
+
const path48 = [];
|
|
52351
52565
|
let current = target;
|
|
52352
52566
|
while (current) {
|
|
52353
52567
|
const currentEntry = COMMAND_REGISTRY[current];
|
|
52354
52568
|
if (!currentEntry)
|
|
52355
52569
|
break;
|
|
52356
52570
|
if (visited.has(current)) {
|
|
52357
|
-
const cycleStart =
|
|
52571
|
+
const cycleStart = path48.indexOf(current);
|
|
52358
52572
|
const fullChain = [
|
|
52359
52573
|
name,
|
|
52360
|
-
...
|
|
52574
|
+
...path48.slice(0, cycleStart > 0 ? cycleStart : path48.length),
|
|
52361
52575
|
current
|
|
52362
52576
|
].join(" \u2192 ");
|
|
52363
52577
|
errors5.push(`Circular alias detected: ${fullChain}`);
|
|
52364
52578
|
break;
|
|
52365
52579
|
}
|
|
52366
52580
|
visited.add(current);
|
|
52367
|
-
|
|
52581
|
+
path48.push(current);
|
|
52368
52582
|
current = currentEntry.aliasOf || "";
|
|
52369
52583
|
}
|
|
52370
52584
|
}
|
|
@@ -52875,53 +53089,53 @@ init_cache_paths();
|
|
|
52875
53089
|
init_constants();
|
|
52876
53090
|
import * as fs29 from "fs";
|
|
52877
53091
|
import * as os7 from "os";
|
|
52878
|
-
import * as
|
|
53092
|
+
import * as path48 from "path";
|
|
52879
53093
|
var { version: version4 } = package_default;
|
|
52880
53094
|
var CONFIG_DIR = getPluginConfigDir();
|
|
52881
|
-
var OPENCODE_CONFIG_PATH =
|
|
52882
|
-
var PLUGIN_CONFIG_PATH =
|
|
52883
|
-
var PROMPTS_DIR =
|
|
53095
|
+
var OPENCODE_CONFIG_PATH = path48.join(CONFIG_DIR, "opencode.json");
|
|
53096
|
+
var PLUGIN_CONFIG_PATH = path48.join(CONFIG_DIR, "opencode-swarm.json");
|
|
53097
|
+
var PROMPTS_DIR = path48.join(CONFIG_DIR, "opencode-swarm");
|
|
52884
53098
|
var OPENCODE_PLUGIN_CACHE_PATHS = getPluginCachePaths();
|
|
52885
53099
|
var OPENCODE_PLUGIN_LOCK_FILE_PATHS = getPluginLockFilePaths();
|
|
52886
53100
|
function isSafeCachePath(p) {
|
|
52887
|
-
const resolved =
|
|
52888
|
-
const home =
|
|
53101
|
+
const resolved = path48.resolve(p);
|
|
53102
|
+
const home = path48.resolve(os7.homedir());
|
|
52889
53103
|
if (resolved === "/" || resolved === home || resolved.length <= home.length) {
|
|
52890
53104
|
return false;
|
|
52891
53105
|
}
|
|
52892
|
-
const segments = resolved.split(
|
|
53106
|
+
const segments = resolved.split(path48.sep).filter((s) => s.length > 0);
|
|
52893
53107
|
if (segments.length < 4) {
|
|
52894
53108
|
return false;
|
|
52895
53109
|
}
|
|
52896
|
-
const leaf =
|
|
53110
|
+
const leaf = path48.basename(resolved);
|
|
52897
53111
|
if (leaf !== "opencode-swarm@latest" && leaf !== "opencode-swarm") {
|
|
52898
53112
|
return false;
|
|
52899
53113
|
}
|
|
52900
|
-
const parent =
|
|
53114
|
+
const parent = path48.basename(path48.dirname(resolved));
|
|
52901
53115
|
if (parent !== "packages" && parent !== "node_modules") {
|
|
52902
53116
|
return false;
|
|
52903
53117
|
}
|
|
52904
|
-
const grandparent =
|
|
53118
|
+
const grandparent = path48.basename(path48.dirname(path48.dirname(resolved)));
|
|
52905
53119
|
if (grandparent !== "opencode") {
|
|
52906
53120
|
return false;
|
|
52907
53121
|
}
|
|
52908
53122
|
return true;
|
|
52909
53123
|
}
|
|
52910
53124
|
function isSafeLockFilePath(p) {
|
|
52911
|
-
const resolved =
|
|
52912
|
-
const home =
|
|
53125
|
+
const resolved = path48.resolve(p);
|
|
53126
|
+
const home = path48.resolve(os7.homedir());
|
|
52913
53127
|
if (resolved === "/" || resolved === home || resolved.length <= home.length) {
|
|
52914
53128
|
return false;
|
|
52915
53129
|
}
|
|
52916
|
-
const segments = resolved.split(
|
|
53130
|
+
const segments = resolved.split(path48.sep).filter((s) => s.length > 0);
|
|
52917
53131
|
if (segments.length < 4) {
|
|
52918
53132
|
return false;
|
|
52919
53133
|
}
|
|
52920
|
-
const leaf =
|
|
53134
|
+
const leaf = path48.basename(resolved);
|
|
52921
53135
|
if (leaf !== "bun.lock" && leaf !== "bun.lockb" && leaf !== "package-lock.json") {
|
|
52922
53136
|
return false;
|
|
52923
53137
|
}
|
|
52924
|
-
const parent =
|
|
53138
|
+
const parent = path48.basename(path48.dirname(resolved));
|
|
52925
53139
|
if (parent !== "opencode") {
|
|
52926
53140
|
return false;
|
|
52927
53141
|
}
|
|
@@ -52947,8 +53161,8 @@ function saveJson(filepath, data) {
|
|
|
52947
53161
|
}
|
|
52948
53162
|
function writeProjectConfigIfMissing(cwd) {
|
|
52949
53163
|
try {
|
|
52950
|
-
const opencodeDir =
|
|
52951
|
-
const projectConfigPath =
|
|
53164
|
+
const opencodeDir = path48.join(cwd, ".opencode");
|
|
53165
|
+
const projectConfigPath = path48.join(opencodeDir, "opencode-swarm.json");
|
|
52952
53166
|
if (fs29.existsSync(projectConfigPath)) {
|
|
52953
53167
|
return;
|
|
52954
53168
|
}
|
|
@@ -52965,7 +53179,7 @@ async function install() {
|
|
|
52965
53179
|
`);
|
|
52966
53180
|
ensureDir(CONFIG_DIR);
|
|
52967
53181
|
ensureDir(PROMPTS_DIR);
|
|
52968
|
-
const LEGACY_CONFIG_PATH =
|
|
53182
|
+
const LEGACY_CONFIG_PATH = path48.join(CONFIG_DIR, "config.json");
|
|
52969
53183
|
let opencodeConfig = loadJson(OPENCODE_CONFIG_PATH);
|
|
52970
53184
|
if (!opencodeConfig) {
|
|
52971
53185
|
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.
|