opencode-swarm 4.5.0 → 5.0.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +95 -4
- package/dist/commands/agents.d.ts +2 -1
- package/dist/commands/archive.d.ts +5 -0
- package/dist/commands/evidence.d.ts +5 -0
- package/dist/commands/index.d.ts +2 -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 +65 -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 +1271 -120
- 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,48 @@ 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 DEFAULT_ARCHITECT_PROFILE = {
|
|
13627
|
+
max_tool_calls: 600,
|
|
13628
|
+
max_duration_minutes: 90,
|
|
13629
|
+
max_consecutive_errors: 8,
|
|
13630
|
+
warning_threshold: 0.7
|
|
13631
|
+
};
|
|
13632
|
+
var GuardrailsConfigSchema = exports_external.object({
|
|
13633
|
+
enabled: exports_external.boolean().default(true),
|
|
13634
|
+
max_tool_calls: exports_external.number().min(10).max(1000).default(200),
|
|
13635
|
+
max_duration_minutes: exports_external.number().min(1).max(120).default(30),
|
|
13636
|
+
max_repetitions: exports_external.number().min(3).max(50).default(10),
|
|
13637
|
+
max_consecutive_errors: exports_external.number().min(2).max(20).default(5),
|
|
13638
|
+
warning_threshold: exports_external.number().min(0.1).max(0.9).default(0.5),
|
|
13639
|
+
profiles: exports_external.record(exports_external.string(), GuardrailsProfileSchema).optional()
|
|
13640
|
+
});
|
|
13641
|
+
function resolveGuardrailsConfig(base, agentName) {
|
|
13642
|
+
if (!agentName) {
|
|
13643
|
+
return base;
|
|
13644
|
+
}
|
|
13645
|
+
const builtIn = agentName === ORCHESTRATOR_NAME ? DEFAULT_ARCHITECT_PROFILE : undefined;
|
|
13646
|
+
const userProfile = base.profiles?.[agentName];
|
|
13647
|
+
if (!builtIn && !userProfile) {
|
|
13648
|
+
return base;
|
|
13649
|
+
}
|
|
13650
|
+
return { ...base, ...builtIn, ...userProfile };
|
|
13651
|
+
}
|
|
13591
13652
|
var PluginConfigSchema = exports_external.object({
|
|
13592
13653
|
agents: exports_external.record(exports_external.string(), AgentOverrideConfigSchema).optional(),
|
|
13593
13654
|
swarms: exports_external.record(exports_external.string(), SwarmConfigSchema).optional(),
|
|
@@ -13595,12 +13656,12 @@ var PluginConfigSchema = exports_external.object({
|
|
|
13595
13656
|
qa_retry_limit: exports_external.number().min(1).max(10).default(3),
|
|
13596
13657
|
inject_phase_reminders: exports_external.boolean().default(true),
|
|
13597
13658
|
hooks: HooksConfigSchema.optional(),
|
|
13598
|
-
context_budget: ContextBudgetConfigSchema.optional()
|
|
13659
|
+
context_budget: ContextBudgetConfigSchema.optional(),
|
|
13660
|
+
guardrails: GuardrailsConfigSchema.optional(),
|
|
13661
|
+
evidence: EvidenceConfigSchema.optional()
|
|
13599
13662
|
});
|
|
13663
|
+
|
|
13600
13664
|
// src/config/loader.ts
|
|
13601
|
-
import * as fs from "fs";
|
|
13602
|
-
import * as os from "os";
|
|
13603
|
-
import * as path from "path";
|
|
13604
13665
|
var CONFIG_FILENAME = "opencode-swarm.json";
|
|
13605
13666
|
var PROMPTS_DIR_NAME = "opencode-swarm";
|
|
13606
13667
|
var MAX_CONFIG_FILE_BYTES = 102400;
|
|
@@ -13693,6 +13754,125 @@ function loadAgentPrompt(agentName) {
|
|
|
13693
13754
|
}
|
|
13694
13755
|
return result;
|
|
13695
13756
|
}
|
|
13757
|
+
// src/config/plan-schema.ts
|
|
13758
|
+
var TaskStatusSchema = exports_external.enum([
|
|
13759
|
+
"pending",
|
|
13760
|
+
"in_progress",
|
|
13761
|
+
"completed",
|
|
13762
|
+
"blocked"
|
|
13763
|
+
]);
|
|
13764
|
+
var TaskSizeSchema = exports_external.enum(["small", "medium", "large"]);
|
|
13765
|
+
var PhaseStatusSchema = exports_external.enum([
|
|
13766
|
+
"pending",
|
|
13767
|
+
"in_progress",
|
|
13768
|
+
"complete",
|
|
13769
|
+
"blocked"
|
|
13770
|
+
]);
|
|
13771
|
+
var MigrationStatusSchema = exports_external.enum([
|
|
13772
|
+
"native",
|
|
13773
|
+
"migrated",
|
|
13774
|
+
"migration_failed"
|
|
13775
|
+
]);
|
|
13776
|
+
var TaskSchema = exports_external.object({
|
|
13777
|
+
id: exports_external.string(),
|
|
13778
|
+
phase: exports_external.number().int().min(1),
|
|
13779
|
+
status: TaskStatusSchema.default("pending"),
|
|
13780
|
+
size: TaskSizeSchema.default("small"),
|
|
13781
|
+
description: exports_external.string().min(1),
|
|
13782
|
+
depends: exports_external.array(exports_external.string()).default([]),
|
|
13783
|
+
acceptance: exports_external.string().optional(),
|
|
13784
|
+
files_touched: exports_external.array(exports_external.string()).default([]),
|
|
13785
|
+
evidence_path: exports_external.string().optional(),
|
|
13786
|
+
blocked_reason: exports_external.string().optional()
|
|
13787
|
+
});
|
|
13788
|
+
var PhaseSchema = exports_external.object({
|
|
13789
|
+
id: exports_external.number().int().min(1),
|
|
13790
|
+
name: exports_external.string().min(1),
|
|
13791
|
+
status: PhaseStatusSchema.default("pending"),
|
|
13792
|
+
tasks: exports_external.array(TaskSchema).default([])
|
|
13793
|
+
});
|
|
13794
|
+
var PlanSchema = exports_external.object({
|
|
13795
|
+
schema_version: exports_external.literal("1.0.0"),
|
|
13796
|
+
title: exports_external.string().min(1),
|
|
13797
|
+
swarm: exports_external.string().min(1),
|
|
13798
|
+
current_phase: exports_external.number().int().min(1),
|
|
13799
|
+
phases: exports_external.array(PhaseSchema).min(1),
|
|
13800
|
+
migration_status: MigrationStatusSchema.optional()
|
|
13801
|
+
});
|
|
13802
|
+
// src/config/evidence-schema.ts
|
|
13803
|
+
var EVIDENCE_MAX_JSON_BYTES = 500 * 1024;
|
|
13804
|
+
var EVIDENCE_MAX_PATCH_BYTES = 5 * 1024 * 1024;
|
|
13805
|
+
var EVIDENCE_MAX_TASK_BYTES = 20 * 1024 * 1024;
|
|
13806
|
+
var EvidenceTypeSchema = exports_external.enum([
|
|
13807
|
+
"review",
|
|
13808
|
+
"test",
|
|
13809
|
+
"diff",
|
|
13810
|
+
"approval",
|
|
13811
|
+
"note"
|
|
13812
|
+
]);
|
|
13813
|
+
var EvidenceVerdictSchema = exports_external.enum([
|
|
13814
|
+
"pass",
|
|
13815
|
+
"fail",
|
|
13816
|
+
"approved",
|
|
13817
|
+
"rejected",
|
|
13818
|
+
"info"
|
|
13819
|
+
]);
|
|
13820
|
+
var BaseEvidenceSchema = exports_external.object({
|
|
13821
|
+
task_id: exports_external.string().min(1),
|
|
13822
|
+
type: EvidenceTypeSchema,
|
|
13823
|
+
timestamp: exports_external.string().datetime(),
|
|
13824
|
+
agent: exports_external.string().min(1),
|
|
13825
|
+
verdict: EvidenceVerdictSchema,
|
|
13826
|
+
summary: exports_external.string().min(1),
|
|
13827
|
+
metadata: exports_external.record(exports_external.string(), exports_external.unknown()).optional()
|
|
13828
|
+
});
|
|
13829
|
+
var ReviewEvidenceSchema = BaseEvidenceSchema.extend({
|
|
13830
|
+
type: exports_external.literal("review"),
|
|
13831
|
+
risk: exports_external.enum(["low", "medium", "high", "critical"]),
|
|
13832
|
+
issues: exports_external.array(exports_external.object({
|
|
13833
|
+
severity: exports_external.enum(["error", "warning", "info"]),
|
|
13834
|
+
message: exports_external.string().min(1),
|
|
13835
|
+
file: exports_external.string().optional(),
|
|
13836
|
+
line: exports_external.number().int().optional()
|
|
13837
|
+
})).default([])
|
|
13838
|
+
});
|
|
13839
|
+
var TestEvidenceSchema = BaseEvidenceSchema.extend({
|
|
13840
|
+
type: exports_external.literal("test"),
|
|
13841
|
+
tests_passed: exports_external.number().int().min(0),
|
|
13842
|
+
tests_failed: exports_external.number().int().min(0),
|
|
13843
|
+
test_file: exports_external.string().optional(),
|
|
13844
|
+
failures: exports_external.array(exports_external.object({
|
|
13845
|
+
name: exports_external.string().min(1),
|
|
13846
|
+
message: exports_external.string().min(1)
|
|
13847
|
+
})).default([])
|
|
13848
|
+
});
|
|
13849
|
+
var DiffEvidenceSchema = BaseEvidenceSchema.extend({
|
|
13850
|
+
type: exports_external.literal("diff"),
|
|
13851
|
+
files_changed: exports_external.array(exports_external.string()).default([]),
|
|
13852
|
+
additions: exports_external.number().int().min(0).default(0),
|
|
13853
|
+
deletions: exports_external.number().int().min(0).default(0),
|
|
13854
|
+
patch_path: exports_external.string().optional()
|
|
13855
|
+
});
|
|
13856
|
+
var ApprovalEvidenceSchema = BaseEvidenceSchema.extend({
|
|
13857
|
+
type: exports_external.literal("approval")
|
|
13858
|
+
});
|
|
13859
|
+
var NoteEvidenceSchema = BaseEvidenceSchema.extend({
|
|
13860
|
+
type: exports_external.literal("note")
|
|
13861
|
+
});
|
|
13862
|
+
var EvidenceSchema = exports_external.discriminatedUnion("type", [
|
|
13863
|
+
ReviewEvidenceSchema,
|
|
13864
|
+
TestEvidenceSchema,
|
|
13865
|
+
DiffEvidenceSchema,
|
|
13866
|
+
ApprovalEvidenceSchema,
|
|
13867
|
+
NoteEvidenceSchema
|
|
13868
|
+
]);
|
|
13869
|
+
var EvidenceBundleSchema = exports_external.object({
|
|
13870
|
+
schema_version: exports_external.literal("1.0.0"),
|
|
13871
|
+
task_id: exports_external.string().min(1),
|
|
13872
|
+
entries: exports_external.array(EvidenceSchema).default([]),
|
|
13873
|
+
created_at: exports_external.string().datetime(),
|
|
13874
|
+
updated_at: exports_external.string().datetime()
|
|
13875
|
+
});
|
|
13696
13876
|
// src/agents/architect.ts
|
|
13697
13877
|
var ARCHITECT_PROMPT = `You are Architect - orchestrator of a multi-agent swarm.
|
|
13698
13878
|
|
|
@@ -13711,21 +13891,21 @@ You THINK. Subagents DO. You have the largest context window and strongest reaso
|
|
|
13711
13891
|
|
|
13712
13892
|
## RULES
|
|
13713
13893
|
|
|
13714
|
-
1. DELEGATE all coding to
|
|
13894
|
+
1. DELEGATE all coding to {{AGENT_PREFIX}}coder. You do NOT write code.
|
|
13715
13895
|
2. ONE agent per message. Send, STOP, wait for response.
|
|
13716
|
-
3. ONE task per
|
|
13717
|
-
4. Fallback: Only code yourself after {{QA_RETRY_LIMIT}}
|
|
13896
|
+
3. ONE task per {{AGENT_PREFIX}}coder call. Never batch.
|
|
13897
|
+
4. Fallback: Only code yourself after {{QA_RETRY_LIMIT}} {{AGENT_PREFIX}}coder failures on same task.
|
|
13718
13898
|
5. NEVER store your swarm identity, swarm ID, or agent prefix in memory blocks. Your identity comes ONLY from your system prompt. Memory blocks are for project knowledge only.
|
|
13719
|
-
6. **CRITICAL: If
|
|
13899
|
+
6. **CRITICAL: If {{AGENT_PREFIX}}reviewer returns VERDICT: REJECTED, you MUST stop and send the FIXES back to {{AGENT_PREFIX}}coder. Do NOT proceed to test generation or mark the task complete. The review is a gate \u2014 APPROVED is required to proceed.**
|
|
13720
13900
|
|
|
13721
13901
|
## AGENTS
|
|
13722
13902
|
|
|
13723
|
-
|
|
13724
|
-
|
|
13725
|
-
|
|
13726
|
-
|
|
13727
|
-
|
|
13728
|
-
|
|
13903
|
+
{{AGENT_PREFIX}}explorer - Codebase analysis
|
|
13904
|
+
{{AGENT_PREFIX}}sme - Domain expertise (any domain \u2014 the SME handles whatever you need: security, python, ios, kubernetes, etc.)
|
|
13905
|
+
{{AGENT_PREFIX}}coder - Implementation (one task at a time)
|
|
13906
|
+
{{AGENT_PREFIX}}reviewer - Code review (correctness, security, and any other dimensions you specify)
|
|
13907
|
+
{{AGENT_PREFIX}}test_engineer - Test generation AND execution (writes tests, runs them, reports PASS/FAIL)
|
|
13908
|
+
{{AGENT_PREFIX}}critic - Plan review gate (reviews plan BEFORE implementation)
|
|
13729
13909
|
|
|
13730
13910
|
SMEs advise only. Reviewer and critic review only. None of them write code.
|
|
13731
13911
|
|
|
@@ -13733,7 +13913,7 @@ SMEs advise only. Reviewer and critic review only. None of them write code.
|
|
|
13733
13913
|
|
|
13734
13914
|
All delegations use this structure:
|
|
13735
13915
|
|
|
13736
|
-
|
|
13916
|
+
{{AGENT_PREFIX}}[agent]
|
|
13737
13917
|
TASK: [single objective]
|
|
13738
13918
|
FILE: [path] (if applicable)
|
|
13739
13919
|
INPUT: [what to analyze/use]
|
|
@@ -13742,43 +13922,43 @@ CONSTRAINT: [what NOT to do]
|
|
|
13742
13922
|
|
|
13743
13923
|
Examples:
|
|
13744
13924
|
|
|
13745
|
-
|
|
13925
|
+
{{AGENT_PREFIX}}explorer
|
|
13746
13926
|
TASK: Analyze codebase for auth implementation
|
|
13747
13927
|
INPUT: Focus on src/auth/, src/middleware/
|
|
13748
13928
|
OUTPUT: Structure, frameworks, key files, relevant domains
|
|
13749
13929
|
|
|
13750
|
-
|
|
13930
|
+
{{AGENT_PREFIX}}sme
|
|
13751
13931
|
TASK: Review auth token patterns
|
|
13752
13932
|
DOMAIN: security
|
|
13753
13933
|
INPUT: src/auth/login.ts uses JWT with RS256
|
|
13754
13934
|
OUTPUT: Security considerations, recommended patterns
|
|
13755
13935
|
CONSTRAINT: Focus on auth only, not general code style
|
|
13756
13936
|
|
|
13757
|
-
|
|
13937
|
+
{{AGENT_PREFIX}}sme
|
|
13758
13938
|
TASK: Advise on state management approach
|
|
13759
13939
|
DOMAIN: ios
|
|
13760
13940
|
INPUT: Building a SwiftUI app with offline-first sync
|
|
13761
13941
|
OUTPUT: Recommended patterns, frameworks, gotchas
|
|
13762
13942
|
|
|
13763
|
-
|
|
13943
|
+
{{AGENT_PREFIX}}coder
|
|
13764
13944
|
TASK: Add input validation to login
|
|
13765
13945
|
FILE: src/auth/login.ts
|
|
13766
13946
|
INPUT: Validate email format, password >= 8 chars
|
|
13767
13947
|
OUTPUT: Modified file
|
|
13768
13948
|
CONSTRAINT: Do not modify other functions
|
|
13769
13949
|
|
|
13770
|
-
|
|
13950
|
+
{{AGENT_PREFIX}}reviewer
|
|
13771
13951
|
TASK: Review login validation
|
|
13772
13952
|
FILE: src/auth/login.ts
|
|
13773
13953
|
CHECK: [security, correctness, edge-cases]
|
|
13774
13954
|
OUTPUT: VERDICT + RISK + ISSUES
|
|
13775
13955
|
|
|
13776
|
-
|
|
13956
|
+
{{AGENT_PREFIX}}test_engineer
|
|
13777
13957
|
TASK: Generate and run login validation tests
|
|
13778
13958
|
FILE: src/auth/login.ts
|
|
13779
13959
|
OUTPUT: Test file at src/auth/login.test.ts + VERDICT: PASS/FAIL with failure details
|
|
13780
13960
|
|
|
13781
|
-
|
|
13961
|
+
{{AGENT_PREFIX}}critic
|
|
13782
13962
|
TASK: Review plan for user authentication feature
|
|
13783
13963
|
PLAN: [paste the plan.md content]
|
|
13784
13964
|
CONTEXT: [codebase summary from explorer]
|
|
@@ -13804,7 +13984,7 @@ Ambiguous request \u2192 Ask up to 3 questions, wait for answers
|
|
|
13804
13984
|
Clear request \u2192 Phase 2
|
|
13805
13985
|
|
|
13806
13986
|
### Phase 2: Discover
|
|
13807
|
-
Delegate to
|
|
13987
|
+
Delegate to {{AGENT_PREFIX}}explorer. Wait for response.
|
|
13808
13988
|
For complex tasks, make a second explorer call focused on risk/gap analysis:
|
|
13809
13989
|
- Hidden requirements, unstated assumptions, scope risks
|
|
13810
13990
|
- Existing patterns that the implementation must follow
|
|
@@ -13812,7 +13992,7 @@ For complex tasks, make a second explorer call focused on risk/gap analysis:
|
|
|
13812
13992
|
### Phase 3: Consult SMEs
|
|
13813
13993
|
Check .swarm/context.md for cached guidance first.
|
|
13814
13994
|
Identify 1-3 relevant domains from the task requirements.
|
|
13815
|
-
Call
|
|
13995
|
+
Call {{AGENT_PREFIX}}sme once per domain, serially. Max 3 SME calls per project phase.
|
|
13816
13996
|
Re-consult if a new domain emerges or if significant changes require fresh evaluation.
|
|
13817
13997
|
Cache guidance in context.md.
|
|
13818
13998
|
|
|
@@ -13826,7 +14006,7 @@ Create .swarm/context.md:
|
|
|
13826
14006
|
- Decisions, patterns, SME cache, file map
|
|
13827
14007
|
|
|
13828
14008
|
### Phase 4.5: Critic Gate
|
|
13829
|
-
Delegate plan to
|
|
14009
|
+
Delegate plan to {{AGENT_PREFIX}}critic for review BEFORE any implementation begins.
|
|
13830
14010
|
- Send the full plan.md content and codebase context summary
|
|
13831
14011
|
- **APPROVED** \u2192 Proceed to Phase 5
|
|
13832
14012
|
- **NEEDS_REVISION** \u2192 Revise the plan based on critic feedback, then resubmit (max 2 revision cycles)
|
|
@@ -13835,18 +14015,18 @@ Delegate plan to @{{AGENT_PREFIX}}critic for review BEFORE any implementation be
|
|
|
13835
14015
|
### Phase 5: Execute
|
|
13836
14016
|
For each task (respecting dependencies):
|
|
13837
14017
|
|
|
13838
|
-
5a.
|
|
13839
|
-
5b.
|
|
14018
|
+
5a. {{AGENT_PREFIX}}coder - Implement (MANDATORY)
|
|
14019
|
+
5b. {{AGENT_PREFIX}}reviewer - Review (specify CHECK dimensions relevant to the change)
|
|
13840
14020
|
5c. **GATE - Check VERDICT:**
|
|
13841
14021
|
- **APPROVED** \u2192 Proceed to 5d
|
|
13842
|
-
- **REJECTED** (attempt < {{QA_RETRY_LIMIT}}) \u2192 STOP. Send FIXES to
|
|
14022
|
+
- **REJECTED** (attempt < {{QA_RETRY_LIMIT}}) \u2192 STOP. Send FIXES to {{AGENT_PREFIX}}coder with specific changes. Retry from 5a. Do NOT proceed to 5d.
|
|
13843
14023
|
- **REJECTED** (attempt {{QA_RETRY_LIMIT}}) \u2192 STOP. Escalate to user or handle directly.
|
|
13844
|
-
5d.
|
|
13845
|
-
5e. If test VERDICT is FAIL \u2192 Send failures to
|
|
14024
|
+
5d. {{AGENT_PREFIX}}test_engineer - Generate AND run tests (ONLY if 5c = APPROVED). Expect VERDICT: PASS/FAIL.
|
|
14025
|
+
5e. If test VERDICT is FAIL \u2192 Send failures to {{AGENT_PREFIX}}coder for fixes, then re-run from 5b.
|
|
13846
14026
|
5f. Update plan.md [x], proceed to next task (ONLY if tests PASS)
|
|
13847
14027
|
|
|
13848
14028
|
### Phase 6: Phase Complete
|
|
13849
|
-
1.
|
|
14029
|
+
1. {{AGENT_PREFIX}}explorer - Rescan
|
|
13850
14030
|
2. Update context.md
|
|
13851
14031
|
3. Summarize to user
|
|
13852
14032
|
4. Ask: "Ready for Phase [N+1]?"
|
|
@@ -14388,12 +14568,12 @@ function getAgentConfigs(config2) {
|
|
|
14388
14568
|
}
|
|
14389
14569
|
|
|
14390
14570
|
// src/commands/agents.ts
|
|
14391
|
-
function handleAgentsCommand(agents) {
|
|
14571
|
+
function handleAgentsCommand(agents, guardrails) {
|
|
14392
14572
|
const entries = Object.entries(agents);
|
|
14393
14573
|
if (entries.length === 0) {
|
|
14394
14574
|
return "No agents registered.";
|
|
14395
14575
|
}
|
|
14396
|
-
const lines = [
|
|
14576
|
+
const lines = [`## Registered Agents (${entries.length} total)`, ""];
|
|
14397
14577
|
for (const [key, agent] of entries) {
|
|
14398
14578
|
const model = agent.config.model || "default";
|
|
14399
14579
|
const temp = agent.config.temperature !== undefined ? agent.config.temperature.toString() : "default";
|
|
@@ -14401,43 +14581,46 @@ function handleAgentsCommand(agents) {
|
|
|
14401
14581
|
const isReadOnly = tools.write === false || tools.edit === false;
|
|
14402
14582
|
const access = isReadOnly ? "\uD83D\uDD12 read-only" : "\u270F\uFE0F read-write";
|
|
14403
14583
|
const desc = agent.description || agent.config.description || "";
|
|
14404
|
-
|
|
14584
|
+
const hasCustomProfile = guardrails?.profiles?.[key] !== undefined;
|
|
14585
|
+
const profileIndicator = hasCustomProfile ? " | \u26A1 custom limits" : "";
|
|
14586
|
+
lines.push(`- **${key}** | model: \`${model}\` | temp: ${temp} | ${access}${profileIndicator}`);
|
|
14405
14587
|
if (desc) {
|
|
14406
14588
|
lines.push(` ${desc}`);
|
|
14407
14589
|
}
|
|
14408
14590
|
}
|
|
14591
|
+
if (guardrails?.profiles && Object.keys(guardrails.profiles).length > 0) {
|
|
14592
|
+
lines.push("", "### Guardrail Profiles", "");
|
|
14593
|
+
for (const [profileName, profile] of Object.entries(guardrails.profiles)) {
|
|
14594
|
+
const overrides = [];
|
|
14595
|
+
if (profile.max_tool_calls !== undefined) {
|
|
14596
|
+
overrides.push(`max_tool_calls=${profile.max_tool_calls}`);
|
|
14597
|
+
}
|
|
14598
|
+
if (profile.max_duration_minutes !== undefined) {
|
|
14599
|
+
overrides.push(`max_duration_minutes=${profile.max_duration_minutes}`);
|
|
14600
|
+
}
|
|
14601
|
+
if (profile.max_repetitions !== undefined) {
|
|
14602
|
+
overrides.push(`max_repetitions=${profile.max_repetitions}`);
|
|
14603
|
+
}
|
|
14604
|
+
if (profile.max_consecutive_errors !== undefined) {
|
|
14605
|
+
overrides.push(`max_consecutive_errors=${profile.max_consecutive_errors}`);
|
|
14606
|
+
}
|
|
14607
|
+
if (profile.warning_threshold !== undefined) {
|
|
14608
|
+
overrides.push(`warning_threshold=${profile.warning_threshold}`);
|
|
14609
|
+
}
|
|
14610
|
+
const overrideStr = overrides.length > 0 ? overrides.join(", ") : "no overrides";
|
|
14611
|
+
lines.push(`- **${profileName}**: ${overrideStr}`);
|
|
14612
|
+
}
|
|
14613
|
+
}
|
|
14409
14614
|
return lines.join(`
|
|
14410
14615
|
`);
|
|
14411
14616
|
}
|
|
14412
14617
|
|
|
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
|
-
}
|
|
14618
|
+
// src/evidence/manager.ts
|
|
14619
|
+
import { mkdirSync, readdirSync, renameSync, rmSync, statSync as statSync2 } from "fs";
|
|
14620
|
+
import * as path3 from "path";
|
|
14438
14621
|
|
|
14439
14622
|
// src/hooks/utils.ts
|
|
14440
|
-
import * as
|
|
14623
|
+
import * as path2 from "path";
|
|
14441
14624
|
|
|
14442
14625
|
// src/utils/errors.ts
|
|
14443
14626
|
class SwarmError extends Error {
|
|
@@ -14504,14 +14687,14 @@ function validateSwarmPath(directory, filename) {
|
|
|
14504
14687
|
if (/\.\.[/\\]/.test(filename)) {
|
|
14505
14688
|
throw new Error("Invalid filename: path traversal detected");
|
|
14506
14689
|
}
|
|
14507
|
-
const baseDir =
|
|
14508
|
-
const resolved =
|
|
14690
|
+
const baseDir = path2.normalize(path2.resolve(directory, ".swarm"));
|
|
14691
|
+
const resolved = path2.normalize(path2.resolve(baseDir, filename));
|
|
14509
14692
|
if (process.platform === "win32") {
|
|
14510
|
-
if (!resolved.toLowerCase().startsWith((baseDir +
|
|
14693
|
+
if (!resolved.toLowerCase().startsWith((baseDir + path2.sep).toLowerCase())) {
|
|
14511
14694
|
throw new Error("Invalid filename: path escapes .swarm directory");
|
|
14512
14695
|
}
|
|
14513
14696
|
} else {
|
|
14514
|
-
if (!resolved.startsWith(baseDir +
|
|
14697
|
+
if (!resolved.startsWith(baseDir + path2.sep)) {
|
|
14515
14698
|
throw new Error("Invalid filename: path escapes .swarm directory");
|
|
14516
14699
|
}
|
|
14517
14700
|
}
|
|
@@ -14534,30 +14717,535 @@ function estimateTokens(text) {
|
|
|
14534
14717
|
return Math.ceil(text.length * 0.33);
|
|
14535
14718
|
}
|
|
14536
14719
|
|
|
14720
|
+
// src/evidence/manager.ts
|
|
14721
|
+
var TASK_ID_REGEX = /^[\w-]+(\.[\w-]+)*$/;
|
|
14722
|
+
function sanitizeTaskId(taskId) {
|
|
14723
|
+
if (!taskId || taskId.length === 0) {
|
|
14724
|
+
throw new Error("Invalid task ID: empty string");
|
|
14725
|
+
}
|
|
14726
|
+
if (/\0/.test(taskId)) {
|
|
14727
|
+
throw new Error("Invalid task ID: contains null bytes");
|
|
14728
|
+
}
|
|
14729
|
+
for (let i = 0;i < taskId.length; i++) {
|
|
14730
|
+
if (taskId.charCodeAt(i) < 32) {
|
|
14731
|
+
throw new Error("Invalid task ID: contains control characters");
|
|
14732
|
+
}
|
|
14733
|
+
}
|
|
14734
|
+
if (taskId.includes("..") || taskId.includes("../") || taskId.includes("..\\")) {
|
|
14735
|
+
throw new Error("Invalid task ID: path traversal detected");
|
|
14736
|
+
}
|
|
14737
|
+
if (!TASK_ID_REGEX.test(taskId)) {
|
|
14738
|
+
throw new Error(`Invalid task ID: must match pattern ^[\\w-]+(\\.[\\w-]+)*$, got "${taskId}"`);
|
|
14739
|
+
}
|
|
14740
|
+
return taskId;
|
|
14741
|
+
}
|
|
14742
|
+
async function loadEvidence(directory, taskId) {
|
|
14743
|
+
const sanitizedTaskId = sanitizeTaskId(taskId);
|
|
14744
|
+
const relativePath = path3.join("evidence", sanitizedTaskId, "evidence.json");
|
|
14745
|
+
validateSwarmPath(directory, relativePath);
|
|
14746
|
+
const content = await readSwarmFileAsync(directory, relativePath);
|
|
14747
|
+
if (content === null) {
|
|
14748
|
+
return null;
|
|
14749
|
+
}
|
|
14750
|
+
try {
|
|
14751
|
+
const parsed = JSON.parse(content);
|
|
14752
|
+
const validated = EvidenceBundleSchema.parse(parsed);
|
|
14753
|
+
return validated;
|
|
14754
|
+
} catch (error49) {
|
|
14755
|
+
warn(`Evidence bundle validation failed for task ${sanitizedTaskId}: ${error49 instanceof Error ? error49.message : String(error49)}`);
|
|
14756
|
+
return null;
|
|
14757
|
+
}
|
|
14758
|
+
}
|
|
14759
|
+
async function listEvidenceTaskIds(directory) {
|
|
14760
|
+
const evidenceBasePath = validateSwarmPath(directory, "evidence");
|
|
14761
|
+
try {
|
|
14762
|
+
statSync2(evidenceBasePath);
|
|
14763
|
+
} catch {
|
|
14764
|
+
return [];
|
|
14765
|
+
}
|
|
14766
|
+
let entries;
|
|
14767
|
+
try {
|
|
14768
|
+
entries = readdirSync(evidenceBasePath);
|
|
14769
|
+
} catch {
|
|
14770
|
+
return [];
|
|
14771
|
+
}
|
|
14772
|
+
const taskIds = [];
|
|
14773
|
+
for (const entry of entries) {
|
|
14774
|
+
const entryPath = path3.join(evidenceBasePath, entry);
|
|
14775
|
+
try {
|
|
14776
|
+
const stats = statSync2(entryPath);
|
|
14777
|
+
if (!stats.isDirectory()) {
|
|
14778
|
+
continue;
|
|
14779
|
+
}
|
|
14780
|
+
sanitizeTaskId(entry);
|
|
14781
|
+
taskIds.push(entry);
|
|
14782
|
+
} catch (error49) {
|
|
14783
|
+
if (error49 instanceof Error && !error49.message.startsWith("Invalid task ID")) {
|
|
14784
|
+
warn(`Error reading evidence entry '${entry}': ${error49.message}`);
|
|
14785
|
+
}
|
|
14786
|
+
}
|
|
14787
|
+
}
|
|
14788
|
+
return taskIds.sort();
|
|
14789
|
+
}
|
|
14790
|
+
async function deleteEvidence(directory, taskId) {
|
|
14791
|
+
const sanitizedTaskId = sanitizeTaskId(taskId);
|
|
14792
|
+
const relativePath = path3.join("evidence", sanitizedTaskId);
|
|
14793
|
+
const evidenceDir = validateSwarmPath(directory, relativePath);
|
|
14794
|
+
try {
|
|
14795
|
+
statSync2(evidenceDir);
|
|
14796
|
+
} catch {
|
|
14797
|
+
return false;
|
|
14798
|
+
}
|
|
14799
|
+
try {
|
|
14800
|
+
rmSync(evidenceDir, { recursive: true, force: true });
|
|
14801
|
+
return true;
|
|
14802
|
+
} catch (error49) {
|
|
14803
|
+
warn(`Failed to delete evidence for task ${sanitizedTaskId}: ${error49 instanceof Error ? error49.message : String(error49)}`);
|
|
14804
|
+
return false;
|
|
14805
|
+
}
|
|
14806
|
+
}
|
|
14807
|
+
async function archiveEvidence(directory, maxAgeDays, maxBundles) {
|
|
14808
|
+
const taskIds = await listEvidenceTaskIds(directory);
|
|
14809
|
+
const cutoffDate = new Date;
|
|
14810
|
+
cutoffDate.setDate(cutoffDate.getDate() - maxAgeDays);
|
|
14811
|
+
const cutoffIso = cutoffDate.toISOString();
|
|
14812
|
+
const archived = [];
|
|
14813
|
+
const remainingBundles = [];
|
|
14814
|
+
for (const taskId of taskIds) {
|
|
14815
|
+
const bundle = await loadEvidence(directory, taskId);
|
|
14816
|
+
if (!bundle) {
|
|
14817
|
+
continue;
|
|
14818
|
+
}
|
|
14819
|
+
if (bundle.updated_at < cutoffIso) {
|
|
14820
|
+
const deleted = await deleteEvidence(directory, taskId);
|
|
14821
|
+
if (deleted) {
|
|
14822
|
+
archived.push(taskId);
|
|
14823
|
+
}
|
|
14824
|
+
} else {
|
|
14825
|
+
remainingBundles.push({
|
|
14826
|
+
taskId,
|
|
14827
|
+
updatedAt: bundle.updated_at
|
|
14828
|
+
});
|
|
14829
|
+
}
|
|
14830
|
+
}
|
|
14831
|
+
if (maxBundles !== undefined && remainingBundles.length > maxBundles) {
|
|
14832
|
+
remainingBundles.sort((a, b) => a.updatedAt.localeCompare(b.updatedAt));
|
|
14833
|
+
const toDelete = remainingBundles.length - maxBundles;
|
|
14834
|
+
for (let i = 0;i < toDelete; i++) {
|
|
14835
|
+
const deleted = await deleteEvidence(directory, remainingBundles[i].taskId);
|
|
14836
|
+
if (deleted) {
|
|
14837
|
+
archived.push(remainingBundles[i].taskId);
|
|
14838
|
+
}
|
|
14839
|
+
}
|
|
14840
|
+
}
|
|
14841
|
+
return archived;
|
|
14842
|
+
}
|
|
14843
|
+
|
|
14844
|
+
// src/commands/archive.ts
|
|
14845
|
+
async function handleArchiveCommand(directory, args) {
|
|
14846
|
+
const config2 = loadPluginConfig(directory);
|
|
14847
|
+
const maxAgeDays = config2?.evidence?.max_age_days ?? 90;
|
|
14848
|
+
const maxBundles = config2?.evidence?.max_bundles ?? 1000;
|
|
14849
|
+
const dryRun = args.includes("--dry-run");
|
|
14850
|
+
const beforeTaskIds = await listEvidenceTaskIds(directory);
|
|
14851
|
+
if (beforeTaskIds.length === 0) {
|
|
14852
|
+
return "No evidence bundles to archive.";
|
|
14853
|
+
}
|
|
14854
|
+
if (dryRun) {
|
|
14855
|
+
const cutoffDate = new Date;
|
|
14856
|
+
cutoffDate.setDate(cutoffDate.getDate() - maxAgeDays);
|
|
14857
|
+
const cutoffIso = cutoffDate.toISOString();
|
|
14858
|
+
const wouldArchiveAge = [];
|
|
14859
|
+
const remainingBundles = [];
|
|
14860
|
+
for (const taskId of beforeTaskIds) {
|
|
14861
|
+
const bundle = await loadEvidence(directory, taskId);
|
|
14862
|
+
if (bundle && bundle.updated_at < cutoffIso) {
|
|
14863
|
+
wouldArchiveAge.push(taskId);
|
|
14864
|
+
} else if (bundle) {
|
|
14865
|
+
remainingBundles.push({ taskId, updatedAt: bundle.updated_at });
|
|
14866
|
+
}
|
|
14867
|
+
}
|
|
14868
|
+
const wouldArchiveMaxBundles = [];
|
|
14869
|
+
const remainingAfterAge = beforeTaskIds.length - wouldArchiveAge.length;
|
|
14870
|
+
if (remainingAfterAge > maxBundles) {
|
|
14871
|
+
remainingBundles.sort((a, b) => a.updatedAt.localeCompare(b.updatedAt));
|
|
14872
|
+
const excessCount = remainingAfterAge - maxBundles;
|
|
14873
|
+
wouldArchiveMaxBundles.push(...remainingBundles.slice(0, excessCount).map((b) => b.taskId));
|
|
14874
|
+
}
|
|
14875
|
+
const totalWouldArchive = wouldArchiveAge.length + wouldArchiveMaxBundles.length;
|
|
14876
|
+
if (totalWouldArchive === 0) {
|
|
14877
|
+
return `No evidence bundles older than ${maxAgeDays} days found, and bundle count (${beforeTaskIds.length}) is within max_bundles limit (${maxBundles}).`;
|
|
14878
|
+
}
|
|
14879
|
+
const lines2 = [
|
|
14880
|
+
"## Archive Preview (dry run)",
|
|
14881
|
+
"",
|
|
14882
|
+
`**Retention**: ${maxAgeDays} days`,
|
|
14883
|
+
`**Max bundles**: ${maxBundles}`,
|
|
14884
|
+
`**Would archive**: ${totalWouldArchive} bundle(s)`
|
|
14885
|
+
];
|
|
14886
|
+
if (wouldArchiveAge.length > 0) {
|
|
14887
|
+
lines2.push("", `**Age-based (${wouldArchiveAge.length})**:`, ...wouldArchiveAge.map((id) => `- ${id}`));
|
|
14888
|
+
}
|
|
14889
|
+
if (wouldArchiveMaxBundles.length > 0) {
|
|
14890
|
+
lines2.push("", `**Max bundles limit (${wouldArchiveMaxBundles.length})**:`, ...wouldArchiveMaxBundles.map((id) => `- ${id}`));
|
|
14891
|
+
}
|
|
14892
|
+
return lines2.join(`
|
|
14893
|
+
`);
|
|
14894
|
+
}
|
|
14895
|
+
const archived = await archiveEvidence(directory, maxAgeDays, maxBundles);
|
|
14896
|
+
if (archived.length === 0) {
|
|
14897
|
+
return `No evidence bundles older than ${maxAgeDays} days found.`;
|
|
14898
|
+
}
|
|
14899
|
+
const lines = [
|
|
14900
|
+
"## Evidence Archived",
|
|
14901
|
+
"",
|
|
14902
|
+
`**Retention**: ${maxAgeDays} days`,
|
|
14903
|
+
`**Archived**: ${archived.length} bundle(s)`,
|
|
14904
|
+
"",
|
|
14905
|
+
...archived.map((id) => `- ${id}`)
|
|
14906
|
+
];
|
|
14907
|
+
return lines.join(`
|
|
14908
|
+
`);
|
|
14909
|
+
}
|
|
14910
|
+
|
|
14911
|
+
// src/commands/config.ts
|
|
14912
|
+
import * as os2 from "os";
|
|
14913
|
+
import * as path4 from "path";
|
|
14914
|
+
function getUserConfigDir2() {
|
|
14915
|
+
return process.env.XDG_CONFIG_HOME || path4.join(os2.homedir(), ".config");
|
|
14916
|
+
}
|
|
14917
|
+
async function handleConfigCommand(directory, _args) {
|
|
14918
|
+
const config2 = loadPluginConfig(directory);
|
|
14919
|
+
const userConfigPath = path4.join(getUserConfigDir2(), "opencode", "opencode-swarm.json");
|
|
14920
|
+
const projectConfigPath = path4.join(directory, ".opencode", "opencode-swarm.json");
|
|
14921
|
+
const lines = [
|
|
14922
|
+
"## Swarm Configuration",
|
|
14923
|
+
"",
|
|
14924
|
+
"### Config Files",
|
|
14925
|
+
`- User: \`${userConfigPath}\``,
|
|
14926
|
+
`- Project: \`${projectConfigPath}\``,
|
|
14927
|
+
"",
|
|
14928
|
+
"### Resolved Config",
|
|
14929
|
+
"```json",
|
|
14930
|
+
JSON.stringify(config2, null, 2),
|
|
14931
|
+
"```"
|
|
14932
|
+
];
|
|
14933
|
+
return lines.join(`
|
|
14934
|
+
`);
|
|
14935
|
+
}
|
|
14936
|
+
|
|
14937
|
+
// src/plan/manager.ts
|
|
14938
|
+
import * as path5 from "path";
|
|
14939
|
+
async function loadPlanJsonOnly(directory) {
|
|
14940
|
+
const planJsonContent = await readSwarmFileAsync(directory, "plan.json");
|
|
14941
|
+
if (planJsonContent !== null) {
|
|
14942
|
+
try {
|
|
14943
|
+
const parsed = JSON.parse(planJsonContent);
|
|
14944
|
+
const validated = PlanSchema.parse(parsed);
|
|
14945
|
+
return validated;
|
|
14946
|
+
} catch (error49) {
|
|
14947
|
+
warn(`Plan validation failed for .swarm/plan.json: ${error49 instanceof Error ? error49.message : String(error49)}`);
|
|
14948
|
+
}
|
|
14949
|
+
}
|
|
14950
|
+
return null;
|
|
14951
|
+
}
|
|
14952
|
+
async function loadPlan(directory) {
|
|
14953
|
+
const planJsonContent = await readSwarmFileAsync(directory, "plan.json");
|
|
14954
|
+
if (planJsonContent !== null) {
|
|
14955
|
+
try {
|
|
14956
|
+
const parsed = JSON.parse(planJsonContent);
|
|
14957
|
+
const validated = PlanSchema.parse(parsed);
|
|
14958
|
+
return validated;
|
|
14959
|
+
} catch (error49) {
|
|
14960
|
+
warn(`Plan validation failed for .swarm/plan.json: ${error49 instanceof Error ? error49.message : String(error49)}`);
|
|
14961
|
+
}
|
|
14962
|
+
}
|
|
14963
|
+
const planMdContent = await readSwarmFileAsync(directory, "plan.md");
|
|
14964
|
+
if (planMdContent !== null) {
|
|
14965
|
+
const migrated = migrateLegacyPlan(planMdContent);
|
|
14966
|
+
await savePlan(directory, migrated);
|
|
14967
|
+
return migrated;
|
|
14968
|
+
}
|
|
14969
|
+
return null;
|
|
14970
|
+
}
|
|
14971
|
+
async function savePlan(directory, plan) {
|
|
14972
|
+
const validated = PlanSchema.parse(plan);
|
|
14973
|
+
const swarmDir = path5.resolve(directory, ".swarm");
|
|
14974
|
+
const planPath = path5.join(swarmDir, "plan.json");
|
|
14975
|
+
const tempPath = path5.join(swarmDir, `plan.json.tmp.${Date.now()}`);
|
|
14976
|
+
await Bun.write(tempPath, JSON.stringify(validated, null, 2));
|
|
14977
|
+
const { renameSync: renameSync2 } = await import("fs");
|
|
14978
|
+
renameSync2(tempPath, planPath);
|
|
14979
|
+
const markdown = derivePlanMarkdown(validated);
|
|
14980
|
+
await Bun.write(path5.join(swarmDir, "plan.md"), markdown);
|
|
14981
|
+
}
|
|
14982
|
+
function derivePlanMarkdown(plan) {
|
|
14983
|
+
const statusMap = {
|
|
14984
|
+
pending: "PENDING",
|
|
14985
|
+
in_progress: "IN PROGRESS",
|
|
14986
|
+
complete: "COMPLETE",
|
|
14987
|
+
blocked: "BLOCKED"
|
|
14988
|
+
};
|
|
14989
|
+
const now = new Date().toISOString();
|
|
14990
|
+
const phaseStatus = statusMap[plan.phases[plan.current_phase - 1]?.status] || "PENDING";
|
|
14991
|
+
let markdown = `# ${plan.title}
|
|
14992
|
+
Swarm: ${plan.swarm}
|
|
14993
|
+
Phase: ${plan.current_phase} [${phaseStatus}] | Updated: ${now}
|
|
14994
|
+
`;
|
|
14995
|
+
for (const phase of plan.phases) {
|
|
14996
|
+
const phaseStatusText = statusMap[phase.status] || "PENDING";
|
|
14997
|
+
markdown += `
|
|
14998
|
+
## Phase ${phase.id}: ${phase.name} [${phaseStatusText}]
|
|
14999
|
+
`;
|
|
15000
|
+
let currentTaskMarked = false;
|
|
15001
|
+
for (const task of phase.tasks) {
|
|
15002
|
+
let taskLine = "";
|
|
15003
|
+
let suffix = "";
|
|
15004
|
+
if (task.status === "completed") {
|
|
15005
|
+
taskLine = `- [x] ${task.id}: ${task.description}`;
|
|
15006
|
+
} else if (task.status === "blocked") {
|
|
15007
|
+
taskLine = `- [BLOCKED] ${task.id}: ${task.description}`;
|
|
15008
|
+
if (task.blocked_reason) {
|
|
15009
|
+
taskLine += ` - ${task.blocked_reason}`;
|
|
15010
|
+
}
|
|
15011
|
+
} else {
|
|
15012
|
+
taskLine = `- [ ] ${task.id}: ${task.description}`;
|
|
15013
|
+
}
|
|
15014
|
+
taskLine += ` [${task.size.toUpperCase()}]`;
|
|
15015
|
+
if (task.depends.length > 0) {
|
|
15016
|
+
suffix += ` (depends: ${task.depends.join(", ")})`;
|
|
15017
|
+
}
|
|
15018
|
+
if (phase.id === plan.current_phase && task.status === "in_progress" && !currentTaskMarked) {
|
|
15019
|
+
suffix += " \u2190 CURRENT";
|
|
15020
|
+
currentTaskMarked = true;
|
|
15021
|
+
}
|
|
15022
|
+
markdown += `${taskLine}${suffix}
|
|
15023
|
+
`;
|
|
15024
|
+
}
|
|
15025
|
+
}
|
|
15026
|
+
const phaseSections = markdown.split(`
|
|
15027
|
+
## `);
|
|
15028
|
+
if (phaseSections.length > 1) {
|
|
15029
|
+
const header = phaseSections[0];
|
|
15030
|
+
const phases = phaseSections.slice(1).map((p) => `## ${p}`);
|
|
15031
|
+
markdown = `${header}
|
|
15032
|
+
---
|
|
15033
|
+
${phases.join(`
|
|
15034
|
+
---
|
|
15035
|
+
`)}`;
|
|
15036
|
+
}
|
|
15037
|
+
return `${markdown.trim()}
|
|
15038
|
+
`;
|
|
15039
|
+
}
|
|
15040
|
+
function migrateLegacyPlan(planContent, swarmId) {
|
|
15041
|
+
const lines = planContent.split(`
|
|
15042
|
+
`);
|
|
15043
|
+
let title = "Untitled Plan";
|
|
15044
|
+
let swarm = swarmId || "default-swarm";
|
|
15045
|
+
let currentPhaseNum = 1;
|
|
15046
|
+
const phases = [];
|
|
15047
|
+
let currentPhase = null;
|
|
15048
|
+
for (const line of lines) {
|
|
15049
|
+
const trimmed = line.trim();
|
|
15050
|
+
if (trimmed.startsWith("# ") && title === "Untitled Plan") {
|
|
15051
|
+
title = trimmed.substring(2).trim();
|
|
15052
|
+
continue;
|
|
15053
|
+
}
|
|
15054
|
+
if (trimmed.startsWith("Swarm:")) {
|
|
15055
|
+
swarm = trimmed.substring(6).trim();
|
|
15056
|
+
continue;
|
|
15057
|
+
}
|
|
15058
|
+
if (trimmed.startsWith("Phase:")) {
|
|
15059
|
+
const match = trimmed.match(/Phase:\s*(\d+)/i);
|
|
15060
|
+
if (match) {
|
|
15061
|
+
currentPhaseNum = parseInt(match[1], 10);
|
|
15062
|
+
}
|
|
15063
|
+
continue;
|
|
15064
|
+
}
|
|
15065
|
+
const phaseMatch = trimmed.match(/^##\s*Phase\s+(\d+)(?::\s*([^[]+))?\s*(?:\[([^\]]+)\])?/i);
|
|
15066
|
+
if (phaseMatch) {
|
|
15067
|
+
if (currentPhase !== null) {
|
|
15068
|
+
phases.push(currentPhase);
|
|
15069
|
+
}
|
|
15070
|
+
const phaseId = parseInt(phaseMatch[1], 10);
|
|
15071
|
+
const phaseName = phaseMatch[2]?.trim() || `Phase ${phaseId}`;
|
|
15072
|
+
const statusText = phaseMatch[3]?.toLowerCase() || "pending";
|
|
15073
|
+
const statusMap = {
|
|
15074
|
+
complete: "complete",
|
|
15075
|
+
completed: "complete",
|
|
15076
|
+
"in progress": "in_progress",
|
|
15077
|
+
in_progress: "in_progress",
|
|
15078
|
+
inprogress: "in_progress",
|
|
15079
|
+
pending: "pending",
|
|
15080
|
+
blocked: "blocked"
|
|
15081
|
+
};
|
|
15082
|
+
currentPhase = {
|
|
15083
|
+
id: phaseId,
|
|
15084
|
+
name: phaseName,
|
|
15085
|
+
status: statusMap[statusText] || "pending",
|
|
15086
|
+
tasks: []
|
|
15087
|
+
};
|
|
15088
|
+
continue;
|
|
15089
|
+
}
|
|
15090
|
+
const taskMatch = trimmed.match(/^-\s*\[([^\]]+)\]\s+(\d+\.\d+):\s*(.+?)(?:\s*\[(\w+)\])?(?:\s*-\s*(.+))?$/i);
|
|
15091
|
+
if (taskMatch && currentPhase !== null) {
|
|
15092
|
+
const checkbox = taskMatch[1].toLowerCase();
|
|
15093
|
+
const taskId = taskMatch[2];
|
|
15094
|
+
let description = taskMatch[3].trim();
|
|
15095
|
+
const sizeText = taskMatch[4]?.toLowerCase() || "small";
|
|
15096
|
+
let blockedReason;
|
|
15097
|
+
const dependsMatch = description.match(/\s*\(depends:\s*([^)]+)\)$/i);
|
|
15098
|
+
const depends = [];
|
|
15099
|
+
if (dependsMatch) {
|
|
15100
|
+
const depsText = dependsMatch[1];
|
|
15101
|
+
depends.push(...depsText.split(",").map((d) => d.trim()));
|
|
15102
|
+
description = description.substring(0, dependsMatch.index).trim();
|
|
15103
|
+
}
|
|
15104
|
+
let status = "pending";
|
|
15105
|
+
if (checkbox === "x") {
|
|
15106
|
+
status = "completed";
|
|
15107
|
+
} else if (checkbox === "blocked") {
|
|
15108
|
+
status = "blocked";
|
|
15109
|
+
const blockedReasonMatch = taskMatch[5];
|
|
15110
|
+
if (blockedReasonMatch) {
|
|
15111
|
+
blockedReason = blockedReasonMatch.trim();
|
|
15112
|
+
}
|
|
15113
|
+
}
|
|
15114
|
+
const sizeMap = {
|
|
15115
|
+
small: "small",
|
|
15116
|
+
medium: "medium",
|
|
15117
|
+
large: "large"
|
|
15118
|
+
};
|
|
15119
|
+
const task = {
|
|
15120
|
+
id: taskId,
|
|
15121
|
+
phase: currentPhase.id,
|
|
15122
|
+
status,
|
|
15123
|
+
size: sizeMap[sizeText] || "small",
|
|
15124
|
+
description,
|
|
15125
|
+
depends,
|
|
15126
|
+
acceptance: undefined,
|
|
15127
|
+
files_touched: [],
|
|
15128
|
+
evidence_path: undefined,
|
|
15129
|
+
blocked_reason: blockedReason
|
|
15130
|
+
};
|
|
15131
|
+
currentPhase.tasks.push(task);
|
|
15132
|
+
}
|
|
15133
|
+
}
|
|
15134
|
+
if (currentPhase !== null) {
|
|
15135
|
+
phases.push(currentPhase);
|
|
15136
|
+
}
|
|
15137
|
+
let migrationStatus = "migrated";
|
|
15138
|
+
if (phases.length === 0) {
|
|
15139
|
+
migrationStatus = "migration_failed";
|
|
15140
|
+
phases.push({
|
|
15141
|
+
id: 1,
|
|
15142
|
+
name: "Migration Failed",
|
|
15143
|
+
status: "blocked",
|
|
15144
|
+
tasks: [
|
|
15145
|
+
{
|
|
15146
|
+
id: "1.1",
|
|
15147
|
+
phase: 1,
|
|
15148
|
+
status: "blocked",
|
|
15149
|
+
size: "large",
|
|
15150
|
+
description: "Review and restructure plan manually",
|
|
15151
|
+
depends: [],
|
|
15152
|
+
files_touched: [],
|
|
15153
|
+
blocked_reason: "Legacy plan could not be parsed automatically"
|
|
15154
|
+
}
|
|
15155
|
+
]
|
|
15156
|
+
});
|
|
15157
|
+
}
|
|
15158
|
+
phases.sort((a, b) => a.id - b.id);
|
|
15159
|
+
const plan = {
|
|
15160
|
+
schema_version: "1.0.0",
|
|
15161
|
+
title,
|
|
15162
|
+
swarm,
|
|
15163
|
+
current_phase: currentPhaseNum,
|
|
15164
|
+
phases,
|
|
15165
|
+
migration_status: migrationStatus
|
|
15166
|
+
};
|
|
15167
|
+
return plan;
|
|
15168
|
+
}
|
|
15169
|
+
|
|
14537
15170
|
// src/commands/diagnose.ts
|
|
14538
15171
|
async function handleDiagnoseCommand(directory, _args) {
|
|
14539
15172
|
const checks3 = [];
|
|
14540
|
-
const
|
|
14541
|
-
|
|
14542
|
-
|
|
14543
|
-
|
|
14544
|
-
|
|
14545
|
-
|
|
15173
|
+
const plan = await loadPlanJsonOnly(directory);
|
|
15174
|
+
if (plan) {
|
|
15175
|
+
checks3.push({
|
|
15176
|
+
name: "plan.json",
|
|
15177
|
+
status: "\u2705",
|
|
15178
|
+
detail: "Valid schema (v1.0.0)"
|
|
15179
|
+
});
|
|
15180
|
+
if (plan.migration_status === "migrated") {
|
|
14546
15181
|
checks3.push({
|
|
14547
|
-
name: "
|
|
15182
|
+
name: "Migration",
|
|
14548
15183
|
status: "\u2705",
|
|
14549
|
-
detail: "
|
|
15184
|
+
detail: "Plan was migrated from legacy plan.md"
|
|
15185
|
+
});
|
|
15186
|
+
} else if (plan.migration_status === "migration_failed") {
|
|
15187
|
+
checks3.push({
|
|
15188
|
+
name: "Migration",
|
|
15189
|
+
status: "\u274C",
|
|
15190
|
+
detail: "Migration from plan.md failed \u2014 review manually"
|
|
15191
|
+
});
|
|
15192
|
+
}
|
|
15193
|
+
const allTaskIds = new Set;
|
|
15194
|
+
for (const phase of plan.phases) {
|
|
15195
|
+
for (const task of phase.tasks) {
|
|
15196
|
+
allTaskIds.add(task.id);
|
|
15197
|
+
}
|
|
15198
|
+
}
|
|
15199
|
+
const missingDeps = [];
|
|
15200
|
+
for (const phase of plan.phases) {
|
|
15201
|
+
for (const task of phase.tasks) {
|
|
15202
|
+
for (const dep of task.depends) {
|
|
15203
|
+
if (!allTaskIds.has(dep)) {
|
|
15204
|
+
missingDeps.push(`${task.id} depends on missing ${dep}`);
|
|
15205
|
+
}
|
|
15206
|
+
}
|
|
15207
|
+
}
|
|
15208
|
+
}
|
|
15209
|
+
if (missingDeps.length > 0) {
|
|
15210
|
+
checks3.push({
|
|
15211
|
+
name: "Task DAG",
|
|
15212
|
+
status: "\u274C",
|
|
15213
|
+
detail: `Missing dependencies: ${missingDeps.join(", ")}`
|
|
14550
15214
|
});
|
|
15215
|
+
} else {
|
|
15216
|
+
checks3.push({
|
|
15217
|
+
name: "Task DAG",
|
|
15218
|
+
status: "\u2705",
|
|
15219
|
+
detail: "All dependencies resolved"
|
|
15220
|
+
});
|
|
15221
|
+
}
|
|
15222
|
+
} else {
|
|
15223
|
+
const planContent = await readSwarmFileAsync(directory, "plan.md");
|
|
15224
|
+
if (planContent) {
|
|
15225
|
+
const hasPhases = /^## Phase \d+/m.test(planContent);
|
|
15226
|
+
const hasTasks = /^- \[[ x]\]/m.test(planContent);
|
|
15227
|
+
if (hasPhases && hasTasks) {
|
|
15228
|
+
checks3.push({
|
|
15229
|
+
name: "plan.md",
|
|
15230
|
+
status: "\u2705",
|
|
15231
|
+
detail: "Found with valid phase structure"
|
|
15232
|
+
});
|
|
15233
|
+
} else {
|
|
15234
|
+
checks3.push({
|
|
15235
|
+
name: "plan.md",
|
|
15236
|
+
status: "\u274C",
|
|
15237
|
+
detail: "Found but missing phase/task structure"
|
|
15238
|
+
});
|
|
15239
|
+
}
|
|
14551
15240
|
} else {
|
|
14552
15241
|
checks3.push({
|
|
14553
15242
|
name: "plan.md",
|
|
14554
15243
|
status: "\u274C",
|
|
14555
|
-
detail: "
|
|
15244
|
+
detail: "Not found"
|
|
14556
15245
|
});
|
|
14557
15246
|
}
|
|
14558
|
-
} else {
|
|
14559
|
-
checks3.push({ name: "plan.md", status: "\u274C", detail: "Not found" });
|
|
14560
15247
|
}
|
|
15248
|
+
const contextContent = await readSwarmFileAsync(directory, "context.md");
|
|
14561
15249
|
if (contextContent) {
|
|
14562
15250
|
checks3.push({ name: "context.md", status: "\u2705", detail: "Found" });
|
|
14563
15251
|
} else {
|
|
@@ -14585,6 +15273,39 @@ async function handleDiagnoseCommand(directory, _args) {
|
|
|
14585
15273
|
detail: "Invalid configuration"
|
|
14586
15274
|
});
|
|
14587
15275
|
}
|
|
15276
|
+
if (plan) {
|
|
15277
|
+
const completedTaskIds = [];
|
|
15278
|
+
for (const phase of plan.phases) {
|
|
15279
|
+
for (const task of phase.tasks) {
|
|
15280
|
+
if (task.status === "completed") {
|
|
15281
|
+
completedTaskIds.push(task.id);
|
|
15282
|
+
}
|
|
15283
|
+
}
|
|
15284
|
+
}
|
|
15285
|
+
if (completedTaskIds.length > 0) {
|
|
15286
|
+
const evidenceTaskIds = new Set(await listEvidenceTaskIds(directory));
|
|
15287
|
+
const missingEvidence = completedTaskIds.filter((id) => !evidenceTaskIds.has(id));
|
|
15288
|
+
if (missingEvidence.length === 0) {
|
|
15289
|
+
checks3.push({
|
|
15290
|
+
name: "Evidence",
|
|
15291
|
+
status: "\u2705",
|
|
15292
|
+
detail: `All ${completedTaskIds.length} completed tasks have evidence`
|
|
15293
|
+
});
|
|
15294
|
+
} else {
|
|
15295
|
+
checks3.push({
|
|
15296
|
+
name: "Evidence",
|
|
15297
|
+
status: "\u274C",
|
|
15298
|
+
detail: `${missingEvidence.length} completed task(s) missing evidence: ${missingEvidence.join(", ")}`
|
|
15299
|
+
});
|
|
15300
|
+
}
|
|
15301
|
+
} else {
|
|
15302
|
+
checks3.push({
|
|
15303
|
+
name: "Evidence",
|
|
15304
|
+
status: "\u2705",
|
|
15305
|
+
detail: "No completed tasks yet"
|
|
15306
|
+
});
|
|
15307
|
+
}
|
|
15308
|
+
}
|
|
14588
15309
|
const passCount = checks3.filter((c) => c.status === "\u2705").length;
|
|
14589
15310
|
const totalCount = checks3.length;
|
|
14590
15311
|
const allPassed = passCount === totalCount;
|
|
@@ -14599,14 +15320,98 @@ async function handleDiagnoseCommand(directory, _args) {
|
|
|
14599
15320
|
`);
|
|
14600
15321
|
}
|
|
14601
15322
|
|
|
15323
|
+
// src/commands/evidence.ts
|
|
15324
|
+
async function handleEvidenceCommand(directory, args) {
|
|
15325
|
+
if (args.length === 0) {
|
|
15326
|
+
const taskIds = await listEvidenceTaskIds(directory);
|
|
15327
|
+
if (taskIds.length === 0) {
|
|
15328
|
+
return "No evidence bundles found.";
|
|
15329
|
+
}
|
|
15330
|
+
const tableLines = [
|
|
15331
|
+
"## Evidence Bundles",
|
|
15332
|
+
"",
|
|
15333
|
+
"| Task | Entries | Last Updated |",
|
|
15334
|
+
"|------|---------|-------------|"
|
|
15335
|
+
];
|
|
15336
|
+
for (const taskId2 of taskIds) {
|
|
15337
|
+
const bundle2 = await loadEvidence(directory, taskId2);
|
|
15338
|
+
if (bundle2) {
|
|
15339
|
+
const entryCount = bundle2.entries.length;
|
|
15340
|
+
const lastUpdated = bundle2.updated_at;
|
|
15341
|
+
tableLines.push(`| ${taskId2} | ${entryCount} | ${lastUpdated} |`);
|
|
15342
|
+
} else {
|
|
15343
|
+
tableLines.push(`| ${taskId2} | ? | unknown |`);
|
|
15344
|
+
}
|
|
15345
|
+
}
|
|
15346
|
+
return tableLines.join(`
|
|
15347
|
+
`);
|
|
15348
|
+
}
|
|
15349
|
+
const taskId = args[0];
|
|
15350
|
+
const bundle = await loadEvidence(directory, taskId);
|
|
15351
|
+
if (!bundle) {
|
|
15352
|
+
return `No evidence found for task ${taskId}.`;
|
|
15353
|
+
}
|
|
15354
|
+
const lines = [
|
|
15355
|
+
`## Evidence for Task ${taskId}`,
|
|
15356
|
+
"",
|
|
15357
|
+
`**Created**: ${bundle.created_at}`,
|
|
15358
|
+
`**Updated**: ${bundle.updated_at}`,
|
|
15359
|
+
`**Entries**: ${bundle.entries.length}`
|
|
15360
|
+
];
|
|
15361
|
+
if (bundle.entries.length > 0) {
|
|
15362
|
+
lines.push("");
|
|
15363
|
+
}
|
|
15364
|
+
for (let i = 0;i < bundle.entries.length; i++) {
|
|
15365
|
+
const entry = bundle.entries[i];
|
|
15366
|
+
lines.push(...formatEntry(i + 1, entry));
|
|
15367
|
+
}
|
|
15368
|
+
return lines.join(`
|
|
15369
|
+
`);
|
|
15370
|
+
}
|
|
15371
|
+
function formatEntry(index, entry) {
|
|
15372
|
+
const lines = [];
|
|
15373
|
+
const verdictEmoji = getVerdictEmoji(entry.verdict);
|
|
15374
|
+
lines.push(`### Entry ${index}: ${entry.type} (${entry.verdict}) ${verdictEmoji}`);
|
|
15375
|
+
lines.push(`- **Agent**: ${entry.agent}`);
|
|
15376
|
+
lines.push(`- **Summary**: ${entry.summary}`);
|
|
15377
|
+
lines.push(`- **Time**: ${entry.timestamp}`);
|
|
15378
|
+
if (entry.type === "review") {
|
|
15379
|
+
const reviewEntry = entry;
|
|
15380
|
+
lines.push(`- **Risk Level**: ${reviewEntry.risk}`);
|
|
15381
|
+
if (reviewEntry.issues && reviewEntry.issues.length > 0) {
|
|
15382
|
+
lines.push(`- **Issues**: ${reviewEntry.issues.length}`);
|
|
15383
|
+
}
|
|
15384
|
+
} else if (entry.type === "test") {
|
|
15385
|
+
const testEntry = entry;
|
|
15386
|
+
lines.push(`- **Tests**: ${testEntry.tests_passed} passed, ${testEntry.tests_failed} failed`);
|
|
15387
|
+
}
|
|
15388
|
+
lines.push("");
|
|
15389
|
+
return lines;
|
|
15390
|
+
}
|
|
15391
|
+
function getVerdictEmoji(verdict) {
|
|
15392
|
+
switch (verdict) {
|
|
15393
|
+
case "pass":
|
|
15394
|
+
case "approved":
|
|
15395
|
+
return "\u2705";
|
|
15396
|
+
case "fail":
|
|
15397
|
+
case "rejected":
|
|
15398
|
+
return "\u274C";
|
|
15399
|
+
case "info":
|
|
15400
|
+
return "\u2139\uFE0F";
|
|
15401
|
+
default:
|
|
15402
|
+
return "";
|
|
15403
|
+
}
|
|
15404
|
+
}
|
|
15405
|
+
|
|
14602
15406
|
// src/commands/export.ts
|
|
14603
15407
|
async function handleExportCommand(directory, _args) {
|
|
15408
|
+
const planStructured = await loadPlanJsonOnly(directory);
|
|
14604
15409
|
const planContent = await readSwarmFileAsync(directory, "plan.md");
|
|
14605
15410
|
const contextContent = await readSwarmFileAsync(directory, "context.md");
|
|
14606
15411
|
const exportData = {
|
|
14607
15412
|
version: "4.5.0",
|
|
14608
15413
|
exported: new Date().toISOString(),
|
|
14609
|
-
plan: planContent,
|
|
15414
|
+
plan: planStructured || planContent,
|
|
14610
15415
|
context: contextContent
|
|
14611
15416
|
};
|
|
14612
15417
|
const lines = [
|
|
@@ -14622,6 +15427,34 @@ async function handleExportCommand(directory, _args) {
|
|
|
14622
15427
|
|
|
14623
15428
|
// src/commands/history.ts
|
|
14624
15429
|
async function handleHistoryCommand(directory, _args) {
|
|
15430
|
+
const plan = await loadPlanJsonOnly(directory);
|
|
15431
|
+
if (plan) {
|
|
15432
|
+
if (plan.phases.length === 0) {
|
|
15433
|
+
return "No history available.";
|
|
15434
|
+
}
|
|
15435
|
+
const tableLines2 = [
|
|
15436
|
+
"## Swarm History",
|
|
15437
|
+
"",
|
|
15438
|
+
"| Phase | Name | Status | Tasks |",
|
|
15439
|
+
"|-------|------|--------|-------|"
|
|
15440
|
+
];
|
|
15441
|
+
for (const phase of plan.phases) {
|
|
15442
|
+
const statusMap = {
|
|
15443
|
+
complete: "COMPLETE",
|
|
15444
|
+
in_progress: "IN PROGRESS",
|
|
15445
|
+
pending: "PENDING",
|
|
15446
|
+
blocked: "BLOCKED"
|
|
15447
|
+
};
|
|
15448
|
+
const statusText = statusMap[phase.status] || "PENDING";
|
|
15449
|
+
const statusIcon = phase.status === "complete" ? "\u2705" : phase.status === "in_progress" ? "\uD83D\uDD04" : phase.status === "blocked" ? "\uD83D\uDEAB" : "\u23F3";
|
|
15450
|
+
const completed = phase.tasks.filter((t) => t.status === "completed").length;
|
|
15451
|
+
const total = phase.tasks.length;
|
|
15452
|
+
const tasks = total > 0 ? `${completed}/${total}` : "-";
|
|
15453
|
+
tableLines2.push(`| ${phase.id} | ${phase.name} | ${statusIcon} ${statusText} | ${tasks} |`);
|
|
15454
|
+
}
|
|
15455
|
+
return tableLines2.join(`
|
|
15456
|
+
`);
|
|
15457
|
+
}
|
|
14625
15458
|
const planContent = await readSwarmFileAsync(directory, "plan.md");
|
|
14626
15459
|
if (!planContent) {
|
|
14627
15460
|
return "No history available.";
|
|
@@ -14673,6 +15506,46 @@ async function handleHistoryCommand(directory, _args) {
|
|
|
14673
15506
|
|
|
14674
15507
|
// src/commands/plan.ts
|
|
14675
15508
|
async function handlePlanCommand(directory, args) {
|
|
15509
|
+
const plan = await loadPlanJsonOnly(directory);
|
|
15510
|
+
if (plan) {
|
|
15511
|
+
if (args.length === 0) {
|
|
15512
|
+
return derivePlanMarkdown(plan);
|
|
15513
|
+
}
|
|
15514
|
+
const phaseNum2 = parseInt(args[0], 10);
|
|
15515
|
+
if (Number.isNaN(phaseNum2)) {
|
|
15516
|
+
return derivePlanMarkdown(plan);
|
|
15517
|
+
}
|
|
15518
|
+
const phase = plan.phases.find((p) => p.id === phaseNum2);
|
|
15519
|
+
if (!phase) {
|
|
15520
|
+
return `Phase ${phaseNum2} not found in plan.`;
|
|
15521
|
+
}
|
|
15522
|
+
const fullMarkdown = derivePlanMarkdown(plan);
|
|
15523
|
+
const lines2 = fullMarkdown.split(`
|
|
15524
|
+
`);
|
|
15525
|
+
const phaseLines2 = [];
|
|
15526
|
+
let inTargetPhase2 = false;
|
|
15527
|
+
for (const line of lines2) {
|
|
15528
|
+
const phaseMatch = line.match(/^## Phase (\d+)/);
|
|
15529
|
+
if (phaseMatch) {
|
|
15530
|
+
const num = parseInt(phaseMatch[1], 10);
|
|
15531
|
+
if (num === phaseNum2) {
|
|
15532
|
+
inTargetPhase2 = true;
|
|
15533
|
+
phaseLines2.push(line);
|
|
15534
|
+
continue;
|
|
15535
|
+
} else if (inTargetPhase2) {
|
|
15536
|
+
break;
|
|
15537
|
+
}
|
|
15538
|
+
}
|
|
15539
|
+
if (inTargetPhase2 && line.trim() === "---" && phaseLines2.length > 1) {
|
|
15540
|
+
break;
|
|
15541
|
+
}
|
|
15542
|
+
if (inTargetPhase2) {
|
|
15543
|
+
phaseLines2.push(line);
|
|
15544
|
+
}
|
|
15545
|
+
}
|
|
15546
|
+
return phaseLines2.length > 0 ? phaseLines2.join(`
|
|
15547
|
+
`).trim() : `Phase ${phaseNum2} not found in plan.`;
|
|
15548
|
+
}
|
|
14676
15549
|
const planContent = await readSwarmFileAsync(directory, "plan.md");
|
|
14677
15550
|
if (!planContent) {
|
|
14678
15551
|
return "No active swarm plan found.";
|
|
@@ -14900,13 +15773,81 @@ function extractPatterns(contextContent, maxChars = 500) {
|
|
|
14900
15773
|
}
|
|
14901
15774
|
return `${trimmed.slice(0, maxChars)}...`;
|
|
14902
15775
|
}
|
|
15776
|
+
function extractCurrentPhaseFromPlan(plan) {
|
|
15777
|
+
const phase = plan.phases.find((p) => p.id === plan.current_phase);
|
|
15778
|
+
if (!phase)
|
|
15779
|
+
return null;
|
|
15780
|
+
const statusMap = {
|
|
15781
|
+
pending: "PENDING",
|
|
15782
|
+
in_progress: "IN PROGRESS",
|
|
15783
|
+
complete: "COMPLETE",
|
|
15784
|
+
blocked: "BLOCKED"
|
|
15785
|
+
};
|
|
15786
|
+
const statusText = statusMap[phase.status] || "PENDING";
|
|
15787
|
+
return `Phase ${phase.id}: ${phase.name} [${statusText}]`;
|
|
15788
|
+
}
|
|
15789
|
+
function extractCurrentTaskFromPlan(plan) {
|
|
15790
|
+
const phase = plan.phases.find((p) => p.id === plan.current_phase);
|
|
15791
|
+
if (!phase)
|
|
15792
|
+
return null;
|
|
15793
|
+
const inProgress = phase.tasks.find((t) => t.status === "in_progress");
|
|
15794
|
+
if (inProgress) {
|
|
15795
|
+
const deps = inProgress.depends.length > 0 ? ` (depends: ${inProgress.depends.join(", ")})` : "";
|
|
15796
|
+
return `- [ ] ${inProgress.id}: ${inProgress.description} [${inProgress.size.toUpperCase()}]${deps} \u2190 CURRENT`;
|
|
15797
|
+
}
|
|
15798
|
+
const pending = phase.tasks.find((t) => t.status === "pending");
|
|
15799
|
+
if (pending) {
|
|
15800
|
+
const deps = pending.depends.length > 0 ? ` (depends: ${pending.depends.join(", ")})` : "";
|
|
15801
|
+
return `- [ ] ${pending.id}: ${pending.description} [${pending.size.toUpperCase()}]${deps}`;
|
|
15802
|
+
}
|
|
15803
|
+
return null;
|
|
15804
|
+
}
|
|
15805
|
+
function extractIncompleteTasksFromPlan(plan, maxChars = 500) {
|
|
15806
|
+
const phase = plan.phases.find((p) => p.id === plan.current_phase);
|
|
15807
|
+
if (!phase)
|
|
15808
|
+
return null;
|
|
15809
|
+
const incomplete = phase.tasks.filter((t) => t.status === "pending" || t.status === "in_progress");
|
|
15810
|
+
if (incomplete.length === 0)
|
|
15811
|
+
return null;
|
|
15812
|
+
const lines = incomplete.map((t) => {
|
|
15813
|
+
const deps = t.depends.length > 0 ? ` (depends: ${t.depends.join(", ")})` : "";
|
|
15814
|
+
return `- [ ] ${t.id}: ${t.description} [${t.size.toUpperCase()}]${deps}`;
|
|
15815
|
+
});
|
|
15816
|
+
const text = lines.join(`
|
|
15817
|
+
`);
|
|
15818
|
+
if (text.length <= maxChars)
|
|
15819
|
+
return text;
|
|
15820
|
+
return `${text.slice(0, maxChars)}...`;
|
|
15821
|
+
}
|
|
14903
15822
|
|
|
14904
15823
|
// src/commands/status.ts
|
|
14905
15824
|
async function handleStatusCommand(directory, agents) {
|
|
15825
|
+
const plan = await loadPlan(directory);
|
|
15826
|
+
if (plan && plan.migration_status !== "migration_failed") {
|
|
15827
|
+
const currentPhase2 = extractCurrentPhaseFromPlan(plan) || "Unknown";
|
|
15828
|
+
let completedTasks2 = 0;
|
|
15829
|
+
let totalTasks2 = 0;
|
|
15830
|
+
for (const phase of plan.phases) {
|
|
15831
|
+
for (const task of phase.tasks) {
|
|
15832
|
+
totalTasks2++;
|
|
15833
|
+
if (task.status === "completed")
|
|
15834
|
+
completedTasks2++;
|
|
15835
|
+
}
|
|
15836
|
+
}
|
|
15837
|
+
const agentCount2 = Object.keys(agents).length;
|
|
15838
|
+
const lines2 = [
|
|
15839
|
+
"## Swarm Status",
|
|
15840
|
+
"",
|
|
15841
|
+
`**Current Phase**: ${currentPhase2}`,
|
|
15842
|
+
`**Tasks**: ${completedTasks2}/${totalTasks2} complete`,
|
|
15843
|
+
`**Agents**: ${agentCount2} registered`
|
|
15844
|
+
];
|
|
15845
|
+
return lines2.join(`
|
|
15846
|
+
`);
|
|
15847
|
+
}
|
|
14906
15848
|
const planContent = await readSwarmFileAsync(directory, "plan.md");
|
|
14907
|
-
if (!planContent)
|
|
15849
|
+
if (!planContent)
|
|
14908
15850
|
return "No active swarm plan found.";
|
|
14909
|
-
}
|
|
14910
15851
|
const currentPhase = extractCurrentPhase(planContent) || "Unknown";
|
|
14911
15852
|
const completedTasks = (planContent.match(/^- \[x\]/gm) || []).length;
|
|
14912
15853
|
const incompleteTasks = (planContent.match(/^- \[ \]/gm) || []).length;
|
|
@@ -14932,6 +15873,8 @@ var HELP_TEXT = [
|
|
|
14932
15873
|
"- `/swarm agents` \u2014 List registered agents",
|
|
14933
15874
|
"- `/swarm history` \u2014 Show completed phases summary",
|
|
14934
15875
|
"- `/swarm config` \u2014 Show current resolved configuration",
|
|
15876
|
+
"- `/swarm evidence [taskId]` \u2014 Show evidence bundles",
|
|
15877
|
+
"- `/swarm archive [--dry-run]` \u2014 Archive old evidence bundles",
|
|
14935
15878
|
"- `/swarm diagnose` \u2014 Run health check on swarm state",
|
|
14936
15879
|
"- `/swarm export` \u2014 Export plan and context as JSON",
|
|
14937
15880
|
"- `/swarm reset --confirm` \u2014 Clear swarm state files"
|
|
@@ -14952,8 +15895,14 @@ function createSwarmCommandHandler(directory, agents) {
|
|
|
14952
15895
|
case "plan":
|
|
14953
15896
|
text = await handlePlanCommand(directory, args);
|
|
14954
15897
|
break;
|
|
14955
|
-
case "agents":
|
|
14956
|
-
|
|
15898
|
+
case "agents": {
|
|
15899
|
+
const pluginConfig = loadPluginConfig(directory);
|
|
15900
|
+
const guardrailsConfig = pluginConfig?.guardrails ? GuardrailsConfigSchema.parse(pluginConfig.guardrails) : undefined;
|
|
15901
|
+
text = handleAgentsCommand(agents, guardrailsConfig);
|
|
15902
|
+
break;
|
|
15903
|
+
}
|
|
15904
|
+
case "archive":
|
|
15905
|
+
text = await handleArchiveCommand(directory, args);
|
|
14957
15906
|
break;
|
|
14958
15907
|
case "history":
|
|
14959
15908
|
text = await handleHistoryCommand(directory, args);
|
|
@@ -14961,6 +15910,9 @@ function createSwarmCommandHandler(directory, agents) {
|
|
|
14961
15910
|
case "config":
|
|
14962
15911
|
text = await handleConfigCommand(directory, args);
|
|
14963
15912
|
break;
|
|
15913
|
+
case "evidence":
|
|
15914
|
+
text = await handleEvidenceCommand(directory, args);
|
|
15915
|
+
break;
|
|
14964
15916
|
case "diagnose":
|
|
14965
15917
|
text = await handleDiagnoseCommand(directory, args);
|
|
14966
15918
|
break;
|
|
@@ -14986,8 +15938,34 @@ var swarmState = {
|
|
|
14986
15938
|
toolAggregates: new Map,
|
|
14987
15939
|
activeAgent: new Map,
|
|
14988
15940
|
delegationChains: new Map,
|
|
14989
|
-
pendingEvents: 0
|
|
15941
|
+
pendingEvents: 0,
|
|
15942
|
+
agentSessions: new Map
|
|
14990
15943
|
};
|
|
15944
|
+
function startAgentSession(sessionId, agentName, staleDurationMs = 3600000) {
|
|
15945
|
+
const now = Date.now();
|
|
15946
|
+
const staleIds = [];
|
|
15947
|
+
for (const [id, session] of swarmState.agentSessions) {
|
|
15948
|
+
if (now - session.startTime > staleDurationMs) {
|
|
15949
|
+
staleIds.push(id);
|
|
15950
|
+
}
|
|
15951
|
+
}
|
|
15952
|
+
for (const id of staleIds) {
|
|
15953
|
+
swarmState.agentSessions.delete(id);
|
|
15954
|
+
}
|
|
15955
|
+
const sessionState = {
|
|
15956
|
+
agentName,
|
|
15957
|
+
startTime: now,
|
|
15958
|
+
toolCallCount: 0,
|
|
15959
|
+
consecutiveErrors: 0,
|
|
15960
|
+
recentToolCalls: [],
|
|
15961
|
+
warningIssued: false,
|
|
15962
|
+
hardLimitHit: false
|
|
15963
|
+
};
|
|
15964
|
+
swarmState.agentSessions.set(sessionId, sessionState);
|
|
15965
|
+
}
|
|
15966
|
+
function getAgentSession(sessionId) {
|
|
15967
|
+
return swarmState.agentSessions.get(sessionId);
|
|
15968
|
+
}
|
|
14991
15969
|
|
|
14992
15970
|
// src/hooks/agent-activity.ts
|
|
14993
15971
|
function createAgentActivityHooks(config2, directory) {
|
|
@@ -15057,8 +16035,8 @@ async function doFlush(directory) {
|
|
|
15057
16035
|
const activitySection = renderActivitySection();
|
|
15058
16036
|
const updated = replaceOrAppendSection(existing, "## Agent Activity", activitySection);
|
|
15059
16037
|
const flushedCount = swarmState.pendingEvents;
|
|
15060
|
-
const
|
|
15061
|
-
await Bun.write(
|
|
16038
|
+
const path6 = `${directory}/.swarm/context.md`;
|
|
16039
|
+
await Bun.write(path6, updated);
|
|
15062
16040
|
swarmState.pendingEvents = Math.max(0, swarmState.pendingEvents - flushedCount);
|
|
15063
16041
|
} catch (error49) {
|
|
15064
16042
|
warn("Agent activity flush failed:", error49);
|
|
@@ -15107,13 +16085,29 @@ function createCompactionCustomizerHook(config2, directory) {
|
|
|
15107
16085
|
}
|
|
15108
16086
|
return {
|
|
15109
16087
|
"experimental.session.compacting": safeHook(async (_input, output) => {
|
|
15110
|
-
const planContent = await readSwarmFileAsync(directory, "plan.md");
|
|
15111
16088
|
const contextContent = await readSwarmFileAsync(directory, "context.md");
|
|
15112
|
-
|
|
15113
|
-
|
|
16089
|
+
const plan = await loadPlan(directory);
|
|
16090
|
+
if (plan && plan.migration_status !== "migration_failed") {
|
|
16091
|
+
const currentPhase = extractCurrentPhaseFromPlan(plan);
|
|
15114
16092
|
if (currentPhase) {
|
|
15115
16093
|
output.context.push(`[SWARM PLAN] ${currentPhase}`);
|
|
15116
16094
|
}
|
|
16095
|
+
const incompleteTasks = extractIncompleteTasksFromPlan(plan);
|
|
16096
|
+
if (incompleteTasks) {
|
|
16097
|
+
output.context.push(`[SWARM TASKS] ${incompleteTasks}`);
|
|
16098
|
+
}
|
|
16099
|
+
} else {
|
|
16100
|
+
const planContent = await readSwarmFileAsync(directory, "plan.md");
|
|
16101
|
+
if (planContent) {
|
|
16102
|
+
const currentPhase = extractCurrentPhase(planContent);
|
|
16103
|
+
if (currentPhase) {
|
|
16104
|
+
output.context.push(`[SWARM PLAN] ${currentPhase}`);
|
|
16105
|
+
}
|
|
16106
|
+
const incompleteTasks = extractIncompleteTasks(planContent);
|
|
16107
|
+
if (incompleteTasks) {
|
|
16108
|
+
output.context.push(`[SWARM TASKS] ${incompleteTasks}`);
|
|
16109
|
+
}
|
|
16110
|
+
}
|
|
15117
16111
|
}
|
|
15118
16112
|
if (contextContent) {
|
|
15119
16113
|
const decisionsSummary = extractDecisions(contextContent);
|
|
@@ -15121,12 +16115,6 @@ function createCompactionCustomizerHook(config2, directory) {
|
|
|
15121
16115
|
output.context.push(`[SWARM DECISIONS] ${decisionsSummary}`);
|
|
15122
16116
|
}
|
|
15123
16117
|
}
|
|
15124
|
-
if (planContent) {
|
|
15125
|
-
const incompleteTasks = extractIncompleteTasks(planContent);
|
|
15126
|
-
if (incompleteTasks) {
|
|
15127
|
-
output.context.push(`[SWARM TASKS] ${incompleteTasks}`);
|
|
15128
|
-
}
|
|
15129
|
-
}
|
|
15130
16118
|
if (contextContent) {
|
|
15131
16119
|
const patterns = extractPatterns(contextContent);
|
|
15132
16120
|
if (patterns) {
|
|
@@ -15221,6 +16209,140 @@ function createDelegationTrackerHook(config2) {
|
|
|
15221
16209
|
}
|
|
15222
16210
|
};
|
|
15223
16211
|
}
|
|
16212
|
+
// src/hooks/guardrails.ts
|
|
16213
|
+
function createGuardrailsHooks(config2) {
|
|
16214
|
+
if (config2.enabled === false) {
|
|
16215
|
+
return {
|
|
16216
|
+
toolBefore: async () => {},
|
|
16217
|
+
toolAfter: async () => {},
|
|
16218
|
+
messagesTransform: async () => {}
|
|
16219
|
+
};
|
|
16220
|
+
}
|
|
16221
|
+
return {
|
|
16222
|
+
toolBefore: async (input, output) => {
|
|
16223
|
+
let session = getAgentSession(input.sessionID);
|
|
16224
|
+
if (!session) {
|
|
16225
|
+
startAgentSession(input.sessionID, "unknown");
|
|
16226
|
+
session = getAgentSession(input.sessionID);
|
|
16227
|
+
if (!session) {
|
|
16228
|
+
warn(`Failed to create session for ${input.sessionID}`);
|
|
16229
|
+
return;
|
|
16230
|
+
}
|
|
16231
|
+
}
|
|
16232
|
+
const agentConfig = resolveGuardrailsConfig(config2, session.agentName);
|
|
16233
|
+
if (session.hardLimitHit) {
|
|
16234
|
+
throw new Error("\uD83D\uDED1 CIRCUIT BREAKER: Agent blocked. Hard limit was previously triggered. Stop making tool calls and return your progress summary.");
|
|
16235
|
+
}
|
|
16236
|
+
session.toolCallCount++;
|
|
16237
|
+
const hash2 = hashArgs(output.args);
|
|
16238
|
+
session.recentToolCalls.push({
|
|
16239
|
+
tool: input.tool,
|
|
16240
|
+
argsHash: hash2,
|
|
16241
|
+
timestamp: Date.now()
|
|
16242
|
+
});
|
|
16243
|
+
if (session.recentToolCalls.length > 20) {
|
|
16244
|
+
session.recentToolCalls.shift();
|
|
16245
|
+
}
|
|
16246
|
+
let repetitionCount = 0;
|
|
16247
|
+
if (session.recentToolCalls.length > 0) {
|
|
16248
|
+
const lastEntry = session.recentToolCalls[session.recentToolCalls.length - 1];
|
|
16249
|
+
for (let i = session.recentToolCalls.length - 1;i >= 0; i--) {
|
|
16250
|
+
const entry = session.recentToolCalls[i];
|
|
16251
|
+
if (entry.tool === lastEntry.tool && entry.argsHash === lastEntry.argsHash) {
|
|
16252
|
+
repetitionCount++;
|
|
16253
|
+
} else {
|
|
16254
|
+
break;
|
|
16255
|
+
}
|
|
16256
|
+
}
|
|
16257
|
+
}
|
|
16258
|
+
const elapsedMinutes = (Date.now() - session.startTime) / 60000;
|
|
16259
|
+
if (session.toolCallCount >= agentConfig.max_tool_calls) {
|
|
16260
|
+
session.hardLimitHit = true;
|
|
16261
|
+
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.`);
|
|
16262
|
+
}
|
|
16263
|
+
if (elapsedMinutes >= agentConfig.max_duration_minutes) {
|
|
16264
|
+
session.hardLimitHit = true;
|
|
16265
|
+
throw new Error(`\uD83D\uDED1 CIRCUIT BREAKER: Duration limit reached (${Math.floor(elapsedMinutes)} min). Stop making tool calls and return your progress summary.`);
|
|
16266
|
+
}
|
|
16267
|
+
if (repetitionCount >= agentConfig.max_repetitions) {
|
|
16268
|
+
session.hardLimitHit = true;
|
|
16269
|
+
throw new Error(`\uD83D\uDED1 CIRCUIT BREAKER: Repetition detected (same call ${repetitionCount} times). Stop making tool calls and return your progress summary.`);
|
|
16270
|
+
}
|
|
16271
|
+
if (session.consecutiveErrors >= agentConfig.max_consecutive_errors) {
|
|
16272
|
+
session.hardLimitHit = true;
|
|
16273
|
+
throw new Error(`\uD83D\uDED1 CIRCUIT BREAKER: Too many consecutive errors (${session.consecutiveErrors}). Stop making tool calls and return your progress summary.`);
|
|
16274
|
+
}
|
|
16275
|
+
if (!session.warningIssued) {
|
|
16276
|
+
const toolWarning = session.toolCallCount >= agentConfig.max_tool_calls * agentConfig.warning_threshold;
|
|
16277
|
+
const durationWarning = elapsedMinutes >= agentConfig.max_duration_minutes * agentConfig.warning_threshold;
|
|
16278
|
+
const repetitionWarning = repetitionCount >= agentConfig.max_repetitions * agentConfig.warning_threshold;
|
|
16279
|
+
const errorWarning = session.consecutiveErrors >= agentConfig.max_consecutive_errors * agentConfig.warning_threshold;
|
|
16280
|
+
if (toolWarning || durationWarning || repetitionWarning || errorWarning) {
|
|
16281
|
+
session.warningIssued = true;
|
|
16282
|
+
}
|
|
16283
|
+
}
|
|
16284
|
+
},
|
|
16285
|
+
toolAfter: async (input, output) => {
|
|
16286
|
+
const session = getAgentSession(input.sessionID);
|
|
16287
|
+
if (!session) {
|
|
16288
|
+
return;
|
|
16289
|
+
}
|
|
16290
|
+
const hasError = output.output === null || output.output === undefined;
|
|
16291
|
+
if (hasError) {
|
|
16292
|
+
session.consecutiveErrors++;
|
|
16293
|
+
} else {
|
|
16294
|
+
session.consecutiveErrors = 0;
|
|
16295
|
+
}
|
|
16296
|
+
},
|
|
16297
|
+
messagesTransform: async (_input, output) => {
|
|
16298
|
+
const messages = output.messages;
|
|
16299
|
+
if (!messages || messages.length === 0) {
|
|
16300
|
+
return;
|
|
16301
|
+
}
|
|
16302
|
+
const lastMessage = messages[messages.length - 1];
|
|
16303
|
+
let sessionId = lastMessage.info?.sessionID;
|
|
16304
|
+
if (!sessionId) {
|
|
16305
|
+
for (const [id, session2] of swarmState.agentSessions) {
|
|
16306
|
+
if (session2.warningIssued || session2.hardLimitHit) {
|
|
16307
|
+
sessionId = id;
|
|
16308
|
+
break;
|
|
16309
|
+
}
|
|
16310
|
+
}
|
|
16311
|
+
}
|
|
16312
|
+
if (!sessionId) {
|
|
16313
|
+
return;
|
|
16314
|
+
}
|
|
16315
|
+
const session = getAgentSession(sessionId);
|
|
16316
|
+
if (!session || !session.warningIssued && !session.hardLimitHit) {
|
|
16317
|
+
return;
|
|
16318
|
+
}
|
|
16319
|
+
const textPart = lastMessage.parts.find((part) => part.type === "text" && typeof part.text === "string");
|
|
16320
|
+
if (!textPart) {
|
|
16321
|
+
return;
|
|
16322
|
+
}
|
|
16323
|
+
if (session.hardLimitHit) {
|
|
16324
|
+
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.]
|
|
16325
|
+
|
|
16326
|
+
` + textPart.text;
|
|
16327
|
+
} else if (session.warningIssued) {
|
|
16328
|
+
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.]
|
|
16329
|
+
|
|
16330
|
+
` + textPart.text;
|
|
16331
|
+
}
|
|
16332
|
+
}
|
|
16333
|
+
};
|
|
16334
|
+
}
|
|
16335
|
+
function hashArgs(args) {
|
|
16336
|
+
try {
|
|
16337
|
+
if (typeof args !== "object" || args === null) {
|
|
16338
|
+
return 0;
|
|
16339
|
+
}
|
|
16340
|
+
const sortedKeys = Object.keys(args).sort();
|
|
16341
|
+
return Number(Bun.hash(JSON.stringify(args, sortedKeys)));
|
|
16342
|
+
} catch {
|
|
16343
|
+
return 0;
|
|
16344
|
+
}
|
|
16345
|
+
}
|
|
15224
16346
|
// src/hooks/pipeline-tracker.ts
|
|
15225
16347
|
var PHASE_REMINDER = `<swarm_reminder>
|
|
15226
16348
|
\u26A0\uFE0F ARCHITECT WORKFLOW REMINDER:
|
|
@@ -15283,29 +16405,51 @@ function createSystemEnhancerHook(config2, directory) {
|
|
|
15283
16405
|
return {
|
|
15284
16406
|
"experimental.chat.system.transform": safeHook(async (_input, output) => {
|
|
15285
16407
|
try {
|
|
15286
|
-
|
|
16408
|
+
let tryInject = function(text) {
|
|
16409
|
+
const tokens = estimateTokens(text);
|
|
16410
|
+
if (injectedTokens + tokens > maxInjectionTokens) {
|
|
16411
|
+
return;
|
|
16412
|
+
}
|
|
16413
|
+
output.system.push(text);
|
|
16414
|
+
injectedTokens += tokens;
|
|
16415
|
+
};
|
|
16416
|
+
const maxInjectionTokens = config2.context_budget?.max_injection_tokens ?? Number.POSITIVE_INFINITY;
|
|
16417
|
+
let injectedTokens = 0;
|
|
15287
16418
|
const contextContent = await readSwarmFileAsync(directory, "context.md");
|
|
15288
|
-
|
|
15289
|
-
|
|
16419
|
+
const plan = await loadPlan(directory);
|
|
16420
|
+
if (plan && plan.migration_status !== "migration_failed") {
|
|
16421
|
+
const currentPhase = extractCurrentPhaseFromPlan(plan);
|
|
15290
16422
|
if (currentPhase) {
|
|
15291
|
-
|
|
16423
|
+
tryInject(`[SWARM CONTEXT] Current phase: ${currentPhase}`);
|
|
15292
16424
|
}
|
|
15293
|
-
const currentTask =
|
|
16425
|
+
const currentTask = extractCurrentTaskFromPlan(plan);
|
|
15294
16426
|
if (currentTask) {
|
|
15295
|
-
|
|
16427
|
+
tryInject(`[SWARM CONTEXT] Current task: ${currentTask}`);
|
|
16428
|
+
}
|
|
16429
|
+
} else {
|
|
16430
|
+
const planContent = await readSwarmFileAsync(directory, "plan.md");
|
|
16431
|
+
if (planContent) {
|
|
16432
|
+
const currentPhase = extractCurrentPhase(planContent);
|
|
16433
|
+
if (currentPhase) {
|
|
16434
|
+
tryInject(`[SWARM CONTEXT] Current phase: ${currentPhase}`);
|
|
16435
|
+
}
|
|
16436
|
+
const currentTask = extractCurrentTask(planContent);
|
|
16437
|
+
if (currentTask) {
|
|
16438
|
+
tryInject(`[SWARM CONTEXT] Current task: ${currentTask}`);
|
|
16439
|
+
}
|
|
15296
16440
|
}
|
|
15297
16441
|
}
|
|
15298
16442
|
if (contextContent) {
|
|
15299
16443
|
const decisions = extractDecisions(contextContent, 200);
|
|
15300
16444
|
if (decisions) {
|
|
15301
|
-
|
|
16445
|
+
tryInject(`[SWARM CONTEXT] Key decisions: ${decisions}`);
|
|
15302
16446
|
}
|
|
15303
16447
|
if (config2.hooks?.agent_activity !== false && _input.sessionID) {
|
|
15304
16448
|
const activeAgent = swarmState.activeAgent.get(_input.sessionID);
|
|
15305
16449
|
if (activeAgent) {
|
|
15306
16450
|
const agentContext = extractAgentContext(contextContent, activeAgent, config2.hooks?.agent_awareness_max_chars ?? 300);
|
|
15307
16451
|
if (agentContext) {
|
|
15308
|
-
|
|
16452
|
+
tryInject(`[SWARM AGENT CONTEXT] ${agentContext}`);
|
|
15309
16453
|
}
|
|
15310
16454
|
}
|
|
15311
16455
|
}
|
|
@@ -16077,10 +17221,10 @@ function mergeDefs2(...defs) {
|
|
|
16077
17221
|
function cloneDef2(schema) {
|
|
16078
17222
|
return mergeDefs2(schema._zod.def);
|
|
16079
17223
|
}
|
|
16080
|
-
function getElementAtPath2(obj,
|
|
16081
|
-
if (!
|
|
17224
|
+
function getElementAtPath2(obj, path6) {
|
|
17225
|
+
if (!path6)
|
|
16082
17226
|
return obj;
|
|
16083
|
-
return
|
|
17227
|
+
return path6.reduce((acc, key) => acc?.[key], obj);
|
|
16084
17228
|
}
|
|
16085
17229
|
function promiseAllObject2(promisesObj) {
|
|
16086
17230
|
const keys = Object.keys(promisesObj);
|
|
@@ -16439,11 +17583,11 @@ function aborted2(x, startIndex = 0) {
|
|
|
16439
17583
|
}
|
|
16440
17584
|
return false;
|
|
16441
17585
|
}
|
|
16442
|
-
function prefixIssues2(
|
|
17586
|
+
function prefixIssues2(path6, issues) {
|
|
16443
17587
|
return issues.map((iss) => {
|
|
16444
17588
|
var _a2;
|
|
16445
17589
|
(_a2 = iss).path ?? (_a2.path = []);
|
|
16446
|
-
iss.path.unshift(
|
|
17590
|
+
iss.path.unshift(path6);
|
|
16447
17591
|
return iss;
|
|
16448
17592
|
});
|
|
16449
17593
|
}
|
|
@@ -16611,7 +17755,7 @@ function treeifyError2(error49, _mapper) {
|
|
|
16611
17755
|
return issue3.message;
|
|
16612
17756
|
};
|
|
16613
17757
|
const result = { errors: [] };
|
|
16614
|
-
const processError = (error50,
|
|
17758
|
+
const processError = (error50, path6 = []) => {
|
|
16615
17759
|
var _a2, _b;
|
|
16616
17760
|
for (const issue3 of error50.issues) {
|
|
16617
17761
|
if (issue3.code === "invalid_union" && issue3.errors.length) {
|
|
@@ -16621,7 +17765,7 @@ function treeifyError2(error49, _mapper) {
|
|
|
16621
17765
|
} else if (issue3.code === "invalid_element") {
|
|
16622
17766
|
processError({ issues: issue3.issues }, issue3.path);
|
|
16623
17767
|
} else {
|
|
16624
|
-
const fullpath = [...
|
|
17768
|
+
const fullpath = [...path6, ...issue3.path];
|
|
16625
17769
|
if (fullpath.length === 0) {
|
|
16626
17770
|
result.errors.push(mapper(issue3));
|
|
16627
17771
|
continue;
|
|
@@ -16653,8 +17797,8 @@ function treeifyError2(error49, _mapper) {
|
|
|
16653
17797
|
}
|
|
16654
17798
|
function toDotPath2(_path) {
|
|
16655
17799
|
const segs = [];
|
|
16656
|
-
const
|
|
16657
|
-
for (const seg of
|
|
17800
|
+
const path6 = _path.map((seg) => typeof seg === "object" ? seg.key : seg);
|
|
17801
|
+
for (const seg of path6) {
|
|
16658
17802
|
if (typeof seg === "number")
|
|
16659
17803
|
segs.push(`[${seg}]`);
|
|
16660
17804
|
else if (typeof seg === "symbol")
|
|
@@ -27850,7 +28994,7 @@ Use these as DOMAIN values when delegating to @sme.`;
|
|
|
27850
28994
|
});
|
|
27851
28995
|
// src/tools/file-extractor.ts
|
|
27852
28996
|
import * as fs3 from "fs";
|
|
27853
|
-
import * as
|
|
28997
|
+
import * as path6 from "path";
|
|
27854
28998
|
var EXT_MAP = {
|
|
27855
28999
|
python: ".py",
|
|
27856
29000
|
py: ".py",
|
|
@@ -27928,12 +29072,12 @@ var extract_code_blocks = tool({
|
|
|
27928
29072
|
if (prefix) {
|
|
27929
29073
|
filename = `${prefix}_${filename}`;
|
|
27930
29074
|
}
|
|
27931
|
-
let filepath =
|
|
27932
|
-
const base =
|
|
27933
|
-
const ext =
|
|
29075
|
+
let filepath = path6.join(targetDir, filename);
|
|
29076
|
+
const base = path6.basename(filepath, path6.extname(filepath));
|
|
29077
|
+
const ext = path6.extname(filepath);
|
|
27934
29078
|
let counter = 1;
|
|
27935
29079
|
while (fs3.existsSync(filepath)) {
|
|
27936
|
-
filepath =
|
|
29080
|
+
filepath = path6.join(targetDir, `${base}_${counter}${ext}`);
|
|
27937
29081
|
counter++;
|
|
27938
29082
|
}
|
|
27939
29083
|
try {
|
|
@@ -27965,7 +29109,7 @@ Errors:
|
|
|
27965
29109
|
var GITINGEST_TIMEOUT_MS = 1e4;
|
|
27966
29110
|
var GITINGEST_MAX_RESPONSE_BYTES = 5242880;
|
|
27967
29111
|
var GITINGEST_MAX_RETRIES = 2;
|
|
27968
|
-
var delay = (ms) => new Promise((
|
|
29112
|
+
var delay = (ms) => new Promise((resolve3) => setTimeout(resolve3, ms));
|
|
27969
29113
|
async function fetchGitingest(args) {
|
|
27970
29114
|
for (let attempt = 0;attempt <= GITINGEST_MAX_RETRIES; attempt++) {
|
|
27971
29115
|
try {
|
|
@@ -28054,6 +29198,8 @@ var OpenCodeSwarm = async (ctx) => {
|
|
|
28054
29198
|
const commandHandler = createSwarmCommandHandler(ctx.directory, Object.fromEntries(agentDefinitions.map((agent) => [agent.name, agent])));
|
|
28055
29199
|
const activityHooks = createAgentActivityHooks(config3, ctx.directory);
|
|
28056
29200
|
const delegationHandler = createDelegationTrackerHook(config3);
|
|
29201
|
+
const guardrailsConfig = GuardrailsConfigSchema.parse(config3.guardrails ?? {});
|
|
29202
|
+
const guardrailsHooks = createGuardrailsHooks(guardrailsConfig);
|
|
28057
29203
|
log("Plugin initialized", {
|
|
28058
29204
|
directory: ctx.directory,
|
|
28059
29205
|
maxIterations: config3.max_iterations,
|
|
@@ -28066,7 +29212,8 @@ var OpenCodeSwarm = async (ctx) => {
|
|
|
28066
29212
|
contextBudget: !!contextBudgetHandler,
|
|
28067
29213
|
commands: true,
|
|
28068
29214
|
agentActivity: config3.hooks?.agent_activity !== false,
|
|
28069
|
-
delegationTracker: config3.hooks?.delegation_tracker === true
|
|
29215
|
+
delegationTracker: config3.hooks?.delegation_tracker === true,
|
|
29216
|
+
guardrails: guardrailsConfig.enabled
|
|
28070
29217
|
}
|
|
28071
29218
|
});
|
|
28072
29219
|
return {
|
|
@@ -28097,13 +29244,17 @@ var OpenCodeSwarm = async (ctx) => {
|
|
|
28097
29244
|
},
|
|
28098
29245
|
"experimental.chat.messages.transform": composeHandlers(...[
|
|
28099
29246
|
pipelineHook["experimental.chat.messages.transform"],
|
|
28100
|
-
contextBudgetHandler
|
|
29247
|
+
contextBudgetHandler,
|
|
29248
|
+
guardrailsHooks.messagesTransform
|
|
28101
29249
|
].filter((fn) => Boolean(fn))),
|
|
28102
29250
|
"experimental.chat.system.transform": systemEnhancerHook["experimental.chat.system.transform"],
|
|
28103
29251
|
"experimental.session.compacting": compactionHook["experimental.session.compacting"],
|
|
28104
29252
|
"command.execute.before": safeHook(commandHandler),
|
|
28105
|
-
"tool.execute.before":
|
|
28106
|
-
|
|
29253
|
+
"tool.execute.before": async (input, output) => {
|
|
29254
|
+
await guardrailsHooks.toolBefore(input, output);
|
|
29255
|
+
await safeHook(activityHooks.toolBefore)(input, output);
|
|
29256
|
+
},
|
|
29257
|
+
"tool.execute.after": composeHandlers(activityHooks.toolAfter, guardrailsHooks.toolAfter),
|
|
28107
29258
|
"chat.message": safeHook(delegationHandler)
|
|
28108
29259
|
};
|
|
28109
29260
|
};
|