opencode-swarm 4.4.0 → 5.0.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/README.md +155 -28
- package/dist/agents/index.d.ts +6 -0
- package/dist/commands/agents.d.ts +2 -1
- package/dist/commands/archive.d.ts +5 -0
- package/dist/commands/diagnose.d.ts +5 -0
- package/dist/commands/evidence.d.ts +5 -0
- package/dist/commands/export.d.ts +5 -0
- package/dist/commands/index.d.ts +5 -0
- package/dist/commands/reset.d.ts +6 -0
- package/dist/config/evidence-schema.d.ts +353 -0
- package/dist/config/index.d.ts +7 -3
- package/dist/config/plan-schema.d.ts +124 -0
- package/dist/config/schema.d.ts +63 -0
- package/dist/evidence/index.d.ts +1 -0
- package/dist/evidence/manager.d.ts +38 -0
- package/dist/guardrails.js +178 -0
- package/dist/hooks/extractors.d.ts +13 -0
- package/dist/hooks/guardrails.d.ts +52 -0
- package/dist/hooks/index.d.ts +2 -1
- package/dist/index.js +1385 -105
- package/dist/plan/index.d.ts +1 -0
- package/dist/plan/manager.d.ts +33 -0
- package/dist/state.d.ts +44 -0
- package/dist/state.js +48 -0
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -1,5 +1,20 @@
|
|
|
1
1
|
// @bun
|
|
2
|
+
var __create = Object.create;
|
|
3
|
+
var __getProtoOf = Object.getPrototypeOf;
|
|
2
4
|
var __defProp = Object.defineProperty;
|
|
5
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
6
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
7
|
+
var __toESM = (mod, isNodeMode, target) => {
|
|
8
|
+
target = mod != null ? __create(__getProtoOf(mod)) : {};
|
|
9
|
+
const to = isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target;
|
|
10
|
+
for (let key of __getOwnPropNames(mod))
|
|
11
|
+
if (!__hasOwnProp.call(to, key))
|
|
12
|
+
__defProp(to, key, {
|
|
13
|
+
get: () => mod[key],
|
|
14
|
+
enumerable: true
|
|
15
|
+
});
|
|
16
|
+
return to;
|
|
17
|
+
};
|
|
3
18
|
var __export = (target, all) => {
|
|
4
19
|
for (var name in all)
|
|
5
20
|
__defProp(target, name, {
|
|
@@ -9,6 +24,7 @@ var __export = (target, all) => {
|
|
|
9
24
|
set: (newValue) => all[name] = () => newValue
|
|
10
25
|
});
|
|
11
26
|
};
|
|
27
|
+
var __require = import.meta.require;
|
|
12
28
|
|
|
13
29
|
// src/config/constants.ts
|
|
14
30
|
var QA_AGENTS = ["reviewer", "critic"];
|
|
@@ -33,6 +49,11 @@ var DEFAULT_MODELS = {
|
|
|
33
49
|
critic: "google/gemini-2.0-flash",
|
|
34
50
|
default: "google/gemini-2.0-flash"
|
|
35
51
|
};
|
|
52
|
+
// src/config/loader.ts
|
|
53
|
+
import * as fs from "fs";
|
|
54
|
+
import * as os from "os";
|
|
55
|
+
import * as path from "path";
|
|
56
|
+
|
|
36
57
|
// node_modules/zod/v4/classic/external.js
|
|
37
58
|
var exports_external = {};
|
|
38
59
|
__export(exports_external, {
|
|
@@ -13586,8 +13607,38 @@ var ContextBudgetConfigSchema = exports_external.object({
|
|
|
13586
13607
|
enabled: exports_external.boolean().default(true),
|
|
13587
13608
|
warn_threshold: exports_external.number().min(0).max(1).default(0.7),
|
|
13588
13609
|
critical_threshold: exports_external.number().min(0).max(1).default(0.9),
|
|
13589
|
-
model_limits: exports_external.record(exports_external.string(), exports_external.number().min(1000)).default({ default: 128000 })
|
|
13610
|
+
model_limits: exports_external.record(exports_external.string(), exports_external.number().min(1000)).default({ default: 128000 }),
|
|
13611
|
+
max_injection_tokens: exports_external.number().min(100).max(50000).default(4000)
|
|
13590
13612
|
});
|
|
13613
|
+
var EvidenceConfigSchema = exports_external.object({
|
|
13614
|
+
enabled: exports_external.boolean().default(true),
|
|
13615
|
+
max_age_days: exports_external.number().min(1).max(365).default(90),
|
|
13616
|
+
max_bundles: exports_external.number().min(10).max(1e4).default(1000),
|
|
13617
|
+
auto_archive: exports_external.boolean().default(false)
|
|
13618
|
+
});
|
|
13619
|
+
var GuardrailsProfileSchema = exports_external.object({
|
|
13620
|
+
max_tool_calls: exports_external.number().min(10).max(1000).optional(),
|
|
13621
|
+
max_duration_minutes: exports_external.number().min(1).max(120).optional(),
|
|
13622
|
+
max_repetitions: exports_external.number().min(3).max(50).optional(),
|
|
13623
|
+
max_consecutive_errors: exports_external.number().min(2).max(20).optional(),
|
|
13624
|
+
warning_threshold: exports_external.number().min(0.1).max(0.9).optional()
|
|
13625
|
+
});
|
|
13626
|
+
var GuardrailsConfigSchema = exports_external.object({
|
|
13627
|
+
enabled: exports_external.boolean().default(true),
|
|
13628
|
+
max_tool_calls: exports_external.number().min(10).max(1000).default(200),
|
|
13629
|
+
max_duration_minutes: exports_external.number().min(1).max(120).default(30),
|
|
13630
|
+
max_repetitions: exports_external.number().min(3).max(50).default(10),
|
|
13631
|
+
max_consecutive_errors: exports_external.number().min(2).max(20).default(5),
|
|
13632
|
+
warning_threshold: exports_external.number().min(0.1).max(0.9).default(0.5),
|
|
13633
|
+
profiles: exports_external.record(exports_external.string(), GuardrailsProfileSchema).optional()
|
|
13634
|
+
});
|
|
13635
|
+
function resolveGuardrailsConfig(base, agentName) {
|
|
13636
|
+
if (!agentName || !base.profiles?.[agentName]) {
|
|
13637
|
+
return base;
|
|
13638
|
+
}
|
|
13639
|
+
const profile = base.profiles[agentName];
|
|
13640
|
+
return { ...base, ...profile };
|
|
13641
|
+
}
|
|
13591
13642
|
var PluginConfigSchema = exports_external.object({
|
|
13592
13643
|
agents: exports_external.record(exports_external.string(), AgentOverrideConfigSchema).optional(),
|
|
13593
13644
|
swarms: exports_external.record(exports_external.string(), SwarmConfigSchema).optional(),
|
|
@@ -13595,12 +13646,12 @@ var PluginConfigSchema = exports_external.object({
|
|
|
13595
13646
|
qa_retry_limit: exports_external.number().min(1).max(10).default(3),
|
|
13596
13647
|
inject_phase_reminders: exports_external.boolean().default(true),
|
|
13597
13648
|
hooks: HooksConfigSchema.optional(),
|
|
13598
|
-
context_budget: ContextBudgetConfigSchema.optional()
|
|
13649
|
+
context_budget: ContextBudgetConfigSchema.optional(),
|
|
13650
|
+
guardrails: GuardrailsConfigSchema.optional(),
|
|
13651
|
+
evidence: EvidenceConfigSchema.optional()
|
|
13599
13652
|
});
|
|
13653
|
+
|
|
13600
13654
|
// src/config/loader.ts
|
|
13601
|
-
import * as fs from "fs";
|
|
13602
|
-
import * as os from "os";
|
|
13603
|
-
import * as path from "path";
|
|
13604
13655
|
var CONFIG_FILENAME = "opencode-swarm.json";
|
|
13605
13656
|
var PROMPTS_DIR_NAME = "opencode-swarm";
|
|
13606
13657
|
var MAX_CONFIG_FILE_BYTES = 102400;
|
|
@@ -13693,6 +13744,125 @@ function loadAgentPrompt(agentName) {
|
|
|
13693
13744
|
}
|
|
13694
13745
|
return result;
|
|
13695
13746
|
}
|
|
13747
|
+
// src/config/plan-schema.ts
|
|
13748
|
+
var TaskStatusSchema = exports_external.enum([
|
|
13749
|
+
"pending",
|
|
13750
|
+
"in_progress",
|
|
13751
|
+
"completed",
|
|
13752
|
+
"blocked"
|
|
13753
|
+
]);
|
|
13754
|
+
var TaskSizeSchema = exports_external.enum(["small", "medium", "large"]);
|
|
13755
|
+
var PhaseStatusSchema = exports_external.enum([
|
|
13756
|
+
"pending",
|
|
13757
|
+
"in_progress",
|
|
13758
|
+
"complete",
|
|
13759
|
+
"blocked"
|
|
13760
|
+
]);
|
|
13761
|
+
var MigrationStatusSchema = exports_external.enum([
|
|
13762
|
+
"native",
|
|
13763
|
+
"migrated",
|
|
13764
|
+
"migration_failed"
|
|
13765
|
+
]);
|
|
13766
|
+
var TaskSchema = exports_external.object({
|
|
13767
|
+
id: exports_external.string(),
|
|
13768
|
+
phase: exports_external.number().int().min(1),
|
|
13769
|
+
status: TaskStatusSchema.default("pending"),
|
|
13770
|
+
size: TaskSizeSchema.default("small"),
|
|
13771
|
+
description: exports_external.string().min(1),
|
|
13772
|
+
depends: exports_external.array(exports_external.string()).default([]),
|
|
13773
|
+
acceptance: exports_external.string().optional(),
|
|
13774
|
+
files_touched: exports_external.array(exports_external.string()).default([]),
|
|
13775
|
+
evidence_path: exports_external.string().optional(),
|
|
13776
|
+
blocked_reason: exports_external.string().optional()
|
|
13777
|
+
});
|
|
13778
|
+
var PhaseSchema = exports_external.object({
|
|
13779
|
+
id: exports_external.number().int().min(1),
|
|
13780
|
+
name: exports_external.string().min(1),
|
|
13781
|
+
status: PhaseStatusSchema.default("pending"),
|
|
13782
|
+
tasks: exports_external.array(TaskSchema).default([])
|
|
13783
|
+
});
|
|
13784
|
+
var PlanSchema = exports_external.object({
|
|
13785
|
+
schema_version: exports_external.literal("1.0.0"),
|
|
13786
|
+
title: exports_external.string().min(1),
|
|
13787
|
+
swarm: exports_external.string().min(1),
|
|
13788
|
+
current_phase: exports_external.number().int().min(1),
|
|
13789
|
+
phases: exports_external.array(PhaseSchema).min(1),
|
|
13790
|
+
migration_status: MigrationStatusSchema.optional()
|
|
13791
|
+
});
|
|
13792
|
+
// src/config/evidence-schema.ts
|
|
13793
|
+
var EVIDENCE_MAX_JSON_BYTES = 500 * 1024;
|
|
13794
|
+
var EVIDENCE_MAX_PATCH_BYTES = 5 * 1024 * 1024;
|
|
13795
|
+
var EVIDENCE_MAX_TASK_BYTES = 20 * 1024 * 1024;
|
|
13796
|
+
var EvidenceTypeSchema = exports_external.enum([
|
|
13797
|
+
"review",
|
|
13798
|
+
"test",
|
|
13799
|
+
"diff",
|
|
13800
|
+
"approval",
|
|
13801
|
+
"note"
|
|
13802
|
+
]);
|
|
13803
|
+
var EvidenceVerdictSchema = exports_external.enum([
|
|
13804
|
+
"pass",
|
|
13805
|
+
"fail",
|
|
13806
|
+
"approved",
|
|
13807
|
+
"rejected",
|
|
13808
|
+
"info"
|
|
13809
|
+
]);
|
|
13810
|
+
var BaseEvidenceSchema = exports_external.object({
|
|
13811
|
+
task_id: exports_external.string().min(1),
|
|
13812
|
+
type: EvidenceTypeSchema,
|
|
13813
|
+
timestamp: exports_external.string().datetime(),
|
|
13814
|
+
agent: exports_external.string().min(1),
|
|
13815
|
+
verdict: EvidenceVerdictSchema,
|
|
13816
|
+
summary: exports_external.string().min(1),
|
|
13817
|
+
metadata: exports_external.record(exports_external.string(), exports_external.unknown()).optional()
|
|
13818
|
+
});
|
|
13819
|
+
var ReviewEvidenceSchema = BaseEvidenceSchema.extend({
|
|
13820
|
+
type: exports_external.literal("review"),
|
|
13821
|
+
risk: exports_external.enum(["low", "medium", "high", "critical"]),
|
|
13822
|
+
issues: exports_external.array(exports_external.object({
|
|
13823
|
+
severity: exports_external.enum(["error", "warning", "info"]),
|
|
13824
|
+
message: exports_external.string().min(1),
|
|
13825
|
+
file: exports_external.string().optional(),
|
|
13826
|
+
line: exports_external.number().int().optional()
|
|
13827
|
+
})).default([])
|
|
13828
|
+
});
|
|
13829
|
+
var TestEvidenceSchema = BaseEvidenceSchema.extend({
|
|
13830
|
+
type: exports_external.literal("test"),
|
|
13831
|
+
tests_passed: exports_external.number().int().min(0),
|
|
13832
|
+
tests_failed: exports_external.number().int().min(0),
|
|
13833
|
+
test_file: exports_external.string().optional(),
|
|
13834
|
+
failures: exports_external.array(exports_external.object({
|
|
13835
|
+
name: exports_external.string().min(1),
|
|
13836
|
+
message: exports_external.string().min(1)
|
|
13837
|
+
})).default([])
|
|
13838
|
+
});
|
|
13839
|
+
var DiffEvidenceSchema = BaseEvidenceSchema.extend({
|
|
13840
|
+
type: exports_external.literal("diff"),
|
|
13841
|
+
files_changed: exports_external.array(exports_external.string()).default([]),
|
|
13842
|
+
additions: exports_external.number().int().min(0).default(0),
|
|
13843
|
+
deletions: exports_external.number().int().min(0).default(0),
|
|
13844
|
+
patch_path: exports_external.string().optional()
|
|
13845
|
+
});
|
|
13846
|
+
var ApprovalEvidenceSchema = BaseEvidenceSchema.extend({
|
|
13847
|
+
type: exports_external.literal("approval")
|
|
13848
|
+
});
|
|
13849
|
+
var NoteEvidenceSchema = BaseEvidenceSchema.extend({
|
|
13850
|
+
type: exports_external.literal("note")
|
|
13851
|
+
});
|
|
13852
|
+
var EvidenceSchema = exports_external.discriminatedUnion("type", [
|
|
13853
|
+
ReviewEvidenceSchema,
|
|
13854
|
+
TestEvidenceSchema,
|
|
13855
|
+
DiffEvidenceSchema,
|
|
13856
|
+
ApprovalEvidenceSchema,
|
|
13857
|
+
NoteEvidenceSchema
|
|
13858
|
+
]);
|
|
13859
|
+
var EvidenceBundleSchema = exports_external.object({
|
|
13860
|
+
schema_version: exports_external.literal("1.0.0"),
|
|
13861
|
+
task_id: exports_external.string().min(1),
|
|
13862
|
+
entries: exports_external.array(EvidenceSchema).default([]),
|
|
13863
|
+
created_at: exports_external.string().datetime(),
|
|
13864
|
+
updated_at: exports_external.string().datetime()
|
|
13865
|
+
});
|
|
13696
13866
|
// src/agents/architect.ts
|
|
13697
13867
|
var ARCHITECT_PROMPT = `You are Architect - orchestrator of a multi-agent swarm.
|
|
13698
13868
|
|
|
@@ -14246,28 +14416,28 @@ ${customAppendPrompt}`;
|
|
|
14246
14416
|
}
|
|
14247
14417
|
|
|
14248
14418
|
// src/agents/index.ts
|
|
14249
|
-
function
|
|
14250
|
-
|
|
14251
|
-
|
|
14252
|
-
|
|
14419
|
+
function stripSwarmPrefix(agentName, swarmPrefix) {
|
|
14420
|
+
if (!swarmPrefix || !agentName)
|
|
14421
|
+
return agentName;
|
|
14422
|
+
const prefixWithUnderscore = `${swarmPrefix}_`;
|
|
14423
|
+
if (agentName.startsWith(prefixWithUnderscore)) {
|
|
14424
|
+
return agentName.substring(prefixWithUnderscore.length);
|
|
14253
14425
|
}
|
|
14426
|
+
return agentName;
|
|
14427
|
+
}
|
|
14428
|
+
function getModelForAgent(agentName, swarmAgents, swarmPrefix) {
|
|
14429
|
+
const baseAgentName = stripSwarmPrefix(agentName, swarmPrefix);
|
|
14254
14430
|
const explicit = swarmAgents?.[baseAgentName]?.model;
|
|
14255
14431
|
if (explicit)
|
|
14256
14432
|
return explicit;
|
|
14257
14433
|
return DEFAULT_MODELS[baseAgentName] ?? DEFAULT_MODELS.default;
|
|
14258
14434
|
}
|
|
14259
14435
|
function isAgentDisabled(agentName, swarmAgents, swarmPrefix) {
|
|
14260
|
-
|
|
14261
|
-
if (swarmPrefix && agentName.startsWith(`${swarmPrefix}_`)) {
|
|
14262
|
-
baseAgentName = agentName.substring(swarmPrefix.length + 1);
|
|
14263
|
-
}
|
|
14436
|
+
const baseAgentName = stripSwarmPrefix(agentName, swarmPrefix);
|
|
14264
14437
|
return swarmAgents?.[baseAgentName]?.disabled === true;
|
|
14265
14438
|
}
|
|
14266
14439
|
function getTemperatureOverride(agentName, swarmAgents, swarmPrefix) {
|
|
14267
|
-
|
|
14268
|
-
if (swarmPrefix && agentName.startsWith(`${swarmPrefix}_`)) {
|
|
14269
|
-
baseAgentName = agentName.substring(swarmPrefix.length + 1);
|
|
14270
|
-
}
|
|
14440
|
+
const baseAgentName = stripSwarmPrefix(agentName, swarmPrefix);
|
|
14271
14441
|
return swarmAgents?.[baseAgentName]?.temperature;
|
|
14272
14442
|
}
|
|
14273
14443
|
function applyOverrides(agent, swarmAgents, swarmPrefix) {
|
|
@@ -14388,12 +14558,12 @@ function getAgentConfigs(config2) {
|
|
|
14388
14558
|
}
|
|
14389
14559
|
|
|
14390
14560
|
// src/commands/agents.ts
|
|
14391
|
-
function handleAgentsCommand(agents) {
|
|
14561
|
+
function handleAgentsCommand(agents, guardrails) {
|
|
14392
14562
|
const entries = Object.entries(agents);
|
|
14393
14563
|
if (entries.length === 0) {
|
|
14394
14564
|
return "No agents registered.";
|
|
14395
14565
|
}
|
|
14396
|
-
const lines = [
|
|
14566
|
+
const lines = [`## Registered Agents (${entries.length} total)`, ""];
|
|
14397
14567
|
for (const [key, agent] of entries) {
|
|
14398
14568
|
const model = agent.config.model || "default";
|
|
14399
14569
|
const temp = agent.config.temperature !== undefined ? agent.config.temperature.toString() : "default";
|
|
@@ -14401,43 +14571,46 @@ function handleAgentsCommand(agents) {
|
|
|
14401
14571
|
const isReadOnly = tools.write === false || tools.edit === false;
|
|
14402
14572
|
const access = isReadOnly ? "\uD83D\uDD12 read-only" : "\u270F\uFE0F read-write";
|
|
14403
14573
|
const desc = agent.description || agent.config.description || "";
|
|
14404
|
-
|
|
14574
|
+
const hasCustomProfile = guardrails?.profiles?.[key] !== undefined;
|
|
14575
|
+
const profileIndicator = hasCustomProfile ? " | \u26A1 custom limits" : "";
|
|
14576
|
+
lines.push(`- **${key}** | model: \`${model}\` | temp: ${temp} | ${access}${profileIndicator}`);
|
|
14405
14577
|
if (desc) {
|
|
14406
14578
|
lines.push(` ${desc}`);
|
|
14407
14579
|
}
|
|
14408
14580
|
}
|
|
14581
|
+
if (guardrails?.profiles && Object.keys(guardrails.profiles).length > 0) {
|
|
14582
|
+
lines.push("", "### Guardrail Profiles", "");
|
|
14583
|
+
for (const [profileName, profile] of Object.entries(guardrails.profiles)) {
|
|
14584
|
+
const overrides = [];
|
|
14585
|
+
if (profile.max_tool_calls !== undefined) {
|
|
14586
|
+
overrides.push(`max_tool_calls=${profile.max_tool_calls}`);
|
|
14587
|
+
}
|
|
14588
|
+
if (profile.max_duration_minutes !== undefined) {
|
|
14589
|
+
overrides.push(`max_duration_minutes=${profile.max_duration_minutes}`);
|
|
14590
|
+
}
|
|
14591
|
+
if (profile.max_repetitions !== undefined) {
|
|
14592
|
+
overrides.push(`max_repetitions=${profile.max_repetitions}`);
|
|
14593
|
+
}
|
|
14594
|
+
if (profile.max_consecutive_errors !== undefined) {
|
|
14595
|
+
overrides.push(`max_consecutive_errors=${profile.max_consecutive_errors}`);
|
|
14596
|
+
}
|
|
14597
|
+
if (profile.warning_threshold !== undefined) {
|
|
14598
|
+
overrides.push(`warning_threshold=${profile.warning_threshold}`);
|
|
14599
|
+
}
|
|
14600
|
+
const overrideStr = overrides.length > 0 ? overrides.join(", ") : "no overrides";
|
|
14601
|
+
lines.push(`- **${profileName}**: ${overrideStr}`);
|
|
14602
|
+
}
|
|
14603
|
+
}
|
|
14409
14604
|
return lines.join(`
|
|
14410
14605
|
`);
|
|
14411
14606
|
}
|
|
14412
14607
|
|
|
14413
|
-
// src/
|
|
14414
|
-
import
|
|
14415
|
-
import * as
|
|
14416
|
-
function getUserConfigDir2() {
|
|
14417
|
-
return process.env.XDG_CONFIG_HOME || path2.join(os2.homedir(), ".config");
|
|
14418
|
-
}
|
|
14419
|
-
async function handleConfigCommand(directory, _args) {
|
|
14420
|
-
const config2 = loadPluginConfig(directory);
|
|
14421
|
-
const userConfigPath = path2.join(getUserConfigDir2(), "opencode", "opencode-swarm.json");
|
|
14422
|
-
const projectConfigPath = path2.join(directory, ".opencode", "opencode-swarm.json");
|
|
14423
|
-
const lines = [
|
|
14424
|
-
"## Swarm Configuration",
|
|
14425
|
-
"",
|
|
14426
|
-
"### Config Files",
|
|
14427
|
-
`- User: \`${userConfigPath}\``,
|
|
14428
|
-
`- Project: \`${projectConfigPath}\``,
|
|
14429
|
-
"",
|
|
14430
|
-
"### Resolved Config",
|
|
14431
|
-
"```json",
|
|
14432
|
-
JSON.stringify(config2, null, 2),
|
|
14433
|
-
"```"
|
|
14434
|
-
];
|
|
14435
|
-
return lines.join(`
|
|
14436
|
-
`);
|
|
14437
|
-
}
|
|
14608
|
+
// src/evidence/manager.ts
|
|
14609
|
+
import { mkdirSync, readdirSync, renameSync, rmSync, statSync as statSync2 } from "fs";
|
|
14610
|
+
import * as path3 from "path";
|
|
14438
14611
|
|
|
14439
14612
|
// src/hooks/utils.ts
|
|
14440
|
-
import * as
|
|
14613
|
+
import * as path2 from "path";
|
|
14441
14614
|
|
|
14442
14615
|
// src/utils/errors.ts
|
|
14443
14616
|
class SwarmError extends Error {
|
|
@@ -14504,14 +14677,14 @@ function validateSwarmPath(directory, filename) {
|
|
|
14504
14677
|
if (/\.\.[/\\]/.test(filename)) {
|
|
14505
14678
|
throw new Error("Invalid filename: path traversal detected");
|
|
14506
14679
|
}
|
|
14507
|
-
const baseDir =
|
|
14508
|
-
const resolved =
|
|
14680
|
+
const baseDir = path2.normalize(path2.resolve(directory, ".swarm"));
|
|
14681
|
+
const resolved = path2.normalize(path2.resolve(baseDir, filename));
|
|
14509
14682
|
if (process.platform === "win32") {
|
|
14510
|
-
if (!resolved.toLowerCase().startsWith((baseDir +
|
|
14683
|
+
if (!resolved.toLowerCase().startsWith((baseDir + path2.sep).toLowerCase())) {
|
|
14511
14684
|
throw new Error("Invalid filename: path escapes .swarm directory");
|
|
14512
14685
|
}
|
|
14513
14686
|
} else {
|
|
14514
|
-
if (!resolved.startsWith(baseDir +
|
|
14687
|
+
if (!resolved.startsWith(baseDir + path2.sep)) {
|
|
14515
14688
|
throw new Error("Invalid filename: path escapes .swarm directory");
|
|
14516
14689
|
}
|
|
14517
14690
|
}
|
|
@@ -14534,8 +14707,744 @@ function estimateTokens(text) {
|
|
|
14534
14707
|
return Math.ceil(text.length * 0.33);
|
|
14535
14708
|
}
|
|
14536
14709
|
|
|
14710
|
+
// src/evidence/manager.ts
|
|
14711
|
+
var TASK_ID_REGEX = /^[\w-]+(\.[\w-]+)*$/;
|
|
14712
|
+
function sanitizeTaskId(taskId) {
|
|
14713
|
+
if (!taskId || taskId.length === 0) {
|
|
14714
|
+
throw new Error("Invalid task ID: empty string");
|
|
14715
|
+
}
|
|
14716
|
+
if (/\0/.test(taskId)) {
|
|
14717
|
+
throw new Error("Invalid task ID: contains null bytes");
|
|
14718
|
+
}
|
|
14719
|
+
for (let i = 0;i < taskId.length; i++) {
|
|
14720
|
+
if (taskId.charCodeAt(i) < 32) {
|
|
14721
|
+
throw new Error("Invalid task ID: contains control characters");
|
|
14722
|
+
}
|
|
14723
|
+
}
|
|
14724
|
+
if (taskId.includes("..") || taskId.includes("../") || taskId.includes("..\\")) {
|
|
14725
|
+
throw new Error("Invalid task ID: path traversal detected");
|
|
14726
|
+
}
|
|
14727
|
+
if (!TASK_ID_REGEX.test(taskId)) {
|
|
14728
|
+
throw new Error(`Invalid task ID: must match pattern ^[\\w-]+(\\.[\\w-]+)*$, got "${taskId}"`);
|
|
14729
|
+
}
|
|
14730
|
+
return taskId;
|
|
14731
|
+
}
|
|
14732
|
+
async function loadEvidence(directory, taskId) {
|
|
14733
|
+
const sanitizedTaskId = sanitizeTaskId(taskId);
|
|
14734
|
+
const relativePath = path3.join("evidence", sanitizedTaskId, "evidence.json");
|
|
14735
|
+
validateSwarmPath(directory, relativePath);
|
|
14736
|
+
const content = await readSwarmFileAsync(directory, relativePath);
|
|
14737
|
+
if (content === null) {
|
|
14738
|
+
return null;
|
|
14739
|
+
}
|
|
14740
|
+
try {
|
|
14741
|
+
const parsed = JSON.parse(content);
|
|
14742
|
+
const validated = EvidenceBundleSchema.parse(parsed);
|
|
14743
|
+
return validated;
|
|
14744
|
+
} catch (error49) {
|
|
14745
|
+
warn(`Evidence bundle validation failed for task ${sanitizedTaskId}: ${error49 instanceof Error ? error49.message : String(error49)}`);
|
|
14746
|
+
return null;
|
|
14747
|
+
}
|
|
14748
|
+
}
|
|
14749
|
+
async function listEvidenceTaskIds(directory) {
|
|
14750
|
+
const evidenceBasePath = validateSwarmPath(directory, "evidence");
|
|
14751
|
+
try {
|
|
14752
|
+
statSync2(evidenceBasePath);
|
|
14753
|
+
} catch {
|
|
14754
|
+
return [];
|
|
14755
|
+
}
|
|
14756
|
+
let entries;
|
|
14757
|
+
try {
|
|
14758
|
+
entries = readdirSync(evidenceBasePath);
|
|
14759
|
+
} catch {
|
|
14760
|
+
return [];
|
|
14761
|
+
}
|
|
14762
|
+
const taskIds = [];
|
|
14763
|
+
for (const entry of entries) {
|
|
14764
|
+
const entryPath = path3.join(evidenceBasePath, entry);
|
|
14765
|
+
try {
|
|
14766
|
+
const stats = statSync2(entryPath);
|
|
14767
|
+
if (!stats.isDirectory()) {
|
|
14768
|
+
continue;
|
|
14769
|
+
}
|
|
14770
|
+
sanitizeTaskId(entry);
|
|
14771
|
+
taskIds.push(entry);
|
|
14772
|
+
} catch (error49) {
|
|
14773
|
+
if (error49 instanceof Error && !error49.message.startsWith("Invalid task ID")) {
|
|
14774
|
+
warn(`Error reading evidence entry '${entry}': ${error49.message}`);
|
|
14775
|
+
}
|
|
14776
|
+
}
|
|
14777
|
+
}
|
|
14778
|
+
return taskIds.sort();
|
|
14779
|
+
}
|
|
14780
|
+
async function deleteEvidence(directory, taskId) {
|
|
14781
|
+
const sanitizedTaskId = sanitizeTaskId(taskId);
|
|
14782
|
+
const relativePath = path3.join("evidence", sanitizedTaskId);
|
|
14783
|
+
const evidenceDir = validateSwarmPath(directory, relativePath);
|
|
14784
|
+
try {
|
|
14785
|
+
statSync2(evidenceDir);
|
|
14786
|
+
} catch {
|
|
14787
|
+
return false;
|
|
14788
|
+
}
|
|
14789
|
+
try {
|
|
14790
|
+
rmSync(evidenceDir, { recursive: true, force: true });
|
|
14791
|
+
return true;
|
|
14792
|
+
} catch (error49) {
|
|
14793
|
+
warn(`Failed to delete evidence for task ${sanitizedTaskId}: ${error49 instanceof Error ? error49.message : String(error49)}`);
|
|
14794
|
+
return false;
|
|
14795
|
+
}
|
|
14796
|
+
}
|
|
14797
|
+
async function archiveEvidence(directory, maxAgeDays, maxBundles) {
|
|
14798
|
+
const taskIds = await listEvidenceTaskIds(directory);
|
|
14799
|
+
const cutoffDate = new Date;
|
|
14800
|
+
cutoffDate.setDate(cutoffDate.getDate() - maxAgeDays);
|
|
14801
|
+
const cutoffIso = cutoffDate.toISOString();
|
|
14802
|
+
const archived = [];
|
|
14803
|
+
const remainingBundles = [];
|
|
14804
|
+
for (const taskId of taskIds) {
|
|
14805
|
+
const bundle = await loadEvidence(directory, taskId);
|
|
14806
|
+
if (!bundle) {
|
|
14807
|
+
continue;
|
|
14808
|
+
}
|
|
14809
|
+
if (bundle.updated_at < cutoffIso) {
|
|
14810
|
+
const deleted = await deleteEvidence(directory, taskId);
|
|
14811
|
+
if (deleted) {
|
|
14812
|
+
archived.push(taskId);
|
|
14813
|
+
}
|
|
14814
|
+
} else {
|
|
14815
|
+
remainingBundles.push({
|
|
14816
|
+
taskId,
|
|
14817
|
+
updatedAt: bundle.updated_at
|
|
14818
|
+
});
|
|
14819
|
+
}
|
|
14820
|
+
}
|
|
14821
|
+
if (maxBundles !== undefined && remainingBundles.length > maxBundles) {
|
|
14822
|
+
remainingBundles.sort((a, b) => a.updatedAt.localeCompare(b.updatedAt));
|
|
14823
|
+
const toDelete = remainingBundles.length - maxBundles;
|
|
14824
|
+
for (let i = 0;i < toDelete; i++) {
|
|
14825
|
+
const deleted = await deleteEvidence(directory, remainingBundles[i].taskId);
|
|
14826
|
+
if (deleted) {
|
|
14827
|
+
archived.push(remainingBundles[i].taskId);
|
|
14828
|
+
}
|
|
14829
|
+
}
|
|
14830
|
+
}
|
|
14831
|
+
return archived;
|
|
14832
|
+
}
|
|
14833
|
+
|
|
14834
|
+
// src/commands/archive.ts
|
|
14835
|
+
async function handleArchiveCommand(directory, args) {
|
|
14836
|
+
const config2 = loadPluginConfig(directory);
|
|
14837
|
+
const maxAgeDays = config2?.evidence?.max_age_days ?? 90;
|
|
14838
|
+
const maxBundles = config2?.evidence?.max_bundles ?? 1000;
|
|
14839
|
+
const dryRun = args.includes("--dry-run");
|
|
14840
|
+
const beforeTaskIds = await listEvidenceTaskIds(directory);
|
|
14841
|
+
if (beforeTaskIds.length === 0) {
|
|
14842
|
+
return "No evidence bundles to archive.";
|
|
14843
|
+
}
|
|
14844
|
+
if (dryRun) {
|
|
14845
|
+
const cutoffDate = new Date;
|
|
14846
|
+
cutoffDate.setDate(cutoffDate.getDate() - maxAgeDays);
|
|
14847
|
+
const cutoffIso = cutoffDate.toISOString();
|
|
14848
|
+
const wouldArchiveAge = [];
|
|
14849
|
+
const remainingBundles = [];
|
|
14850
|
+
for (const taskId of beforeTaskIds) {
|
|
14851
|
+
const bundle = await loadEvidence(directory, taskId);
|
|
14852
|
+
if (bundle && bundle.updated_at < cutoffIso) {
|
|
14853
|
+
wouldArchiveAge.push(taskId);
|
|
14854
|
+
} else if (bundle) {
|
|
14855
|
+
remainingBundles.push({ taskId, updatedAt: bundle.updated_at });
|
|
14856
|
+
}
|
|
14857
|
+
}
|
|
14858
|
+
const wouldArchiveMaxBundles = [];
|
|
14859
|
+
const remainingAfterAge = beforeTaskIds.length - wouldArchiveAge.length;
|
|
14860
|
+
if (remainingAfterAge > maxBundles) {
|
|
14861
|
+
remainingBundles.sort((a, b) => a.updatedAt.localeCompare(b.updatedAt));
|
|
14862
|
+
const excessCount = remainingAfterAge - maxBundles;
|
|
14863
|
+
wouldArchiveMaxBundles.push(...remainingBundles.slice(0, excessCount).map((b) => b.taskId));
|
|
14864
|
+
}
|
|
14865
|
+
const totalWouldArchive = wouldArchiveAge.length + wouldArchiveMaxBundles.length;
|
|
14866
|
+
if (totalWouldArchive === 0) {
|
|
14867
|
+
return `No evidence bundles older than ${maxAgeDays} days found, and bundle count (${beforeTaskIds.length}) is within max_bundles limit (${maxBundles}).`;
|
|
14868
|
+
}
|
|
14869
|
+
const lines2 = [
|
|
14870
|
+
"## Archive Preview (dry run)",
|
|
14871
|
+
"",
|
|
14872
|
+
`**Retention**: ${maxAgeDays} days`,
|
|
14873
|
+
`**Max bundles**: ${maxBundles}`,
|
|
14874
|
+
`**Would archive**: ${totalWouldArchive} bundle(s)`
|
|
14875
|
+
];
|
|
14876
|
+
if (wouldArchiveAge.length > 0) {
|
|
14877
|
+
lines2.push("", `**Age-based (${wouldArchiveAge.length})**:`, ...wouldArchiveAge.map((id) => `- ${id}`));
|
|
14878
|
+
}
|
|
14879
|
+
if (wouldArchiveMaxBundles.length > 0) {
|
|
14880
|
+
lines2.push("", `**Max bundles limit (${wouldArchiveMaxBundles.length})**:`, ...wouldArchiveMaxBundles.map((id) => `- ${id}`));
|
|
14881
|
+
}
|
|
14882
|
+
return lines2.join(`
|
|
14883
|
+
`);
|
|
14884
|
+
}
|
|
14885
|
+
const archived = await archiveEvidence(directory, maxAgeDays, maxBundles);
|
|
14886
|
+
if (archived.length === 0) {
|
|
14887
|
+
return `No evidence bundles older than ${maxAgeDays} days found.`;
|
|
14888
|
+
}
|
|
14889
|
+
const lines = [
|
|
14890
|
+
"## Evidence Archived",
|
|
14891
|
+
"",
|
|
14892
|
+
`**Retention**: ${maxAgeDays} days`,
|
|
14893
|
+
`**Archived**: ${archived.length} bundle(s)`,
|
|
14894
|
+
"",
|
|
14895
|
+
...archived.map((id) => `- ${id}`)
|
|
14896
|
+
];
|
|
14897
|
+
return lines.join(`
|
|
14898
|
+
`);
|
|
14899
|
+
}
|
|
14900
|
+
|
|
14901
|
+
// src/commands/config.ts
|
|
14902
|
+
import * as os2 from "os";
|
|
14903
|
+
import * as path4 from "path";
|
|
14904
|
+
function getUserConfigDir2() {
|
|
14905
|
+
return process.env.XDG_CONFIG_HOME || path4.join(os2.homedir(), ".config");
|
|
14906
|
+
}
|
|
14907
|
+
async function handleConfigCommand(directory, _args) {
|
|
14908
|
+
const config2 = loadPluginConfig(directory);
|
|
14909
|
+
const userConfigPath = path4.join(getUserConfigDir2(), "opencode", "opencode-swarm.json");
|
|
14910
|
+
const projectConfigPath = path4.join(directory, ".opencode", "opencode-swarm.json");
|
|
14911
|
+
const lines = [
|
|
14912
|
+
"## Swarm Configuration",
|
|
14913
|
+
"",
|
|
14914
|
+
"### Config Files",
|
|
14915
|
+
`- User: \`${userConfigPath}\``,
|
|
14916
|
+
`- Project: \`${projectConfigPath}\``,
|
|
14917
|
+
"",
|
|
14918
|
+
"### Resolved Config",
|
|
14919
|
+
"```json",
|
|
14920
|
+
JSON.stringify(config2, null, 2),
|
|
14921
|
+
"```"
|
|
14922
|
+
];
|
|
14923
|
+
return lines.join(`
|
|
14924
|
+
`);
|
|
14925
|
+
}
|
|
14926
|
+
|
|
14927
|
+
// src/plan/manager.ts
|
|
14928
|
+
import * as path5 from "path";
|
|
14929
|
+
async function loadPlanJsonOnly(directory) {
|
|
14930
|
+
const planJsonContent = await readSwarmFileAsync(directory, "plan.json");
|
|
14931
|
+
if (planJsonContent !== null) {
|
|
14932
|
+
try {
|
|
14933
|
+
const parsed = JSON.parse(planJsonContent);
|
|
14934
|
+
const validated = PlanSchema.parse(parsed);
|
|
14935
|
+
return validated;
|
|
14936
|
+
} catch (error49) {
|
|
14937
|
+
warn(`Plan validation failed for .swarm/plan.json: ${error49 instanceof Error ? error49.message : String(error49)}`);
|
|
14938
|
+
}
|
|
14939
|
+
}
|
|
14940
|
+
return null;
|
|
14941
|
+
}
|
|
14942
|
+
async function loadPlan(directory) {
|
|
14943
|
+
const planJsonContent = await readSwarmFileAsync(directory, "plan.json");
|
|
14944
|
+
if (planJsonContent !== null) {
|
|
14945
|
+
try {
|
|
14946
|
+
const parsed = JSON.parse(planJsonContent);
|
|
14947
|
+
const validated = PlanSchema.parse(parsed);
|
|
14948
|
+
return validated;
|
|
14949
|
+
} catch (error49) {
|
|
14950
|
+
warn(`Plan validation failed for .swarm/plan.json: ${error49 instanceof Error ? error49.message : String(error49)}`);
|
|
14951
|
+
}
|
|
14952
|
+
}
|
|
14953
|
+
const planMdContent = await readSwarmFileAsync(directory, "plan.md");
|
|
14954
|
+
if (planMdContent !== null) {
|
|
14955
|
+
const migrated = migrateLegacyPlan(planMdContent);
|
|
14956
|
+
await savePlan(directory, migrated);
|
|
14957
|
+
return migrated;
|
|
14958
|
+
}
|
|
14959
|
+
return null;
|
|
14960
|
+
}
|
|
14961
|
+
async function savePlan(directory, plan) {
|
|
14962
|
+
const validated = PlanSchema.parse(plan);
|
|
14963
|
+
const swarmDir = path5.resolve(directory, ".swarm");
|
|
14964
|
+
const planPath = path5.join(swarmDir, "plan.json");
|
|
14965
|
+
const tempPath = path5.join(swarmDir, `plan.json.tmp.${Date.now()}`);
|
|
14966
|
+
await Bun.write(tempPath, JSON.stringify(validated, null, 2));
|
|
14967
|
+
const { renameSync: renameSync2 } = await import("fs");
|
|
14968
|
+
renameSync2(tempPath, planPath);
|
|
14969
|
+
const markdown = derivePlanMarkdown(validated);
|
|
14970
|
+
await Bun.write(path5.join(swarmDir, "plan.md"), markdown);
|
|
14971
|
+
}
|
|
14972
|
+
function derivePlanMarkdown(plan) {
|
|
14973
|
+
const statusMap = {
|
|
14974
|
+
pending: "PENDING",
|
|
14975
|
+
in_progress: "IN PROGRESS",
|
|
14976
|
+
complete: "COMPLETE",
|
|
14977
|
+
blocked: "BLOCKED"
|
|
14978
|
+
};
|
|
14979
|
+
const now = new Date().toISOString();
|
|
14980
|
+
const phaseStatus = statusMap[plan.phases[plan.current_phase - 1]?.status] || "PENDING";
|
|
14981
|
+
let markdown = `# ${plan.title}
|
|
14982
|
+
Swarm: ${plan.swarm}
|
|
14983
|
+
Phase: ${plan.current_phase} [${phaseStatus}] | Updated: ${now}
|
|
14984
|
+
`;
|
|
14985
|
+
for (const phase of plan.phases) {
|
|
14986
|
+
const phaseStatusText = statusMap[phase.status] || "PENDING";
|
|
14987
|
+
markdown += `
|
|
14988
|
+
## Phase ${phase.id}: ${phase.name} [${phaseStatusText}]
|
|
14989
|
+
`;
|
|
14990
|
+
let currentTaskMarked = false;
|
|
14991
|
+
for (const task of phase.tasks) {
|
|
14992
|
+
let taskLine = "";
|
|
14993
|
+
let suffix = "";
|
|
14994
|
+
if (task.status === "completed") {
|
|
14995
|
+
taskLine = `- [x] ${task.id}: ${task.description}`;
|
|
14996
|
+
} else if (task.status === "blocked") {
|
|
14997
|
+
taskLine = `- [BLOCKED] ${task.id}: ${task.description}`;
|
|
14998
|
+
if (task.blocked_reason) {
|
|
14999
|
+
taskLine += ` - ${task.blocked_reason}`;
|
|
15000
|
+
}
|
|
15001
|
+
} else {
|
|
15002
|
+
taskLine = `- [ ] ${task.id}: ${task.description}`;
|
|
15003
|
+
}
|
|
15004
|
+
taskLine += ` [${task.size.toUpperCase()}]`;
|
|
15005
|
+
if (task.depends.length > 0) {
|
|
15006
|
+
suffix += ` (depends: ${task.depends.join(", ")})`;
|
|
15007
|
+
}
|
|
15008
|
+
if (phase.id === plan.current_phase && task.status === "in_progress" && !currentTaskMarked) {
|
|
15009
|
+
suffix += " \u2190 CURRENT";
|
|
15010
|
+
currentTaskMarked = true;
|
|
15011
|
+
}
|
|
15012
|
+
markdown += `${taskLine}${suffix}
|
|
15013
|
+
`;
|
|
15014
|
+
}
|
|
15015
|
+
}
|
|
15016
|
+
const phaseSections = markdown.split(`
|
|
15017
|
+
## `);
|
|
15018
|
+
if (phaseSections.length > 1) {
|
|
15019
|
+
const header = phaseSections[0];
|
|
15020
|
+
const phases = phaseSections.slice(1).map((p) => `## ${p}`);
|
|
15021
|
+
markdown = `${header}
|
|
15022
|
+
---
|
|
15023
|
+
${phases.join(`
|
|
15024
|
+
---
|
|
15025
|
+
`)}`;
|
|
15026
|
+
}
|
|
15027
|
+
return `${markdown.trim()}
|
|
15028
|
+
`;
|
|
15029
|
+
}
|
|
15030
|
+
function migrateLegacyPlan(planContent, swarmId) {
|
|
15031
|
+
const lines = planContent.split(`
|
|
15032
|
+
`);
|
|
15033
|
+
let title = "Untitled Plan";
|
|
15034
|
+
let swarm = swarmId || "default-swarm";
|
|
15035
|
+
let currentPhaseNum = 1;
|
|
15036
|
+
const phases = [];
|
|
15037
|
+
let currentPhase = null;
|
|
15038
|
+
for (const line of lines) {
|
|
15039
|
+
const trimmed = line.trim();
|
|
15040
|
+
if (trimmed.startsWith("# ") && title === "Untitled Plan") {
|
|
15041
|
+
title = trimmed.substring(2).trim();
|
|
15042
|
+
continue;
|
|
15043
|
+
}
|
|
15044
|
+
if (trimmed.startsWith("Swarm:")) {
|
|
15045
|
+
swarm = trimmed.substring(6).trim();
|
|
15046
|
+
continue;
|
|
15047
|
+
}
|
|
15048
|
+
if (trimmed.startsWith("Phase:")) {
|
|
15049
|
+
const match = trimmed.match(/Phase:\s*(\d+)/i);
|
|
15050
|
+
if (match) {
|
|
15051
|
+
currentPhaseNum = parseInt(match[1], 10);
|
|
15052
|
+
}
|
|
15053
|
+
continue;
|
|
15054
|
+
}
|
|
15055
|
+
const phaseMatch = trimmed.match(/^##\s*Phase\s+(\d+)(?::\s*([^[]+))?\s*(?:\[([^\]]+)\])?/i);
|
|
15056
|
+
if (phaseMatch) {
|
|
15057
|
+
if (currentPhase !== null) {
|
|
15058
|
+
phases.push(currentPhase);
|
|
15059
|
+
}
|
|
15060
|
+
const phaseId = parseInt(phaseMatch[1], 10);
|
|
15061
|
+
const phaseName = phaseMatch[2]?.trim() || `Phase ${phaseId}`;
|
|
15062
|
+
const statusText = phaseMatch[3]?.toLowerCase() || "pending";
|
|
15063
|
+
const statusMap = {
|
|
15064
|
+
complete: "complete",
|
|
15065
|
+
completed: "complete",
|
|
15066
|
+
"in progress": "in_progress",
|
|
15067
|
+
in_progress: "in_progress",
|
|
15068
|
+
inprogress: "in_progress",
|
|
15069
|
+
pending: "pending",
|
|
15070
|
+
blocked: "blocked"
|
|
15071
|
+
};
|
|
15072
|
+
currentPhase = {
|
|
15073
|
+
id: phaseId,
|
|
15074
|
+
name: phaseName,
|
|
15075
|
+
status: statusMap[statusText] || "pending",
|
|
15076
|
+
tasks: []
|
|
15077
|
+
};
|
|
15078
|
+
continue;
|
|
15079
|
+
}
|
|
15080
|
+
const taskMatch = trimmed.match(/^-\s*\[([^\]]+)\]\s+(\d+\.\d+):\s*(.+?)(?:\s*\[(\w+)\])?(?:\s*-\s*(.+))?$/i);
|
|
15081
|
+
if (taskMatch && currentPhase !== null) {
|
|
15082
|
+
const checkbox = taskMatch[1].toLowerCase();
|
|
15083
|
+
const taskId = taskMatch[2];
|
|
15084
|
+
let description = taskMatch[3].trim();
|
|
15085
|
+
const sizeText = taskMatch[4]?.toLowerCase() || "small";
|
|
15086
|
+
let blockedReason;
|
|
15087
|
+
const dependsMatch = description.match(/\s*\(depends:\s*([^)]+)\)$/i);
|
|
15088
|
+
const depends = [];
|
|
15089
|
+
if (dependsMatch) {
|
|
15090
|
+
const depsText = dependsMatch[1];
|
|
15091
|
+
depends.push(...depsText.split(",").map((d) => d.trim()));
|
|
15092
|
+
description = description.substring(0, dependsMatch.index).trim();
|
|
15093
|
+
}
|
|
15094
|
+
let status = "pending";
|
|
15095
|
+
if (checkbox === "x") {
|
|
15096
|
+
status = "completed";
|
|
15097
|
+
} else if (checkbox === "blocked") {
|
|
15098
|
+
status = "blocked";
|
|
15099
|
+
const blockedReasonMatch = taskMatch[5];
|
|
15100
|
+
if (blockedReasonMatch) {
|
|
15101
|
+
blockedReason = blockedReasonMatch.trim();
|
|
15102
|
+
}
|
|
15103
|
+
}
|
|
15104
|
+
const sizeMap = {
|
|
15105
|
+
small: "small",
|
|
15106
|
+
medium: "medium",
|
|
15107
|
+
large: "large"
|
|
15108
|
+
};
|
|
15109
|
+
const task = {
|
|
15110
|
+
id: taskId,
|
|
15111
|
+
phase: currentPhase.id,
|
|
15112
|
+
status,
|
|
15113
|
+
size: sizeMap[sizeText] || "small",
|
|
15114
|
+
description,
|
|
15115
|
+
depends,
|
|
15116
|
+
acceptance: undefined,
|
|
15117
|
+
files_touched: [],
|
|
15118
|
+
evidence_path: undefined,
|
|
15119
|
+
blocked_reason: blockedReason
|
|
15120
|
+
};
|
|
15121
|
+
currentPhase.tasks.push(task);
|
|
15122
|
+
}
|
|
15123
|
+
}
|
|
15124
|
+
if (currentPhase !== null) {
|
|
15125
|
+
phases.push(currentPhase);
|
|
15126
|
+
}
|
|
15127
|
+
let migrationStatus = "migrated";
|
|
15128
|
+
if (phases.length === 0) {
|
|
15129
|
+
migrationStatus = "migration_failed";
|
|
15130
|
+
phases.push({
|
|
15131
|
+
id: 1,
|
|
15132
|
+
name: "Migration Failed",
|
|
15133
|
+
status: "blocked",
|
|
15134
|
+
tasks: [
|
|
15135
|
+
{
|
|
15136
|
+
id: "1.1",
|
|
15137
|
+
phase: 1,
|
|
15138
|
+
status: "blocked",
|
|
15139
|
+
size: "large",
|
|
15140
|
+
description: "Review and restructure plan manually",
|
|
15141
|
+
depends: [],
|
|
15142
|
+
files_touched: [],
|
|
15143
|
+
blocked_reason: "Legacy plan could not be parsed automatically"
|
|
15144
|
+
}
|
|
15145
|
+
]
|
|
15146
|
+
});
|
|
15147
|
+
}
|
|
15148
|
+
phases.sort((a, b) => a.id - b.id);
|
|
15149
|
+
const plan = {
|
|
15150
|
+
schema_version: "1.0.0",
|
|
15151
|
+
title,
|
|
15152
|
+
swarm,
|
|
15153
|
+
current_phase: currentPhaseNum,
|
|
15154
|
+
phases,
|
|
15155
|
+
migration_status: migrationStatus
|
|
15156
|
+
};
|
|
15157
|
+
return plan;
|
|
15158
|
+
}
|
|
15159
|
+
|
|
15160
|
+
// src/commands/diagnose.ts
|
|
15161
|
+
async function handleDiagnoseCommand(directory, _args) {
|
|
15162
|
+
const checks3 = [];
|
|
15163
|
+
const plan = await loadPlanJsonOnly(directory);
|
|
15164
|
+
if (plan) {
|
|
15165
|
+
checks3.push({
|
|
15166
|
+
name: "plan.json",
|
|
15167
|
+
status: "\u2705",
|
|
15168
|
+
detail: "Valid schema (v1.0.0)"
|
|
15169
|
+
});
|
|
15170
|
+
if (plan.migration_status === "migrated") {
|
|
15171
|
+
checks3.push({
|
|
15172
|
+
name: "Migration",
|
|
15173
|
+
status: "\u2705",
|
|
15174
|
+
detail: "Plan was migrated from legacy plan.md"
|
|
15175
|
+
});
|
|
15176
|
+
} else if (plan.migration_status === "migration_failed") {
|
|
15177
|
+
checks3.push({
|
|
15178
|
+
name: "Migration",
|
|
15179
|
+
status: "\u274C",
|
|
15180
|
+
detail: "Migration from plan.md failed \u2014 review manually"
|
|
15181
|
+
});
|
|
15182
|
+
}
|
|
15183
|
+
const allTaskIds = new Set;
|
|
15184
|
+
for (const phase of plan.phases) {
|
|
15185
|
+
for (const task of phase.tasks) {
|
|
15186
|
+
allTaskIds.add(task.id);
|
|
15187
|
+
}
|
|
15188
|
+
}
|
|
15189
|
+
const missingDeps = [];
|
|
15190
|
+
for (const phase of plan.phases) {
|
|
15191
|
+
for (const task of phase.tasks) {
|
|
15192
|
+
for (const dep of task.depends) {
|
|
15193
|
+
if (!allTaskIds.has(dep)) {
|
|
15194
|
+
missingDeps.push(`${task.id} depends on missing ${dep}`);
|
|
15195
|
+
}
|
|
15196
|
+
}
|
|
15197
|
+
}
|
|
15198
|
+
}
|
|
15199
|
+
if (missingDeps.length > 0) {
|
|
15200
|
+
checks3.push({
|
|
15201
|
+
name: "Task DAG",
|
|
15202
|
+
status: "\u274C",
|
|
15203
|
+
detail: `Missing dependencies: ${missingDeps.join(", ")}`
|
|
15204
|
+
});
|
|
15205
|
+
} else {
|
|
15206
|
+
checks3.push({
|
|
15207
|
+
name: "Task DAG",
|
|
15208
|
+
status: "\u2705",
|
|
15209
|
+
detail: "All dependencies resolved"
|
|
15210
|
+
});
|
|
15211
|
+
}
|
|
15212
|
+
} else {
|
|
15213
|
+
const planContent = await readSwarmFileAsync(directory, "plan.md");
|
|
15214
|
+
if (planContent) {
|
|
15215
|
+
const hasPhases = /^## Phase \d+/m.test(planContent);
|
|
15216
|
+
const hasTasks = /^- \[[ x]\]/m.test(planContent);
|
|
15217
|
+
if (hasPhases && hasTasks) {
|
|
15218
|
+
checks3.push({
|
|
15219
|
+
name: "plan.md",
|
|
15220
|
+
status: "\u2705",
|
|
15221
|
+
detail: "Found with valid phase structure"
|
|
15222
|
+
});
|
|
15223
|
+
} else {
|
|
15224
|
+
checks3.push({
|
|
15225
|
+
name: "plan.md",
|
|
15226
|
+
status: "\u274C",
|
|
15227
|
+
detail: "Found but missing phase/task structure"
|
|
15228
|
+
});
|
|
15229
|
+
}
|
|
15230
|
+
} else {
|
|
15231
|
+
checks3.push({
|
|
15232
|
+
name: "plan.md",
|
|
15233
|
+
status: "\u274C",
|
|
15234
|
+
detail: "Not found"
|
|
15235
|
+
});
|
|
15236
|
+
}
|
|
15237
|
+
}
|
|
15238
|
+
const contextContent = await readSwarmFileAsync(directory, "context.md");
|
|
15239
|
+
if (contextContent) {
|
|
15240
|
+
checks3.push({ name: "context.md", status: "\u2705", detail: "Found" });
|
|
15241
|
+
} else {
|
|
15242
|
+
checks3.push({ name: "context.md", status: "\u274C", detail: "Not found" });
|
|
15243
|
+
}
|
|
15244
|
+
try {
|
|
15245
|
+
const config2 = loadPluginConfig(directory);
|
|
15246
|
+
if (config2) {
|
|
15247
|
+
checks3.push({
|
|
15248
|
+
name: "Plugin config",
|
|
15249
|
+
status: "\u2705",
|
|
15250
|
+
detail: "Valid configuration loaded"
|
|
15251
|
+
});
|
|
15252
|
+
} else {
|
|
15253
|
+
checks3.push({
|
|
15254
|
+
name: "Plugin config",
|
|
15255
|
+
status: "\u2705",
|
|
15256
|
+
detail: "Using defaults (no custom config)"
|
|
15257
|
+
});
|
|
15258
|
+
}
|
|
15259
|
+
} catch {
|
|
15260
|
+
checks3.push({
|
|
15261
|
+
name: "Plugin config",
|
|
15262
|
+
status: "\u274C",
|
|
15263
|
+
detail: "Invalid configuration"
|
|
15264
|
+
});
|
|
15265
|
+
}
|
|
15266
|
+
if (plan) {
|
|
15267
|
+
const completedTaskIds = [];
|
|
15268
|
+
for (const phase of plan.phases) {
|
|
15269
|
+
for (const task of phase.tasks) {
|
|
15270
|
+
if (task.status === "completed") {
|
|
15271
|
+
completedTaskIds.push(task.id);
|
|
15272
|
+
}
|
|
15273
|
+
}
|
|
15274
|
+
}
|
|
15275
|
+
if (completedTaskIds.length > 0) {
|
|
15276
|
+
const evidenceTaskIds = new Set(await listEvidenceTaskIds(directory));
|
|
15277
|
+
const missingEvidence = completedTaskIds.filter((id) => !evidenceTaskIds.has(id));
|
|
15278
|
+
if (missingEvidence.length === 0) {
|
|
15279
|
+
checks3.push({
|
|
15280
|
+
name: "Evidence",
|
|
15281
|
+
status: "\u2705",
|
|
15282
|
+
detail: `All ${completedTaskIds.length} completed tasks have evidence`
|
|
15283
|
+
});
|
|
15284
|
+
} else {
|
|
15285
|
+
checks3.push({
|
|
15286
|
+
name: "Evidence",
|
|
15287
|
+
status: "\u274C",
|
|
15288
|
+
detail: `${missingEvidence.length} completed task(s) missing evidence: ${missingEvidence.join(", ")}`
|
|
15289
|
+
});
|
|
15290
|
+
}
|
|
15291
|
+
} else {
|
|
15292
|
+
checks3.push({
|
|
15293
|
+
name: "Evidence",
|
|
15294
|
+
status: "\u2705",
|
|
15295
|
+
detail: "No completed tasks yet"
|
|
15296
|
+
});
|
|
15297
|
+
}
|
|
15298
|
+
}
|
|
15299
|
+
const passCount = checks3.filter((c) => c.status === "\u2705").length;
|
|
15300
|
+
const totalCount = checks3.length;
|
|
15301
|
+
const allPassed = passCount === totalCount;
|
|
15302
|
+
const lines = [
|
|
15303
|
+
"## Swarm Health Check",
|
|
15304
|
+
"",
|
|
15305
|
+
...checks3.map((c) => `- ${c.status} **${c.name}**: ${c.detail}`),
|
|
15306
|
+
"",
|
|
15307
|
+
`**Result**: ${allPassed ? "\u2705 All checks passed" : `\u26A0\uFE0F ${passCount}/${totalCount} checks passed`}`
|
|
15308
|
+
];
|
|
15309
|
+
return lines.join(`
|
|
15310
|
+
`);
|
|
15311
|
+
}
|
|
15312
|
+
|
|
15313
|
+
// src/commands/evidence.ts
|
|
15314
|
+
async function handleEvidenceCommand(directory, args) {
|
|
15315
|
+
if (args.length === 0) {
|
|
15316
|
+
const taskIds = await listEvidenceTaskIds(directory);
|
|
15317
|
+
if (taskIds.length === 0) {
|
|
15318
|
+
return "No evidence bundles found.";
|
|
15319
|
+
}
|
|
15320
|
+
const tableLines = [
|
|
15321
|
+
"## Evidence Bundles",
|
|
15322
|
+
"",
|
|
15323
|
+
"| Task | Entries | Last Updated |",
|
|
15324
|
+
"|------|---------|-------------|"
|
|
15325
|
+
];
|
|
15326
|
+
for (const taskId2 of taskIds) {
|
|
15327
|
+
const bundle2 = await loadEvidence(directory, taskId2);
|
|
15328
|
+
if (bundle2) {
|
|
15329
|
+
const entryCount = bundle2.entries.length;
|
|
15330
|
+
const lastUpdated = bundle2.updated_at;
|
|
15331
|
+
tableLines.push(`| ${taskId2} | ${entryCount} | ${lastUpdated} |`);
|
|
15332
|
+
} else {
|
|
15333
|
+
tableLines.push(`| ${taskId2} | ? | unknown |`);
|
|
15334
|
+
}
|
|
15335
|
+
}
|
|
15336
|
+
return tableLines.join(`
|
|
15337
|
+
`);
|
|
15338
|
+
}
|
|
15339
|
+
const taskId = args[0];
|
|
15340
|
+
const bundle = await loadEvidence(directory, taskId);
|
|
15341
|
+
if (!bundle) {
|
|
15342
|
+
return `No evidence found for task ${taskId}.`;
|
|
15343
|
+
}
|
|
15344
|
+
const lines = [
|
|
15345
|
+
`## Evidence for Task ${taskId}`,
|
|
15346
|
+
"",
|
|
15347
|
+
`**Created**: ${bundle.created_at}`,
|
|
15348
|
+
`**Updated**: ${bundle.updated_at}`,
|
|
15349
|
+
`**Entries**: ${bundle.entries.length}`
|
|
15350
|
+
];
|
|
15351
|
+
if (bundle.entries.length > 0) {
|
|
15352
|
+
lines.push("");
|
|
15353
|
+
}
|
|
15354
|
+
for (let i = 0;i < bundle.entries.length; i++) {
|
|
15355
|
+
const entry = bundle.entries[i];
|
|
15356
|
+
lines.push(...formatEntry(i + 1, entry));
|
|
15357
|
+
}
|
|
15358
|
+
return lines.join(`
|
|
15359
|
+
`);
|
|
15360
|
+
}
|
|
15361
|
+
function formatEntry(index, entry) {
|
|
15362
|
+
const lines = [];
|
|
15363
|
+
const verdictEmoji = getVerdictEmoji(entry.verdict);
|
|
15364
|
+
lines.push(`### Entry ${index}: ${entry.type} (${entry.verdict}) ${verdictEmoji}`);
|
|
15365
|
+
lines.push(`- **Agent**: ${entry.agent}`);
|
|
15366
|
+
lines.push(`- **Summary**: ${entry.summary}`);
|
|
15367
|
+
lines.push(`- **Time**: ${entry.timestamp}`);
|
|
15368
|
+
if (entry.type === "review") {
|
|
15369
|
+
const reviewEntry = entry;
|
|
15370
|
+
lines.push(`- **Risk Level**: ${reviewEntry.risk}`);
|
|
15371
|
+
if (reviewEntry.issues && reviewEntry.issues.length > 0) {
|
|
15372
|
+
lines.push(`- **Issues**: ${reviewEntry.issues.length}`);
|
|
15373
|
+
}
|
|
15374
|
+
} else if (entry.type === "test") {
|
|
15375
|
+
const testEntry = entry;
|
|
15376
|
+
lines.push(`- **Tests**: ${testEntry.tests_passed} passed, ${testEntry.tests_failed} failed`);
|
|
15377
|
+
}
|
|
15378
|
+
lines.push("");
|
|
15379
|
+
return lines;
|
|
15380
|
+
}
|
|
15381
|
+
function getVerdictEmoji(verdict) {
|
|
15382
|
+
switch (verdict) {
|
|
15383
|
+
case "pass":
|
|
15384
|
+
case "approved":
|
|
15385
|
+
return "\u2705";
|
|
15386
|
+
case "fail":
|
|
15387
|
+
case "rejected":
|
|
15388
|
+
return "\u274C";
|
|
15389
|
+
case "info":
|
|
15390
|
+
return "\u2139\uFE0F";
|
|
15391
|
+
default:
|
|
15392
|
+
return "";
|
|
15393
|
+
}
|
|
15394
|
+
}
|
|
15395
|
+
|
|
15396
|
+
// src/commands/export.ts
|
|
15397
|
+
async function handleExportCommand(directory, _args) {
|
|
15398
|
+
const planStructured = await loadPlanJsonOnly(directory);
|
|
15399
|
+
const planContent = await readSwarmFileAsync(directory, "plan.md");
|
|
15400
|
+
const contextContent = await readSwarmFileAsync(directory, "context.md");
|
|
15401
|
+
const exportData = {
|
|
15402
|
+
version: "4.5.0",
|
|
15403
|
+
exported: new Date().toISOString(),
|
|
15404
|
+
plan: planStructured || planContent,
|
|
15405
|
+
context: contextContent
|
|
15406
|
+
};
|
|
15407
|
+
const lines = [
|
|
15408
|
+
"## Swarm Export",
|
|
15409
|
+
"",
|
|
15410
|
+
"```json",
|
|
15411
|
+
JSON.stringify(exportData, null, 2),
|
|
15412
|
+
"```"
|
|
15413
|
+
];
|
|
15414
|
+
return lines.join(`
|
|
15415
|
+
`);
|
|
15416
|
+
}
|
|
15417
|
+
|
|
14537
15418
|
// src/commands/history.ts
|
|
14538
15419
|
async function handleHistoryCommand(directory, _args) {
|
|
15420
|
+
const plan = await loadPlanJsonOnly(directory);
|
|
15421
|
+
if (plan) {
|
|
15422
|
+
if (plan.phases.length === 0) {
|
|
15423
|
+
return "No history available.";
|
|
15424
|
+
}
|
|
15425
|
+
const tableLines2 = [
|
|
15426
|
+
"## Swarm History",
|
|
15427
|
+
"",
|
|
15428
|
+
"| Phase | Name | Status | Tasks |",
|
|
15429
|
+
"|-------|------|--------|-------|"
|
|
15430
|
+
];
|
|
15431
|
+
for (const phase of plan.phases) {
|
|
15432
|
+
const statusMap = {
|
|
15433
|
+
complete: "COMPLETE",
|
|
15434
|
+
in_progress: "IN PROGRESS",
|
|
15435
|
+
pending: "PENDING",
|
|
15436
|
+
blocked: "BLOCKED"
|
|
15437
|
+
};
|
|
15438
|
+
const statusText = statusMap[phase.status] || "PENDING";
|
|
15439
|
+
const statusIcon = phase.status === "complete" ? "\u2705" : phase.status === "in_progress" ? "\uD83D\uDD04" : phase.status === "blocked" ? "\uD83D\uDEAB" : "\u23F3";
|
|
15440
|
+
const completed = phase.tasks.filter((t) => t.status === "completed").length;
|
|
15441
|
+
const total = phase.tasks.length;
|
|
15442
|
+
const tasks = total > 0 ? `${completed}/${total}` : "-";
|
|
15443
|
+
tableLines2.push(`| ${phase.id} | ${phase.name} | ${statusIcon} ${statusText} | ${tasks} |`);
|
|
15444
|
+
}
|
|
15445
|
+
return tableLines2.join(`
|
|
15446
|
+
`);
|
|
15447
|
+
}
|
|
14539
15448
|
const planContent = await readSwarmFileAsync(directory, "plan.md");
|
|
14540
15449
|
if (!planContent) {
|
|
14541
15450
|
return "No history available.";
|
|
@@ -14587,6 +15496,46 @@ async function handleHistoryCommand(directory, _args) {
|
|
|
14587
15496
|
|
|
14588
15497
|
// src/commands/plan.ts
|
|
14589
15498
|
async function handlePlanCommand(directory, args) {
|
|
15499
|
+
const plan = await loadPlanJsonOnly(directory);
|
|
15500
|
+
if (plan) {
|
|
15501
|
+
if (args.length === 0) {
|
|
15502
|
+
return derivePlanMarkdown(plan);
|
|
15503
|
+
}
|
|
15504
|
+
const phaseNum2 = parseInt(args[0], 10);
|
|
15505
|
+
if (Number.isNaN(phaseNum2)) {
|
|
15506
|
+
return derivePlanMarkdown(plan);
|
|
15507
|
+
}
|
|
15508
|
+
const phase = plan.phases.find((p) => p.id === phaseNum2);
|
|
15509
|
+
if (!phase) {
|
|
15510
|
+
return `Phase ${phaseNum2} not found in plan.`;
|
|
15511
|
+
}
|
|
15512
|
+
const fullMarkdown = derivePlanMarkdown(plan);
|
|
15513
|
+
const lines2 = fullMarkdown.split(`
|
|
15514
|
+
`);
|
|
15515
|
+
const phaseLines2 = [];
|
|
15516
|
+
let inTargetPhase2 = false;
|
|
15517
|
+
for (const line of lines2) {
|
|
15518
|
+
const phaseMatch = line.match(/^## Phase (\d+)/);
|
|
15519
|
+
if (phaseMatch) {
|
|
15520
|
+
const num = parseInt(phaseMatch[1], 10);
|
|
15521
|
+
if (num === phaseNum2) {
|
|
15522
|
+
inTargetPhase2 = true;
|
|
15523
|
+
phaseLines2.push(line);
|
|
15524
|
+
continue;
|
|
15525
|
+
} else if (inTargetPhase2) {
|
|
15526
|
+
break;
|
|
15527
|
+
}
|
|
15528
|
+
}
|
|
15529
|
+
if (inTargetPhase2 && line.trim() === "---" && phaseLines2.length > 1) {
|
|
15530
|
+
break;
|
|
15531
|
+
}
|
|
15532
|
+
if (inTargetPhase2) {
|
|
15533
|
+
phaseLines2.push(line);
|
|
15534
|
+
}
|
|
15535
|
+
}
|
|
15536
|
+
return phaseLines2.length > 0 ? phaseLines2.join(`
|
|
15537
|
+
`).trim() : `Phase ${phaseNum2} not found in plan.`;
|
|
15538
|
+
}
|
|
14590
15539
|
const planContent = await readSwarmFileAsync(directory, "plan.md");
|
|
14591
15540
|
if (!planContent) {
|
|
14592
15541
|
return "No active swarm plan found.";
|
|
@@ -14628,6 +15577,47 @@ async function handlePlanCommand(directory, args) {
|
|
|
14628
15577
|
`).trim();
|
|
14629
15578
|
}
|
|
14630
15579
|
|
|
15580
|
+
// src/commands/reset.ts
|
|
15581
|
+
import * as fs2 from "fs";
|
|
15582
|
+
async function handleResetCommand(directory, args) {
|
|
15583
|
+
const hasConfirm = args.includes("--confirm");
|
|
15584
|
+
if (!hasConfirm) {
|
|
15585
|
+
return [
|
|
15586
|
+
"## Swarm Reset",
|
|
15587
|
+
"",
|
|
15588
|
+
"\u26A0\uFE0F This will delete plan.md and context.md from .swarm/",
|
|
15589
|
+
"",
|
|
15590
|
+
"**Tip**: Run `/swarm export` first to backup your state.",
|
|
15591
|
+
"",
|
|
15592
|
+
"To confirm, run: `/swarm reset --confirm`"
|
|
15593
|
+
].join(`
|
|
15594
|
+
`);
|
|
15595
|
+
}
|
|
15596
|
+
const filesToReset = ["plan.md", "context.md"];
|
|
15597
|
+
const results = [];
|
|
15598
|
+
for (const filename of filesToReset) {
|
|
15599
|
+
try {
|
|
15600
|
+
const resolvedPath = validateSwarmPath(directory, filename);
|
|
15601
|
+
if (fs2.existsSync(resolvedPath)) {
|
|
15602
|
+
fs2.unlinkSync(resolvedPath);
|
|
15603
|
+
results.push(`- \u2705 Deleted ${filename}`);
|
|
15604
|
+
} else {
|
|
15605
|
+
results.push(`- \u23ED\uFE0F ${filename} not found (skipped)`);
|
|
15606
|
+
}
|
|
15607
|
+
} catch {
|
|
15608
|
+
results.push(`- \u274C Failed to delete ${filename}`);
|
|
15609
|
+
}
|
|
15610
|
+
}
|
|
15611
|
+
return [
|
|
15612
|
+
"## Swarm Reset Complete",
|
|
15613
|
+
"",
|
|
15614
|
+
...results,
|
|
15615
|
+
"",
|
|
15616
|
+
"Swarm state has been cleared. Start fresh with a new plan."
|
|
15617
|
+
].join(`
|
|
15618
|
+
`);
|
|
15619
|
+
}
|
|
15620
|
+
|
|
14631
15621
|
// src/hooks/extractors.ts
|
|
14632
15622
|
function extractCurrentPhase(planContent) {
|
|
14633
15623
|
if (!planContent) {
|
|
@@ -14773,13 +15763,81 @@ function extractPatterns(contextContent, maxChars = 500) {
|
|
|
14773
15763
|
}
|
|
14774
15764
|
return `${trimmed.slice(0, maxChars)}...`;
|
|
14775
15765
|
}
|
|
15766
|
+
function extractCurrentPhaseFromPlan(plan) {
|
|
15767
|
+
const phase = plan.phases.find((p) => p.id === plan.current_phase);
|
|
15768
|
+
if (!phase)
|
|
15769
|
+
return null;
|
|
15770
|
+
const statusMap = {
|
|
15771
|
+
pending: "PENDING",
|
|
15772
|
+
in_progress: "IN PROGRESS",
|
|
15773
|
+
complete: "COMPLETE",
|
|
15774
|
+
blocked: "BLOCKED"
|
|
15775
|
+
};
|
|
15776
|
+
const statusText = statusMap[phase.status] || "PENDING";
|
|
15777
|
+
return `Phase ${phase.id}: ${phase.name} [${statusText}]`;
|
|
15778
|
+
}
|
|
15779
|
+
function extractCurrentTaskFromPlan(plan) {
|
|
15780
|
+
const phase = plan.phases.find((p) => p.id === plan.current_phase);
|
|
15781
|
+
if (!phase)
|
|
15782
|
+
return null;
|
|
15783
|
+
const inProgress = phase.tasks.find((t) => t.status === "in_progress");
|
|
15784
|
+
if (inProgress) {
|
|
15785
|
+
const deps = inProgress.depends.length > 0 ? ` (depends: ${inProgress.depends.join(", ")})` : "";
|
|
15786
|
+
return `- [ ] ${inProgress.id}: ${inProgress.description} [${inProgress.size.toUpperCase()}]${deps} \u2190 CURRENT`;
|
|
15787
|
+
}
|
|
15788
|
+
const pending = phase.tasks.find((t) => t.status === "pending");
|
|
15789
|
+
if (pending) {
|
|
15790
|
+
const deps = pending.depends.length > 0 ? ` (depends: ${pending.depends.join(", ")})` : "";
|
|
15791
|
+
return `- [ ] ${pending.id}: ${pending.description} [${pending.size.toUpperCase()}]${deps}`;
|
|
15792
|
+
}
|
|
15793
|
+
return null;
|
|
15794
|
+
}
|
|
15795
|
+
function extractIncompleteTasksFromPlan(plan, maxChars = 500) {
|
|
15796
|
+
const phase = plan.phases.find((p) => p.id === plan.current_phase);
|
|
15797
|
+
if (!phase)
|
|
15798
|
+
return null;
|
|
15799
|
+
const incomplete = phase.tasks.filter((t) => t.status === "pending" || t.status === "in_progress");
|
|
15800
|
+
if (incomplete.length === 0)
|
|
15801
|
+
return null;
|
|
15802
|
+
const lines = incomplete.map((t) => {
|
|
15803
|
+
const deps = t.depends.length > 0 ? ` (depends: ${t.depends.join(", ")})` : "";
|
|
15804
|
+
return `- [ ] ${t.id}: ${t.description} [${t.size.toUpperCase()}]${deps}`;
|
|
15805
|
+
});
|
|
15806
|
+
const text = lines.join(`
|
|
15807
|
+
`);
|
|
15808
|
+
if (text.length <= maxChars)
|
|
15809
|
+
return text;
|
|
15810
|
+
return `${text.slice(0, maxChars)}...`;
|
|
15811
|
+
}
|
|
14776
15812
|
|
|
14777
15813
|
// src/commands/status.ts
|
|
14778
15814
|
async function handleStatusCommand(directory, agents) {
|
|
15815
|
+
const plan = await loadPlan(directory);
|
|
15816
|
+
if (plan && plan.migration_status !== "migration_failed") {
|
|
15817
|
+
const currentPhase2 = extractCurrentPhaseFromPlan(plan) || "Unknown";
|
|
15818
|
+
let completedTasks2 = 0;
|
|
15819
|
+
let totalTasks2 = 0;
|
|
15820
|
+
for (const phase of plan.phases) {
|
|
15821
|
+
for (const task of phase.tasks) {
|
|
15822
|
+
totalTasks2++;
|
|
15823
|
+
if (task.status === "completed")
|
|
15824
|
+
completedTasks2++;
|
|
15825
|
+
}
|
|
15826
|
+
}
|
|
15827
|
+
const agentCount2 = Object.keys(agents).length;
|
|
15828
|
+
const lines2 = [
|
|
15829
|
+
"## Swarm Status",
|
|
15830
|
+
"",
|
|
15831
|
+
`**Current Phase**: ${currentPhase2}`,
|
|
15832
|
+
`**Tasks**: ${completedTasks2}/${totalTasks2} complete`,
|
|
15833
|
+
`**Agents**: ${agentCount2} registered`
|
|
15834
|
+
];
|
|
15835
|
+
return lines2.join(`
|
|
15836
|
+
`);
|
|
15837
|
+
}
|
|
14779
15838
|
const planContent = await readSwarmFileAsync(directory, "plan.md");
|
|
14780
|
-
if (!planContent)
|
|
15839
|
+
if (!planContent)
|
|
14781
15840
|
return "No active swarm plan found.";
|
|
14782
|
-
}
|
|
14783
15841
|
const currentPhase = extractCurrentPhase(planContent) || "Unknown";
|
|
14784
15842
|
const completedTasks = (planContent.match(/^- \[x\]/gm) || []).length;
|
|
14785
15843
|
const incompleteTasks = (planContent.match(/^- \[ \]/gm) || []).length;
|
|
@@ -14804,7 +15862,12 @@ var HELP_TEXT = [
|
|
|
14804
15862
|
"- `/swarm plan [phase]` \u2014 Show plan (optionally filter by phase number)",
|
|
14805
15863
|
"- `/swarm agents` \u2014 List registered agents",
|
|
14806
15864
|
"- `/swarm history` \u2014 Show completed phases summary",
|
|
14807
|
-
"- `/swarm config` \u2014 Show current resolved configuration"
|
|
15865
|
+
"- `/swarm config` \u2014 Show current resolved configuration",
|
|
15866
|
+
"- `/swarm evidence [taskId]` \u2014 Show evidence bundles",
|
|
15867
|
+
"- `/swarm archive [--dry-run]` \u2014 Archive old evidence bundles",
|
|
15868
|
+
"- `/swarm diagnose` \u2014 Run health check on swarm state",
|
|
15869
|
+
"- `/swarm export` \u2014 Export plan and context as JSON",
|
|
15870
|
+
"- `/swarm reset --confirm` \u2014 Clear swarm state files"
|
|
14808
15871
|
].join(`
|
|
14809
15872
|
`);
|
|
14810
15873
|
function createSwarmCommandHandler(directory, agents) {
|
|
@@ -14822,8 +15885,14 @@ function createSwarmCommandHandler(directory, agents) {
|
|
|
14822
15885
|
case "plan":
|
|
14823
15886
|
text = await handlePlanCommand(directory, args);
|
|
14824
15887
|
break;
|
|
14825
|
-
case "agents":
|
|
14826
|
-
|
|
15888
|
+
case "agents": {
|
|
15889
|
+
const pluginConfig = loadPluginConfig(directory);
|
|
15890
|
+
const guardrailsConfig = pluginConfig?.guardrails ? GuardrailsConfigSchema.parse(pluginConfig.guardrails) : undefined;
|
|
15891
|
+
text = handleAgentsCommand(agents, guardrailsConfig);
|
|
15892
|
+
break;
|
|
15893
|
+
}
|
|
15894
|
+
case "archive":
|
|
15895
|
+
text = await handleArchiveCommand(directory, args);
|
|
14827
15896
|
break;
|
|
14828
15897
|
case "history":
|
|
14829
15898
|
text = await handleHistoryCommand(directory, args);
|
|
@@ -14831,6 +15900,18 @@ function createSwarmCommandHandler(directory, agents) {
|
|
|
14831
15900
|
case "config":
|
|
14832
15901
|
text = await handleConfigCommand(directory, args);
|
|
14833
15902
|
break;
|
|
15903
|
+
case "evidence":
|
|
15904
|
+
text = await handleEvidenceCommand(directory, args);
|
|
15905
|
+
break;
|
|
15906
|
+
case "diagnose":
|
|
15907
|
+
text = await handleDiagnoseCommand(directory, args);
|
|
15908
|
+
break;
|
|
15909
|
+
case "export":
|
|
15910
|
+
text = await handleExportCommand(directory, args);
|
|
15911
|
+
break;
|
|
15912
|
+
case "reset":
|
|
15913
|
+
text = await handleResetCommand(directory, args);
|
|
15914
|
+
break;
|
|
14834
15915
|
default:
|
|
14835
15916
|
text = HELP_TEXT;
|
|
14836
15917
|
break;
|
|
@@ -14847,8 +15928,34 @@ var swarmState = {
|
|
|
14847
15928
|
toolAggregates: new Map,
|
|
14848
15929
|
activeAgent: new Map,
|
|
14849
15930
|
delegationChains: new Map,
|
|
14850
|
-
pendingEvents: 0
|
|
15931
|
+
pendingEvents: 0,
|
|
15932
|
+
agentSessions: new Map
|
|
14851
15933
|
};
|
|
15934
|
+
function startAgentSession(sessionId, agentName, staleDurationMs = 3600000) {
|
|
15935
|
+
const now = Date.now();
|
|
15936
|
+
const staleIds = [];
|
|
15937
|
+
for (const [id, session] of swarmState.agentSessions) {
|
|
15938
|
+
if (now - session.startTime > staleDurationMs) {
|
|
15939
|
+
staleIds.push(id);
|
|
15940
|
+
}
|
|
15941
|
+
}
|
|
15942
|
+
for (const id of staleIds) {
|
|
15943
|
+
swarmState.agentSessions.delete(id);
|
|
15944
|
+
}
|
|
15945
|
+
const sessionState = {
|
|
15946
|
+
agentName,
|
|
15947
|
+
startTime: now,
|
|
15948
|
+
toolCallCount: 0,
|
|
15949
|
+
consecutiveErrors: 0,
|
|
15950
|
+
recentToolCalls: [],
|
|
15951
|
+
warningIssued: false,
|
|
15952
|
+
hardLimitHit: false
|
|
15953
|
+
};
|
|
15954
|
+
swarmState.agentSessions.set(sessionId, sessionState);
|
|
15955
|
+
}
|
|
15956
|
+
function getAgentSession(sessionId) {
|
|
15957
|
+
return swarmState.agentSessions.get(sessionId);
|
|
15958
|
+
}
|
|
14852
15959
|
|
|
14853
15960
|
// src/hooks/agent-activity.ts
|
|
14854
15961
|
function createAgentActivityHooks(config2, directory) {
|
|
@@ -14918,8 +16025,8 @@ async function doFlush(directory) {
|
|
|
14918
16025
|
const activitySection = renderActivitySection();
|
|
14919
16026
|
const updated = replaceOrAppendSection(existing, "## Agent Activity", activitySection);
|
|
14920
16027
|
const flushedCount = swarmState.pendingEvents;
|
|
14921
|
-
const
|
|
14922
|
-
await Bun.write(
|
|
16028
|
+
const path6 = `${directory}/.swarm/context.md`;
|
|
16029
|
+
await Bun.write(path6, updated);
|
|
14923
16030
|
swarmState.pendingEvents = Math.max(0, swarmState.pendingEvents - flushedCount);
|
|
14924
16031
|
} catch (error49) {
|
|
14925
16032
|
warn("Agent activity flush failed:", error49);
|
|
@@ -14945,19 +16052,19 @@ function renderActivitySection() {
|
|
|
14945
16052
|
function replaceOrAppendSection(content, heading, newSection) {
|
|
14946
16053
|
const headingIndex = content.indexOf(heading);
|
|
14947
16054
|
if (headingIndex === -1) {
|
|
14948
|
-
return content.trimEnd()
|
|
16055
|
+
return `${content.trimEnd()}
|
|
14949
16056
|
|
|
14950
|
-
|
|
16057
|
+
${newSection}
|
|
14951
16058
|
`;
|
|
14952
16059
|
}
|
|
14953
16060
|
const afterHeading = content.substring(headingIndex + heading.length);
|
|
14954
16061
|
const nextHeadingMatch = afterHeading.match(/\n## /);
|
|
14955
16062
|
if (nextHeadingMatch && nextHeadingMatch.index !== undefined) {
|
|
14956
16063
|
const endIndex = headingIndex + heading.length + nextHeadingMatch.index;
|
|
14957
|
-
return content.substring(0, headingIndex)
|
|
14958
|
-
|
|
16064
|
+
return `${content.substring(0, headingIndex)}${newSection}
|
|
16065
|
+
${content.substring(endIndex + 1)}`;
|
|
14959
16066
|
}
|
|
14960
|
-
return content.substring(0, headingIndex)
|
|
16067
|
+
return `${content.substring(0, headingIndex)}${newSection}
|
|
14961
16068
|
`;
|
|
14962
16069
|
}
|
|
14963
16070
|
// src/hooks/compaction-customizer.ts
|
|
@@ -14968,13 +16075,29 @@ function createCompactionCustomizerHook(config2, directory) {
|
|
|
14968
16075
|
}
|
|
14969
16076
|
return {
|
|
14970
16077
|
"experimental.session.compacting": safeHook(async (_input, output) => {
|
|
14971
|
-
const planContent = await readSwarmFileAsync(directory, "plan.md");
|
|
14972
16078
|
const contextContent = await readSwarmFileAsync(directory, "context.md");
|
|
14973
|
-
|
|
14974
|
-
|
|
16079
|
+
const plan = await loadPlan(directory);
|
|
16080
|
+
if (plan && plan.migration_status !== "migration_failed") {
|
|
16081
|
+
const currentPhase = extractCurrentPhaseFromPlan(plan);
|
|
14975
16082
|
if (currentPhase) {
|
|
14976
16083
|
output.context.push(`[SWARM PLAN] ${currentPhase}`);
|
|
14977
16084
|
}
|
|
16085
|
+
const incompleteTasks = extractIncompleteTasksFromPlan(plan);
|
|
16086
|
+
if (incompleteTasks) {
|
|
16087
|
+
output.context.push(`[SWARM TASKS] ${incompleteTasks}`);
|
|
16088
|
+
}
|
|
16089
|
+
} else {
|
|
16090
|
+
const planContent = await readSwarmFileAsync(directory, "plan.md");
|
|
16091
|
+
if (planContent) {
|
|
16092
|
+
const currentPhase = extractCurrentPhase(planContent);
|
|
16093
|
+
if (currentPhase) {
|
|
16094
|
+
output.context.push(`[SWARM PLAN] ${currentPhase}`);
|
|
16095
|
+
}
|
|
16096
|
+
const incompleteTasks = extractIncompleteTasks(planContent);
|
|
16097
|
+
if (incompleteTasks) {
|
|
16098
|
+
output.context.push(`[SWARM TASKS] ${incompleteTasks}`);
|
|
16099
|
+
}
|
|
16100
|
+
}
|
|
14978
16101
|
}
|
|
14979
16102
|
if (contextContent) {
|
|
14980
16103
|
const decisionsSummary = extractDecisions(contextContent);
|
|
@@ -14982,12 +16105,6 @@ function createCompactionCustomizerHook(config2, directory) {
|
|
|
14982
16105
|
output.context.push(`[SWARM DECISIONS] ${decisionsSummary}`);
|
|
14983
16106
|
}
|
|
14984
16107
|
}
|
|
14985
|
-
if (planContent) {
|
|
14986
|
-
const incompleteTasks = extractIncompleteTasks(planContent);
|
|
14987
|
-
if (incompleteTasks) {
|
|
14988
|
-
output.context.push(`[SWARM TASKS] ${incompleteTasks}`);
|
|
14989
|
-
}
|
|
14990
|
-
}
|
|
14991
16108
|
if (contextContent) {
|
|
14992
16109
|
const patterns = extractPatterns(contextContent);
|
|
14993
16110
|
if (patterns) {
|
|
@@ -15082,6 +16199,140 @@ function createDelegationTrackerHook(config2) {
|
|
|
15082
16199
|
}
|
|
15083
16200
|
};
|
|
15084
16201
|
}
|
|
16202
|
+
// src/hooks/guardrails.ts
|
|
16203
|
+
function createGuardrailsHooks(config2) {
|
|
16204
|
+
if (config2.enabled === false) {
|
|
16205
|
+
return {
|
|
16206
|
+
toolBefore: async () => {},
|
|
16207
|
+
toolAfter: async () => {},
|
|
16208
|
+
messagesTransform: async () => {}
|
|
16209
|
+
};
|
|
16210
|
+
}
|
|
16211
|
+
return {
|
|
16212
|
+
toolBefore: async (input, output) => {
|
|
16213
|
+
let session = getAgentSession(input.sessionID);
|
|
16214
|
+
if (!session) {
|
|
16215
|
+
startAgentSession(input.sessionID, "unknown");
|
|
16216
|
+
session = getAgentSession(input.sessionID);
|
|
16217
|
+
if (!session) {
|
|
16218
|
+
warn(`Failed to create session for ${input.sessionID}`);
|
|
16219
|
+
return;
|
|
16220
|
+
}
|
|
16221
|
+
}
|
|
16222
|
+
const agentConfig = resolveGuardrailsConfig(config2, session.agentName);
|
|
16223
|
+
if (session.hardLimitHit) {
|
|
16224
|
+
throw new Error("\uD83D\uDED1 CIRCUIT BREAKER: Agent blocked. Hard limit was previously triggered. Stop making tool calls and return your progress summary.");
|
|
16225
|
+
}
|
|
16226
|
+
session.toolCallCount++;
|
|
16227
|
+
const hash2 = hashArgs(output.args);
|
|
16228
|
+
session.recentToolCalls.push({
|
|
16229
|
+
tool: input.tool,
|
|
16230
|
+
argsHash: hash2,
|
|
16231
|
+
timestamp: Date.now()
|
|
16232
|
+
});
|
|
16233
|
+
if (session.recentToolCalls.length > 20) {
|
|
16234
|
+
session.recentToolCalls.shift();
|
|
16235
|
+
}
|
|
16236
|
+
let repetitionCount = 0;
|
|
16237
|
+
if (session.recentToolCalls.length > 0) {
|
|
16238
|
+
const lastEntry = session.recentToolCalls[session.recentToolCalls.length - 1];
|
|
16239
|
+
for (let i = session.recentToolCalls.length - 1;i >= 0; i--) {
|
|
16240
|
+
const entry = session.recentToolCalls[i];
|
|
16241
|
+
if (entry.tool === lastEntry.tool && entry.argsHash === lastEntry.argsHash) {
|
|
16242
|
+
repetitionCount++;
|
|
16243
|
+
} else {
|
|
16244
|
+
break;
|
|
16245
|
+
}
|
|
16246
|
+
}
|
|
16247
|
+
}
|
|
16248
|
+
const elapsedMinutes = (Date.now() - session.startTime) / 60000;
|
|
16249
|
+
if (session.toolCallCount >= agentConfig.max_tool_calls) {
|
|
16250
|
+
session.hardLimitHit = true;
|
|
16251
|
+
throw new Error(`\uD83D\uDED1 CIRCUIT BREAKER: Tool call limit reached (${session.toolCallCount}/${agentConfig.max_tool_calls}). Stop making tool calls and return your progress summary.`);
|
|
16252
|
+
}
|
|
16253
|
+
if (elapsedMinutes >= agentConfig.max_duration_minutes) {
|
|
16254
|
+
session.hardLimitHit = true;
|
|
16255
|
+
throw new Error(`\uD83D\uDED1 CIRCUIT BREAKER: Duration limit reached (${Math.floor(elapsedMinutes)} min). Stop making tool calls and return your progress summary.`);
|
|
16256
|
+
}
|
|
16257
|
+
if (repetitionCount >= agentConfig.max_repetitions) {
|
|
16258
|
+
session.hardLimitHit = true;
|
|
16259
|
+
throw new Error(`\uD83D\uDED1 CIRCUIT BREAKER: Repetition detected (same call ${repetitionCount} times). Stop making tool calls and return your progress summary.`);
|
|
16260
|
+
}
|
|
16261
|
+
if (session.consecutiveErrors >= agentConfig.max_consecutive_errors) {
|
|
16262
|
+
session.hardLimitHit = true;
|
|
16263
|
+
throw new Error(`\uD83D\uDED1 CIRCUIT BREAKER: Too many consecutive errors (${session.consecutiveErrors}). Stop making tool calls and return your progress summary.`);
|
|
16264
|
+
}
|
|
16265
|
+
if (!session.warningIssued) {
|
|
16266
|
+
const toolWarning = session.toolCallCount >= agentConfig.max_tool_calls * agentConfig.warning_threshold;
|
|
16267
|
+
const durationWarning = elapsedMinutes >= agentConfig.max_duration_minutes * agentConfig.warning_threshold;
|
|
16268
|
+
const repetitionWarning = repetitionCount >= agentConfig.max_repetitions * agentConfig.warning_threshold;
|
|
16269
|
+
const errorWarning = session.consecutiveErrors >= agentConfig.max_consecutive_errors * agentConfig.warning_threshold;
|
|
16270
|
+
if (toolWarning || durationWarning || repetitionWarning || errorWarning) {
|
|
16271
|
+
session.warningIssued = true;
|
|
16272
|
+
}
|
|
16273
|
+
}
|
|
16274
|
+
},
|
|
16275
|
+
toolAfter: async (input, output) => {
|
|
16276
|
+
const session = getAgentSession(input.sessionID);
|
|
16277
|
+
if (!session) {
|
|
16278
|
+
return;
|
|
16279
|
+
}
|
|
16280
|
+
const hasError = output.output === null || output.output === undefined;
|
|
16281
|
+
if (hasError) {
|
|
16282
|
+
session.consecutiveErrors++;
|
|
16283
|
+
} else {
|
|
16284
|
+
session.consecutiveErrors = 0;
|
|
16285
|
+
}
|
|
16286
|
+
},
|
|
16287
|
+
messagesTransform: async (_input, output) => {
|
|
16288
|
+
const messages = output.messages;
|
|
16289
|
+
if (!messages || messages.length === 0) {
|
|
16290
|
+
return;
|
|
16291
|
+
}
|
|
16292
|
+
const lastMessage = messages[messages.length - 1];
|
|
16293
|
+
let sessionId = lastMessage.info?.sessionID;
|
|
16294
|
+
if (!sessionId) {
|
|
16295
|
+
for (const [id, session2] of swarmState.agentSessions) {
|
|
16296
|
+
if (session2.warningIssued || session2.hardLimitHit) {
|
|
16297
|
+
sessionId = id;
|
|
16298
|
+
break;
|
|
16299
|
+
}
|
|
16300
|
+
}
|
|
16301
|
+
}
|
|
16302
|
+
if (!sessionId) {
|
|
16303
|
+
return;
|
|
16304
|
+
}
|
|
16305
|
+
const session = getAgentSession(sessionId);
|
|
16306
|
+
if (!session || !session.warningIssued && !session.hardLimitHit) {
|
|
16307
|
+
return;
|
|
16308
|
+
}
|
|
16309
|
+
const textPart = lastMessage.parts.find((part) => part.type === "text" && typeof part.text === "string");
|
|
16310
|
+
if (!textPart) {
|
|
16311
|
+
return;
|
|
16312
|
+
}
|
|
16313
|
+
if (session.hardLimitHit) {
|
|
16314
|
+
textPart.text = `[\uD83D\uDED1 CIRCUIT BREAKER ACTIVE: You have exceeded your resource limits. Do NOT make any more tool calls. Immediately return a summary of your progress so far. Any further tool calls will be blocked.]
|
|
16315
|
+
|
|
16316
|
+
` + textPart.text;
|
|
16317
|
+
} else if (session.warningIssued) {
|
|
16318
|
+
textPart.text = `[\u26A0\uFE0F GUARDRAIL WARNING: You are approaching resource limits. Please wrap up your current task efficiently. Avoid unnecessary tool calls and prepare to return your results soon.]
|
|
16319
|
+
|
|
16320
|
+
` + textPart.text;
|
|
16321
|
+
}
|
|
16322
|
+
}
|
|
16323
|
+
};
|
|
16324
|
+
}
|
|
16325
|
+
function hashArgs(args) {
|
|
16326
|
+
try {
|
|
16327
|
+
if (typeof args !== "object" || args === null) {
|
|
16328
|
+
return 0;
|
|
16329
|
+
}
|
|
16330
|
+
const sortedKeys = Object.keys(args).sort();
|
|
16331
|
+
return Number(Bun.hash(JSON.stringify(args, sortedKeys)));
|
|
16332
|
+
} catch {
|
|
16333
|
+
return 0;
|
|
16334
|
+
}
|
|
16335
|
+
}
|
|
15085
16336
|
// src/hooks/pipeline-tracker.ts
|
|
15086
16337
|
var PHASE_REMINDER = `<swarm_reminder>
|
|
15087
16338
|
\u26A0\uFE0F ARCHITECT WORKFLOW REMINDER:
|
|
@@ -15144,29 +16395,51 @@ function createSystemEnhancerHook(config2, directory) {
|
|
|
15144
16395
|
return {
|
|
15145
16396
|
"experimental.chat.system.transform": safeHook(async (_input, output) => {
|
|
15146
16397
|
try {
|
|
15147
|
-
|
|
16398
|
+
let tryInject = function(text) {
|
|
16399
|
+
const tokens = estimateTokens(text);
|
|
16400
|
+
if (injectedTokens + tokens > maxInjectionTokens) {
|
|
16401
|
+
return;
|
|
16402
|
+
}
|
|
16403
|
+
output.system.push(text);
|
|
16404
|
+
injectedTokens += tokens;
|
|
16405
|
+
};
|
|
16406
|
+
const maxInjectionTokens = config2.context_budget?.max_injection_tokens ?? Number.POSITIVE_INFINITY;
|
|
16407
|
+
let injectedTokens = 0;
|
|
15148
16408
|
const contextContent = await readSwarmFileAsync(directory, "context.md");
|
|
15149
|
-
|
|
15150
|
-
|
|
16409
|
+
const plan = await loadPlan(directory);
|
|
16410
|
+
if (plan && plan.migration_status !== "migration_failed") {
|
|
16411
|
+
const currentPhase = extractCurrentPhaseFromPlan(plan);
|
|
15151
16412
|
if (currentPhase) {
|
|
15152
|
-
|
|
16413
|
+
tryInject(`[SWARM CONTEXT] Current phase: ${currentPhase}`);
|
|
15153
16414
|
}
|
|
15154
|
-
const currentTask =
|
|
16415
|
+
const currentTask = extractCurrentTaskFromPlan(plan);
|
|
15155
16416
|
if (currentTask) {
|
|
15156
|
-
|
|
16417
|
+
tryInject(`[SWARM CONTEXT] Current task: ${currentTask}`);
|
|
16418
|
+
}
|
|
16419
|
+
} else {
|
|
16420
|
+
const planContent = await readSwarmFileAsync(directory, "plan.md");
|
|
16421
|
+
if (planContent) {
|
|
16422
|
+
const currentPhase = extractCurrentPhase(planContent);
|
|
16423
|
+
if (currentPhase) {
|
|
16424
|
+
tryInject(`[SWARM CONTEXT] Current phase: ${currentPhase}`);
|
|
16425
|
+
}
|
|
16426
|
+
const currentTask = extractCurrentTask(planContent);
|
|
16427
|
+
if (currentTask) {
|
|
16428
|
+
tryInject(`[SWARM CONTEXT] Current task: ${currentTask}`);
|
|
16429
|
+
}
|
|
15157
16430
|
}
|
|
15158
16431
|
}
|
|
15159
16432
|
if (contextContent) {
|
|
15160
16433
|
const decisions = extractDecisions(contextContent, 200);
|
|
15161
16434
|
if (decisions) {
|
|
15162
|
-
|
|
16435
|
+
tryInject(`[SWARM CONTEXT] Key decisions: ${decisions}`);
|
|
15163
16436
|
}
|
|
15164
16437
|
if (config2.hooks?.agent_activity !== false && _input.sessionID) {
|
|
15165
16438
|
const activeAgent = swarmState.activeAgent.get(_input.sessionID);
|
|
15166
16439
|
if (activeAgent) {
|
|
15167
16440
|
const agentContext = extractAgentContext(contextContent, activeAgent, config2.hooks?.agent_awareness_max_chars ?? 300);
|
|
15168
16441
|
if (agentContext) {
|
|
15169
|
-
|
|
16442
|
+
tryInject(`[SWARM AGENT CONTEXT] ${agentContext}`);
|
|
15170
16443
|
}
|
|
15171
16444
|
}
|
|
15172
16445
|
}
|
|
@@ -15205,7 +16478,7 @@ ${activitySection}`;
|
|
|
15205
16478
|
break;
|
|
15206
16479
|
}
|
|
15207
16480
|
if (contextSummary.length > maxChars) {
|
|
15208
|
-
return contextSummary.substring(0, maxChars - 3)
|
|
16481
|
+
return `${contextSummary.substring(0, maxChars - 3)}...`;
|
|
15209
16482
|
}
|
|
15210
16483
|
return contextSummary;
|
|
15211
16484
|
}
|
|
@@ -15938,10 +17211,10 @@ function mergeDefs2(...defs) {
|
|
|
15938
17211
|
function cloneDef2(schema) {
|
|
15939
17212
|
return mergeDefs2(schema._zod.def);
|
|
15940
17213
|
}
|
|
15941
|
-
function getElementAtPath2(obj,
|
|
15942
|
-
if (!
|
|
17214
|
+
function getElementAtPath2(obj, path6) {
|
|
17215
|
+
if (!path6)
|
|
15943
17216
|
return obj;
|
|
15944
|
-
return
|
|
17217
|
+
return path6.reduce((acc, key) => acc?.[key], obj);
|
|
15945
17218
|
}
|
|
15946
17219
|
function promiseAllObject2(promisesObj) {
|
|
15947
17220
|
const keys = Object.keys(promisesObj);
|
|
@@ -16300,11 +17573,11 @@ function aborted2(x, startIndex = 0) {
|
|
|
16300
17573
|
}
|
|
16301
17574
|
return false;
|
|
16302
17575
|
}
|
|
16303
|
-
function prefixIssues2(
|
|
17576
|
+
function prefixIssues2(path6, issues) {
|
|
16304
17577
|
return issues.map((iss) => {
|
|
16305
17578
|
var _a2;
|
|
16306
17579
|
(_a2 = iss).path ?? (_a2.path = []);
|
|
16307
|
-
iss.path.unshift(
|
|
17580
|
+
iss.path.unshift(path6);
|
|
16308
17581
|
return iss;
|
|
16309
17582
|
});
|
|
16310
17583
|
}
|
|
@@ -16472,7 +17745,7 @@ function treeifyError2(error49, _mapper) {
|
|
|
16472
17745
|
return issue3.message;
|
|
16473
17746
|
};
|
|
16474
17747
|
const result = { errors: [] };
|
|
16475
|
-
const processError = (error50,
|
|
17748
|
+
const processError = (error50, path6 = []) => {
|
|
16476
17749
|
var _a2, _b;
|
|
16477
17750
|
for (const issue3 of error50.issues) {
|
|
16478
17751
|
if (issue3.code === "invalid_union" && issue3.errors.length) {
|
|
@@ -16482,7 +17755,7 @@ function treeifyError2(error49, _mapper) {
|
|
|
16482
17755
|
} else if (issue3.code === "invalid_element") {
|
|
16483
17756
|
processError({ issues: issue3.issues }, issue3.path);
|
|
16484
17757
|
} else {
|
|
16485
|
-
const fullpath = [...
|
|
17758
|
+
const fullpath = [...path6, ...issue3.path];
|
|
16486
17759
|
if (fullpath.length === 0) {
|
|
16487
17760
|
result.errors.push(mapper(issue3));
|
|
16488
17761
|
continue;
|
|
@@ -16514,8 +17787,8 @@ function treeifyError2(error49, _mapper) {
|
|
|
16514
17787
|
}
|
|
16515
17788
|
function toDotPath2(_path) {
|
|
16516
17789
|
const segs = [];
|
|
16517
|
-
const
|
|
16518
|
-
for (const seg of
|
|
17790
|
+
const path6 = _path.map((seg) => typeof seg === "object" ? seg.key : seg);
|
|
17791
|
+
for (const seg of path6) {
|
|
16519
17792
|
if (typeof seg === "number")
|
|
16520
17793
|
segs.push(`[${seg}]`);
|
|
16521
17794
|
else if (typeof seg === "symbol")
|
|
@@ -27710,8 +28983,8 @@ Use these as DOMAIN values when delegating to @sme.`;
|
|
|
27710
28983
|
}
|
|
27711
28984
|
});
|
|
27712
28985
|
// src/tools/file-extractor.ts
|
|
27713
|
-
import * as
|
|
27714
|
-
import * as
|
|
28986
|
+
import * as fs3 from "fs";
|
|
28987
|
+
import * as path6 from "path";
|
|
27715
28988
|
var EXT_MAP = {
|
|
27716
28989
|
python: ".py",
|
|
27717
28990
|
py: ".py",
|
|
@@ -27773,8 +29046,8 @@ var extract_code_blocks = tool({
|
|
|
27773
29046
|
execute: async (args) => {
|
|
27774
29047
|
const { content, output_dir, prefix } = args;
|
|
27775
29048
|
const targetDir = output_dir || process.cwd();
|
|
27776
|
-
if (!
|
|
27777
|
-
|
|
29049
|
+
if (!fs3.existsSync(targetDir)) {
|
|
29050
|
+
fs3.mkdirSync(targetDir, { recursive: true });
|
|
27778
29051
|
}
|
|
27779
29052
|
const pattern = /```(\w*)\n([\s\S]*?)```/g;
|
|
27780
29053
|
const matches = [...content.matchAll(pattern)];
|
|
@@ -27789,16 +29062,16 @@ var extract_code_blocks = tool({
|
|
|
27789
29062
|
if (prefix) {
|
|
27790
29063
|
filename = `${prefix}_${filename}`;
|
|
27791
29064
|
}
|
|
27792
|
-
let filepath =
|
|
27793
|
-
const base =
|
|
27794
|
-
const ext =
|
|
29065
|
+
let filepath = path6.join(targetDir, filename);
|
|
29066
|
+
const base = path6.basename(filepath, path6.extname(filepath));
|
|
29067
|
+
const ext = path6.extname(filepath);
|
|
27795
29068
|
let counter = 1;
|
|
27796
|
-
while (
|
|
27797
|
-
filepath =
|
|
29069
|
+
while (fs3.existsSync(filepath)) {
|
|
29070
|
+
filepath = path6.join(targetDir, `${base}_${counter}${ext}`);
|
|
27798
29071
|
counter++;
|
|
27799
29072
|
}
|
|
27800
29073
|
try {
|
|
27801
|
-
|
|
29074
|
+
fs3.writeFileSync(filepath, code.trim(), "utf-8");
|
|
27802
29075
|
savedFiles.push(filepath);
|
|
27803
29076
|
} catch (error93) {
|
|
27804
29077
|
errors5.push(`Failed to save ${filename}: ${error93 instanceof Error ? error93.message : String(error93)}`);
|
|
@@ -27826,7 +29099,7 @@ Errors:
|
|
|
27826
29099
|
var GITINGEST_TIMEOUT_MS = 1e4;
|
|
27827
29100
|
var GITINGEST_MAX_RESPONSE_BYTES = 5242880;
|
|
27828
29101
|
var GITINGEST_MAX_RETRIES = 2;
|
|
27829
|
-
var delay = (ms) => new Promise((
|
|
29102
|
+
var delay = (ms) => new Promise((resolve3) => setTimeout(resolve3, ms));
|
|
27830
29103
|
async function fetchGitingest(args) {
|
|
27831
29104
|
for (let attempt = 0;attempt <= GITINGEST_MAX_RETRIES; attempt++) {
|
|
27832
29105
|
try {
|
|
@@ -27915,6 +29188,8 @@ var OpenCodeSwarm = async (ctx) => {
|
|
|
27915
29188
|
const commandHandler = createSwarmCommandHandler(ctx.directory, Object.fromEntries(agentDefinitions.map((agent) => [agent.name, agent])));
|
|
27916
29189
|
const activityHooks = createAgentActivityHooks(config3, ctx.directory);
|
|
27917
29190
|
const delegationHandler = createDelegationTrackerHook(config3);
|
|
29191
|
+
const guardrailsConfig = GuardrailsConfigSchema.parse(config3.guardrails ?? {});
|
|
29192
|
+
const guardrailsHooks = createGuardrailsHooks(guardrailsConfig);
|
|
27918
29193
|
log("Plugin initialized", {
|
|
27919
29194
|
directory: ctx.directory,
|
|
27920
29195
|
maxIterations: config3.max_iterations,
|
|
@@ -27927,7 +29202,8 @@ var OpenCodeSwarm = async (ctx) => {
|
|
|
27927
29202
|
contextBudget: !!contextBudgetHandler,
|
|
27928
29203
|
commands: true,
|
|
27929
29204
|
agentActivity: config3.hooks?.agent_activity !== false,
|
|
27930
|
-
delegationTracker: config3.hooks?.delegation_tracker === true
|
|
29205
|
+
delegationTracker: config3.hooks?.delegation_tracker === true,
|
|
29206
|
+
guardrails: guardrailsConfig.enabled
|
|
27931
29207
|
}
|
|
27932
29208
|
});
|
|
27933
29209
|
return {
|
|
@@ -27958,13 +29234,17 @@ var OpenCodeSwarm = async (ctx) => {
|
|
|
27958
29234
|
},
|
|
27959
29235
|
"experimental.chat.messages.transform": composeHandlers(...[
|
|
27960
29236
|
pipelineHook["experimental.chat.messages.transform"],
|
|
27961
|
-
contextBudgetHandler
|
|
29237
|
+
contextBudgetHandler,
|
|
29238
|
+
guardrailsHooks.messagesTransform
|
|
27962
29239
|
].filter((fn) => Boolean(fn))),
|
|
27963
29240
|
"experimental.chat.system.transform": systemEnhancerHook["experimental.chat.system.transform"],
|
|
27964
29241
|
"experimental.session.compacting": compactionHook["experimental.session.compacting"],
|
|
27965
29242
|
"command.execute.before": safeHook(commandHandler),
|
|
27966
|
-
"tool.execute.before":
|
|
27967
|
-
|
|
29243
|
+
"tool.execute.before": async (input, output) => {
|
|
29244
|
+
await guardrailsHooks.toolBefore(input, output);
|
|
29245
|
+
await safeHook(activityHooks.toolBefore)(input, output);
|
|
29246
|
+
},
|
|
29247
|
+
"tool.execute.after": composeHandlers(activityHooks.toolAfter, guardrailsHooks.toolAfter),
|
|
27968
29248
|
"chat.message": safeHook(delegationHandler)
|
|
27969
29249
|
};
|
|
27970
29250
|
};
|