opencode-swarm 7.21.3 → 7.21.4
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
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.21.
|
|
37
|
+
version: "7.21.4",
|
|
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",
|
|
@@ -16043,7 +16043,7 @@ var init_manager = __esm(() => {
|
|
|
16043
16043
|
|
|
16044
16044
|
// src/commands/acknowledge-spec-drift.ts
|
|
16045
16045
|
import { promises as fsPromises3 } from "fs";
|
|
16046
|
-
async function handleAcknowledgeSpecDriftCommand(directory, _args) {
|
|
16046
|
+
async function handleAcknowledgeSpecDriftCommand(directory, _args, acknowledgedBy = "unknown") {
|
|
16047
16047
|
const specStalenessPath = validateSwarmPath(directory, "spec-staleness.json");
|
|
16048
16048
|
let stalenessContent;
|
|
16049
16049
|
try {
|
|
@@ -16084,7 +16084,7 @@ async function handleAcknowledgeSpecDriftCommand(directory, _args) {
|
|
|
16084
16084
|
timestamp: new Date().toISOString(),
|
|
16085
16085
|
phase,
|
|
16086
16086
|
planTitle,
|
|
16087
|
-
acknowledgedBy
|
|
16087
|
+
acknowledgedBy,
|
|
16088
16088
|
previousHash: stalenessData.specHash_plan,
|
|
16089
16089
|
newHash: currentHash
|
|
16090
16090
|
};
|
|
@@ -52031,7 +52031,8 @@ async function executeSwarmCommand(args) {
|
|
|
52031
52031
|
directory,
|
|
52032
52032
|
args: resolved.remainingArgs,
|
|
52033
52033
|
sessionID,
|
|
52034
|
-
agents
|
|
52034
|
+
agents,
|
|
52035
|
+
source: "chat"
|
|
52035
52036
|
});
|
|
52036
52037
|
} catch (_err) {
|
|
52037
52038
|
const cmdName = tokens[0] || "unknown";
|
|
@@ -52063,6 +52064,12 @@ function classifySwarmCommandToolUse(resolved) {
|
|
|
52063
52064
|
const canonicalKey = canonicalCommandKey(resolved);
|
|
52064
52065
|
const args = resolved.remainingArgs;
|
|
52065
52066
|
if (!SWARM_COMMAND_TOOL_ALLOWLIST.has(canonicalKey)) {
|
|
52067
|
+
if (HUMAN_ONLY_SWARM_COMMANDS.has(canonicalKey)) {
|
|
52068
|
+
return {
|
|
52069
|
+
allowed: false,
|
|
52070
|
+
message: `/swarm ${canonicalKey} is a human-only command. ` + `Present the situation to the user and ask them to run \`/swarm ${canonicalKey}\` themselves ` + `(or \`bunx opencode-swarm run ${canonicalKey}\` from a terminal). ` + `You MUST NOT run it yourself via Bash, swarm_command, or any other tool \u2014 ` + `the runtime guardrail will block such attempts.`
|
|
52071
|
+
};
|
|
52072
|
+
}
|
|
52066
52073
|
return {
|
|
52067
52074
|
allowed: false,
|
|
52068
52075
|
message: `/swarm ${canonicalKey} is not available through the chat tool yet.
|
|
@@ -52152,7 +52159,7 @@ function classifySwarmCommandChatFallbackUse(resolved) {
|
|
|
52152
52159
|
}
|
|
52153
52160
|
return { allowed: true };
|
|
52154
52161
|
}
|
|
52155
|
-
var SWARM_COMMAND_TOOL_COMMANDS, SWARM_COMMAND_TOOL_ALLOWLIST, NO_ARGS, SUMMARY_ID_PATTERN, TASK_ID_PATTERN;
|
|
52162
|
+
var SWARM_COMMAND_TOOL_COMMANDS, SWARM_COMMAND_TOOL_ALLOWLIST, HUMAN_ONLY_SWARM_COMMANDS, NO_ARGS, SUMMARY_ID_PATTERN, TASK_ID_PATTERN;
|
|
52156
52163
|
var init_tool_policy = __esm(() => {
|
|
52157
52164
|
init_command_dispatch();
|
|
52158
52165
|
SWARM_COMMAND_TOOL_COMMANDS = [
|
|
@@ -52198,6 +52205,13 @@ var init_tool_policy = __esm(() => {
|
|
|
52198
52205
|
"sync-plan",
|
|
52199
52206
|
"export"
|
|
52200
52207
|
]);
|
|
52208
|
+
HUMAN_ONLY_SWARM_COMMANDS = new Set([
|
|
52209
|
+
"acknowledge-spec-drift",
|
|
52210
|
+
"reset",
|
|
52211
|
+
"reset-session",
|
|
52212
|
+
"rollback",
|
|
52213
|
+
"checkpoint"
|
|
52214
|
+
]);
|
|
52201
52215
|
NO_ARGS = new Set([
|
|
52202
52216
|
"agents",
|
|
52203
52217
|
"config",
|
|
@@ -52755,7 +52769,7 @@ var init_registry = __esm(() => {
|
|
|
52755
52769
|
init_write_retro2();
|
|
52756
52770
|
COMMAND_REGISTRY = {
|
|
52757
52771
|
"acknowledge-spec-drift": {
|
|
52758
|
-
handler: (ctx) => handleAcknowledgeSpecDriftCommand(ctx.directory, ctx.args),
|
|
52772
|
+
handler: (ctx) => handleAcknowledgeSpecDriftCommand(ctx.directory, ctx.args, ctx.source === "cli" ? "cli" : ctx.source === "chat" ? "user" : "unknown"),
|
|
52759
52773
|
description: "Acknowledge that the spec has drifted from the plan and suppress further warnings",
|
|
52760
52774
|
args: "",
|
|
52761
52775
|
category: "diagnostics"
|
|
@@ -53601,7 +53615,8 @@ Valid commands: ${VALID_COMMANDS.join(", ")}`);
|
|
|
53601
53615
|
directory: cwd,
|
|
53602
53616
|
args: resolved.remainingArgs,
|
|
53603
53617
|
sessionID: "",
|
|
53604
|
-
agents: {}
|
|
53618
|
+
agents: {},
|
|
53619
|
+
source: "cli"
|
|
53605
53620
|
});
|
|
53606
53621
|
console.log(result);
|
|
53607
53622
|
return 0;
|
|
@@ -1,5 +1,17 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Caller identification for spec-drift acknowledgment audit trail.
|
|
3
|
+
* Previously hardcoded as 'architect' — see issue #890, where the architect
|
|
4
|
+
* could shell out to `bunx opencode-swarm run acknowledge-spec-drift` and
|
|
5
|
+
* the resulting event mis-attributed the action. Callers now pass an
|
|
6
|
+
* explicit actor so events.jsonl can distinguish the legitimate paths
|
|
7
|
+
* ('user' from chat slash command, 'cli' from a real terminal) from any
|
|
8
|
+
* unidentified caller ('unknown'). The Bash guardrail
|
|
9
|
+
* (`src/hooks/guardrails.ts` section 23) blocks the agent-shell bypass at
|
|
10
|
+
* the runtime layer; this parameter exists for forensic clarity.
|
|
11
|
+
*/
|
|
12
|
+
export type SpecDriftAcknowledgedBy = 'user' | 'cli' | 'unknown';
|
|
1
13
|
/**
|
|
2
14
|
* Handle /swarm acknowledge-spec-drift command
|
|
3
15
|
* Acknowledges and clears a previously detected spec drift staleness warning
|
|
4
16
|
*/
|
|
5
|
-
export declare function handleAcknowledgeSpecDriftCommand(directory: string, _args: string[]): Promise<string>;
|
|
17
|
+
export declare function handleAcknowledgeSpecDriftCommand(directory: string, _args: string[], acknowledgedBy?: SpecDriftAcknowledgedBy): Promise<string>;
|
|
@@ -8,6 +8,14 @@ export type CommandContext = {
|
|
|
8
8
|
args: string[];
|
|
9
9
|
sessionID: string;
|
|
10
10
|
agents: Record<string, AgentDefinition>;
|
|
11
|
+
/**
|
|
12
|
+
* Dispatch path identifier. Issue #890: forensic audit trail for
|
|
13
|
+
* commands that need to distinguish "user typed /swarm <cmd>" (chat)
|
|
14
|
+
* from "user ran bunx opencode-swarm run <cmd>" (cli). Handlers that
|
|
15
|
+
* don't care can ignore this field. Optional for backwards-compatibility
|
|
16
|
+
* with existing callers.
|
|
17
|
+
*/
|
|
18
|
+
source?: 'cli' | 'chat';
|
|
11
19
|
};
|
|
12
20
|
export type CommandResult = Promise<string>;
|
|
13
21
|
export type CommandCategory = 'core' | 'agent' | 'config' | 'diagnostics' | 'utility';
|
|
@@ -2,5 +2,14 @@ import type { ResolvedSwarmCommand, SwarmCommandPolicyResult } from './command-d
|
|
|
2
2
|
export declare const SWARM_COMMAND_TOOL_COMMANDS: readonly ["agents", "config", "config doctor", "config-doctor", "doctor", "doctor tools", "status", "show-plan", "plan", "help", "history", "evidence", "evidence summary", "evidence-summary", "retrieve", "diagnose", "preflight", "benchmark", "knowledge", "sync-plan", "export", "list-agents"];
|
|
3
3
|
export type SwarmCommandToolInputCommand = (typeof SWARM_COMMAND_TOOL_COMMANDS)[number];
|
|
4
4
|
export declare const SWARM_COMMAND_TOOL_ALLOWLIST: Set<string>;
|
|
5
|
+
/**
|
|
6
|
+
* Issue #890: subcommands that must be invoked by a human user, not by the
|
|
7
|
+
* agent. The runtime Bash guardrail
|
|
8
|
+
* (`src/hooks/guardrails.ts` section 23) blocks the equivalent
|
|
9
|
+
* `bunx opencode-swarm run <cmd>` shell invocation; this set drives the
|
|
10
|
+
* chat-tool refusal message so the agent is told to surface to the user
|
|
11
|
+
* instead of being pointed at the CLI bypass it just attempted.
|
|
12
|
+
*/
|
|
13
|
+
export declare const HUMAN_ONLY_SWARM_COMMANDS: Set<string>;
|
|
5
14
|
export declare function classifySwarmCommandToolUse(resolved: ResolvedSwarmCommand): SwarmCommandPolicyResult;
|
|
6
15
|
export declare function classifySwarmCommandChatFallbackUse(resolved: ResolvedSwarmCommand): SwarmCommandPolicyResult;
|
package/dist/index.js
CHANGED
|
@@ -33,7 +33,7 @@ var package_default;
|
|
|
33
33
|
var init_package = __esm(() => {
|
|
34
34
|
package_default = {
|
|
35
35
|
name: "opencode-swarm",
|
|
36
|
-
version: "7.21.
|
|
36
|
+
version: "7.21.4",
|
|
37
37
|
description: "Architect-centric agentic swarm plugin for OpenCode - hub-and-spoke orchestration with SME consultation, code generation, and QA review",
|
|
38
38
|
main: "dist/index.js",
|
|
39
39
|
types: "dist/index.d.ts",
|
|
@@ -18564,7 +18564,7 @@ var init_manager = __esm(() => {
|
|
|
18564
18564
|
|
|
18565
18565
|
// src/commands/acknowledge-spec-drift.ts
|
|
18566
18566
|
import { promises as fsPromises4 } from "node:fs";
|
|
18567
|
-
async function handleAcknowledgeSpecDriftCommand(directory, _args) {
|
|
18567
|
+
async function handleAcknowledgeSpecDriftCommand(directory, _args, acknowledgedBy = "unknown") {
|
|
18568
18568
|
const specStalenessPath = validateSwarmPath(directory, "spec-staleness.json");
|
|
18569
18569
|
let stalenessContent;
|
|
18570
18570
|
try {
|
|
@@ -18605,7 +18605,7 @@ async function handleAcknowledgeSpecDriftCommand(directory, _args) {
|
|
|
18605
18605
|
timestamp: new Date().toISOString(),
|
|
18606
18606
|
phase,
|
|
18607
18607
|
planTitle,
|
|
18608
|
-
acknowledgedBy
|
|
18608
|
+
acknowledgedBy,
|
|
18609
18609
|
previousHash: stalenessData.specHash_plan,
|
|
18610
18610
|
newHash: currentHash
|
|
18611
18611
|
};
|
|
@@ -24962,6 +24962,45 @@ function createGuardrailsHooks(directory, directoryOrConfig, config2, authorityC
|
|
|
24962
24962
|
if (/^7z\b.*\s-sdel\b/i.test(seg) && /\.swarm(?:[\x5c/\s]|$)/i.test(seg)) {
|
|
24963
24963
|
throw new Error(`BLOCKED: "7z" with delete-source flag targeting .swarm/ detected — archive with source deletion under .swarm/ is not allowed`);
|
|
24964
24964
|
}
|
|
24965
|
+
{
|
|
24966
|
+
const HUMAN_ONLY_SWARM_SUBCOMMANDS = new Set([
|
|
24967
|
+
"acknowledge-spec-drift",
|
|
24968
|
+
"reset",
|
|
24969
|
+
"reset-session",
|
|
24970
|
+
"rollback",
|
|
24971
|
+
"checkpoint"
|
|
24972
|
+
]);
|
|
24973
|
+
let probe = seg.replace(/^(?:[A-Za-z_][A-Za-z0-9_]*=\S+\s+)+/, "").replace(/^eval(?:\s+--)?\s+["']?/, "").replace(/["']\s*$/, "").replace(/^\$\(\s*/, "").replace(/^\(\s*/, "").replace(/\s*\)$/, "").replace(/^`/, "").replace(/`$/, "").trim();
|
|
24974
|
+
for (let i2 = 0;i2 < 4; i2++) {
|
|
24975
|
+
const before = probe;
|
|
24976
|
+
probe = probe.replace(/^env\s+(?:-i\b|--ignore-environment\b|-u\s+\S+|-[a-zA-Z]+\s+)*\s*/, "").replace(/^command\s+(?:-[pvV]\s+)*/, "").replace(/^(?:[A-Za-z_][A-Za-z0-9_]*=\S+\s+)+/, "").trim();
|
|
24977
|
+
if (probe === before)
|
|
24978
|
+
break;
|
|
24979
|
+
}
|
|
24980
|
+
const swarmCliBypassMatch = probe.match(/^\\?(?:bunx|npx|pnpx|npm(?:\s+(?:exec|x)(?:\s+--)?)?|pnpm(?:\s+(?:dlx|exec))?|yarn(?:\s+(?:dlx|exec))?|bun(?:\s+x)?|node|deno\s+run|tsx|ts-node)\b[^|;&]*?\bopencode-swarm\b[^|;&]*?\brun\s+([A-Za-z0-9_-]+)/i);
|
|
24981
|
+
if (swarmCliBypassMatch && HUMAN_ONLY_SWARM_SUBCOMMANDS.has(swarmCliBypassMatch[1])) {
|
|
24982
|
+
throw new Error(`BLOCKED: "${swarmCliBypassMatch[1]}" is a human-only swarm command and may not be invoked from shell by an agent. ` + `Present the situation to the user and ask them to run \`/swarm ${swarmCliBypassMatch[1]}\` themselves.`);
|
|
24983
|
+
}
|
|
24984
|
+
const swarmBareBinMatch = probe.match(/^\\?opencode-swarm\b[^|;&]*?\brun\s+([A-Za-z0-9_-]+)/i);
|
|
24985
|
+
if (swarmBareBinMatch && HUMAN_ONLY_SWARM_SUBCOMMANDS.has(swarmBareBinMatch[1])) {
|
|
24986
|
+
throw new Error(`BLOCKED: "${swarmBareBinMatch[1]}" is a human-only swarm command and may not be invoked from shell by an agent. ` + `Present the situation to the user and ask them to run \`/swarm ${swarmBareBinMatch[1]}\` themselves.`);
|
|
24987
|
+
}
|
|
24988
|
+
const swarmCliPathMatch = probe.match(/\bcli[/\\]+index\.[mc]?(?:js|ts)\b[^|;&]*?\brun\s+([A-Za-z0-9_-]+)/i);
|
|
24989
|
+
if (swarmCliPathMatch && HUMAN_ONLY_SWARM_SUBCOMMANDS.has(swarmCliPathMatch[1])) {
|
|
24990
|
+
throw new Error(`BLOCKED: "${swarmCliPathMatch[1]}" is a human-only swarm command and may not be invoked from shell by an agent. ` + `Present the situation to the user and ask them to run \`/swarm ${swarmCliPathMatch[1]}\` themselves.`);
|
|
24991
|
+
}
|
|
24992
|
+
}
|
|
24993
|
+
{
|
|
24994
|
+
const normForPathCheck = seg.replace(/\\/g, "/").replace(/\/(?:\.\/+)+/g, "/").replace(/\/{2,}/g, "/");
|
|
24995
|
+
if (/\.swarm\/spec-staleness\.json\b/i.test(normForPathCheck)) {
|
|
24996
|
+
const trimmed = seg.trim();
|
|
24997
|
+
const looksReadOnly = /^(?:cat|less|more|head|tail|file|stat|ls|dir|Get-Content|gc|Get-Item|gi|type)\b/i.test(trimmed);
|
|
24998
|
+
const hasWriteRedirect = />{1,2}\s*[^\s>]/.test(trimmed);
|
|
24999
|
+
if (!looksReadOnly || hasWriteRedirect) {
|
|
25000
|
+
throw new Error("BLOCKED: shell command targeting .swarm/spec-staleness.json detected. " + "This file is system-managed and gates plan-mutating tools while spec drift is unresolved. " + "Present the drift to the user and ask them to run /swarm clarify or /swarm acknowledge-spec-drift.");
|
|
25001
|
+
}
|
|
25002
|
+
}
|
|
25003
|
+
}
|
|
24965
25004
|
}
|
|
24966
25005
|
}
|
|
24967
25006
|
async function checkGateLimits(params) {
|
|
@@ -25149,11 +25188,37 @@ function createGuardrailsHooks(directory, directoryOrConfig, config2, authorityC
|
|
|
25149
25188
|
}
|
|
25150
25189
|
}
|
|
25151
25190
|
}
|
|
25191
|
+
function extractAllPatchPayloads(args2) {
|
|
25192
|
+
const toolArgs = args2;
|
|
25193
|
+
if (!toolArgs)
|
|
25194
|
+
return [];
|
|
25195
|
+
const out2 = [];
|
|
25196
|
+
for (const key of ["patch", "input", "diff"]) {
|
|
25197
|
+
const v = toolArgs[key];
|
|
25198
|
+
if (typeof v === "string" && v.length > 0)
|
|
25199
|
+
out2.push(v);
|
|
25200
|
+
}
|
|
25201
|
+
const cmd = toolArgs.cmd;
|
|
25202
|
+
if (Array.isArray(cmd)) {
|
|
25203
|
+
for (const entry of cmd) {
|
|
25204
|
+
if (typeof entry === "string" && entry.length > 0)
|
|
25205
|
+
out2.push(entry);
|
|
25206
|
+
}
|
|
25207
|
+
}
|
|
25208
|
+
return out2;
|
|
25209
|
+
}
|
|
25210
|
+
function patchPayloadHasHumanOnlyInvocation(args2) {
|
|
25211
|
+
const payloads = extractAllPatchPayloads(args2);
|
|
25212
|
+
if (payloads.length === 0)
|
|
25213
|
+
return false;
|
|
25214
|
+
const re = /\bopencode-swarm\b[\s\S]*?\brun\s+(?:acknowledge-spec-drift|reset|reset-session|rollback|checkpoint)\b/i;
|
|
25215
|
+
return payloads.some((p) => re.test(p));
|
|
25216
|
+
}
|
|
25152
25217
|
function extractPatchTargetPaths(tool, args2) {
|
|
25153
25218
|
if (tool !== "apply_patch" && tool !== "patch")
|
|
25154
25219
|
return [];
|
|
25155
25220
|
const toolArgs = args2;
|
|
25156
|
-
const patchText = toolArgs?.input ?? toolArgs?.patch ?? (Array.isArray(toolArgs?.cmd) ? toolArgs.cmd[1] : undefined);
|
|
25221
|
+
const patchText = toolArgs?.input ?? toolArgs?.patch ?? toolArgs?.diff ?? (Array.isArray(toolArgs?.cmd) ? toolArgs.cmd[1] : undefined);
|
|
25157
25222
|
if (typeof patchText !== "string")
|
|
25158
25223
|
return [];
|
|
25159
25224
|
if (patchText.length > 1e6) {
|
|
@@ -25201,6 +25266,11 @@ function createGuardrailsHooks(directory, directoryOrConfig, config2, authorityC
|
|
|
25201
25266
|
function handlePlanAndScopeProtection(sessionID, tool, args2) {
|
|
25202
25267
|
const toolArgs = args2;
|
|
25203
25268
|
const targetPath = toolArgs?.filePath ?? toolArgs?.path ?? toolArgs?.file ?? toolArgs?.target;
|
|
25269
|
+
if (tool === "apply_patch" || tool === "patch") {
|
|
25270
|
+
if (patchPayloadHasHumanOnlyInvocation(args2)) {
|
|
25271
|
+
throw new Error("BLOCKED: apply_patch would introduce a script invoking a human-only swarm CLI subcommand. " + "Present the situation to the user and ask them to run the command themselves.");
|
|
25272
|
+
}
|
|
25273
|
+
}
|
|
25204
25274
|
if (typeof targetPath === "string" && targetPath.length > 0) {
|
|
25205
25275
|
const resolvedTarget = path10.resolve(effectiveDirectory, targetPath).toLowerCase();
|
|
25206
25276
|
const planMdPath = path10.resolve(effectiveDirectory, ".swarm", "plan.md").toLowerCase();
|
|
@@ -25208,15 +25278,27 @@ function createGuardrailsHooks(directory, directoryOrConfig, config2, authorityC
|
|
|
25208
25278
|
if (resolvedTarget === planMdPath || resolvedTarget === planJsonPath) {
|
|
25209
25279
|
throw new Error("PLAN STATE VIOLATION: Direct writes to .swarm/plan.md and .swarm/plan.json are blocked. " + "plan.md is auto-regenerated from plan.json by PlanSyncWorker. " + "Use save_plan for ALL structural plan changes (adding/removing tasks, updating descriptions, dependencies, or phase names). " + "Use update_task_status() for task status only. " + "Use phase_complete() for phase transitions only.");
|
|
25210
25280
|
}
|
|
25281
|
+
const specStalenessPath = path10.resolve(effectiveDirectory, ".swarm", "spec-staleness.json").toLowerCase();
|
|
25282
|
+
if (resolvedTarget === specStalenessPath) {
|
|
25283
|
+
throw new Error("SPEC_DRIFT_VIOLATION: Direct writes to .swarm/spec-staleness.json are blocked. " + "This file is system-managed and gates plan-mutating tools while spec drift is unresolved. " + "Present the drift to the user and ask them to run /swarm clarify or /swarm acknowledge-spec-drift.");
|
|
25284
|
+
}
|
|
25285
|
+
const content = toolArgs?.content ?? toolArgs?.text ?? toolArgs?.new_string ?? toolArgs?.newText;
|
|
25286
|
+
if (typeof content === "string" && /\bopencode-swarm\b[\s\S]*?\brun\s+(?:acknowledge-spec-drift|reset|reset-session|rollback|checkpoint)\b/i.test(content)) {
|
|
25287
|
+
throw new Error("BLOCKED: write/edit tool would create a script invoking a human-only swarm CLI subcommand. " + "Present the situation to the user and ask them to run the command themselves.");
|
|
25288
|
+
}
|
|
25211
25289
|
}
|
|
25212
25290
|
if (!targetPath && (tool === "apply_patch" || tool === "patch")) {
|
|
25213
25291
|
for (const p of extractPatchTargetPaths(tool, args2)) {
|
|
25214
25292
|
const resolvedP = path10.resolve(effectiveDirectory, p);
|
|
25215
25293
|
const planMdPath = path10.resolve(effectiveDirectory, ".swarm", "plan.md").toLowerCase();
|
|
25216
25294
|
const planJsonPath = path10.resolve(effectiveDirectory, ".swarm", "plan.json").toLowerCase();
|
|
25295
|
+
const specStalenessPath = path10.resolve(effectiveDirectory, ".swarm", "spec-staleness.json").toLowerCase();
|
|
25217
25296
|
if (resolvedP.toLowerCase() === planMdPath || resolvedP.toLowerCase() === planJsonPath) {
|
|
25218
25297
|
throw new Error("PLAN STATE VIOLATION: Direct writes to .swarm/plan.md and .swarm/plan.json are blocked. " + "plan.md is auto-regenerated from plan.json by PlanSyncWorker. " + "Use save_plan for ALL structural plan changes (adding/removing tasks, updating descriptions, dependencies, or phase names). " + "Use update_task_status() for task status only. " + "Use phase_complete() for phase transitions only.");
|
|
25219
25298
|
}
|
|
25299
|
+
if (resolvedP.toLowerCase() === specStalenessPath) {
|
|
25300
|
+
throw new Error("SPEC_DRIFT_VIOLATION: Direct writes to .swarm/spec-staleness.json are blocked. " + "This file is system-managed and gates plan-mutating tools while spec drift is unresolved. " + "Present the drift to the user and ask them to run /swarm clarify or /swarm acknowledge-spec-drift.");
|
|
25301
|
+
}
|
|
25220
25302
|
if (isOutsideSwarmDir(p, effectiveDirectory) && (isSourceCodePath(p) || hasTraversalSegments(p))) {
|
|
25221
25303
|
const session = swarmState.agentSessions.get(sessionID);
|
|
25222
25304
|
if (session) {
|
|
@@ -61048,7 +61130,8 @@ async function executeSwarmCommand(args2) {
|
|
|
61048
61130
|
directory,
|
|
61049
61131
|
args: resolved.remainingArgs,
|
|
61050
61132
|
sessionID,
|
|
61051
|
-
agents
|
|
61133
|
+
agents,
|
|
61134
|
+
source: "chat"
|
|
61052
61135
|
});
|
|
61053
61136
|
} catch (_err) {
|
|
61054
61137
|
const cmdName = tokens[0] || "unknown";
|
|
@@ -61080,6 +61163,12 @@ function classifySwarmCommandToolUse(resolved) {
|
|
|
61080
61163
|
const canonicalKey = canonicalCommandKey(resolved);
|
|
61081
61164
|
const args2 = resolved.remainingArgs;
|
|
61082
61165
|
if (!SWARM_COMMAND_TOOL_ALLOWLIST.has(canonicalKey)) {
|
|
61166
|
+
if (HUMAN_ONLY_SWARM_COMMANDS.has(canonicalKey)) {
|
|
61167
|
+
return {
|
|
61168
|
+
allowed: false,
|
|
61169
|
+
message: `/swarm ${canonicalKey} is a human-only command. ` + `Present the situation to the user and ask them to run \`/swarm ${canonicalKey}\` themselves ` + `(or \`bunx opencode-swarm run ${canonicalKey}\` from a terminal). ` + `You MUST NOT run it yourself via Bash, swarm_command, or any other tool — ` + `the runtime guardrail will block such attempts.`
|
|
61170
|
+
};
|
|
61171
|
+
}
|
|
61083
61172
|
return {
|
|
61084
61173
|
allowed: false,
|
|
61085
61174
|
message: `/swarm ${canonicalKey} is not available through the chat tool yet.
|
|
@@ -61169,7 +61258,7 @@ function classifySwarmCommandChatFallbackUse(resolved) {
|
|
|
61169
61258
|
}
|
|
61170
61259
|
return { allowed: true };
|
|
61171
61260
|
}
|
|
61172
|
-
var SWARM_COMMAND_TOOL_COMMANDS, SWARM_COMMAND_TOOL_ALLOWLIST, NO_ARGS, SUMMARY_ID_PATTERN, TASK_ID_PATTERN;
|
|
61261
|
+
var SWARM_COMMAND_TOOL_COMMANDS, SWARM_COMMAND_TOOL_ALLOWLIST, HUMAN_ONLY_SWARM_COMMANDS, NO_ARGS, SUMMARY_ID_PATTERN, TASK_ID_PATTERN;
|
|
61173
61262
|
var init_tool_policy = __esm(() => {
|
|
61174
61263
|
init_command_dispatch();
|
|
61175
61264
|
SWARM_COMMAND_TOOL_COMMANDS = [
|
|
@@ -61215,6 +61304,13 @@ var init_tool_policy = __esm(() => {
|
|
|
61215
61304
|
"sync-plan",
|
|
61216
61305
|
"export"
|
|
61217
61306
|
]);
|
|
61307
|
+
HUMAN_ONLY_SWARM_COMMANDS = new Set([
|
|
61308
|
+
"acknowledge-spec-drift",
|
|
61309
|
+
"reset",
|
|
61310
|
+
"reset-session",
|
|
61311
|
+
"rollback",
|
|
61312
|
+
"checkpoint"
|
|
61313
|
+
]);
|
|
61218
61314
|
NO_ARGS = new Set([
|
|
61219
61315
|
"agents",
|
|
61220
61316
|
"config",
|
|
@@ -61772,7 +61868,7 @@ var init_registry = __esm(() => {
|
|
|
61772
61868
|
init_write_retro2();
|
|
61773
61869
|
COMMAND_REGISTRY = {
|
|
61774
61870
|
"acknowledge-spec-drift": {
|
|
61775
|
-
handler: (ctx) => handleAcknowledgeSpecDriftCommand(ctx.directory, ctx.args),
|
|
61871
|
+
handler: (ctx) => handleAcknowledgeSpecDriftCommand(ctx.directory, ctx.args, ctx.source === "cli" ? "cli" : ctx.source === "chat" ? "user" : "unknown"),
|
|
61776
61872
|
description: "Acknowledge that the spec has drifted from the plan and suppress further warnings",
|
|
61777
61873
|
args: "",
|
|
61778
61874
|
category: "diagnostics"
|
|
@@ -63028,7 +63124,7 @@ Continue handling small touch-ups (typos, cross-references) inline.
|
|
|
63028
63124
|
## SLASH COMMANDS
|
|
63029
63125
|
{{SLASH_COMMANDS}}
|
|
63030
63126
|
Commands above are documented with args and behavioral details. Run commands via /swarm <command> [args].
|
|
63031
|
-
Outside OpenCode, invoke any plugin command via: \`bunx opencode-swarm run <command> [args]\` (e.g. \`bunx opencode-swarm run knowledge migrate\`). Do not use \`bun -e\` or look for \`src/commands/\` — those paths are internal to the plugin source and do not exist in user project directories.
|
|
63127
|
+
Outside OpenCode, invoke any plugin command via: \`bunx opencode-swarm run <command> [args]\` (e.g. \`bunx opencode-swarm run knowledge migrate\`). Do not use \`bun -e\` or look for \`src/commands/\` — those paths are internal to the plugin source and do not exist in user project directories. EXCEPTION — human-only commands (including but not limited to \`acknowledge-spec-drift\`, \`reset\`, \`reset-session\`, \`rollback\`, \`checkpoint\`, and any command that releases a runtime safety gate or destroys plan state): you MUST present these to the user and ask them to run the command themselves. Never invoke a human-only command via Bash, swarm_command, or chat fallback. The runtime guardrail will block such attempts; if a Bash call returns \`BLOCKED\` with a "human-only" message, do not retry under a different shell form — present the situation to the user instead.
|
|
63032
63128
|
|
|
63033
63129
|
SMEs advise only. Reviewer and critic review only. None of them write code.
|
|
63034
63130
|
|
|
@@ -63916,10 +64012,17 @@ If resuming a project with an existing approved plan, CRITIC-GATE is already sat
|
|
|
63916
64012
|
- This rule is satisfied by the save_plan tool's own spec gate — it exists as a reminder that planning requires a spec.
|
|
63917
64013
|
|
|
63918
64014
|
6k. SPEC-STALENESS GUARD:
|
|
63919
|
-
- If _specStale or .swarm/spec-staleness.json exists, the Architect MUST
|
|
63920
|
-
|
|
63921
|
-
|
|
63922
|
-
-
|
|
64015
|
+
- If _specStale or .swarm/spec-staleness.json exists, the Architect MUST stop
|
|
64016
|
+
and SURFACE THE DRIFT TO THE USER. The user (not the Architect) then runs
|
|
64017
|
+
either:
|
|
64018
|
+
- /swarm clarify to update the spec and align it with the plan, OR
|
|
64019
|
+
- /swarm acknowledge-spec-drift to acknowledge the drift and suppress further warnings
|
|
64020
|
+
- The Architect MUST NOT run /swarm acknowledge-spec-drift itself — not via
|
|
64021
|
+
the swarm_command tool, not via the chat fallback, and NOT by shelling out
|
|
64022
|
+
to \`bunx opencode-swarm run acknowledge-spec-drift\` (or any equivalent
|
|
64023
|
+
\`npx\`/\`node\`/\`bun\` invocation). Any such self-invocation is a
|
|
64024
|
+
control-bypass and will be refused by the runtime guardrails.
|
|
64025
|
+
- Do NOT proceed with implementation until the user resolves the staleness.
|
|
63923
64026
|
- When re-saving a plan in response to spec drift, save_plan REQUIRES that ANY task
|
|
63924
64027
|
present in the prior plan but absent from the new args.phases be enumerated
|
|
63925
64028
|
in removed_task_ids with a removal_reason. save_plan will reject the call
|
|
@@ -63930,8 +64033,8 @@ If resuming a project with an existing approved plan, CRITIC-GATE is already sat
|
|
|
63930
64033
|
- While .swarm/spec-staleness.json exists, the runtime STRUCTURALLY BLOCKS the
|
|
63931
64034
|
following tools (SPEC_DRIFT_BLOCKED_TOOLS): save_plan, update_task_status,
|
|
63932
64035
|
phase_complete, lean_turbo_run_phase, lean_turbo_acquire_locks. If a call
|
|
63933
|
-
returns SPEC_DRIFT_BLOCK, do NOT retry;
|
|
63934
|
-
|
|
64036
|
+
returns SPEC_DRIFT_BLOCK, do NOT retry; surface the drift to the user and
|
|
64037
|
+
WAIT for them to run /swarm clarify or /swarm acknowledge-spec-drift.
|
|
63935
64038
|
|
|
63936
64039
|
### MODE: EXECUTE
|
|
63937
64040
|
For each task (respecting dependencies):
|
|
@@ -65528,9 +65631,10 @@ WORKFLOW:
|
|
|
65528
65631
|
- TODO comments in code (those go through the task system, not code comments)
|
|
65529
65632
|
|
|
65530
65633
|
## RELEASE NOTES
|
|
65531
|
-
When writing release notes (docs/releases/
|
|
65532
|
-
-
|
|
65533
|
-
-
|
|
65634
|
+
When writing release notes (docs/releases/pending/<slug>.md):
|
|
65635
|
+
- Do NOT determine the next version. Do NOT create docs/releases/vX.Y.Z.md. release-please owns the version; the release workflow aggregates pending fragments.
|
|
65636
|
+
- Pick a short, kebab-case slug describing your change (e.g. spec-drift-self-ack-guardrail.md). Pick one unlikely to collide with concurrent PRs.
|
|
65637
|
+
- Follow the established format in existing release notes files (descriptive topic heading, not a version prefix).
|
|
65534
65638
|
- Include: overview, breaking changes (if any), new features, bug fixes, internal improvements
|
|
65535
65639
|
- Do NOT manually edit package.json version, CHANGELOG.md, or .release-please-manifest.json — release-please owns these
|
|
65536
65640
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "opencode-swarm",
|
|
3
|
-
"version": "7.21.
|
|
3
|
+
"version": "7.21.4",
|
|
4
4
|
"description": "Architect-centric agentic swarm plugin for OpenCode - hub-and-spoke orchestration with SME consultation, code generation, and QA review",
|
|
5
5
|
"main": "dist/index.js",
|
|
6
6
|
"types": "dist/index.d.ts",
|