opencode-swarm 5.1.8 β 6.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +80 -14
- package/dist/agents/index.d.ts +1 -1
- package/dist/agents/reviewer.d.ts +3 -0
- package/dist/config/loader.d.ts +2 -0
- package/dist/config/schema.d.ts +17 -0
- package/dist/hooks/delegation-tracker.d.ts +1 -1
- package/dist/index.js +427 -118
- package/dist/state.d.ts +64 -18
- package/dist/tools/diff.d.ts +18 -0
- package/dist/tools/index.d.ts +2 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
<p align="center">
|
|
2
|
-
<img src="https://img.shields.io/badge/version-
|
|
2
|
+
<img src="https://img.shields.io/badge/version-6.0.0-blue" alt="Version">
|
|
3
3
|
<img src="https://img.shields.io/badge/license-MIT-green" alt="License">
|
|
4
4
|
<img src="https://img.shields.io/badge/opencode-plugin-purple" alt="OpenCode Plugin">
|
|
5
5
|
<img src="https://img.shields.io/badge/agents-7-orange" alt="Agents">
|
|
6
|
-
<img src="https://img.shields.io/badge/tests-
|
|
6
|
+
<img src="https://img.shields.io/badge/tests-1188-brightgreen" alt="Tests">
|
|
7
7
|
</p>
|
|
8
8
|
|
|
9
9
|
<h1 align="center">π OpenCode Swarm</h1>
|
|
@@ -138,15 +138,24 @@ OpenCode Swarm:
|
|
|
138
138
|
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
|
139
139
|
β PHASE 5: Execute (per task) β
|
|
140
140
|
β β
|
|
141
|
-
β βββββββββββ ββββββββββββββ ββββββββββββββββ
|
|
142
|
-
β β @coder β β β @reviewer β β β @test β
|
|
143
|
-
β β 1 task β β check all β β write + run β
|
|
144
|
-
β βββββββββββ ββββββββββββββ ββββββββββββββββ
|
|
145
|
-
β β
|
|
146
|
-
β β
|
|
147
|
-
β
|
|
141
|
+
β βββββββββββ βββββββββ ββββββββββββββ ββββββββββββββββ β
|
|
142
|
+
β β @coder β β β diff β β β @reviewer β β β @test β β
|
|
143
|
+
β β 1 task β β tool β β check all β β write + run β β
|
|
144
|
+
β βββββββββββ βββββββββ ββββββββββββββ ββββββββββββββββ β
|
|
145
|
+
β β β β β β
|
|
146
|
+
β β Contract β If REJECTED: If FAIL: fix β
|
|
147
|
+
β β changes? β retry from coder + retest β
|
|
148
|
+
β β β β β β
|
|
149
|
+
β β βΌ β βΌ β
|
|
150
|
+
β β βββββββββββ β ββββββββββββββββ ββββββββββββββββ β
|
|
151
|
+
β β β@explorerβ β β @reviewer β β β @test β β
|
|
152
|
+
β β β impact β β β security-onlyβ β adversarial β β
|
|
153
|
+
β β βanalysis β β β (if match) β β (attacks) β β
|
|
154
|
+
β β βββββββββββ β ββββββββββββββββ ββββββββββββββββ β
|
|
155
|
+
β β β β
|
|
156
|
+
β βββββββββββββββββ β
|
|
148
157
|
β β
|
|
149
|
-
β Update plan.md: [x] Task complete (only
|
|
158
|
+
β Update plan.md: [x] Task complete (only after ALL gates pass) β
|
|
150
159
|
β Next task... β
|
|
151
160
|
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
|
152
161
|
β
|
|
@@ -334,6 +343,19 @@ bunx opencode-swarm uninstall --clean
|
|
|
334
343
|
|
|
335
344
|
## What's New
|
|
336
345
|
|
|
346
|
+
### v6.0.0 β Core QA & Security Gates
|
|
347
|
+
- **Dual-pass security reviewer** β After the general reviewer APPROVES, the architect automatically triggers a second security-only review pass when the changed file matches security-sensitive paths (`auth`, `crypto`, `session`, `token`, `middleware`, `api`, `security`) or the coder's output contains security keywords. Configurable via `review_passes` config.
|
|
348
|
+
- **Adversarial testing** β After verification tests PASS, the test engineer is re-delegated with adversarial-only framing: attack vectors, boundary violations, and injection attempts. Pure prompt engineering, no new infrastructure.
|
|
349
|
+
- **Integration impact analysis** β After the coder completes, the `diff` tool detects contract changes (exported functions, interfaces, types). If found, the explorer runs impact analysis across dependents before review begins.
|
|
350
|
+
- **`diff` tool** β New agent-accessible tool providing structured git diff with numstat parsing, contract change detection, configurable base ref (`HEAD`/staged/unstaged), path filtering, and 500-line truncation.
|
|
351
|
+
- **87 new tests** β 1188 total tests across 53+ files (up from 1101 in v5.2.0).
|
|
352
|
+
|
|
353
|
+
### v5.2.0 β Per-Invocation Guardrails
|
|
354
|
+
- **Per-invocation budget isolation** β Guardrail limits (tool calls, duration, errors) now reset with each agent delegation. Second invocation of the same agent gets a fresh budget, preventing false circuit breaker trips in long-running projects.
|
|
355
|
+
- **Architect protocol enforcement** β New mandatory QA gate rules: every coder task must go through reviewer approval + test_engineer verification before the next coder task. Protocol violations detected at runtime with warning injection.
|
|
356
|
+
- **Invocation window observability** β Circuit breaker logs now include `invocationId` and `windowKey` for precise debugging of which specific agent invocation hit limits.
|
|
357
|
+
- **67 new tests** β 1101 total tests across 48 files (up from 1034 in v5.1.x).
|
|
358
|
+
|
|
337
359
|
### v5.0.0 β Verifiable Execution
|
|
338
360
|
- **Canonical plan schema** β Machine-readable `plan.json` with Zod-validated `PlanSchema`/`TaskSchema`/`PhaseSchema`. Automatic migration from legacy `plan.md` format. Structured status tracking (`pending`, `in_progress`, `completed`, `blocked`).
|
|
339
361
|
- **Evidence bundles** β Per-task execution evidence persisted to `.swarm/evidence/`. Five evidence types: `review`, `test`, `diff`, `approval`, `note`. Sanitized task IDs, atomic writes, configurable size limits. `/swarm evidence` to view, `/swarm archive` to manage retention.
|
|
@@ -403,7 +425,7 @@ All features are opt-in via configuration. See [Installation Guide](docs/install
|
|
|
403
425
|
### β
Quality Assurance
|
|
404
426
|
| Agent | Role |
|
|
405
427
|
|-------|------|
|
|
406
|
-
| `reviewer` |
|
|
428
|
+
| `reviewer` | Dual-pass review: correctness review first, then automatic security-only pass for security-sensitive files. The architect specifies CHECK dimensions per call. OWASP Top 10 categories built in. |
|
|
407
429
|
| `critic` | Plan review gate. Reviews the architect's plan BEFORE implementation β checks completeness, feasibility, scope, dependencies, and flags AI-slop. |
|
|
408
430
|
|
|
409
431
|
---
|
|
@@ -510,8 +532,52 @@ Override limits for specific agents that need more (or less) room:
|
|
|
510
532
|
|
|
511
533
|
Profiles merge with base config β only specified fields are overridden.
|
|
512
534
|
|
|
535
|
+
### Review Passes
|
|
536
|
+
|
|
537
|
+
Control the dual-pass security review behavior:
|
|
538
|
+
|
|
539
|
+
```jsonc
|
|
540
|
+
{
|
|
541
|
+
"review_passes": {
|
|
542
|
+
"always_security_review": false, // default: false (only on security-sensitive files)
|
|
543
|
+
"security_globs": [ // default patterns:
|
|
544
|
+
"**/*auth*", "**/*crypto*",
|
|
545
|
+
"**/*session*", "**/*token*",
|
|
546
|
+
"**/*middleware*", "**/*api*",
|
|
547
|
+
"**/*security*"
|
|
548
|
+
]
|
|
549
|
+
}
|
|
550
|
+
}
|
|
551
|
+
```
|
|
552
|
+
|
|
553
|
+
Set `always_security_review: true` to run the security pass on every task, regardless of file path.
|
|
554
|
+
|
|
555
|
+
### Integration Analysis
|
|
556
|
+
|
|
557
|
+
Control whether contract change detection triggers impact analysis:
|
|
558
|
+
|
|
559
|
+
```jsonc
|
|
560
|
+
{
|
|
561
|
+
"integration_analysis": {
|
|
562
|
+
"enabled": true // default: true
|
|
563
|
+
}
|
|
564
|
+
}
|
|
565
|
+
```
|
|
566
|
+
|
|
513
567
|
> **Architect is exempt/unlimited by default:** The architect agent has no guardrail limits by default. To override, add a `profiles.architect` entry in your guardrails config.
|
|
514
568
|
|
|
569
|
+
### Per-Invocation Budgets
|
|
570
|
+
|
|
571
|
+
Guardrail limits are enforced **per-invocation**, not per-session. Each time the architect delegates to an agent, that agent gets a fresh budget of tool calls, duration, and error tolerance.
|
|
572
|
+
|
|
573
|
+
**Example**: If `max_tool_calls: 200`, then:
|
|
574
|
+
- Architect β Coder (task 1) β 200 calls available
|
|
575
|
+
- Coder finishes β Architect β Coder (task 2) β 200 calls available again
|
|
576
|
+
|
|
577
|
+
This prevents long-running projects from accumulating session-wide counters that incorrectly trip the circuit breaker on later tasks.
|
|
578
|
+
|
|
579
|
+
> **Architect is unlimited**: The architect never creates invocation windows and has no guardrail limits by default.
|
|
580
|
+
|
|
515
581
|
### Disable Guardrails
|
|
516
582
|
|
|
517
583
|
```json
|
|
@@ -531,7 +597,7 @@ Profiles merge with base config β only specified fields are overridden.
|
|
|
531
597
|
| Execution | Serial (predictable) | Parallel (chaotic) | Parallel | Configurable |
|
|
532
598
|
| Planning | Phased with acceptance criteria | Ad-hoc | Role-based | Graph-based |
|
|
533
599
|
| Memory | Persistent `.swarm/` files | Session only | Session only | Checkpoints |
|
|
534
|
-
| QA |
|
|
600
|
+
| QA | Dual-pass per-task (review + security + adversarial) | Optional | Optional | Manual |
|
|
535
601
|
| Model mixing | Per-agent configuration | Limited | Limited | Manual |
|
|
536
602
|
| Resume projects | β
Native | β | β | Partial |
|
|
537
603
|
| SME domains | Open-domain (any) | Generic | Generic | Generic |
|
|
@@ -543,7 +609,7 @@ Profiles merge with base config β only specified fields are overridden.
|
|
|
543
609
|
|
|
544
610
|
1. **Plan before code** - Documented phases with acceptance criteria
|
|
545
611
|
2. **One task at a time** - Focused work, quality output
|
|
546
|
-
3. **Review everything immediately** -
|
|
612
|
+
3. **Review everything immediately** - Dual-pass review (correctness + security) with adversarial testing per task
|
|
547
613
|
4. **Cache SME knowledge** - Don't re-ask answered questions
|
|
548
614
|
5. **Persistent memory** - `.swarm/` files survive sessions
|
|
549
615
|
6. **Serial execution** - Predictable, debuggable, no race conditions
|
|
@@ -564,7 +630,7 @@ bun test
|
|
|
564
630
|
bun test tests/unit/config/schema.test.ts
|
|
565
631
|
```
|
|
566
632
|
|
|
567
|
-
|
|
633
|
+
1188 tests across 53+ files covering config, tools, agents, hooks, commands, state, guardrails, evidence, plan schemas, circuit breaker race conditions, invocation windows, multi-invocation isolation, security categories, review/integration schemas, and diff tool. Uses Bun's built-in test runner β zero additional test dependencies.
|
|
568
634
|
|
|
569
635
|
## Troubleshooting
|
|
570
636
|
|
package/dist/agents/index.d.ts
CHANGED
|
@@ -20,6 +20,6 @@ export { createArchitectAgent } from './architect';
|
|
|
20
20
|
export { createCoderAgent } from './coder';
|
|
21
21
|
export { createCriticAgent } from './critic';
|
|
22
22
|
export { createExplorerAgent } from './explorer';
|
|
23
|
-
export { createReviewerAgent } from './reviewer';
|
|
23
|
+
export { createReviewerAgent, SECURITY_CATEGORIES, type SecurityCategory, } from './reviewer';
|
|
24
24
|
export { createSMEAgent } from './sme';
|
|
25
25
|
export { createTestEngineerAgent } from './test-engineer';
|
|
@@ -1,2 +1,5 @@
|
|
|
1
1
|
import type { AgentDefinition } from './architect';
|
|
2
|
+
/** OWASP Top 10 2021 categories for security-focused review passes */
|
|
3
|
+
export declare const SECURITY_CATEGORIES: readonly ["broken-access-control", "cryptographic-failures", "injection", "insecure-design", "security-misconfiguration", "vulnerable-components", "auth-failures", "data-integrity-failures", "logging-monitoring-failures", "ssrf"];
|
|
4
|
+
export type SecurityCategory = (typeof SECURITY_CATEGORIES)[number];
|
|
2
5
|
export declare function createReviewerAgent(model: string, customPrompt?: string, customAppendPrompt?: string): AgentDefinition;
|
package/dist/config/loader.d.ts
CHANGED
|
@@ -13,6 +13,8 @@ export declare function deepMerge<T extends Record<string, unknown>>(base?: T, o
|
|
|
13
13
|
* 2. Project config: <directory>/.opencode/opencode-swarm.json
|
|
14
14
|
*
|
|
15
15
|
* Project config takes precedence. Nested objects are deep-merged.
|
|
16
|
+
* IMPORTANT: Raw configs are merged BEFORE Zod parsing so that
|
|
17
|
+
* Zod defaults don't override explicit user values.
|
|
16
18
|
*/
|
|
17
19
|
export declare function loadPluginConfig(directory: string): PluginConfig;
|
|
18
20
|
/**
|
package/dist/config/schema.d.ts
CHANGED
|
@@ -128,6 +128,15 @@ export declare const SummaryConfigSchema: z.ZodObject<{
|
|
|
128
128
|
retention_days: z.ZodDefault<z.ZodNumber>;
|
|
129
129
|
}, z.core.$strip>;
|
|
130
130
|
export type SummaryConfig = z.infer<typeof SummaryConfigSchema>;
|
|
131
|
+
export declare const ReviewPassesConfigSchema: z.ZodObject<{
|
|
132
|
+
always_security_review: z.ZodDefault<z.ZodBoolean>;
|
|
133
|
+
security_globs: z.ZodDefault<z.ZodArray<z.ZodString>>;
|
|
134
|
+
}, z.core.$strip>;
|
|
135
|
+
export type ReviewPassesConfig = z.infer<typeof ReviewPassesConfigSchema>;
|
|
136
|
+
export declare const IntegrationAnalysisConfigSchema: z.ZodObject<{
|
|
137
|
+
enabled: z.ZodDefault<z.ZodBoolean>;
|
|
138
|
+
}, z.core.$strip>;
|
|
139
|
+
export type IntegrationAnalysisConfig = z.infer<typeof IntegrationAnalysisConfigSchema>;
|
|
131
140
|
export declare const GuardrailsProfileSchema: z.ZodObject<{
|
|
132
141
|
max_tool_calls: z.ZodOptional<z.ZodNumber>;
|
|
133
142
|
max_duration_minutes: z.ZodOptional<z.ZodNumber>;
|
|
@@ -282,6 +291,14 @@ export declare const PluginConfigSchema: z.ZodObject<{
|
|
|
282
291
|
max_stored_bytes: z.ZodDefault<z.ZodNumber>;
|
|
283
292
|
retention_days: z.ZodDefault<z.ZodNumber>;
|
|
284
293
|
}, z.core.$strip>>;
|
|
294
|
+
review_passes: z.ZodOptional<z.ZodObject<{
|
|
295
|
+
always_security_review: z.ZodDefault<z.ZodBoolean>;
|
|
296
|
+
security_globs: z.ZodDefault<z.ZodArray<z.ZodString>>;
|
|
297
|
+
}, z.core.$strip>>;
|
|
298
|
+
integration_analysis: z.ZodOptional<z.ZodObject<{
|
|
299
|
+
enabled: z.ZodDefault<z.ZodBoolean>;
|
|
300
|
+
}, z.core.$strip>>;
|
|
301
|
+
_loadedFromFile: z.ZodDefault<z.ZodBoolean>;
|
|
285
302
|
}, z.core.$strip>;
|
|
286
303
|
export type PluginConfig = z.infer<typeof PluginConfigSchema>;
|
|
287
304
|
export type { AgentName, PipelineAgentName, QAAgentName, } from './constants';
|
|
@@ -8,7 +8,7 @@ import type { PluginConfig } from '../config/schema';
|
|
|
8
8
|
/**
|
|
9
9
|
* Creates the chat.message hook for delegation tracking.
|
|
10
10
|
*/
|
|
11
|
-
export declare function createDelegationTrackerHook(config: PluginConfig): (input: {
|
|
11
|
+
export declare function createDelegationTrackerHook(config: PluginConfig, guardrailsEnabled?: boolean): (input: {
|
|
12
12
|
sessionID: string;
|
|
13
13
|
agent?: string;
|
|
14
14
|
}, output: Record<string, unknown>) => Promise<void>;
|
package/dist/index.js
CHANGED
|
@@ -13630,6 +13630,21 @@ var SummaryConfigSchema = exports_external.object({
|
|
|
13630
13630
|
max_stored_bytes: exports_external.number().min(10240).max(104857600).default(10485760),
|
|
13631
13631
|
retention_days: exports_external.number().min(1).max(365).default(7)
|
|
13632
13632
|
});
|
|
13633
|
+
var ReviewPassesConfigSchema = exports_external.object({
|
|
13634
|
+
always_security_review: exports_external.boolean().default(false),
|
|
13635
|
+
security_globs: exports_external.array(exports_external.string()).default([
|
|
13636
|
+
"**/auth/**",
|
|
13637
|
+
"**/api/**",
|
|
13638
|
+
"**/crypto/**",
|
|
13639
|
+
"**/security/**",
|
|
13640
|
+
"**/middleware/**",
|
|
13641
|
+
"**/session/**",
|
|
13642
|
+
"**/token/**"
|
|
13643
|
+
])
|
|
13644
|
+
});
|
|
13645
|
+
var IntegrationAnalysisConfigSchema = exports_external.object({
|
|
13646
|
+
enabled: exports_external.boolean().default(true)
|
|
13647
|
+
});
|
|
13633
13648
|
var GuardrailsProfileSchema = exports_external.object({
|
|
13634
13649
|
max_tool_calls: exports_external.number().min(0).max(1000).optional(),
|
|
13635
13650
|
max_duration_minutes: exports_external.number().min(0).max(480).optional(),
|
|
@@ -13729,7 +13744,10 @@ var PluginConfigSchema = exports_external.object({
|
|
|
13729
13744
|
context_budget: ContextBudgetConfigSchema.optional(),
|
|
13730
13745
|
guardrails: GuardrailsConfigSchema.optional(),
|
|
13731
13746
|
evidence: EvidenceConfigSchema.optional(),
|
|
13732
|
-
summaries: SummaryConfigSchema.optional()
|
|
13747
|
+
summaries: SummaryConfigSchema.optional(),
|
|
13748
|
+
review_passes: ReviewPassesConfigSchema.optional(),
|
|
13749
|
+
integration_analysis: IntegrationAnalysisConfigSchema.optional(),
|
|
13750
|
+
_loadedFromFile: exports_external.boolean().default(false)
|
|
13733
13751
|
});
|
|
13734
13752
|
|
|
13735
13753
|
// src/config/loader.ts
|
|
@@ -13739,25 +13757,26 @@ var MAX_CONFIG_FILE_BYTES = 102400;
|
|
|
13739
13757
|
function getUserConfigDir() {
|
|
13740
13758
|
return process.env.XDG_CONFIG_HOME || path.join(os.homedir(), ".config");
|
|
13741
13759
|
}
|
|
13742
|
-
function
|
|
13760
|
+
function loadRawConfigFromPath(configPath) {
|
|
13743
13761
|
try {
|
|
13744
13762
|
const stats = fs.statSync(configPath);
|
|
13745
13763
|
if (stats.size > MAX_CONFIG_FILE_BYTES) {
|
|
13746
13764
|
console.warn(`[opencode-swarm] Config file too large (max 100 KB): ${configPath}`);
|
|
13765
|
+
console.warn("[opencode-swarm] \u26A0\uFE0F Guardrails will be DISABLED as a safety precaution. Fix the config file to restore normal operation.");
|
|
13747
13766
|
return null;
|
|
13748
13767
|
}
|
|
13749
13768
|
const content = fs.readFileSync(configPath, "utf-8");
|
|
13750
13769
|
const rawConfig = JSON.parse(content);
|
|
13751
|
-
|
|
13752
|
-
|
|
13753
|
-
console.warn(
|
|
13754
|
-
console.warn(result.error.format());
|
|
13770
|
+
if (typeof rawConfig !== "object" || rawConfig === null || Array.isArray(rawConfig)) {
|
|
13771
|
+
console.warn(`[opencode-swarm] Invalid config at ${configPath}: expected an object`);
|
|
13772
|
+
console.warn("[opencode-swarm] \u26A0\uFE0F Guardrails will be DISABLED as a safety precaution. Fix the config file to restore normal operation.");
|
|
13755
13773
|
return null;
|
|
13756
13774
|
}
|
|
13757
|
-
return
|
|
13775
|
+
return rawConfig;
|
|
13758
13776
|
} catch (error48) {
|
|
13759
13777
|
if (error48 instanceof Error && "code" in error48 && error48.code !== "ENOENT") {
|
|
13760
|
-
console.warn(`[opencode-swarm]
|
|
13778
|
+
console.warn(`[opencode-swarm] \u26A0\uFE0F CONFIG LOAD FAILURE \u2014 config exists at ${configPath} but could not be loaded: ${error48.message}`);
|
|
13779
|
+
console.warn("[opencode-swarm] \u26A0\uFE0F Guardrails will be DISABLED as a safety precaution. Fix the config file to restore normal operation.");
|
|
13761
13780
|
}
|
|
13762
13781
|
return null;
|
|
13763
13782
|
}
|
|
@@ -13779,30 +13798,36 @@ function deepMergeInternal(base, override, depth) {
|
|
|
13779
13798
|
}
|
|
13780
13799
|
return result;
|
|
13781
13800
|
}
|
|
13782
|
-
function deepMerge(base, override) {
|
|
13783
|
-
if (!base)
|
|
13784
|
-
return override;
|
|
13785
|
-
if (!override)
|
|
13786
|
-
return base;
|
|
13787
|
-
return deepMergeInternal(base, override, 0);
|
|
13788
|
-
}
|
|
13789
13801
|
function loadPluginConfig(directory) {
|
|
13790
13802
|
const userConfigPath = path.join(getUserConfigDir(), "opencode", CONFIG_FILENAME);
|
|
13791
13803
|
const projectConfigPath = path.join(directory, ".opencode", CONFIG_FILENAME);
|
|
13792
|
-
|
|
13793
|
-
|
|
13794
|
-
|
|
13795
|
-
|
|
13796
|
-
|
|
13797
|
-
|
|
13798
|
-
|
|
13799
|
-
|
|
13800
|
-
|
|
13801
|
-
|
|
13802
|
-
|
|
13804
|
+
const rawUserConfig = loadRawConfigFromPath(userConfigPath);
|
|
13805
|
+
const rawProjectConfig = loadRawConfigFromPath(projectConfigPath);
|
|
13806
|
+
const loadedFromFile = rawUserConfig !== null || rawProjectConfig !== null;
|
|
13807
|
+
let mergedRaw = rawUserConfig ?? {};
|
|
13808
|
+
if (rawProjectConfig) {
|
|
13809
|
+
mergedRaw = deepMergeInternal(mergedRaw, rawProjectConfig, 0);
|
|
13810
|
+
}
|
|
13811
|
+
const result = PluginConfigSchema.safeParse(mergedRaw);
|
|
13812
|
+
if (!result.success) {
|
|
13813
|
+
if (rawUserConfig) {
|
|
13814
|
+
const userResult = PluginConfigSchema.safeParse(rawUserConfig);
|
|
13815
|
+
if (userResult.success) {
|
|
13816
|
+
console.warn("[opencode-swarm] Project config ignored due to validation errors. Using user config.");
|
|
13817
|
+
return { ...userResult.data, _loadedFromFile: true };
|
|
13818
|
+
}
|
|
13819
|
+
}
|
|
13820
|
+
console.warn("[opencode-swarm] Merged config validation failed:");
|
|
13821
|
+
console.warn(result.error.format());
|
|
13822
|
+
console.warn("[opencode-swarm] \u26A0\uFE0F Guardrails will be DISABLED as a safety precaution. Fix the config file to restore normal operation.");
|
|
13823
|
+
return {
|
|
13824
|
+
max_iterations: 5,
|
|
13825
|
+
qa_retry_limit: 3,
|
|
13826
|
+
inject_phase_reminders: true,
|
|
13827
|
+
_loadedFromFile: false
|
|
13803
13828
|
};
|
|
13804
13829
|
}
|
|
13805
|
-
return
|
|
13830
|
+
return { ...result.data, _loadedFromFile: loadedFromFile };
|
|
13806
13831
|
}
|
|
13807
13832
|
function loadAgentPrompt(agentName) {
|
|
13808
13833
|
const promptsDir = path.join(getUserConfigDir(), "opencode", PROMPTS_DIR_NAME);
|
|
@@ -14015,7 +14040,19 @@ You THINK. Subagents DO. You have the largest context window and strongest reaso
|
|
|
14015
14040
|
3. ONE task per {{AGENT_PREFIX}}coder call. Never batch.
|
|
14016
14041
|
4. Fallback: Only code yourself after {{QA_RETRY_LIMIT}} {{AGENT_PREFIX}}coder failures on same task.
|
|
14017
14042
|
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.
|
|
14018
|
-
6. **
|
|
14043
|
+
6. **CRITIC GATE (Execute BEFORE any implementation work)**:
|
|
14044
|
+
- When you first create a plan, IMMEDIATELY delegate the full plan to {{AGENT_PREFIX}}critic for review
|
|
14045
|
+
- Wait for critic verdict: APPROVED / NEEDS_REVISION / REJECTED
|
|
14046
|
+
- If NEEDS_REVISION: Revise plan and re-submit to critic (max 2 cycles)
|
|
14047
|
+
- If REJECTED after 2 cycles: Escalate to user with explanation
|
|
14048
|
+
- ONLY AFTER critic approval: Proceed to implementation (Phase 3+)
|
|
14049
|
+
7. **MANDATORY QA GATE (Execute AFTER every coder task)** \u2014 sequence: coder \u2192 diff \u2192 review \u2192 security review \u2192 verification tests \u2192 adversarial tests \u2192 next task.
|
|
14050
|
+
- After coder completes: run \`diff\` tool. If \`hasContractChanges\` is true \u2192 delegate {{AGENT_PREFIX}}explorer for integration impact analysis. BREAKING \u2192 return to coder. COMPATIBLE \u2192 proceed.
|
|
14051
|
+
- Delegate {{AGENT_PREFIX}}reviewer with CHECK dimensions. REJECTED \u2192 return to coder (max {{QA_RETRY_LIMIT}} attempts). APPROVED \u2192 continue.
|
|
14052
|
+
- If file matches security globs (auth, api, crypto, security, middleware, session, token) OR coder output contains security keywords \u2192 delegate {{AGENT_PREFIX}}reviewer AGAIN with security-only CHECK. REJECTED \u2192 return to coder.
|
|
14053
|
+
- Delegate {{AGENT_PREFIX}}test_engineer for verification tests. FAIL \u2192 return to coder.
|
|
14054
|
+
- Delegate {{AGENT_PREFIX}}test_engineer for adversarial tests (attack vectors only). FAIL \u2192 return to coder.
|
|
14055
|
+
- All pass \u2192 mark task complete, proceed to next task.
|
|
14019
14056
|
|
|
14020
14057
|
## AGENTS
|
|
14021
14058
|
|
|
@@ -14028,6 +14065,8 @@ You THINK. Subagents DO. You have the largest context window and strongest reaso
|
|
|
14028
14065
|
|
|
14029
14066
|
SMEs advise only. Reviewer and critic review only. None of them write code.
|
|
14030
14067
|
|
|
14068
|
+
Available Tools: diff (structured git diff with contract change detection)
|
|
14069
|
+
|
|
14031
14070
|
## DELEGATION FORMAT
|
|
14032
14071
|
|
|
14033
14072
|
All delegations use this structure:
|
|
@@ -14083,6 +14122,24 @@ PLAN: [paste the plan.md content]
|
|
|
14083
14122
|
CONTEXT: [codebase summary from explorer]
|
|
14084
14123
|
OUTPUT: VERDICT + CONFIDENCE + ISSUES + SUMMARY
|
|
14085
14124
|
|
|
14125
|
+
{{AGENT_PREFIX}}reviewer
|
|
14126
|
+
TASK: Security-only review of login validation
|
|
14127
|
+
FILE: src/auth/login.ts
|
|
14128
|
+
CHECK: [security-only] \u2014 evaluate against OWASP Top 10, scan for hardcoded secrets, injection vectors, insecure crypto, missing input validation
|
|
14129
|
+
OUTPUT: VERDICT + RISK + SECURITY ISSUES ONLY
|
|
14130
|
+
|
|
14131
|
+
{{AGENT_PREFIX}}test_engineer
|
|
14132
|
+
TASK: Adversarial security testing
|
|
14133
|
+
FILE: src/auth/login.ts
|
|
14134
|
+
CONSTRAINT: ONLY attack vectors \u2014 malformed inputs, oversized payloads, injection attempts, auth bypass, boundary violations
|
|
14135
|
+
OUTPUT: Test file + VERDICT: PASS/FAIL
|
|
14136
|
+
|
|
14137
|
+
{{AGENT_PREFIX}}explorer
|
|
14138
|
+
TASK: Integration impact analysis
|
|
14139
|
+
INPUT: Contract changes detected: [list from diff tool]
|
|
14140
|
+
OUTPUT: BREAKING CHANGES + CONSUMERS AFFECTED + VERDICT: BREAKING/COMPATIBLE
|
|
14141
|
+
CONSTRAINT: Read-only. grep for imports/usages of changed exports.
|
|
14142
|
+
|
|
14086
14143
|
## WORKFLOW
|
|
14087
14144
|
|
|
14088
14145
|
### Phase 0: Resume Check
|
|
@@ -14134,15 +14191,13 @@ Delegate plan to {{AGENT_PREFIX}}critic for review BEFORE any implementation beg
|
|
|
14134
14191
|
### Phase 5: Execute
|
|
14135
14192
|
For each task (respecting dependencies):
|
|
14136
14193
|
|
|
14137
|
-
5a. {{AGENT_PREFIX}}coder - Implement
|
|
14138
|
-
5b. {{AGENT_PREFIX}}
|
|
14139
|
-
5c.
|
|
14140
|
-
|
|
14141
|
-
|
|
14142
|
-
|
|
14143
|
-
|
|
14144
|
-
5e. If test VERDICT is FAIL \u2192 Send failures to {{AGENT_PREFIX}}coder for fixes, then re-run from 5b.
|
|
14145
|
-
5f. Update plan.md [x], proceed to next task (ONLY if tests PASS)
|
|
14194
|
+
5a. {{AGENT_PREFIX}}coder - Implement
|
|
14195
|
+
5b. Run \`diff\` tool. If \`hasContractChanges\` \u2192 {{AGENT_PREFIX}}explorer integration analysis. BREAKING \u2192 coder retry.
|
|
14196
|
+
5c. {{AGENT_PREFIX}}reviewer - General review. REJECTED (< {{QA_RETRY_LIMIT}}) \u2192 coder retry. REJECTED ({{QA_RETRY_LIMIT}}) \u2192 escalate.
|
|
14197
|
+
5d. Security gate: if file matches security globs or content has security keywords \u2192 {{AGENT_PREFIX}}reviewer security-only. REJECTED \u2192 coder retry.
|
|
14198
|
+
5e. {{AGENT_PREFIX}}test_engineer - Verification tests. FAIL \u2192 coder retry from 5c.
|
|
14199
|
+
5f. {{AGENT_PREFIX}}test_engineer - Adversarial tests. FAIL \u2192 coder retry from 5c.
|
|
14200
|
+
5g. Update plan.md [x], proceed to next task.
|
|
14146
14201
|
|
|
14147
14202
|
### Phase 6: Phase Complete
|
|
14148
14203
|
1. {{AGENT_PREFIX}}explorer - Rescan
|
|
@@ -15049,39 +15104,33 @@ function startAgentSession(sessionId, agentName, staleDurationMs = 7200000) {
|
|
|
15049
15104
|
}
|
|
15050
15105
|
const sessionState = {
|
|
15051
15106
|
agentName,
|
|
15052
|
-
startTime: now,
|
|
15053
15107
|
lastToolCallTime: now,
|
|
15054
15108
|
lastAgentEventTime: now,
|
|
15055
|
-
|
|
15056
|
-
|
|
15057
|
-
|
|
15058
|
-
|
|
15059
|
-
warningReason: "",
|
|
15060
|
-
hardLimitHit: false,
|
|
15061
|
-
lastSuccessTime: now,
|
|
15062
|
-
delegationActive: false
|
|
15109
|
+
delegationActive: false,
|
|
15110
|
+
activeInvocationId: 0,
|
|
15111
|
+
lastInvocationIdByAgent: {},
|
|
15112
|
+
windows: {}
|
|
15063
15113
|
};
|
|
15064
15114
|
swarmState.agentSessions.set(sessionId, sessionState);
|
|
15065
15115
|
}
|
|
15066
|
-
function getAgentSession(sessionId) {
|
|
15067
|
-
return swarmState.agentSessions.get(sessionId);
|
|
15068
|
-
}
|
|
15069
15116
|
function ensureAgentSession(sessionId, agentName) {
|
|
15070
15117
|
const now = Date.now();
|
|
15071
15118
|
let session = swarmState.agentSessions.get(sessionId);
|
|
15072
15119
|
if (session) {
|
|
15073
15120
|
if (agentName && agentName !== session.agentName) {
|
|
15074
15121
|
session.agentName = agentName;
|
|
15075
|
-
session.startTime = now;
|
|
15076
|
-
session.toolCallCount = 0;
|
|
15077
|
-
session.consecutiveErrors = 0;
|
|
15078
|
-
session.recentToolCalls = [];
|
|
15079
|
-
session.warningIssued = false;
|
|
15080
|
-
session.warningReason = "";
|
|
15081
|
-
session.hardLimitHit = false;
|
|
15082
|
-
session.lastSuccessTime = now;
|
|
15083
15122
|
session.delegationActive = false;
|
|
15084
15123
|
session.lastAgentEventTime = now;
|
|
15124
|
+
if (!session.windows) {
|
|
15125
|
+
session.activeInvocationId = 0;
|
|
15126
|
+
session.lastInvocationIdByAgent = {};
|
|
15127
|
+
session.windows = {};
|
|
15128
|
+
}
|
|
15129
|
+
}
|
|
15130
|
+
if (!session.windows) {
|
|
15131
|
+
session.activeInvocationId = 0;
|
|
15132
|
+
session.lastInvocationIdByAgent = {};
|
|
15133
|
+
session.windows = {};
|
|
15085
15134
|
}
|
|
15086
15135
|
session.lastToolCallTime = now;
|
|
15087
15136
|
return session;
|
|
@@ -15099,6 +15148,58 @@ function updateAgentEventTime(sessionId) {
|
|
|
15099
15148
|
session.lastAgentEventTime = Date.now();
|
|
15100
15149
|
}
|
|
15101
15150
|
}
|
|
15151
|
+
function beginInvocation(sessionId, agentName) {
|
|
15152
|
+
const session = swarmState.agentSessions.get(sessionId);
|
|
15153
|
+
if (!session) {
|
|
15154
|
+
throw new Error(`Cannot begin invocation: session ${sessionId} does not exist`);
|
|
15155
|
+
}
|
|
15156
|
+
const stripped = stripKnownSwarmPrefix(agentName);
|
|
15157
|
+
if (stripped === ORCHESTRATOR_NAME) {
|
|
15158
|
+
return null;
|
|
15159
|
+
}
|
|
15160
|
+
const lastId = session.lastInvocationIdByAgent[stripped] || 0;
|
|
15161
|
+
const newId = lastId + 1;
|
|
15162
|
+
session.lastInvocationIdByAgent[stripped] = newId;
|
|
15163
|
+
session.activeInvocationId = newId;
|
|
15164
|
+
const now = Date.now();
|
|
15165
|
+
const window = {
|
|
15166
|
+
id: newId,
|
|
15167
|
+
agentName: stripped,
|
|
15168
|
+
startedAtMs: now,
|
|
15169
|
+
toolCalls: 0,
|
|
15170
|
+
consecutiveErrors: 0,
|
|
15171
|
+
hardLimitHit: false,
|
|
15172
|
+
lastSuccessTimeMs: now,
|
|
15173
|
+
recentToolCalls: [],
|
|
15174
|
+
warningIssued: false,
|
|
15175
|
+
warningReason: ""
|
|
15176
|
+
};
|
|
15177
|
+
const key = `${stripped}:${newId}`;
|
|
15178
|
+
session.windows[key] = window;
|
|
15179
|
+
pruneOldWindows(sessionId, 24 * 60 * 60 * 1000, 50);
|
|
15180
|
+
return window;
|
|
15181
|
+
}
|
|
15182
|
+
function getActiveWindow(sessionId) {
|
|
15183
|
+
const session = swarmState.agentSessions.get(sessionId);
|
|
15184
|
+
if (!session || !session.windows) {
|
|
15185
|
+
return;
|
|
15186
|
+
}
|
|
15187
|
+
const stripped = stripKnownSwarmPrefix(session.agentName);
|
|
15188
|
+
const key = `${stripped}:${session.activeInvocationId}`;
|
|
15189
|
+
return session.windows[key];
|
|
15190
|
+
}
|
|
15191
|
+
function pruneOldWindows(sessionId, maxAgeMs = 24 * 60 * 60 * 1000, maxWindows = 50) {
|
|
15192
|
+
const session = swarmState.agentSessions.get(sessionId);
|
|
15193
|
+
if (!session || !session.windows) {
|
|
15194
|
+
return;
|
|
15195
|
+
}
|
|
15196
|
+
const now = Date.now();
|
|
15197
|
+
const entries = Object.entries(session.windows);
|
|
15198
|
+
const validByAge = entries.filter(([_, window]) => now - window.startedAtMs < maxAgeMs);
|
|
15199
|
+
const sorted = validByAge.sort((a, b) => b[1].startedAtMs - a[1].startedAtMs);
|
|
15200
|
+
const toKeep = sorted.slice(0, maxWindows);
|
|
15201
|
+
session.windows = Object.fromEntries(toKeep);
|
|
15202
|
+
}
|
|
15102
15203
|
|
|
15103
15204
|
// src/commands/benchmark.ts
|
|
15104
15205
|
var CI = {
|
|
@@ -15119,11 +15220,10 @@ async function handleBenchmarkCommand(directory, args) {
|
|
|
15119
15220
|
hardLimits: 0,
|
|
15120
15221
|
warnings: 0
|
|
15121
15222
|
};
|
|
15122
|
-
|
|
15123
|
-
|
|
15124
|
-
|
|
15125
|
-
|
|
15126
|
-
e.warnings++;
|
|
15223
|
+
const windows = Object.values(s.windows);
|
|
15224
|
+
e.toolCalls += windows.reduce((sum, w) => sum + w.toolCalls, 0);
|
|
15225
|
+
e.hardLimits += windows.filter((w) => w.hardLimitHit).length;
|
|
15226
|
+
e.warnings += windows.filter((w) => w.warningIssued).length;
|
|
15127
15227
|
agentMap.set(s.agentName, e);
|
|
15128
15228
|
}
|
|
15129
15229
|
const agentHealth = Array.from(agentMap.entries()).map(([a, v]) => ({
|
|
@@ -16731,6 +16831,29 @@ function createDelegationGateHook(config2) {
|
|
|
16731
16831
|
if (batchingMatches && batchingMatches.length > 0) {
|
|
16732
16832
|
warnings.push("Batching language detected. Break compound objectives into separate coder calls.");
|
|
16733
16833
|
}
|
|
16834
|
+
const sessionID = lastUserMessage.info?.sessionID;
|
|
16835
|
+
if (sessionID) {
|
|
16836
|
+
const delegationChain = swarmState.delegationChains.get(sessionID);
|
|
16837
|
+
if (delegationChain && delegationChain.length >= 2) {
|
|
16838
|
+
const coderIndices = [];
|
|
16839
|
+
for (let i = delegationChain.length - 1;i >= 0; i--) {
|
|
16840
|
+
if (stripKnownSwarmPrefix(delegationChain[i].to).includes("coder")) {
|
|
16841
|
+
coderIndices.unshift(i);
|
|
16842
|
+
if (coderIndices.length === 2)
|
|
16843
|
+
break;
|
|
16844
|
+
}
|
|
16845
|
+
}
|
|
16846
|
+
if (coderIndices.length === 2) {
|
|
16847
|
+
const prevCoderIndex = coderIndices[0];
|
|
16848
|
+
const betweenCoders = delegationChain.slice(prevCoderIndex + 1);
|
|
16849
|
+
const hasReviewer = betweenCoders.some((d) => stripKnownSwarmPrefix(d.to) === "reviewer");
|
|
16850
|
+
const hasTestEngineer = betweenCoders.some((d) => stripKnownSwarmPrefix(d.to) === "test_engineer");
|
|
16851
|
+
if (!hasReviewer || !hasTestEngineer) {
|
|
16852
|
+
warnings.push(`\u26A0\uFE0F PROTOCOL VIOLATION: Previous coder task completed, but QA gate was skipped. ` + `You MUST delegate to reviewer (code review) and test_engineer (test execution) ` + `before starting a new coder task. Review RULES 7-8 in your system prompt.`);
|
|
16853
|
+
}
|
|
16854
|
+
}
|
|
16855
|
+
}
|
|
16856
|
+
}
|
|
16734
16857
|
if (warnings.length === 0)
|
|
16735
16858
|
return;
|
|
16736
16859
|
const warningText = `[\u26A0\uFE0F DELEGATION GATE: Your coder delegation may be too complex. Issues:
|
|
@@ -16744,7 +16867,7 @@ ${originalText}`;
|
|
|
16744
16867
|
};
|
|
16745
16868
|
}
|
|
16746
16869
|
// src/hooks/delegation-tracker.ts
|
|
16747
|
-
function createDelegationTrackerHook(config2) {
|
|
16870
|
+
function createDelegationTrackerHook(config2, guardrailsEnabled = true) {
|
|
16748
16871
|
return async (input, _output) => {
|
|
16749
16872
|
const now = Date.now();
|
|
16750
16873
|
if (!input.agent || input.agent === "") {
|
|
@@ -16766,6 +16889,9 @@ function createDelegationTrackerHook(config2) {
|
|
|
16766
16889
|
const isArchitect = strippedAgent === ORCHESTRATOR_NAME;
|
|
16767
16890
|
const session = ensureAgentSession(input.sessionID, agentName);
|
|
16768
16891
|
session.delegationActive = !isArchitect;
|
|
16892
|
+
if (!isArchitect && guardrailsEnabled) {
|
|
16893
|
+
beginInvocation(input.sessionID, agentName);
|
|
16894
|
+
}
|
|
16769
16895
|
if (config2.hooks?.delegation_tracker === true && previousAgent && previousAgent !== agentName) {
|
|
16770
16896
|
const entry = {
|
|
16771
16897
|
from: previousAgent,
|
|
@@ -16814,24 +16940,35 @@ function createGuardrailsHooks(config2) {
|
|
|
16814
16940
|
if (agentConfig.max_duration_minutes === 0 && agentConfig.max_tool_calls === 0) {
|
|
16815
16941
|
return;
|
|
16816
16942
|
}
|
|
16817
|
-
if (
|
|
16943
|
+
if (!getActiveWindow(input.sessionID)) {
|
|
16944
|
+
const fallbackAgent = swarmState.activeAgent.get(input.sessionID) ?? session.agentName;
|
|
16945
|
+
const stripped = stripKnownSwarmPrefix(fallbackAgent);
|
|
16946
|
+
if (stripped !== ORCHESTRATOR_NAME) {
|
|
16947
|
+
beginInvocation(input.sessionID, fallbackAgent);
|
|
16948
|
+
}
|
|
16949
|
+
}
|
|
16950
|
+
const window = getActiveWindow(input.sessionID);
|
|
16951
|
+
if (!window) {
|
|
16952
|
+
return;
|
|
16953
|
+
}
|
|
16954
|
+
if (window.hardLimitHit) {
|
|
16818
16955
|
throw new Error("\uD83D\uDED1 CIRCUIT BREAKER: Agent blocked. Hard limit was previously triggered. Stop making tool calls and return your progress summary.");
|
|
16819
16956
|
}
|
|
16820
|
-
|
|
16957
|
+
window.toolCalls++;
|
|
16821
16958
|
const hash2 = hashArgs(output.args);
|
|
16822
|
-
|
|
16959
|
+
window.recentToolCalls.push({
|
|
16823
16960
|
tool: input.tool,
|
|
16824
16961
|
argsHash: hash2,
|
|
16825
16962
|
timestamp: Date.now()
|
|
16826
16963
|
});
|
|
16827
|
-
if (
|
|
16828
|
-
|
|
16964
|
+
if (window.recentToolCalls.length > 20) {
|
|
16965
|
+
window.recentToolCalls.shift();
|
|
16829
16966
|
}
|
|
16830
16967
|
let repetitionCount = 0;
|
|
16831
|
-
if (
|
|
16832
|
-
const lastEntry =
|
|
16833
|
-
for (let i =
|
|
16834
|
-
const entry =
|
|
16968
|
+
if (window.recentToolCalls.length > 0) {
|
|
16969
|
+
const lastEntry = window.recentToolCalls[window.recentToolCalls.length - 1];
|
|
16970
|
+
for (let i = window.recentToolCalls.length - 1;i >= 0; i--) {
|
|
16971
|
+
const entry = window.recentToolCalls[i];
|
|
16835
16972
|
if (entry.tool === lastEntry.tool && entry.argsHash === lastEntry.argsHash) {
|
|
16836
16973
|
repetitionCount++;
|
|
16837
16974
|
} else {
|
|
@@ -16839,54 +16976,60 @@ function createGuardrailsHooks(config2) {
|
|
|
16839
16976
|
}
|
|
16840
16977
|
}
|
|
16841
16978
|
}
|
|
16842
|
-
const elapsedMinutes = (Date.now() -
|
|
16843
|
-
if (agentConfig.max_tool_calls > 0 &&
|
|
16844
|
-
|
|
16979
|
+
const elapsedMinutes = (Date.now() - window.startedAtMs) / 60000;
|
|
16980
|
+
if (agentConfig.max_tool_calls > 0 && window.toolCalls >= agentConfig.max_tool_calls) {
|
|
16981
|
+
window.hardLimitHit = true;
|
|
16845
16982
|
warn("Circuit breaker: tool call limit hit", {
|
|
16846
16983
|
sessionID: input.sessionID,
|
|
16847
|
-
agentName:
|
|
16984
|
+
agentName: window.agentName,
|
|
16985
|
+
invocationId: window.id,
|
|
16986
|
+
windowKey: `${window.agentName}:${window.id}`,
|
|
16848
16987
|
resolvedMaxCalls: agentConfig.max_tool_calls,
|
|
16849
|
-
currentCalls:
|
|
16988
|
+
currentCalls: window.toolCalls
|
|
16850
16989
|
});
|
|
16851
|
-
throw new Error(`\uD83D\uDED1 LIMIT REACHED: Tool calls exhausted (${
|
|
16990
|
+
throw new Error(`\uD83D\uDED1 LIMIT REACHED: Tool calls exhausted (${window.toolCalls}/${agentConfig.max_tool_calls}). Finish the current operation and return your progress summary.`);
|
|
16852
16991
|
}
|
|
16853
16992
|
if (agentConfig.max_duration_minutes > 0 && elapsedMinutes >= agentConfig.max_duration_minutes) {
|
|
16854
|
-
|
|
16993
|
+
window.hardLimitHit = true;
|
|
16855
16994
|
warn("Circuit breaker: duration limit hit", {
|
|
16856
16995
|
sessionID: input.sessionID,
|
|
16857
|
-
agentName:
|
|
16996
|
+
agentName: window.agentName,
|
|
16997
|
+
invocationId: window.id,
|
|
16998
|
+
windowKey: `${window.agentName}:${window.id}`,
|
|
16858
16999
|
resolvedMaxMinutes: agentConfig.max_duration_minutes,
|
|
16859
17000
|
elapsedMinutes: Math.floor(elapsedMinutes)
|
|
16860
17001
|
});
|
|
16861
17002
|
throw new Error(`\uD83D\uDED1 LIMIT REACHED: Duration exhausted (${Math.floor(elapsedMinutes)}/${agentConfig.max_duration_minutes} min). Finish the current operation and return your progress summary.`);
|
|
16862
17003
|
}
|
|
16863
17004
|
if (repetitionCount >= agentConfig.max_repetitions) {
|
|
16864
|
-
|
|
17005
|
+
window.hardLimitHit = true;
|
|
16865
17006
|
throw new Error(`\uD83D\uDED1 LIMIT REACHED: Repeated the same tool call ${repetitionCount} times. This suggests a loop. Return your progress summary.`);
|
|
16866
17007
|
}
|
|
16867
|
-
if (
|
|
16868
|
-
|
|
16869
|
-
throw new Error(`\uD83D\uDED1 LIMIT REACHED: ${
|
|
17008
|
+
if (window.consecutiveErrors >= agentConfig.max_consecutive_errors) {
|
|
17009
|
+
window.hardLimitHit = true;
|
|
17010
|
+
throw new Error(`\uD83D\uDED1 LIMIT REACHED: ${window.consecutiveErrors} consecutive tool errors detected. Return your progress summary with details of what went wrong.`);
|
|
16870
17011
|
}
|
|
16871
|
-
const idleMinutes = (Date.now() -
|
|
17012
|
+
const idleMinutes = (Date.now() - window.lastSuccessTimeMs) / 60000;
|
|
16872
17013
|
if (idleMinutes >= agentConfig.idle_timeout_minutes) {
|
|
16873
|
-
|
|
17014
|
+
window.hardLimitHit = true;
|
|
16874
17015
|
warn("Circuit breaker: idle timeout hit", {
|
|
16875
17016
|
sessionID: input.sessionID,
|
|
16876
|
-
agentName:
|
|
17017
|
+
agentName: window.agentName,
|
|
17018
|
+
invocationId: window.id,
|
|
17019
|
+
windowKey: `${window.agentName}:${window.id}`,
|
|
16877
17020
|
idleTimeoutMinutes: agentConfig.idle_timeout_minutes,
|
|
16878
17021
|
idleMinutes: Math.floor(idleMinutes)
|
|
16879
17022
|
});
|
|
16880
17023
|
throw new Error(`\uD83D\uDED1 LIMIT REACHED: No successful tool call for ${Math.floor(idleMinutes)} minutes (idle timeout: ${agentConfig.idle_timeout_minutes} min). This suggests the agent may be stuck. Return your progress summary.`);
|
|
16881
17024
|
}
|
|
16882
|
-
if (!
|
|
16883
|
-
const toolPct = agentConfig.max_tool_calls > 0 ?
|
|
17025
|
+
if (!window.warningIssued) {
|
|
17026
|
+
const toolPct = agentConfig.max_tool_calls > 0 ? window.toolCalls / agentConfig.max_tool_calls : 0;
|
|
16884
17027
|
const durationPct = agentConfig.max_duration_minutes > 0 ? elapsedMinutes / agentConfig.max_duration_minutes : 0;
|
|
16885
17028
|
const repPct = repetitionCount / agentConfig.max_repetitions;
|
|
16886
|
-
const errorPct =
|
|
17029
|
+
const errorPct = window.consecutiveErrors / agentConfig.max_consecutive_errors;
|
|
16887
17030
|
const reasons = [];
|
|
16888
17031
|
if (agentConfig.max_tool_calls > 0 && toolPct >= agentConfig.warning_threshold) {
|
|
16889
|
-
reasons.push(`tool calls ${
|
|
17032
|
+
reasons.push(`tool calls ${window.toolCalls}/${agentConfig.max_tool_calls}`);
|
|
16890
17033
|
}
|
|
16891
17034
|
if (durationPct >= agentConfig.warning_threshold) {
|
|
16892
17035
|
reasons.push(`duration ${Math.floor(elapsedMinutes)}/${agentConfig.max_duration_minutes} min`);
|
|
@@ -16895,25 +17038,24 @@ function createGuardrailsHooks(config2) {
|
|
|
16895
17038
|
reasons.push(`repetitions ${repetitionCount}/${agentConfig.max_repetitions}`);
|
|
16896
17039
|
}
|
|
16897
17040
|
if (errorPct >= agentConfig.warning_threshold) {
|
|
16898
|
-
reasons.push(`errors ${
|
|
17041
|
+
reasons.push(`errors ${window.consecutiveErrors}/${agentConfig.max_consecutive_errors}`);
|
|
16899
17042
|
}
|
|
16900
17043
|
if (reasons.length > 0) {
|
|
16901
|
-
|
|
16902
|
-
|
|
17044
|
+
window.warningIssued = true;
|
|
17045
|
+
window.warningReason = reasons.join(", ");
|
|
16903
17046
|
}
|
|
16904
17047
|
}
|
|
16905
17048
|
},
|
|
16906
17049
|
toolAfter: async (input, output) => {
|
|
16907
|
-
const
|
|
16908
|
-
if (!
|
|
17050
|
+
const window = getActiveWindow(input.sessionID);
|
|
17051
|
+
if (!window)
|
|
16909
17052
|
return;
|
|
16910
|
-
}
|
|
16911
17053
|
const hasError = output.output === null || output.output === undefined;
|
|
16912
17054
|
if (hasError) {
|
|
16913
|
-
|
|
17055
|
+
window.consecutiveErrors++;
|
|
16914
17056
|
} else {
|
|
16915
|
-
|
|
16916
|
-
|
|
17057
|
+
window.consecutiveErrors = 0;
|
|
17058
|
+
window.lastSuccessTimeMs = Date.now();
|
|
16917
17059
|
}
|
|
16918
17060
|
},
|
|
16919
17061
|
messagesTransform: async (_input, output) => {
|
|
@@ -16922,32 +17064,24 @@ function createGuardrailsHooks(config2) {
|
|
|
16922
17064
|
return;
|
|
16923
17065
|
}
|
|
16924
17066
|
const lastMessage = messages[messages.length - 1];
|
|
16925
|
-
|
|
16926
|
-
if (!sessionId) {
|
|
16927
|
-
for (const [id, session2] of swarmState.agentSessions) {
|
|
16928
|
-
if (session2.warningIssued || session2.hardLimitHit) {
|
|
16929
|
-
sessionId = id;
|
|
16930
|
-
break;
|
|
16931
|
-
}
|
|
16932
|
-
}
|
|
16933
|
-
}
|
|
17067
|
+
const sessionId = lastMessage.info?.sessionID;
|
|
16934
17068
|
if (!sessionId) {
|
|
16935
17069
|
return;
|
|
16936
17070
|
}
|
|
16937
|
-
const
|
|
16938
|
-
if (!
|
|
17071
|
+
const targetWindow = getActiveWindow(sessionId);
|
|
17072
|
+
if (!targetWindow || !targetWindow.warningIssued && !targetWindow.hardLimitHit) {
|
|
16939
17073
|
return;
|
|
16940
17074
|
}
|
|
16941
17075
|
const textPart = lastMessage.parts.find((part) => part.type === "text" && typeof part.text === "string");
|
|
16942
17076
|
if (!textPart) {
|
|
16943
17077
|
return;
|
|
16944
17078
|
}
|
|
16945
|
-
if (
|
|
17079
|
+
if (targetWindow.hardLimitHit) {
|
|
16946
17080
|
textPart.text = `[\uD83D\uDED1 LIMIT REACHED: Your resource budget is exhausted. Do not make additional tool calls. Return a summary of your progress and any remaining work.]
|
|
16947
17081
|
|
|
16948
17082
|
` + textPart.text;
|
|
16949
|
-
} else if (
|
|
16950
|
-
const reasonSuffix =
|
|
17083
|
+
} else if (targetWindow.warningIssued) {
|
|
17084
|
+
const reasonSuffix = targetWindow.warningReason ? ` (${targetWindow.warningReason})` : "";
|
|
16951
17085
|
textPart.text = `[\u26A0\uFE0F APPROACHING LIMITS${reasonSuffix}: You still have capacity to finish your current step. Complete what you're working on, then return your results.]
|
|
16952
17086
|
|
|
16953
17087
|
` + textPart.text;
|
|
@@ -17142,6 +17276,12 @@ function createSystemEnhancerHook(config2, directory) {
|
|
|
17142
17276
|
}
|
|
17143
17277
|
}
|
|
17144
17278
|
tryInject("[SWARM HINT] Large tool outputs may be auto-summarized. Use /swarm retrieve <id> to get the full content if needed.");
|
|
17279
|
+
if (config2.review_passes?.always_security_review) {
|
|
17280
|
+
tryInject("[SWARM CONFIG] Security review pass is MANDATORY for ALL tasks. Skip file-pattern check \u2014 always run security-only reviewer pass after general review APPROVED.");
|
|
17281
|
+
}
|
|
17282
|
+
if (config2.integration_analysis?.enabled === false) {
|
|
17283
|
+
tryInject("[SWARM CONFIG] Integration analysis is DISABLED. Skip diff tool and integration impact analysis after coder tasks.");
|
|
17284
|
+
}
|
|
17145
17285
|
return;
|
|
17146
17286
|
}
|
|
17147
17287
|
const userScoringConfig = config2.context_budget?.scoring;
|
|
@@ -17221,6 +17361,28 @@ function createSystemEnhancerHook(config2, directory) {
|
|
|
17221
17361
|
}
|
|
17222
17362
|
}
|
|
17223
17363
|
}
|
|
17364
|
+
if (config2.review_passes?.always_security_review) {
|
|
17365
|
+
const text = "[SWARM CONFIG] Security review pass is MANDATORY for ALL tasks. Skip file-pattern check \u2014 always run security-only reviewer pass after general review APPROVED.";
|
|
17366
|
+
candidates.push({
|
|
17367
|
+
id: `candidate-${idCounter++}`,
|
|
17368
|
+
kind: "phase",
|
|
17369
|
+
text,
|
|
17370
|
+
tokens: estimateTokens(text),
|
|
17371
|
+
priority: 1,
|
|
17372
|
+
metadata: { contentType: "prose" }
|
|
17373
|
+
});
|
|
17374
|
+
}
|
|
17375
|
+
if (config2.integration_analysis?.enabled === false) {
|
|
17376
|
+
const text = "[SWARM CONFIG] Integration analysis is DISABLED. Skip diff tool and integration impact analysis after coder tasks.";
|
|
17377
|
+
candidates.push({
|
|
17378
|
+
id: `candidate-${idCounter++}`,
|
|
17379
|
+
kind: "phase",
|
|
17380
|
+
text,
|
|
17381
|
+
tokens: estimateTokens(text),
|
|
17382
|
+
priority: 1,
|
|
17383
|
+
metadata: { contentType: "prose" }
|
|
17384
|
+
});
|
|
17385
|
+
}
|
|
17224
17386
|
const ranked = rankCandidates(candidates, effectiveConfig);
|
|
17225
17387
|
for (const candidate of ranked) {
|
|
17226
17388
|
if (injectedTokens + candidate.tokens > maxInjectionTokens) {
|
|
@@ -17415,6 +17577,9 @@ function createToolSummarizerHook(config2, directory) {
|
|
|
17415
17577
|
}
|
|
17416
17578
|
};
|
|
17417
17579
|
}
|
|
17580
|
+
// src/tools/diff.ts
|
|
17581
|
+
import { execSync } from "child_process";
|
|
17582
|
+
|
|
17418
17583
|
// node_modules/@opencode-ai/plugin/node_modules/zod/v4/classic/external.js
|
|
17419
17584
|
var exports_external2 = {};
|
|
17420
17585
|
__export(exports_external2, {
|
|
@@ -29735,7 +29900,149 @@ function tool(input) {
|
|
|
29735
29900
|
return input;
|
|
29736
29901
|
}
|
|
29737
29902
|
tool.schema = exports_external2;
|
|
29738
|
-
|
|
29903
|
+
// src/tools/diff.ts
|
|
29904
|
+
var MAX_DIFF_LINES = 500;
|
|
29905
|
+
var DIFF_TIMEOUT_MS = 30000;
|
|
29906
|
+
var MAX_BUFFER_BYTES = 5 * 1024 * 1024;
|
|
29907
|
+
var CONTRACT_PATTERNS = [
|
|
29908
|
+
/^[+-]\s*export\s+(function|const|class|interface|type|enum|default)\b/,
|
|
29909
|
+
/^[+-]\s*(interface|type)\s+\w+/,
|
|
29910
|
+
/^[+-]\s*public\s+/,
|
|
29911
|
+
/^[+-]\s*(async\s+)?function\s+\w+\s*\(/
|
|
29912
|
+
];
|
|
29913
|
+
var SAFE_REF_PATTERN = /^[a-zA-Z0-9._\-/~^@{}]+$/;
|
|
29914
|
+
var MAX_REF_LENGTH = 256;
|
|
29915
|
+
var MAX_PATH_LENGTH = 500;
|
|
29916
|
+
var SHELL_METACHARACTERS = /[;|&$`(){}<>!'"]/;
|
|
29917
|
+
function validateBase(base) {
|
|
29918
|
+
if (base.length > MAX_REF_LENGTH) {
|
|
29919
|
+
return `base ref exceeds maximum length of ${MAX_REF_LENGTH}`;
|
|
29920
|
+
}
|
|
29921
|
+
if (!SAFE_REF_PATTERN.test(base)) {
|
|
29922
|
+
return "base contains invalid characters for git ref";
|
|
29923
|
+
}
|
|
29924
|
+
return null;
|
|
29925
|
+
}
|
|
29926
|
+
function validatePaths(paths) {
|
|
29927
|
+
if (!paths)
|
|
29928
|
+
return null;
|
|
29929
|
+
for (const path7 of paths) {
|
|
29930
|
+
if (!path7 || path7.length === 0) {
|
|
29931
|
+
return "empty path not allowed";
|
|
29932
|
+
}
|
|
29933
|
+
if (path7.length > MAX_PATH_LENGTH) {
|
|
29934
|
+
return `path exceeds maximum length of ${MAX_PATH_LENGTH}`;
|
|
29935
|
+
}
|
|
29936
|
+
if (SHELL_METACHARACTERS.test(path7)) {
|
|
29937
|
+
return "path contains shell metacharacters";
|
|
29938
|
+
}
|
|
29939
|
+
}
|
|
29940
|
+
return null;
|
|
29941
|
+
}
|
|
29942
|
+
var diff = tool({
|
|
29943
|
+
description: "Analyze git diff for changed files, exports, interfaces, and function signatures. Returns structured output with contract change detection.",
|
|
29944
|
+
args: {
|
|
29945
|
+
base: tool.schema.string().optional().describe('Base ref to diff against (default: HEAD). Use "staged" for staged changes, "unstaged" for working tree changes.'),
|
|
29946
|
+
paths: tool.schema.array(tool.schema.string()).optional().describe("Optional file paths to restrict diff scope.")
|
|
29947
|
+
},
|
|
29948
|
+
async execute(args, _context) {
|
|
29949
|
+
try {
|
|
29950
|
+
const base = args.base ?? "HEAD";
|
|
29951
|
+
const pathSpec = args.paths?.length ? "-- " + args.paths.join(" ") : "";
|
|
29952
|
+
const baseValidationError = validateBase(base);
|
|
29953
|
+
if (baseValidationError) {
|
|
29954
|
+
const errorResult = {
|
|
29955
|
+
error: `invalid base: ${baseValidationError}`,
|
|
29956
|
+
files: [],
|
|
29957
|
+
contractChanges: [],
|
|
29958
|
+
hasContractChanges: false
|
|
29959
|
+
};
|
|
29960
|
+
return JSON.stringify(errorResult, null, 2);
|
|
29961
|
+
}
|
|
29962
|
+
const pathsValidationError = validatePaths(args.paths);
|
|
29963
|
+
if (pathsValidationError) {
|
|
29964
|
+
const errorResult = {
|
|
29965
|
+
error: `invalid paths: ${pathsValidationError}`,
|
|
29966
|
+
files: [],
|
|
29967
|
+
contractChanges: [],
|
|
29968
|
+
hasContractChanges: false
|
|
29969
|
+
};
|
|
29970
|
+
return JSON.stringify(errorResult, null, 2);
|
|
29971
|
+
}
|
|
29972
|
+
let gitCmd;
|
|
29973
|
+
if (base === "staged") {
|
|
29974
|
+
gitCmd = "git --no-pager diff --cached";
|
|
29975
|
+
} else if (base === "unstaged") {
|
|
29976
|
+
gitCmd = "git --no-pager diff";
|
|
29977
|
+
} else {
|
|
29978
|
+
gitCmd = `git --no-pager diff ${base}`;
|
|
29979
|
+
}
|
|
29980
|
+
const numstatOutput = execSync(gitCmd + " --numstat " + pathSpec, {
|
|
29981
|
+
encoding: "utf-8",
|
|
29982
|
+
timeout: DIFF_TIMEOUT_MS
|
|
29983
|
+
});
|
|
29984
|
+
const fullDiffOutput = execSync(gitCmd + " -U3 " + pathSpec, {
|
|
29985
|
+
encoding: "utf-8",
|
|
29986
|
+
timeout: DIFF_TIMEOUT_MS,
|
|
29987
|
+
maxBuffer: MAX_BUFFER_BYTES
|
|
29988
|
+
});
|
|
29989
|
+
const files = [];
|
|
29990
|
+
const numstatLines = numstatOutput.split(`
|
|
29991
|
+
`);
|
|
29992
|
+
for (const line of numstatLines) {
|
|
29993
|
+
if (!line.trim())
|
|
29994
|
+
continue;
|
|
29995
|
+
const parts = line.split("\t");
|
|
29996
|
+
if (parts.length >= 3) {
|
|
29997
|
+
const additions = parseInt(parts[0]) || 0;
|
|
29998
|
+
const deletions = parseInt(parts[1]) || 0;
|
|
29999
|
+
const path7 = parts[2];
|
|
30000
|
+
files.push({ path: path7, additions, deletions });
|
|
30001
|
+
}
|
|
30002
|
+
}
|
|
30003
|
+
const contractChanges = [];
|
|
30004
|
+
const diffLines = fullDiffOutput.split(`
|
|
30005
|
+
`);
|
|
30006
|
+
let currentFile = "";
|
|
30007
|
+
for (const line of diffLines) {
|
|
30008
|
+
const gitLineMatch = line.match(/^diff --git.* b\/(.+)$/);
|
|
30009
|
+
if (gitLineMatch) {
|
|
30010
|
+
currentFile = gitLineMatch[1];
|
|
30011
|
+
}
|
|
30012
|
+
for (const pattern of CONTRACT_PATTERNS) {
|
|
30013
|
+
if (pattern.test(line)) {
|
|
30014
|
+
const trimmed = line.trim();
|
|
30015
|
+
if (currentFile) {
|
|
30016
|
+
contractChanges.push(`[${currentFile}] ${trimmed}`);
|
|
30017
|
+
} else {
|
|
30018
|
+
contractChanges.push(trimmed);
|
|
30019
|
+
}
|
|
30020
|
+
break;
|
|
30021
|
+
}
|
|
30022
|
+
}
|
|
30023
|
+
}
|
|
30024
|
+
const hasContractChanges = contractChanges.length > 0;
|
|
30025
|
+
const fileCount = files.length;
|
|
30026
|
+
const truncated = diffLines.length > MAX_DIFF_LINES;
|
|
30027
|
+
const summary = truncated ? `${fileCount} files changed. Contract changes: ${hasContractChanges ? "YES" : "NO"}. (truncated to ${MAX_DIFF_LINES} lines)` : `${fileCount} files changed. Contract changes: ${hasContractChanges ? "YES" : "NO"}`;
|
|
30028
|
+
const result = {
|
|
30029
|
+
files,
|
|
30030
|
+
contractChanges,
|
|
30031
|
+
hasContractChanges,
|
|
30032
|
+
summary
|
|
30033
|
+
};
|
|
30034
|
+
return JSON.stringify(result, null, 2);
|
|
30035
|
+
} catch (e) {
|
|
30036
|
+
const errorResult = {
|
|
30037
|
+
error: e instanceof Error ? `git diff failed: ${e.constructor.name}` : "git diff failed: unknown error",
|
|
30038
|
+
files: [],
|
|
30039
|
+
contractChanges: [],
|
|
30040
|
+
hasContractChanges: false
|
|
30041
|
+
};
|
|
30042
|
+
return JSON.stringify(errorResult, null, 2);
|
|
30043
|
+
}
|
|
30044
|
+
}
|
|
30045
|
+
});
|
|
29739
30046
|
// src/tools/domain-detector.ts
|
|
29740
30047
|
var DOMAIN_PATTERNS = {
|
|
29741
30048
|
windows: [
|
|
@@ -30120,9 +30427,10 @@ var OpenCodeSwarm = async (ctx) => {
|
|
|
30120
30427
|
const contextBudgetHandler = createContextBudgetHandler(config3);
|
|
30121
30428
|
const commandHandler = createSwarmCommandHandler(ctx.directory, Object.fromEntries(agentDefinitions.map((agent) => [agent.name, agent])));
|
|
30122
30429
|
const activityHooks = createAgentActivityHooks(config3, ctx.directory);
|
|
30123
|
-
const delegationHandler = createDelegationTrackerHook(config3);
|
|
30124
30430
|
const delegationGateHandler = createDelegationGateHook(config3);
|
|
30125
|
-
const
|
|
30431
|
+
const guardrailsFallback = config3._loadedFromFile ? config3.guardrails ?? {} : { ...config3.guardrails, enabled: false };
|
|
30432
|
+
const guardrailsConfig = GuardrailsConfigSchema.parse(guardrailsFallback);
|
|
30433
|
+
const delegationHandler = createDelegationTrackerHook(config3, guardrailsConfig.enabled);
|
|
30126
30434
|
const guardrailsHooks = createGuardrailsHooks(guardrailsConfig);
|
|
30127
30435
|
const summaryConfig = SummaryConfigSchema.parse(config3.summaries ?? {});
|
|
30128
30436
|
const toolSummarizerHook = createToolSummarizerHook(summaryConfig, ctx.directory);
|
|
@@ -30149,7 +30457,8 @@ var OpenCodeSwarm = async (ctx) => {
|
|
|
30149
30457
|
tool: {
|
|
30150
30458
|
detect_domains,
|
|
30151
30459
|
extract_code_blocks,
|
|
30152
|
-
gitingest
|
|
30460
|
+
gitingest,
|
|
30461
|
+
diff
|
|
30153
30462
|
},
|
|
30154
30463
|
config: async (opencodeConfig) => {
|
|
30155
30464
|
if (!opencodeConfig.agent) {
|
package/dist/state.d.ts
CHANGED
|
@@ -34,37 +34,56 @@ export interface DelegationEntry {
|
|
|
34
34
|
timestamp: number;
|
|
35
35
|
}
|
|
36
36
|
/**
|
|
37
|
-
* Represents per-session state for guardrail tracking
|
|
37
|
+
* Represents per-session state for guardrail tracking.
|
|
38
|
+
* Budget fields (toolCallCount, consecutiveErrors, etc.) have moved to InvocationWindow.
|
|
39
|
+
* This interface now tracks session-level metadata and window management.
|
|
38
40
|
*/
|
|
39
41
|
export interface AgentSessionState {
|
|
40
|
-
/**
|
|
42
|
+
/** Current agent identity for this session */
|
|
41
43
|
agentName: string;
|
|
42
|
-
/**
|
|
43
|
-
startTime: number;
|
|
44
|
-
/** Timestamp of most recent tool call (for stale session eviction) */
|
|
44
|
+
/** Timestamp of most recent tool call (for session-level stale detection) */
|
|
45
45
|
lastToolCallTime: number;
|
|
46
|
-
/** Timestamp of most recent agent identity event (chat.message
|
|
46
|
+
/** Timestamp of most recent agent identity event (chat.message) */
|
|
47
47
|
lastAgentEventTime: number;
|
|
48
|
-
/**
|
|
49
|
-
|
|
50
|
-
/**
|
|
48
|
+
/** Whether active delegation is in progress for this session */
|
|
49
|
+
delegationActive: boolean;
|
|
50
|
+
/** Current active invocation ID for this agent */
|
|
51
|
+
activeInvocationId: number;
|
|
52
|
+
/** Last invocation ID by agent name (e.g., { "coder": 3, "reviewer": 1 }) */
|
|
53
|
+
lastInvocationIdByAgent: Record<string, number>;
|
|
54
|
+
/** Active invocation windows keyed by "${agentName}:${invId}" */
|
|
55
|
+
windows: Record<string, InvocationWindow>;
|
|
56
|
+
}
|
|
57
|
+
/**
|
|
58
|
+
* Represents a single agent invocation window with isolated guardrail budgets.
|
|
59
|
+
* Each time the architect delegates to an agent, a new window is created.
|
|
60
|
+
* Architect never creates windows (unlimited).
|
|
61
|
+
*/
|
|
62
|
+
export interface InvocationWindow {
|
|
63
|
+
/** Unique ID for this invocation (increments per agent type) */
|
|
64
|
+
id: number;
|
|
65
|
+
/** Agent name (stripped of swarm prefix) */
|
|
66
|
+
agentName: string;
|
|
67
|
+
/** Timestamp when this invocation started */
|
|
68
|
+
startedAtMs: number;
|
|
69
|
+
/** Tool calls made in this invocation */
|
|
70
|
+
toolCalls: number;
|
|
71
|
+
/** Consecutive errors in this invocation */
|
|
51
72
|
consecutiveErrors: number;
|
|
52
|
-
/**
|
|
73
|
+
/** Whether hard limit was hit for this invocation */
|
|
74
|
+
hardLimitHit: boolean;
|
|
75
|
+
/** Timestamp of most recent successful tool call */
|
|
76
|
+
lastSuccessTimeMs: number;
|
|
77
|
+
/** Circular buffer of recent tool calls (max 20) for repetition detection */
|
|
53
78
|
recentToolCalls: Array<{
|
|
54
79
|
tool: string;
|
|
55
80
|
argsHash: number;
|
|
56
81
|
timestamp: number;
|
|
57
82
|
}>;
|
|
58
|
-
/** Whether
|
|
83
|
+
/** Whether soft warning has been issued for this invocation */
|
|
59
84
|
warningIssued: boolean;
|
|
60
|
-
/** Human-readable warning reason
|
|
85
|
+
/** Human-readable warning reason */
|
|
61
86
|
warningReason: string;
|
|
62
|
-
/** Whether a hard limit has been triggered */
|
|
63
|
-
hardLimitHit: boolean;
|
|
64
|
-
/** Timestamp of most recent SUCCESSFUL tool call (for idle timeout) */
|
|
65
|
-
lastSuccessTime: number;
|
|
66
|
-
/** Whether active delegation is in progress for this session */
|
|
67
|
-
delegationActive: boolean;
|
|
68
87
|
}
|
|
69
88
|
/**
|
|
70
89
|
* Singleton state object for sharing data across hooks
|
|
@@ -122,3 +141,30 @@ export declare function ensureAgentSession(sessionId: string, agentName?: string
|
|
|
122
141
|
* @param sessionId - The session identifier
|
|
123
142
|
*/
|
|
124
143
|
export declare function updateAgentEventTime(sessionId: string): void;
|
|
144
|
+
/**
|
|
145
|
+
* Begin a new invocation window for the given agent.
|
|
146
|
+
* Increments invocation ID, creates fresh budget counters.
|
|
147
|
+
* Returns null for architect (unlimited, no window).
|
|
148
|
+
*
|
|
149
|
+
* @param sessionId - Session identifier
|
|
150
|
+
* @param agentName - Agent name (with or without swarm prefix)
|
|
151
|
+
* @returns New window or null if architect
|
|
152
|
+
*/
|
|
153
|
+
export declare function beginInvocation(sessionId: string, agentName: string): InvocationWindow | null;
|
|
154
|
+
/**
|
|
155
|
+
* Get the currently active invocation window for the session.
|
|
156
|
+
* Returns undefined if no window exists (e.g., architect session).
|
|
157
|
+
*
|
|
158
|
+
* @param sessionId - Session identifier
|
|
159
|
+
* @returns Active window or undefined
|
|
160
|
+
*/
|
|
161
|
+
export declare function getActiveWindow(sessionId: string): InvocationWindow | undefined;
|
|
162
|
+
/**
|
|
163
|
+
* Prune old invocation windows to prevent unbounded memory growth.
|
|
164
|
+
* Removes windows older than maxAgeMs and keeps only the most recent maxWindows.
|
|
165
|
+
*
|
|
166
|
+
* @param sessionId - Session identifier
|
|
167
|
+
* @param maxAgeMs - Maximum age in milliseconds (default 24 hours)
|
|
168
|
+
* @param maxWindows - Maximum number of windows to keep (default 50)
|
|
169
|
+
*/
|
|
170
|
+
export declare function pruneOldWindows(sessionId: string, maxAgeMs?: number, maxWindows?: number): void;
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { tool } from '@opencode-ai/plugin';
|
|
2
|
+
export interface DiffResult {
|
|
3
|
+
files: Array<{
|
|
4
|
+
path: string;
|
|
5
|
+
additions: number;
|
|
6
|
+
deletions: number;
|
|
7
|
+
}>;
|
|
8
|
+
contractChanges: string[];
|
|
9
|
+
hasContractChanges: boolean;
|
|
10
|
+
summary: string;
|
|
11
|
+
}
|
|
12
|
+
export interface DiffErrorResult {
|
|
13
|
+
error: string;
|
|
14
|
+
files: [];
|
|
15
|
+
contractChanges: [];
|
|
16
|
+
hasContractChanges: false;
|
|
17
|
+
}
|
|
18
|
+
export declare const diff: ReturnType<typeof tool>;
|
package/dist/tools/index.d.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
export { type DiffErrorResult, type DiffResult, diff } from './diff';
|
|
1
2
|
export { detect_domains } from './domain-detector';
|
|
2
3
|
export { extract_code_blocks } from './file-extractor';
|
|
3
|
-
export {
|
|
4
|
+
export { fetchGitingest, type GitingestArgs, gitingest } from './gitingest';
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "opencode-swarm",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "6.0.1",
|
|
4
4
|
"description": "Architect-centric agentic swarm plugin for OpenCode - hub-and-spoke orchestration with SME consultation, code generation, and QA review",
|
|
5
5
|
"main": "dist/index.js",
|
|
6
6
|
"types": "dist/index.d.ts",
|