opencode-swarm 6.14.11 → 6.15.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +3 -0
- package/dist/commands/analyze.d.ts +5 -0
- package/dist/commands/clarify.d.ts +5 -0
- package/dist/commands/index.d.ts +3 -0
- package/dist/commands/specify.d.ts +5 -0
- package/dist/config/schema.d.ts +14 -0
- package/dist/hooks/context-budget.d.ts +3 -0
- package/dist/hooks/index.d.ts +2 -0
- package/dist/hooks/message-priority.d.ts +105 -0
- package/dist/hooks/model-limits.d.ts +96 -0
- package/dist/index.js +734 -14
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -546,6 +546,9 @@ When truncation is active, a footer is appended:
|
|
|
546
546
|
| `/swarm preflight` | Run phase preflight checks |
|
|
547
547
|
| `/swarm config doctor [--fix]` | Config validation with optional auto-fix |
|
|
548
548
|
| `/swarm sync-plan` | Force plan.md regeneration from plan.json |
|
|
549
|
+
| `/swarm specify [description]` | Generate or import a feature specification |
|
|
550
|
+
| `/swarm clarify [topic]` | Clarify and refine an existing feature specification |
|
|
551
|
+
| `/swarm analyze` | Analyze spec.md vs plan.md for requirement coverage gaps |
|
|
549
552
|
|
|
550
553
|
</details>
|
|
551
554
|
|
package/dist/commands/index.d.ts
CHANGED
|
@@ -1,7 +1,9 @@
|
|
|
1
1
|
import type { AgentDefinition } from '../agents';
|
|
2
2
|
export { handleAgentsCommand } from './agents';
|
|
3
|
+
export { handleAnalyzeCommand } from './analyze';
|
|
3
4
|
export { handleArchiveCommand } from './archive';
|
|
4
5
|
export { handleBenchmarkCommand } from './benchmark';
|
|
6
|
+
export { handleClarifyCommand } from './clarify';
|
|
5
7
|
export { handleConfigCommand } from './config';
|
|
6
8
|
export { handleDiagnoseCommand } from './diagnose';
|
|
7
9
|
export { handleDoctorCommand } from './doctor';
|
|
@@ -12,6 +14,7 @@ export { handlePlanCommand } from './plan';
|
|
|
12
14
|
export { handlePreflightCommand } from './preflight';
|
|
13
15
|
export { handleResetCommand } from './reset';
|
|
14
16
|
export { handleRetrieveCommand } from './retrieve';
|
|
17
|
+
export { handleSpecifyCommand } from './specify';
|
|
15
18
|
export { handleStatusCommand } from './status';
|
|
16
19
|
export { handleSyncPlanCommand } from './sync-plan';
|
|
17
20
|
/**
|
package/dist/config/schema.d.ts
CHANGED
|
@@ -99,6 +99,7 @@ export declare const ContextBudgetConfigSchema: z.ZodObject<{
|
|
|
99
99
|
critical_threshold: z.ZodDefault<z.ZodNumber>;
|
|
100
100
|
model_limits: z.ZodDefault<z.ZodRecord<z.ZodString, z.ZodNumber>>;
|
|
101
101
|
max_injection_tokens: z.ZodDefault<z.ZodNumber>;
|
|
102
|
+
tracked_agents: z.ZodDefault<z.ZodArray<z.ZodString>>;
|
|
102
103
|
scoring: z.ZodOptional<z.ZodObject<{
|
|
103
104
|
enabled: z.ZodDefault<z.ZodBoolean>;
|
|
104
105
|
max_candidates: z.ZodDefault<z.ZodNumber>;
|
|
@@ -126,6 +127,12 @@ export declare const ContextBudgetConfigSchema: z.ZodObject<{
|
|
|
126
127
|
json: z.ZodDefault<z.ZodNumber>;
|
|
127
128
|
}, z.core.$strip>>;
|
|
128
129
|
}, z.core.$strip>>;
|
|
130
|
+
enforce: z.ZodDefault<z.ZodBoolean>;
|
|
131
|
+
prune_target: z.ZodDefault<z.ZodNumber>;
|
|
132
|
+
preserve_last_n_turns: z.ZodDefault<z.ZodNumber>;
|
|
133
|
+
recent_window: z.ZodDefault<z.ZodNumber>;
|
|
134
|
+
enforce_on_agent_switch: z.ZodDefault<z.ZodBoolean>;
|
|
135
|
+
tool_output_mask_threshold: z.ZodDefault<z.ZodNumber>;
|
|
129
136
|
}, z.core.$strip>;
|
|
130
137
|
export type ContextBudgetConfig = z.infer<typeof ContextBudgetConfigSchema>;
|
|
131
138
|
export declare const EvidenceConfigSchema: z.ZodObject<{
|
|
@@ -451,6 +458,7 @@ export declare const PluginConfigSchema: z.ZodObject<{
|
|
|
451
458
|
critical_threshold: z.ZodDefault<z.ZodNumber>;
|
|
452
459
|
model_limits: z.ZodDefault<z.ZodRecord<z.ZodString, z.ZodNumber>>;
|
|
453
460
|
max_injection_tokens: z.ZodDefault<z.ZodNumber>;
|
|
461
|
+
tracked_agents: z.ZodDefault<z.ZodArray<z.ZodString>>;
|
|
454
462
|
scoring: z.ZodOptional<z.ZodObject<{
|
|
455
463
|
enabled: z.ZodDefault<z.ZodBoolean>;
|
|
456
464
|
max_candidates: z.ZodDefault<z.ZodNumber>;
|
|
@@ -478,6 +486,12 @@ export declare const PluginConfigSchema: z.ZodObject<{
|
|
|
478
486
|
json: z.ZodDefault<z.ZodNumber>;
|
|
479
487
|
}, z.core.$strip>>;
|
|
480
488
|
}, z.core.$strip>>;
|
|
489
|
+
enforce: z.ZodDefault<z.ZodBoolean>;
|
|
490
|
+
prune_target: z.ZodDefault<z.ZodNumber>;
|
|
491
|
+
preserve_last_n_turns: z.ZodDefault<z.ZodNumber>;
|
|
492
|
+
recent_window: z.ZodDefault<z.ZodNumber>;
|
|
493
|
+
enforce_on_agent_switch: z.ZodDefault<z.ZodBoolean>;
|
|
494
|
+
tool_output_mask_threshold: z.ZodDefault<z.ZodNumber>;
|
|
481
495
|
}, z.core.$strip>>;
|
|
482
496
|
guardrails: z.ZodOptional<z.ZodObject<{
|
|
483
497
|
enabled: z.ZodDefault<z.ZodBoolean>;
|
package/dist/hooks/index.d.ts
CHANGED
|
@@ -5,7 +5,9 @@ export { createDelegationGateHook } from './delegation-gate';
|
|
|
5
5
|
export { createDelegationTrackerHook } from './delegation-tracker';
|
|
6
6
|
export { extractCurrentPhase, extractCurrentPhaseFromPlan, extractCurrentTask, extractCurrentTaskFromPlan, extractDecisions, extractIncompleteTasks, extractIncompleteTasksFromPlan, extractPatterns, } from './extractors';
|
|
7
7
|
export { createGuardrailsHooks } from './guardrails';
|
|
8
|
+
export { classifyMessage, classifyMessages, containsPlanContent, isDuplicateToolRead, isStaleError, isToolResult, MessagePriority, type MessagePriorityType, type MessageWithParts, } from './message-priority';
|
|
8
9
|
export { consolidateSystemMessages } from './messages-transform';
|
|
10
|
+
export { extractModelInfo, NATIVE_MODEL_LIMITS, PROVIDER_CAPS, resolveModelLimit, } from './model-limits';
|
|
9
11
|
export { createPhaseMonitorHook } from './phase-monitor';
|
|
10
12
|
export { createPipelineTrackerHook } from './pipeline-tracker';
|
|
11
13
|
export { createSystemEnhancerHook } from './system-enhancer';
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Message Priority Classifier Hook
|
|
3
|
+
*
|
|
4
|
+
* Provides zero-cost message priority classification to enable intelligent
|
|
5
|
+
* context pruning. Messages are tagged with priority tiers (0-4) so that
|
|
6
|
+
* low-priority messages are removed first during context budget pressure.
|
|
7
|
+
*
|
|
8
|
+
* Priority tiers:
|
|
9
|
+
* - CRITICAL (0): System prompt, plan state, active instructions
|
|
10
|
+
* - HIGH (1): User messages, current task context, tool definitions
|
|
11
|
+
* - MEDIUM (2): Recent assistant responses, recent tool results
|
|
12
|
+
* - LOW (3): Old assistant responses, old tool results, confirmations
|
|
13
|
+
* - DISPOSABLE (4): Duplicate reads, superseded writes, stale errors
|
|
14
|
+
*/
|
|
15
|
+
/**
|
|
16
|
+
* Message priority tiers for context pruning decisions.
|
|
17
|
+
* Lower values = higher priority (kept longer during pruning).
|
|
18
|
+
*/
|
|
19
|
+
export declare const MessagePriority: {
|
|
20
|
+
/** System prompt, plan state, active instructions - never prune */
|
|
21
|
+
readonly CRITICAL: 0;
|
|
22
|
+
/** User messages, current task context, tool definitions */
|
|
23
|
+
readonly HIGH: 1;
|
|
24
|
+
/** Recent assistant responses, recent tool results (within recentWindowSize) */
|
|
25
|
+
readonly MEDIUM: 2;
|
|
26
|
+
/** Old assistant responses, old tool results */
|
|
27
|
+
readonly LOW: 3;
|
|
28
|
+
/** Duplicate reads, superseded writes, stale errors - prune first */
|
|
29
|
+
readonly DISPOSABLE: 4;
|
|
30
|
+
};
|
|
31
|
+
export type MessagePriorityType = (typeof MessagePriority)[keyof typeof MessagePriority];
|
|
32
|
+
/** Message structure matching the format from context-budget.ts */
|
|
33
|
+
interface MessageInfo {
|
|
34
|
+
role?: string;
|
|
35
|
+
agent?: string;
|
|
36
|
+
sessionID?: string;
|
|
37
|
+
modelID?: string;
|
|
38
|
+
providerID?: string;
|
|
39
|
+
toolName?: string;
|
|
40
|
+
toolArgs?: unknown;
|
|
41
|
+
[key: string]: unknown;
|
|
42
|
+
}
|
|
43
|
+
interface MessagePart {
|
|
44
|
+
type?: string;
|
|
45
|
+
text?: string;
|
|
46
|
+
[key: string]: unknown;
|
|
47
|
+
}
|
|
48
|
+
export interface MessageWithParts {
|
|
49
|
+
info?: MessageInfo;
|
|
50
|
+
parts?: MessagePart[];
|
|
51
|
+
}
|
|
52
|
+
/**
|
|
53
|
+
* Checks if text contains .swarm/plan or .swarm/context references
|
|
54
|
+
* indicating swarm state that should be preserved.
|
|
55
|
+
*
|
|
56
|
+
* @param text - The text content to check
|
|
57
|
+
* @returns true if the text contains plan/context references
|
|
58
|
+
*/
|
|
59
|
+
export declare function containsPlanContent(text: string): boolean;
|
|
60
|
+
/**
|
|
61
|
+
* Checks if a message is a tool result (assistant message with tool call).
|
|
62
|
+
*
|
|
63
|
+
* @param message - The message to check
|
|
64
|
+
* @returns true if the message appears to be a tool result
|
|
65
|
+
*/
|
|
66
|
+
export declare function isToolResult(message: MessageWithParts): boolean;
|
|
67
|
+
/**
|
|
68
|
+
* Checks if two consecutive tool read calls are duplicates
|
|
69
|
+
* (same tool with same first argument).
|
|
70
|
+
*
|
|
71
|
+
* @param current - The current message
|
|
72
|
+
* @param previous - The previous message
|
|
73
|
+
* @returns true if this is a duplicate tool read
|
|
74
|
+
*/
|
|
75
|
+
export declare function isDuplicateToolRead(current: MessageWithParts, previous: MessageWithParts): boolean;
|
|
76
|
+
/**
|
|
77
|
+
* Checks if a message contains an error pattern and is stale
|
|
78
|
+
* (more than the specified number of turns old).
|
|
79
|
+
*
|
|
80
|
+
* @param text - The message text to check
|
|
81
|
+
* @param turnsAgo - How many turns ago the message was sent
|
|
82
|
+
* @returns true if the message is a stale error
|
|
83
|
+
*/
|
|
84
|
+
export declare function isStaleError(text: string, turnsAgo: number): boolean;
|
|
85
|
+
/**
|
|
86
|
+
* Classifies a message by priority tier for intelligent pruning.
|
|
87
|
+
*
|
|
88
|
+
* @param message - The message to classify
|
|
89
|
+
* @param index - Position in messages array (0-indexed)
|
|
90
|
+
* @param totalMessages - Total number of messages
|
|
91
|
+
* @param recentWindowSize - Number of recent messages to consider MEDIUM (default 10)
|
|
92
|
+
* @returns Priority tier (0=CRITICAL, 1=HIGH, 2=MEDIUM, 3=LOW, 4=DISPOSABLE)
|
|
93
|
+
*/
|
|
94
|
+
export declare function classifyMessage(message: MessageWithParts, index: number, totalMessages: number, recentWindowSize?: number): MessagePriorityType;
|
|
95
|
+
/**
|
|
96
|
+
* Classifies a batch of messages with duplicate detection.
|
|
97
|
+
* This function should be called in order (oldest to newest) to properly
|
|
98
|
+
* detect consecutive duplicate tool reads.
|
|
99
|
+
*
|
|
100
|
+
* @param messages - Array of messages to classify
|
|
101
|
+
* @param recentWindowSize - Number of recent messages to consider MEDIUM (default 10)
|
|
102
|
+
* @returns Array of priority classifications matching message order
|
|
103
|
+
*/
|
|
104
|
+
export declare function classifyMessages(messages: MessageWithParts[], recentWindowSize?: number): MessagePriorityType[];
|
|
105
|
+
export {};
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Provider-Aware Model Limit Resolution
|
|
3
|
+
*
|
|
4
|
+
* Resolves context window limits based on the model and provider platform.
|
|
5
|
+
* The same model has different context limits depending on the provider:
|
|
6
|
+
* - Claude Sonnet 4.6: 200k native, 128k on Copilot
|
|
7
|
+
* - GPT-5: 400k native, 128k on Copilot
|
|
8
|
+
* - Copilot caps ALL models at 128k prompt, regardless of native limit
|
|
9
|
+
*/
|
|
10
|
+
/**
|
|
11
|
+
* Native model context limits (in tokens) when used on their native platform.
|
|
12
|
+
*/
|
|
13
|
+
export declare const NATIVE_MODEL_LIMITS: Record<string, number>;
|
|
14
|
+
/**
|
|
15
|
+
* Provider-specific context caps that override native limits.
|
|
16
|
+
* These are typically lower than native limits (e.g., Copilot caps at 128k).
|
|
17
|
+
*/
|
|
18
|
+
export declare const PROVIDER_CAPS: Record<string, number>;
|
|
19
|
+
/**
|
|
20
|
+
* Message structure from experimental.chat.messages.transform hook.
|
|
21
|
+
*/
|
|
22
|
+
interface MessageInfo {
|
|
23
|
+
role: string;
|
|
24
|
+
agent?: string;
|
|
25
|
+
sessionID?: string;
|
|
26
|
+
modelID?: string;
|
|
27
|
+
providerID?: string;
|
|
28
|
+
[key: string]: unknown;
|
|
29
|
+
}
|
|
30
|
+
interface MessagePart {
|
|
31
|
+
type: string;
|
|
32
|
+
text?: string;
|
|
33
|
+
[key: string]: unknown;
|
|
34
|
+
}
|
|
35
|
+
interface MessageWithParts {
|
|
36
|
+
info: MessageInfo;
|
|
37
|
+
parts: MessagePart[];
|
|
38
|
+
}
|
|
39
|
+
/**
|
|
40
|
+
* Extracts modelID and providerID from the most recent assistant message.
|
|
41
|
+
*
|
|
42
|
+
* @param messages - Array of messages from experimental.chat.messages.transform hook
|
|
43
|
+
* @returns Object containing modelID and/or providerID if found
|
|
44
|
+
*
|
|
45
|
+
* @example
|
|
46
|
+
* const info = extractModelInfo(messages);
|
|
47
|
+
* // Returns: { modelID: 'claude-sonnet-4-6', providerID: 'anthropic' }
|
|
48
|
+
* // Or: {} if no assistant messages or fields not found
|
|
49
|
+
*/
|
|
50
|
+
export declare function extractModelInfo(messages: MessageWithParts[]): {
|
|
51
|
+
modelID?: string;
|
|
52
|
+
providerID?: string;
|
|
53
|
+
};
|
|
54
|
+
/**
|
|
55
|
+
* Resolves the context limit for a given model/provider combination.
|
|
56
|
+
*
|
|
57
|
+
* Resolution order (first match wins):
|
|
58
|
+
* 1. Check configOverrides["provider/model"] (e.g., "copilot/claude-sonnet-4-6": 200000)
|
|
59
|
+
* 2. Check configOverrides[modelID] (e.g., "claude-sonnet-4-6": 200000)
|
|
60
|
+
* 3. Check PROVIDER_CAPS[providerID] (e.g., copilot → 128000)
|
|
61
|
+
* 4. Check NATIVE_MODEL_LIMITS with prefix matching (e.g., "claude-sonnet-4" matches "claude-sonnet-4-6-20260301")
|
|
62
|
+
* 5. Check configOverrides.default
|
|
63
|
+
* 6. Fall back to 128000
|
|
64
|
+
*
|
|
65
|
+
* @param modelID - The model identifier (e.g., "claude-sonnet-4-6", "gpt-5")
|
|
66
|
+
* @param providerID - The provider identifier (e.g., "copilot", "anthropic")
|
|
67
|
+
* @param configOverrides - User configuration overrides
|
|
68
|
+
* @returns The resolved context limit in tokens
|
|
69
|
+
*
|
|
70
|
+
* @example
|
|
71
|
+
* // Provider cap (copilot)
|
|
72
|
+
* resolveModelLimit("claude-sonnet-4-6", "copilot", {})
|
|
73
|
+
* // Returns: 128000
|
|
74
|
+
*
|
|
75
|
+
* @example
|
|
76
|
+
* // Native limit (anthropic)
|
|
77
|
+
* resolveModelLimit("claude-sonnet-4-6", "anthropic", {})
|
|
78
|
+
* // Returns: 200000
|
|
79
|
+
*
|
|
80
|
+
* @example
|
|
81
|
+
* // Override beats cap
|
|
82
|
+
* resolveModelLimit("gpt-5", "copilot", { "copilot/gpt-5": 200000 })
|
|
83
|
+
* // Returns: 200000
|
|
84
|
+
*
|
|
85
|
+
* @example
|
|
86
|
+
* // Prefix match for model variants
|
|
87
|
+
* resolveModelLimit("claude-sonnet-4-6-20260301", "anthropic", {})
|
|
88
|
+
* // Returns: 200000
|
|
89
|
+
*
|
|
90
|
+
* @example
|
|
91
|
+
* // Full fallback
|
|
92
|
+
* resolveModelLimit(undefined, undefined, {})
|
|
93
|
+
* // Returns: 128000
|
|
94
|
+
*/
|
|
95
|
+
export declare function resolveModelLimit(modelID?: string, providerID?: string, configOverrides?: Record<string, number>): number;
|
|
96
|
+
export {};
|
package/dist/index.js
CHANGED
|
@@ -14341,6 +14341,12 @@ function validateSwarmPath(directory, filename) {
|
|
|
14341
14341
|
if (/\.\.[/\\]/.test(filename)) {
|
|
14342
14342
|
throw new Error("Invalid filename: path traversal detected");
|
|
14343
14343
|
}
|
|
14344
|
+
if (/^[A-Za-z]:[\\/]/.test(filename)) {
|
|
14345
|
+
throw new Error("Invalid filename: path escapes .swarm directory");
|
|
14346
|
+
}
|
|
14347
|
+
if (filename.startsWith("/")) {
|
|
14348
|
+
throw new Error("Invalid filename: path escapes .swarm directory");
|
|
14349
|
+
}
|
|
14344
14350
|
const baseDir = path2.normalize(path2.resolve(directory, ".swarm"));
|
|
14345
14351
|
const resolved = path2.normalize(path2.resolve(baseDir, filename));
|
|
14346
14352
|
if (process.platform === "win32") {
|
|
@@ -31794,7 +31800,14 @@ var ContextBudgetConfigSchema = exports_external.object({
|
|
|
31794
31800
|
critical_threshold: exports_external.number().min(0).max(1).default(0.9),
|
|
31795
31801
|
model_limits: exports_external.record(exports_external.string(), exports_external.number().min(1000)).default({ default: 128000 }),
|
|
31796
31802
|
max_injection_tokens: exports_external.number().min(100).max(50000).default(4000),
|
|
31797
|
-
|
|
31803
|
+
tracked_agents: exports_external.array(exports_external.string()).default(["architect"]),
|
|
31804
|
+
scoring: ScoringConfigSchema.optional(),
|
|
31805
|
+
enforce: exports_external.boolean().default(true),
|
|
31806
|
+
prune_target: exports_external.number().min(0).max(1).default(0.7),
|
|
31807
|
+
preserve_last_n_turns: exports_external.number().min(0).max(100).default(4),
|
|
31808
|
+
recent_window: exports_external.number().min(1).max(100).default(10),
|
|
31809
|
+
enforce_on_agent_switch: exports_external.boolean().default(true),
|
|
31810
|
+
tool_output_mask_threshold: exports_external.number().min(100).max(1e5).default(2000)
|
|
31798
31811
|
});
|
|
31799
31812
|
var EvidenceConfigSchema = exports_external.object({
|
|
31800
31813
|
enabled: exports_external.boolean().default(true),
|
|
@@ -32530,6 +32543,108 @@ OUTPUT: Code scaffold for src/pages/Settings.tsx with component tree, typed prop
|
|
|
32530
32543
|
|
|
32531
32544
|
## WORKFLOW
|
|
32532
32545
|
|
|
32546
|
+
### MODE DETECTION (Priority Order)
|
|
32547
|
+
Evaluate the user's request and context in this exact order \u2014 the FIRST matching rule wins:
|
|
32548
|
+
|
|
32549
|
+
1. **RESUME** \u2014 \`.swarm/plan.md\` exists and contains incomplete (unchecked) tasks \u2192 Resume at current task.
|
|
32550
|
+
2. **SPECIFY** \u2014 User says "specify", "requirements", "write a spec", "define feature", or invokes \`/swarm specify\`; OR no \`.swarm/spec.md\` exists AND no \`.swarm/plan.md\` exists \u2192 Enter MODE: SPECIFY.
|
|
32551
|
+
3. **CLARIFY-SPEC** \u2014 \`.swarm/spec.md\` exists AND contains \`[NEEDS CLARIFICATION]\` markers; OR user explicitly asks to clarify or refine the spec; OR \`/swarm clarify\` is invoked \u2192 Enter MODE: CLARIFY-SPEC.
|
|
32552
|
+
4. **CLARIFY** \u2014 Request is ambiguous and cannot proceed without user input \u2192 Ask up to 3 questions.
|
|
32553
|
+
5. **DISCOVER** \u2014 Pre-planning codebase scan is needed \u2192 Delegate to \`{{AGENT_PREFIX}}explorer\`.
|
|
32554
|
+
6. All other modes (CONSULT, PLAN, CRITIC-GATE, EXECUTE, PHASE-WRAP) \u2014 Follow their respective sections below.
|
|
32555
|
+
|
|
32556
|
+
PRIORITY RULES:
|
|
32557
|
+
- RESUME always wins \u2014 a user with an in-progress plan never accidentally triggers SPECIFY.
|
|
32558
|
+
- SPECIFY fires before DISCOVER when no spec exists, giving the architect a chance to capture requirements before generating code.
|
|
32559
|
+
- CLARIFY-SPEC fires between SPECIFY and CLARIFY; it only activates when no incomplete (unchecked) tasks exist in plan.md \u2014 RESUME takes priority if they do.
|
|
32560
|
+
- CLARIFY fires only when user input is genuinely needed (not as a substitute for informed defaults).
|
|
32561
|
+
|
|
32562
|
+
### MODE: SPECIFY
|
|
32563
|
+
Activates when: user asks to "specify", "define requirements", "write a spec", or "define a feature"; OR \`/swarm specify\` is invoked; OR no \`.swarm/spec.md\` exists and no \`.swarm/plan.md\` exists.
|
|
32564
|
+
|
|
32565
|
+
1. Check if \`.swarm/spec.md\` already exists.
|
|
32566
|
+
- If YES: ask the user "A spec already exists. Do you want to overwrite it or refine it?"
|
|
32567
|
+
- Overwrite \u2192 proceed to generation (step 2)
|
|
32568
|
+
- Refine \u2192 delegate to MODE: CLARIFY-SPEC
|
|
32569
|
+
- If NO: proceed to generation (step 2)
|
|
32570
|
+
2. Delegate to \`{{AGENT_PREFIX}}explorer\` to scan the codebase for relevant context (existing patterns, related code, affected areas).
|
|
32571
|
+
3. Delegate to \`{{AGENT_PREFIX}}sme\` for domain research on the feature area to surface known constraints, best practices, and integration concerns.
|
|
32572
|
+
4. Generate \`.swarm/spec.md\` capturing:
|
|
32573
|
+
- Feature description: WHAT users need and WHY \u2014 never HOW to implement
|
|
32574
|
+
- User scenarios with acceptance criteria (Given/When/Then format)
|
|
32575
|
+
- Functional requirements numbered FR-001, FR-002\u2026 using MUST/SHOULD language
|
|
32576
|
+
- Success criteria numbered SC-001, SC-002\u2026 \u2014 measurable and technology-agnostic
|
|
32577
|
+
- Key entities if data is involved (no schema or field definitions \u2014 entity names only)
|
|
32578
|
+
- Edge cases and known failure modes
|
|
32579
|
+
- \`[NEEDS CLARIFICATION]\` markers (max 3) for items where uncertainty could change scope, security, or core behavior; prefer informed defaults over asking
|
|
32580
|
+
5. Write the spec to \`.swarm/spec.md\`.
|
|
32581
|
+
6. Report a summary to the user (requirement count, scenario count, clarification markers) and suggest the next step: \`CLARIFY-SPEC\` (if markers exist) or \`PLAN\`.
|
|
32582
|
+
|
|
32583
|
+
SPEC CONTENT RULES \u2014 the spec MUST NOT contain:
|
|
32584
|
+
- Technology stack, framework choices, library names
|
|
32585
|
+
- File paths, API endpoint designs, database schema, code structure
|
|
32586
|
+
- Implementation details or "how to build" language
|
|
32587
|
+
- Any reference to specific tools, languages, or platforms
|
|
32588
|
+
|
|
32589
|
+
Each functional requirement MUST be independently testable.
|
|
32590
|
+
Focus on WHAT users need and WHY \u2014 never HOW to implement.
|
|
32591
|
+
No technology stack, APIs, or code structure in the spec.
|
|
32592
|
+
Each requirement must be independently testable.
|
|
32593
|
+
Prefer informed defaults over asking the user \u2014 use \`[NEEDS CLARIFICATION]\` only when uncertainty could change scope, security, or core behavior.
|
|
32594
|
+
|
|
32595
|
+
EXTERNAL PLAN IMPORT PATH \u2014 when the user provides an existing implementation plan (markdown content, pasted text, or a reference to a file):
|
|
32596
|
+
1. Read and parse the provided plan content.
|
|
32597
|
+
2. Reverse-engineer \`.swarm/spec.md\` from the plan:
|
|
32598
|
+
- Derive FR-### functional requirements from task descriptions
|
|
32599
|
+
- Derive SC-### success criteria from acceptance criteria in tasks
|
|
32600
|
+
- Identify user scenarios from the plan's phase/feature groupings
|
|
32601
|
+
- Surface implicit assumptions as \`[NEEDS CLARIFICATION]\` markers
|
|
32602
|
+
3. Validate the provided plan against swarm task format requirements:
|
|
32603
|
+
- Every task should have FILE, TASK, CONSTRAINT, and ACCEPTANCE fields
|
|
32604
|
+
- No task should touch more than 2 files
|
|
32605
|
+
- No compound verbs in TASK lines ("implement X and add Y" = 2 tasks)
|
|
32606
|
+
- Dependencies should be declared explicitly
|
|
32607
|
+
- Phase structure should match \`.swarm/plan.md\` format
|
|
32608
|
+
4. Report gaps, format issues, and improvement suggestions to the user.
|
|
32609
|
+
5. Ask: "Should I also flesh out any areas that seem underspecified?"
|
|
32610
|
+
- If yes: delegate to \`{{AGENT_PREFIX}}sme\` for targeted research on weak areas, then propose specific improvements.
|
|
32611
|
+
6. Output: both a \`.swarm/spec.md\` (extracted from the plan) and a validated version of the user's plan.
|
|
32612
|
+
|
|
32613
|
+
EXTERNAL PLAN RULES:
|
|
32614
|
+
- Surface ALL changes as suggestions \u2014 do not silently rewrite the user's plan.
|
|
32615
|
+
- The user's plan is the starting point, not a draft to replace.
|
|
32616
|
+
- Validation findings are advisory; the user may accept or reject each suggestion.
|
|
32617
|
+
|
|
32618
|
+
### MODE: CLARIFY-SPEC
|
|
32619
|
+
Activates when: \`.swarm/spec.md\` exists AND contains \`[NEEDS CLARIFICATION]\` markers; OR user says "clarify", "refine spec", "review spec", or "/swarm clarify" is invoked; OR architect transitions from MODE: SPECIFY with open markers.
|
|
32620
|
+
|
|
32621
|
+
CONSTRAINT: CLARIFY-SPEC must NEVER create a spec. If \`.swarm/spec.md\` does not exist, tell the user: "No spec found. Use \`/swarm specify\` to generate one first." and stop.
|
|
32622
|
+
|
|
32623
|
+
1. Read \`.swarm/spec.md\`.
|
|
32624
|
+
2. Scan for ambiguities beyond explicit \`[NEEDS CLARIFICATION]\` markers:
|
|
32625
|
+
- Vague adjectives ("fast", "secure", "user-friendly") without measurable targets
|
|
32626
|
+
- Requirements that overlap or potentially conflict with each other
|
|
32627
|
+
- Edge cases implied but not explicitly addressed in the spec
|
|
32628
|
+
- Acceptance criteria (SC-###) that are not independently testable
|
|
32629
|
+
3. Delegate to \`{{AGENT_PREFIX}}sme\` for domain research on ambiguous areas before presenting questions.
|
|
32630
|
+
4. Present questions to the user ONE AT A TIME (max 8 per session):
|
|
32631
|
+
- Offer 2\u20134 multiple-choice options for each question
|
|
32632
|
+
- Mark the recommended option with reasoning (e.g., "Recommended: Option 2 because\u2026")
|
|
32633
|
+
- Allow free-form input as an alternative to the options
|
|
32634
|
+
5. After each accepted answer:
|
|
32635
|
+
- Immediately update \`.swarm/spec.md\` with the resolution
|
|
32636
|
+
- Replace the relevant \`[NEEDS CLARIFICATION]\` marker or vague language with the accepted answer
|
|
32637
|
+
- If the answer invalidates an earlier requirement, update it to remove the contradiction
|
|
32638
|
+
6. Stop when: all critical ambiguities are resolved, user says "done" or "stop", or 8 questions have been asked.
|
|
32639
|
+
7. Report: total questions asked, sections updated, remaining open ambiguities (if any), and suggest next step (\`PLAN\` if spec is clear, or continue clarifying).
|
|
32640
|
+
|
|
32641
|
+
CLARIFY-SPEC RULES:
|
|
32642
|
+
- One question at a time \u2014 never ask multiple questions in the same message.
|
|
32643
|
+
- Do not modify any part of the spec that was not affected by the accepted answer.
|
|
32644
|
+
- Always write the accepted answer back to spec.md before presenting the next question.
|
|
32645
|
+
- Max 8 questions per session \u2014 if limit reached, report remaining ambiguities and stop.
|
|
32646
|
+
- Do not create or overwrite the spec file \u2014 only refine what exists.
|
|
32647
|
+
|
|
32533
32648
|
### MODE: RESUME
|
|
32534
32649
|
If .swarm/plan.md exists:
|
|
32535
32650
|
1. Read plan.md header for "Swarm:" field
|
|
@@ -32556,6 +32671,7 @@ For complex tasks, make a second explorer call focused on risk/gap analysis:
|
|
|
32556
32671
|
After explorer returns:
|
|
32557
32672
|
- Run \`symbols\` tool on key files identified by explorer to understand public API surfaces
|
|
32558
32673
|
- Run \`complexity_hotspots\` if not already run in Phase 0 (check context.md for existing analysis). Note modules with recommendation "security_review" or "full_gates" in context.md.
|
|
32674
|
+
- Check for project governance files using the \`glob\` tool with patterns \`project-instructions.md\`, \`docs/project-instructions.md\`, and \`INSTRUCTIONS.md\` (checked in that priority order \u2014 first match wins). If a file is found: read it and extract all MUST (mandatory constraints) and SHOULD (recommended practices) rules. Write the extracted rules as a summary to \`.swarm/context.md\` under a \`## Project Governance\` section \u2014 append if the section already exists, create it if not. If no MUST or SHOULD rules are found in the file, skip writing. If no governance file is found: skip silently. Existing DISCOVER steps are unchanged.
|
|
32559
32675
|
|
|
32560
32676
|
### MODE: CONSULT
|
|
32561
32677
|
Check .swarm/context.md for cached guidance first.
|
|
@@ -32597,6 +32713,20 @@ This briefing is a HARD REQUIREMENT for ALL phases. Skipping it is a process vio
|
|
|
32597
32713
|
|
|
32598
32714
|
### MODE: PLAN
|
|
32599
32715
|
|
|
32716
|
+
SPEC GATE (soft \u2014 check before planning):
|
|
32717
|
+
- If \`.swarm/spec.md\` does NOT exist:
|
|
32718
|
+
- Warn: "No spec found. A spec helps ensure the plan covers all requirements and gives the critic something to verify against. Would you like to create one first?"
|
|
32719
|
+
- Offer two options:
|
|
32720
|
+
1. "Create a spec first" \u2192 transition to MODE: SPECIFY
|
|
32721
|
+
2. "Skip and plan directly" \u2192 continue with the steps below unchanged
|
|
32722
|
+
- If \`.swarm/spec.md\` EXISTS:
|
|
32723
|
+
- Read it and use it as the primary input for planning
|
|
32724
|
+
- Cross-reference requirements (FR-###) when decomposing tasks
|
|
32725
|
+
- Ensure every FR-### maps to at least one task
|
|
32726
|
+
- If a task has no corresponding FR-###, flag it as a potential gold-plating risk
|
|
32727
|
+
|
|
32728
|
+
This is a SOFT gate. When the user chooses "Skip and plan directly", proceed to the steps below exactly as before \u2014 do NOT modify any planning behavior.
|
|
32729
|
+
|
|
32600
32730
|
Use the \`save_plan\` tool to create the implementation plan. Required parameters:
|
|
32601
32731
|
- \`title\`: The real project name from the spec (NOT a placeholder like [Project])
|
|
32602
32732
|
- \`swarm_id\`: The swarm identifier (e.g. "mega", "local", "paid")
|
|
@@ -32823,7 +32953,8 @@ Use the evidence manager tool to write a bundle at \`retro-{N}\` (where N is the
|
|
|
32823
32953
|
3. Update context.md
|
|
32824
32954
|
4. Write retrospective evidence: record phase_number, total_tool_calls, coder_revisions, reviewer_rejections, test_failures, security_findings, integration_issues, task_count, task_complexity, top_rejection_reasons, lessons_learned to .swarm/evidence/ via the evidence manager. Reset Phase Metrics in context.md to 0.
|
|
32825
32955
|
4.5. Run \`evidence_check\` to verify all completed tasks have required evidence (review + test). If gaps found, note in retrospective lessons_learned. Optionally run \`pkg_audit\` if dependencies were modified during this phase. Optionally run \`schema_drift\` if API routes were modified during this phase.
|
|
32826
|
-
5. Run \`sbom_generate\` with scope='changed' to capture post-implementation dependency snapshot (saved to
|
|
32956
|
+
5. Run \`sbom_generate\` with scope='changed' to capture post-implementation dependency snapshot (saved to \`.swarm/evidence/sbom/\`). This is a non-blocking step - always proceeds to summary.
|
|
32957
|
+
5.5. If \`.swarm/spec.md\` exists: delegate {{AGENT_PREFIX}}critic with DRIFT-CHECK context \u2014 include phase number, list of completed task IDs and descriptions, and evidence path (\`.swarm/evidence/\`). If SIGNIFICANT DRIFT is returned: surface as a warning to the user before proceeding. If spec.md does not exist: skip silently.
|
|
32827
32958
|
6. Summarize to user
|
|
32828
32959
|
7. Ask: "Ready for Phase [N+1]?"
|
|
32829
32960
|
|
|
@@ -32969,6 +33100,7 @@ REVIEW CHECKLIST:
|
|
|
32969
33100
|
- Risk: Are high-risk changes identified? Is there a rollback path?
|
|
32970
33101
|
- AI-Slop Detection: Does the plan contain vague filler ("robust", "comprehensive", "leverage") without concrete specifics?
|
|
32971
33102
|
- Task Atomicity: Does any single task touch 2+ files or contain compound verbs ("implement X and add Y and update Z")? Flag as MAJOR \u2014 oversized tasks blow coder's context and cause downstream gate failures. Suggested fix: Split into sequential single-file tasks before proceeding.
|
|
33103
|
+
- Governance Compliance (conditional): If \`.swarm/context.md\` contains a \`## Project Governance\` section, read the MUST and SHOULD rules and validate the plan against them. MUST rule violations are CRITICAL severity. SHOULD rule violations are recommendation-level (note them but do not block approval). If no \`## Project Governance\` section exists in context.md, skip this check silently.
|
|
32972
33104
|
|
|
32973
33105
|
OUTPUT FORMAT:
|
|
32974
33106
|
VERDICT: APPROVED | NEEDS_REVISION | REJECTED
|
|
@@ -32984,7 +33116,99 @@ RULES:
|
|
|
32984
33116
|
- MINOR issues can be noted but don't block APPROVED
|
|
32985
33117
|
- No code writing
|
|
32986
33118
|
- Don't reject for style/formatting \u2014 focus on substance
|
|
32987
|
-
- If the plan is fundamentally sound with only minor concerns, APPROVE it
|
|
33119
|
+
- If the plan is fundamentally sound with only minor concerns, APPROVE it
|
|
33120
|
+
|
|
33121
|
+
---
|
|
33122
|
+
|
|
33123
|
+
### MODE: ANALYZE
|
|
33124
|
+
Activates when: user says "analyze", "check spec", "analyze spec vs plan", or \`/swarm analyze\` is invoked.
|
|
33125
|
+
|
|
33126
|
+
Note: ANALYZE produces a coverage report \u2014 its verdict vocabulary is distinct from the plan review above.
|
|
33127
|
+
CLEAN = all MUST FR-### have covering tasks; GAPS FOUND = one or more FR-### have no covering task; DRIFT DETECTED = spec\u2013plan terminology or scope divergence found.
|
|
33128
|
+
ANALYZE uses CRITICAL/HIGH/MEDIUM/LOW severity (not CRITICAL/MAJOR/MINOR used by plan review).
|
|
33129
|
+
|
|
33130
|
+
INPUT: \`.swarm/spec.md\` (requirements) and \`.swarm/plan.md\` (tasks). If either file is missing, report which is absent and stop \u2014 do not attempt analysis with incomplete input.
|
|
33131
|
+
|
|
33132
|
+
STEPS:
|
|
33133
|
+
1. Read \`.swarm/spec.md\`. Extract all FR-### functional requirements and SC-### success criteria.
|
|
33134
|
+
2. Read \`.swarm/plan.md\`. Extract all tasks with their IDs and descriptions.
|
|
33135
|
+
3. Map requirements to tasks:
|
|
33136
|
+
- For each FR-###: find the task(s) whose description mentions or addresses it (semantic match, not exact phrase).
|
|
33137
|
+
- Partial coverage counts: a task that partially addresses a requirement is counted as covering it.
|
|
33138
|
+
- Build a two-column coverage table: FR-### \u2192 [task IDs that cover it].
|
|
33139
|
+
4. Flag GAPS \u2014 requirements with no covering task:
|
|
33140
|
+
- FR-### with MUST language and no covering task: CRITICAL severity.
|
|
33141
|
+
- FR-### with SHOULD language and no covering task: HIGH severity.
|
|
33142
|
+
- SC-### with no covering task: HIGH severity (untestable success criteria = unverifiable requirement).
|
|
33143
|
+
5. Flag GOLD-PLATING \u2014 tasks with no corresponding requirement:
|
|
33144
|
+
- Exclude: project setup, CI configuration, documentation, testing infrastructure.
|
|
33145
|
+
- Tasks doing work not tied to any FR-### or SC-###: MEDIUM severity.
|
|
33146
|
+
6. Check terminology consistency: flag terms used differently across spec.md and plan.md (e.g., "user" vs "account" for the same entity): LOW severity.
|
|
33147
|
+
7. Validate task format compliance:
|
|
33148
|
+
- Tasks missing FILE, TASK, CONSTRAINT, or ACCEPTANCE fields: LOW severity.
|
|
33149
|
+
- Tasks with compound verbs: LOW severity.
|
|
33150
|
+
|
|
33151
|
+
OUTPUT FORMAT:
|
|
33152
|
+
VERDICT: CLEAN | GAPS FOUND | DRIFT DETECTED
|
|
33153
|
+
COVERAGE TABLE: [FR-### | Covering Tasks \u2014 list up to top 10; if more than 10 items, show "showing 10 of N" and note total count]
|
|
33154
|
+
GAPS: [top 10 gaps with severity \u2014 if more than 10 items, show "showing 10 of N"]
|
|
33155
|
+
GOLD-PLATING: [top 10 gold-plating findings \u2014 if more than 10 items, show "showing 10 of N"]
|
|
33156
|
+
TERMINOLOGY DRIFT: [top 10 inconsistencies \u2014 if more than 10 items, show "showing 10 of N"]
|
|
33157
|
+
SUMMARY: [1-2 sentence overall assessment]
|
|
33158
|
+
|
|
33159
|
+
ANALYZE RULES:
|
|
33160
|
+
- READ-ONLY: do not create, modify, or delete any file during analysis.
|
|
33161
|
+
- Report only \u2014 no plan edits, no spec edits.
|
|
33162
|
+
- Partial coverage counts as coverage (do not penalize partially addressed requirements).
|
|
33163
|
+
- Report the highest-severity findings first within each section.
|
|
33164
|
+
- If both spec.md and plan.md are present but empty, report CLEAN with a note that both files are empty.
|
|
33165
|
+
|
|
33166
|
+
---
|
|
33167
|
+
|
|
33168
|
+
### MODE: DRIFT-CHECK
|
|
33169
|
+
Activates when: Architect delegates critic with DRIFT-CHECK context after completing a phase.
|
|
33170
|
+
|
|
33171
|
+
Note: ANALYZE detects spec-execution divergence after implementation \u2014 distinct from plan-review (APPROVED/NEEDS_REVISION/REJECTED) and ANALYZE (CLEAN/GAPS FOUND/DRIFT DETECTED).
|
|
33172
|
+
DRIFT-CHECK uses CRITICAL/HIGH/MEDIUM/LOW severity (not CRITICAL/MAJOR/MINOR used by plan review).
|
|
33173
|
+
|
|
33174
|
+
SIGNIFICANT DRIFT verdict = at least one CRITICAL or HIGH finding.
|
|
33175
|
+
MINOR DRIFT verdict = only MEDIUM or LOW findings.
|
|
33176
|
+
CLEAN verdict = no findings.
|
|
33177
|
+
|
|
33178
|
+
INPUT: Phase number (provided in TASK description as "DRIFT-CHECK phase N"). If not provided, ask the user for the phase number before proceeding.
|
|
33179
|
+
|
|
33180
|
+
EDGE CASES:
|
|
33181
|
+
- spec.md is missing: report "spec.md is missing \u2014 DRIFT-CHECK requires a spec to compare against" and stop.
|
|
33182
|
+
- plan.md is missing: report "plan.md is missing \u2014 cannot identify completed tasks for this phase" and stop.
|
|
33183
|
+
- Evidence files are missing: note the absence in the report but proceed with available data.
|
|
33184
|
+
- Invalid phase number (no tasks found for that phase): report "no tasks found for phase N" and stop.
|
|
33185
|
+
|
|
33186
|
+
STEPS:
|
|
33187
|
+
1. Read \`.swarm/spec.md\`. Extract all FR-### requirements relevant to the phase being checked.
|
|
33188
|
+
2. Read \`.swarm/plan.md\`. Extract all tasks marked complete ([x]) for the specified phase.
|
|
33189
|
+
3. Read evidence files in \`.swarm/evidence/\` for the phase (retrospective, review outputs, test outputs).
|
|
33190
|
+
4. For each completed task: compare what was implemented (from evidence) against the FR-### requirements it was supposed to address. Look for:
|
|
33191
|
+
- Scope additions: task implemented more than the FR-### required.
|
|
33192
|
+
- Scope omissions: task implemented less than the FR-### required.
|
|
33193
|
+
- Assumption changes: task used a different approach that may affect other requirements.
|
|
33194
|
+
5. Classify each finding by severity:
|
|
33195
|
+
- CRITICAL: core requirement not implemented, or implementation contradicts requirement.
|
|
33196
|
+
- HIGH: significant scope addition or omission that affects other requirements.
|
|
33197
|
+
- MEDIUM: minor scope difference unlikely to affect other requirements.
|
|
33198
|
+
- LOW: stylistic or naming inconsistency between spec and implementation.
|
|
33199
|
+
6. Produce the full drift report in your response. The Architect will save it to \`.swarm/evidence/phase-{N}-drift.md\`.
|
|
33200
|
+
|
|
33201
|
+
OUTPUT FORMAT:
|
|
33202
|
+
VERDICT: CLEAN | MINOR DRIFT | SIGNIFICANT DRIFT
|
|
33203
|
+
FINDINGS: [list findings with severity, task ID, FR-### reference, description]
|
|
33204
|
+
SUMMARY: [1-2 sentence assessment]
|
|
33205
|
+
|
|
33206
|
+
DRIFT-CHECK RULES:
|
|
33207
|
+
- Advisory: DRIFT-CHECK does NOT block phase transitions. It surfaces information for the Architect and user.
|
|
33208
|
+
- READ-ONLY: do not create, modify, or delete any file.
|
|
33209
|
+
- Output the full report in your response \u2014 do not attempt to write files directly.
|
|
33210
|
+
- If no spec.md exists, stop immediately and report the missing file.
|
|
33211
|
+
- Do not modify the spec.md or plan.md based on findings.`;
|
|
32988
33212
|
function createCriticAgent(model, customPrompt, customAppendPrompt) {
|
|
32989
33213
|
let prompt = CRITIC_PROMPT;
|
|
32990
33214
|
if (customPrompt) {
|
|
@@ -33383,7 +33607,17 @@ RULES:
|
|
|
33383
33607
|
- Be specific: exact names, paths, parameters, versions
|
|
33384
33608
|
- Be concise: under 1500 characters
|
|
33385
33609
|
- Be actionable: info Coder can use directly
|
|
33386
|
-
- No code writing
|
|
33610
|
+
- No code writing
|
|
33611
|
+
|
|
33612
|
+
RESEARCH CACHING:
|
|
33613
|
+
Before fetching any URL or performing external research, check \`.swarm/context.md\` for a \`## Research Sources\` section.
|
|
33614
|
+
- If \`.swarm/context.md\` does not exist or the \`## Research Sources\` section is absent: proceed with fresh research.
|
|
33615
|
+
- If the URL or topic is listed there: reuse the cached summary \u2014 do not fetch the URL again.
|
|
33616
|
+
- If not listed (cache miss): fetch the URL, produce your normal response, then append this line at the end of your response:
|
|
33617
|
+
CACHE-UPDATE: \`[YYYY-MM-DD] [URL or topic]: [1-2 sentence summary]\`
|
|
33618
|
+
The Architect will save this line to \`.swarm/context.md\` under \`## Research Sources\`.
|
|
33619
|
+
- Cache bypass: if the user explicitly requests fresh research ("re-fetch", "ignore cache", "latest"): skip the cache check and fetch directly; still include the CACHE-UPDATE line.
|
|
33620
|
+
- Do NOT write to any file \u2014 SME is read-only. Cache persistence is the Architect's responsibility.`;
|
|
33387
33621
|
function createSMEAgent(model, customPrompt, customAppendPrompt) {
|
|
33388
33622
|
let prompt = SME_PROMPT;
|
|
33389
33623
|
if (customPrompt) {
|
|
@@ -34541,6 +34775,15 @@ function handleAgentsCommand(agents, guardrails) {
|
|
|
34541
34775
|
`);
|
|
34542
34776
|
}
|
|
34543
34777
|
|
|
34778
|
+
// src/commands/analyze.ts
|
|
34779
|
+
async function handleAnalyzeCommand(_directory, args2) {
|
|
34780
|
+
const description = args2.join(" ").trim();
|
|
34781
|
+
if (description) {
|
|
34782
|
+
return `[MODE: ANALYZE] ${description}`;
|
|
34783
|
+
}
|
|
34784
|
+
return "[MODE: ANALYZE] Please analyze the spec against the plan using MODE: ANALYZE.";
|
|
34785
|
+
}
|
|
34786
|
+
|
|
34544
34787
|
// src/commands/archive.ts
|
|
34545
34788
|
init_manager();
|
|
34546
34789
|
async function handleArchiveCommand(directory, args2) {
|
|
@@ -35110,6 +35353,15 @@ async function handleBenchmarkCommand(directory, args2) {
|
|
|
35110
35353
|
`);
|
|
35111
35354
|
}
|
|
35112
35355
|
|
|
35356
|
+
// src/commands/clarify.ts
|
|
35357
|
+
async function handleClarifyCommand(_directory, args2) {
|
|
35358
|
+
const description = args2.join(" ").trim();
|
|
35359
|
+
if (description) {
|
|
35360
|
+
return `[MODE: CLARIFY-SPEC] ${description}`;
|
|
35361
|
+
}
|
|
35362
|
+
return "[MODE: CLARIFY-SPEC] Please enter MODE: CLARIFY-SPEC and clarify the existing spec.";
|
|
35363
|
+
}
|
|
35364
|
+
|
|
35113
35365
|
// src/commands/config.ts
|
|
35114
35366
|
import * as os2 from "os";
|
|
35115
35367
|
import * as path8 from "path";
|
|
@@ -36007,6 +36259,15 @@ ${error93 instanceof Error ? error93.message : String(error93)}`;
|
|
|
36007
36259
|
}
|
|
36008
36260
|
}
|
|
36009
36261
|
|
|
36262
|
+
// src/commands/specify.ts
|
|
36263
|
+
async function handleSpecifyCommand(_directory, args2) {
|
|
36264
|
+
const description = args2.join(" ").trim();
|
|
36265
|
+
if (description) {
|
|
36266
|
+
return `[MODE: SPECIFY] ${description}`;
|
|
36267
|
+
}
|
|
36268
|
+
return "[MODE: SPECIFY] Please enter MODE: SPECIFY and generate a spec for this project.";
|
|
36269
|
+
}
|
|
36270
|
+
|
|
36010
36271
|
// src/hooks/extractors.ts
|
|
36011
36272
|
function extractCurrentPhase(planContent) {
|
|
36012
36273
|
if (!planContent) {
|
|
@@ -36459,7 +36720,10 @@ var HELP_TEXT = [
|
|
|
36459
36720
|
"- `/swarm benchmark [--cumulative] [--ci-gate]` \u2014 Show performance metrics",
|
|
36460
36721
|
"- `/swarm export` \u2014 Export plan and context as JSON",
|
|
36461
36722
|
"- `/swarm reset --confirm` \u2014 Clear swarm state files",
|
|
36462
|
-
"- `/swarm retrieve <id>` \u2014 Retrieve full output from a summary"
|
|
36723
|
+
"- `/swarm retrieve <id>` \u2014 Retrieve full output from a summary",
|
|
36724
|
+
"- `/swarm clarify [topic]` \u2014 Clarify and refine an existing feature specification",
|
|
36725
|
+
"- `/swarm analyze` \u2014 Analyze spec.md vs plan.md for requirement coverage gaps",
|
|
36726
|
+
"- `/swarm specify [description]` \u2014 Generate or import a feature specification"
|
|
36463
36727
|
].join(`
|
|
36464
36728
|
`);
|
|
36465
36729
|
function createSwarmCommandHandler(directory, agents) {
|
|
@@ -36527,6 +36791,15 @@ function createSwarmCommandHandler(directory, agents) {
|
|
|
36527
36791
|
case "retrieve":
|
|
36528
36792
|
text = await handleRetrieveCommand(directory, args2);
|
|
36529
36793
|
break;
|
|
36794
|
+
case "clarify":
|
|
36795
|
+
text = await handleClarifyCommand(directory, args2);
|
|
36796
|
+
break;
|
|
36797
|
+
case "analyze":
|
|
36798
|
+
text = await handleAnalyzeCommand(directory, args2);
|
|
36799
|
+
break;
|
|
36800
|
+
case "specify":
|
|
36801
|
+
text = await handleSpecifyCommand(directory, args2);
|
|
36802
|
+
break;
|
|
36530
36803
|
default:
|
|
36531
36804
|
text = HELP_TEXT;
|
|
36532
36805
|
break;
|
|
@@ -36698,8 +36971,232 @@ function createCompactionCustomizerHook(config3, directory) {
|
|
|
36698
36971
|
})
|
|
36699
36972
|
};
|
|
36700
36973
|
}
|
|
36974
|
+
// src/hooks/context-budget.ts
|
|
36975
|
+
init_utils();
|
|
36976
|
+
|
|
36977
|
+
// src/hooks/message-priority.ts
|
|
36978
|
+
var MessagePriority = {
|
|
36979
|
+
CRITICAL: 0,
|
|
36980
|
+
HIGH: 1,
|
|
36981
|
+
MEDIUM: 2,
|
|
36982
|
+
LOW: 3,
|
|
36983
|
+
DISPOSABLE: 4
|
|
36984
|
+
};
|
|
36985
|
+
function containsPlanContent(text) {
|
|
36986
|
+
if (!text)
|
|
36987
|
+
return false;
|
|
36988
|
+
const lowerText = text.toLowerCase();
|
|
36989
|
+
return lowerText.includes(".swarm/plan") || lowerText.includes(".swarm/context") || lowerText.includes("swarm/plan.md") || lowerText.includes("swarm/context.md");
|
|
36990
|
+
}
|
|
36991
|
+
function isToolResult(message) {
|
|
36992
|
+
if (!message?.info)
|
|
36993
|
+
return false;
|
|
36994
|
+
const role = message.info.role;
|
|
36995
|
+
const toolName = message.info.toolName;
|
|
36996
|
+
return role === "assistant" && !!toolName;
|
|
36997
|
+
}
|
|
36998
|
+
function isDuplicateToolRead(current, previous) {
|
|
36999
|
+
if (!current?.info || !previous?.info)
|
|
37000
|
+
return false;
|
|
37001
|
+
const currentTool = current.info.toolName;
|
|
37002
|
+
const previousTool = previous.info.toolName;
|
|
37003
|
+
if (currentTool !== previousTool)
|
|
37004
|
+
return false;
|
|
37005
|
+
const isReadTool = currentTool?.toLowerCase().includes("read") && previousTool?.toLowerCase().includes("read");
|
|
37006
|
+
if (!isReadTool)
|
|
37007
|
+
return false;
|
|
37008
|
+
const currentArgs = current.info.toolArgs;
|
|
37009
|
+
const previousArgs = previous.info.toolArgs;
|
|
37010
|
+
if (!currentArgs || !previousArgs)
|
|
37011
|
+
return false;
|
|
37012
|
+
const currentKeys = Object.keys(currentArgs);
|
|
37013
|
+
const previousKeys = Object.keys(previousArgs);
|
|
37014
|
+
if (currentKeys.length === 0 || previousKeys.length === 0)
|
|
37015
|
+
return false;
|
|
37016
|
+
const firstKey = currentKeys[0];
|
|
37017
|
+
return currentArgs[firstKey] === previousArgs[firstKey];
|
|
37018
|
+
}
|
|
37019
|
+
function isStaleError(text, turnsAgo) {
|
|
37020
|
+
if (!text)
|
|
37021
|
+
return false;
|
|
37022
|
+
if (turnsAgo <= 6)
|
|
37023
|
+
return false;
|
|
37024
|
+
const lowerText = text.toLowerCase();
|
|
37025
|
+
const errorPatterns = [
|
|
37026
|
+
"error:",
|
|
37027
|
+
"failed to",
|
|
37028
|
+
"could not",
|
|
37029
|
+
"unable to",
|
|
37030
|
+
"exception",
|
|
37031
|
+
"errno",
|
|
37032
|
+
"cannot read",
|
|
37033
|
+
"not found",
|
|
37034
|
+
"access denied",
|
|
37035
|
+
"timeout"
|
|
37036
|
+
];
|
|
37037
|
+
return errorPatterns.some((pattern) => lowerText.includes(pattern));
|
|
37038
|
+
}
|
|
37039
|
+
function extractMessageText(message) {
|
|
37040
|
+
if (!message?.parts || message.parts.length === 0)
|
|
37041
|
+
return "";
|
|
37042
|
+
return message.parts.map((part) => part?.text || "").join("");
|
|
37043
|
+
}
|
|
37044
|
+
function classifyMessage(message, index, totalMessages, recentWindowSize = 10) {
|
|
37045
|
+
const role = message?.info?.role;
|
|
37046
|
+
const text = extractMessageText(message);
|
|
37047
|
+
if (containsPlanContent(text)) {
|
|
37048
|
+
return MessagePriority.CRITICAL;
|
|
37049
|
+
}
|
|
37050
|
+
if (role === "system") {
|
|
37051
|
+
return MessagePriority.CRITICAL;
|
|
37052
|
+
}
|
|
37053
|
+
if (role === "user") {
|
|
37054
|
+
return MessagePriority.HIGH;
|
|
37055
|
+
}
|
|
37056
|
+
if (isToolResult(message)) {
|
|
37057
|
+
const positionFromEnd = totalMessages - 1 - index;
|
|
37058
|
+
if (positionFromEnd < recentWindowSize) {
|
|
37059
|
+
return MessagePriority.MEDIUM;
|
|
37060
|
+
}
|
|
37061
|
+
if (isStaleError(text, positionFromEnd)) {
|
|
37062
|
+
return MessagePriority.DISPOSABLE;
|
|
37063
|
+
}
|
|
37064
|
+
return MessagePriority.LOW;
|
|
37065
|
+
}
|
|
37066
|
+
if (role === "assistant") {
|
|
37067
|
+
const positionFromEnd = totalMessages - 1 - index;
|
|
37068
|
+
if (positionFromEnd < recentWindowSize) {
|
|
37069
|
+
return MessagePriority.MEDIUM;
|
|
37070
|
+
}
|
|
37071
|
+
if (isStaleError(text, positionFromEnd)) {
|
|
37072
|
+
return MessagePriority.DISPOSABLE;
|
|
37073
|
+
}
|
|
37074
|
+
return MessagePriority.LOW;
|
|
37075
|
+
}
|
|
37076
|
+
return MessagePriority.LOW;
|
|
37077
|
+
}
|
|
37078
|
+
function classifyMessages(messages, recentWindowSize = 10) {
|
|
37079
|
+
const results = [];
|
|
37080
|
+
const totalMessages = messages.length;
|
|
37081
|
+
for (let i2 = 0;i2 < messages.length; i2++) {
|
|
37082
|
+
const message = messages[i2];
|
|
37083
|
+
const priority = classifyMessage(message, i2, totalMessages, recentWindowSize);
|
|
37084
|
+
if (i2 > 0) {
|
|
37085
|
+
const current = messages[i2];
|
|
37086
|
+
const previous = messages[i2 - 1];
|
|
37087
|
+
if (isDuplicateToolRead(current, previous)) {
|
|
37088
|
+
if (results[i2 - 1] >= MessagePriority.MEDIUM) {
|
|
37089
|
+
results[i2 - 1] = MessagePriority.DISPOSABLE;
|
|
37090
|
+
}
|
|
37091
|
+
}
|
|
37092
|
+
}
|
|
37093
|
+
results.push(priority);
|
|
37094
|
+
}
|
|
37095
|
+
return results;
|
|
37096
|
+
}
|
|
37097
|
+
|
|
37098
|
+
// src/hooks/model-limits.ts
|
|
37099
|
+
init_utils();
|
|
37100
|
+
var NATIVE_MODEL_LIMITS = {
|
|
37101
|
+
"claude-sonnet-4": 200000,
|
|
37102
|
+
"claude-opus-4": 200000,
|
|
37103
|
+
"claude-haiku-4": 200000,
|
|
37104
|
+
"gpt-5": 400000,
|
|
37105
|
+
"gpt-5.1-codex": 400000,
|
|
37106
|
+
"gpt-5.1": 264000,
|
|
37107
|
+
"gpt-4.1": 1047576,
|
|
37108
|
+
"gemini-2.5-pro": 1048576,
|
|
37109
|
+
"gemini-2.5-flash": 1048576,
|
|
37110
|
+
o3: 200000,
|
|
37111
|
+
"o4-mini": 200000,
|
|
37112
|
+
"deepseek-r1": 163840,
|
|
37113
|
+
"deepseek-chat": 163840,
|
|
37114
|
+
"qwen3.5": 131072
|
|
37115
|
+
};
|
|
37116
|
+
var PROVIDER_CAPS = {
|
|
37117
|
+
copilot: 128000,
|
|
37118
|
+
"github-copilot": 128000
|
|
37119
|
+
};
|
|
37120
|
+
function extractModelInfo(messages) {
|
|
37121
|
+
if (!messages || messages.length === 0) {
|
|
37122
|
+
return {};
|
|
37123
|
+
}
|
|
37124
|
+
for (let i2 = messages.length - 1;i2 >= 0; i2--) {
|
|
37125
|
+
const message = messages[i2];
|
|
37126
|
+
if (!message?.info)
|
|
37127
|
+
continue;
|
|
37128
|
+
if (message.info.role === "assistant") {
|
|
37129
|
+
const modelID = message.info.modelID;
|
|
37130
|
+
const providerID = message.info.providerID;
|
|
37131
|
+
if (modelID || providerID) {
|
|
37132
|
+
return {
|
|
37133
|
+
...modelID ? { modelID } : {},
|
|
37134
|
+
...providerID ? { providerID } : {}
|
|
37135
|
+
};
|
|
37136
|
+
}
|
|
37137
|
+
}
|
|
37138
|
+
}
|
|
37139
|
+
return {};
|
|
37140
|
+
}
|
|
37141
|
+
var loggedFirstCalls = new Set;
|
|
37142
|
+
function resolveModelLimit(modelID, providerID, configOverrides = {}) {
|
|
37143
|
+
const normalizedModelID = modelID ?? "";
|
|
37144
|
+
const normalizedProviderID = providerID ?? "";
|
|
37145
|
+
if (normalizedProviderID && normalizedModelID) {
|
|
37146
|
+
const providerModelKey = `${normalizedProviderID}/${normalizedModelID}`;
|
|
37147
|
+
if (configOverrides[providerModelKey] !== undefined) {
|
|
37148
|
+
logFirstCall(normalizedModelID, normalizedProviderID, "override(provider/model)", configOverrides[providerModelKey]);
|
|
37149
|
+
return configOverrides[providerModelKey];
|
|
37150
|
+
}
|
|
37151
|
+
}
|
|
37152
|
+
if (normalizedModelID && configOverrides[normalizedModelID] !== undefined) {
|
|
37153
|
+
logFirstCall(normalizedModelID, normalizedProviderID, "override(model)", configOverrides[normalizedModelID]);
|
|
37154
|
+
return configOverrides[normalizedModelID];
|
|
37155
|
+
}
|
|
37156
|
+
if (normalizedProviderID && PROVIDER_CAPS[normalizedProviderID] !== undefined) {
|
|
37157
|
+
const cap = PROVIDER_CAPS[normalizedProviderID];
|
|
37158
|
+
logFirstCall(normalizedModelID, normalizedProviderID, "provider_cap", cap);
|
|
37159
|
+
return cap;
|
|
37160
|
+
}
|
|
37161
|
+
if (normalizedModelID) {
|
|
37162
|
+
const matchedLimit = findNativeLimit(normalizedModelID);
|
|
37163
|
+
if (matchedLimit !== undefined) {
|
|
37164
|
+
logFirstCall(normalizedModelID, normalizedProviderID, "native", matchedLimit);
|
|
37165
|
+
return matchedLimit;
|
|
37166
|
+
}
|
|
37167
|
+
}
|
|
37168
|
+
if (configOverrides.default !== undefined) {
|
|
37169
|
+
logFirstCall(normalizedModelID, normalizedProviderID, "default_override", configOverrides.default);
|
|
37170
|
+
return configOverrides.default;
|
|
37171
|
+
}
|
|
37172
|
+
logFirstCall(normalizedModelID, normalizedProviderID, "fallback", 128000);
|
|
37173
|
+
return 128000;
|
|
37174
|
+
}
|
|
37175
|
+
function findNativeLimit(modelID) {
|
|
37176
|
+
if (NATIVE_MODEL_LIMITS[modelID] !== undefined) {
|
|
37177
|
+
return NATIVE_MODEL_LIMITS[modelID];
|
|
37178
|
+
}
|
|
37179
|
+
let bestMatch;
|
|
37180
|
+
for (const key of Object.keys(NATIVE_MODEL_LIMITS)) {
|
|
37181
|
+
if (modelID.startsWith(key)) {
|
|
37182
|
+
if (!bestMatch || key.length > bestMatch.length) {
|
|
37183
|
+
bestMatch = key;
|
|
37184
|
+
}
|
|
37185
|
+
}
|
|
37186
|
+
}
|
|
37187
|
+
return bestMatch ? NATIVE_MODEL_LIMITS[bestMatch] : undefined;
|
|
37188
|
+
}
|
|
37189
|
+
function logFirstCall(modelID, providerID, source, limit) {
|
|
37190
|
+
const key = `${modelID || "unknown"}::${providerID || "unknown"}`;
|
|
37191
|
+
if (!loggedFirstCalls.has(key)) {
|
|
37192
|
+
loggedFirstCalls.add(key);
|
|
37193
|
+
warn(`[model-limits] Resolved limit for ${modelID || "(no model)"}@${providerID || "(no provider)"}: ${limit} (source: ${source})`);
|
|
37194
|
+
}
|
|
37195
|
+
}
|
|
37196
|
+
|
|
36701
37197
|
// src/hooks/context-budget.ts
|
|
36702
37198
|
init_utils2();
|
|
37199
|
+
var lastSeenAgent;
|
|
36703
37200
|
function createContextBudgetHandler(config3) {
|
|
36704
37201
|
const enabled = config3.context_budget?.enabled !== false;
|
|
36705
37202
|
if (!enabled) {
|
|
@@ -36707,14 +37204,19 @@ function createContextBudgetHandler(config3) {
|
|
|
36707
37204
|
}
|
|
36708
37205
|
const warnThreshold = config3.context_budget?.warn_threshold ?? 0.7;
|
|
36709
37206
|
const criticalThreshold = config3.context_budget?.critical_threshold ?? 0.9;
|
|
36710
|
-
const
|
|
36711
|
-
|
|
36712
|
-
|
|
36713
|
-
const modelLimit = modelLimits.default ?? 128000;
|
|
36714
|
-
return async (_input, output) => {
|
|
37207
|
+
const modelLimitsConfig = config3.context_budget?.model_limits ?? {};
|
|
37208
|
+
const loggedLimits = new Set;
|
|
37209
|
+
const handler = async (_input, output) => {
|
|
36715
37210
|
const messages = output?.messages;
|
|
36716
37211
|
if (!messages || messages.length === 0)
|
|
36717
37212
|
return;
|
|
37213
|
+
const { modelID, providerID } = extractModelInfo(messages);
|
|
37214
|
+
const modelLimit = resolveModelLimit(modelID, providerID, modelLimitsConfig);
|
|
37215
|
+
const cacheKey = `${modelID || "unknown"}::${providerID || "unknown"}`;
|
|
37216
|
+
if (!loggedLimits.has(cacheKey)) {
|
|
37217
|
+
loggedLimits.add(cacheKey);
|
|
37218
|
+
warn(`[swarm] Context budget: model=${modelID || "unknown"} provider=${providerID || "unknown"} limit=${modelLimit}`);
|
|
37219
|
+
}
|
|
36718
37220
|
let totalTokens = 0;
|
|
36719
37221
|
for (const message of messages) {
|
|
36720
37222
|
if (!message?.parts)
|
|
@@ -36726,6 +37228,79 @@ function createContextBudgetHandler(config3) {
|
|
|
36726
37228
|
}
|
|
36727
37229
|
}
|
|
36728
37230
|
const usagePercent = totalTokens / modelLimit;
|
|
37231
|
+
let baseAgent;
|
|
37232
|
+
for (let i2 = messages.length - 1;i2 >= 0; i2--) {
|
|
37233
|
+
const msg = messages[i2];
|
|
37234
|
+
if (msg?.info?.role === "user" && msg?.info?.agent) {
|
|
37235
|
+
baseAgent = stripKnownSwarmPrefix(msg.info.agent);
|
|
37236
|
+
break;
|
|
37237
|
+
}
|
|
37238
|
+
}
|
|
37239
|
+
let ratio = usagePercent;
|
|
37240
|
+
if (lastSeenAgent !== undefined && baseAgent !== undefined && baseAgent !== lastSeenAgent) {
|
|
37241
|
+
const enforceOnSwitch = config3.context_budget?.enforce_on_agent_switch ?? true;
|
|
37242
|
+
if (enforceOnSwitch && usagePercent > (config3.context_budget?.warn_threshold ?? 0.7)) {
|
|
37243
|
+
warn(`[swarm] Agent switch detected: ${lastSeenAgent} \u2192 ${baseAgent}, enforcing context budget`, {
|
|
37244
|
+
from: lastSeenAgent,
|
|
37245
|
+
to: baseAgent
|
|
37246
|
+
});
|
|
37247
|
+
ratio = 1;
|
|
37248
|
+
}
|
|
37249
|
+
}
|
|
37250
|
+
lastSeenAgent = baseAgent;
|
|
37251
|
+
if (ratio >= criticalThreshold) {
|
|
37252
|
+
const enforce = config3.context_budget?.enforce ?? true;
|
|
37253
|
+
if (enforce) {
|
|
37254
|
+
const targetTokens = modelLimit * (config3.context_budget?.prune_target ?? 0.7);
|
|
37255
|
+
const recentWindow = config3.context_budget?.recent_window ?? 10;
|
|
37256
|
+
const priorities = classifyMessages(output.messages || [], recentWindow);
|
|
37257
|
+
const toolMaskThreshold = config3.context_budget?.tool_output_mask_threshold ?? 2000;
|
|
37258
|
+
let toolMaskFreedTokens = 0;
|
|
37259
|
+
const maskedIndices = new Set;
|
|
37260
|
+
for (let i2 = 0;i2 < (output.messages || []).length; i2++) {
|
|
37261
|
+
const msg = (output.messages || [])[i2];
|
|
37262
|
+
if (shouldMaskToolOutput(msg, i2, (output.messages || []).length, recentWindow, toolMaskThreshold)) {
|
|
37263
|
+
toolMaskFreedTokens += maskToolOutput(msg, toolMaskThreshold);
|
|
37264
|
+
maskedIndices.add(i2);
|
|
37265
|
+
}
|
|
37266
|
+
}
|
|
37267
|
+
if (toolMaskFreedTokens > 0) {
|
|
37268
|
+
totalTokens -= toolMaskFreedTokens;
|
|
37269
|
+
warn(`[swarm] Tool output masking: masked ${maskedIndices.size} tool results, freed ~${toolMaskFreedTokens} tokens`, {
|
|
37270
|
+
maskedCount: maskedIndices.size,
|
|
37271
|
+
freedTokens: toolMaskFreedTokens
|
|
37272
|
+
});
|
|
37273
|
+
}
|
|
37274
|
+
const preserveLastNTurns = config3.context_budget?.preserve_last_n_turns ?? 4;
|
|
37275
|
+
const removableMessages = identifyRemovableMessages(output.messages || [], priorities, preserveLastNTurns);
|
|
37276
|
+
let freedTokens = 0;
|
|
37277
|
+
const toRemove = new Set;
|
|
37278
|
+
for (const idx of removableMessages) {
|
|
37279
|
+
if (totalTokens - freedTokens <= targetTokens)
|
|
37280
|
+
break;
|
|
37281
|
+
toRemove.add(idx);
|
|
37282
|
+
freedTokens += estimateTokens(extractMessageText2(output.messages[idx]));
|
|
37283
|
+
}
|
|
37284
|
+
const beforeTokens = totalTokens;
|
|
37285
|
+
if (toRemove.size > 0) {
|
|
37286
|
+
const actualFreedTokens = applyObservationMasking(output.messages || [], toRemove);
|
|
37287
|
+
totalTokens -= actualFreedTokens;
|
|
37288
|
+
warn(`[swarm] Context enforcement: pruned ${toRemove.size} messages, freed ${actualFreedTokens} tokens (${beforeTokens}\u2192${totalTokens} of ${modelLimit})`, {
|
|
37289
|
+
pruned: toRemove.size,
|
|
37290
|
+
freedTokens: actualFreedTokens,
|
|
37291
|
+
before: beforeTokens,
|
|
37292
|
+
after: totalTokens,
|
|
37293
|
+
limit: modelLimit
|
|
37294
|
+
});
|
|
37295
|
+
} else if (removableMessages.length === 0 && totalTokens > targetTokens) {
|
|
37296
|
+
warn(`[swarm] Context enforcement: no removable messages found but still ${totalTokens} tokens (target: ${targetTokens})`, {
|
|
37297
|
+
currentTokens: totalTokens,
|
|
37298
|
+
targetTokens,
|
|
37299
|
+
limit: modelLimit
|
|
37300
|
+
});
|
|
37301
|
+
}
|
|
37302
|
+
}
|
|
37303
|
+
}
|
|
36729
37304
|
let lastUserMessageIndex = -1;
|
|
36730
37305
|
for (let i2 = messages.length - 1;i2 >= 0; i2--) {
|
|
36731
37306
|
if (messages[i2]?.info?.role === "user") {
|
|
@@ -36738,8 +37313,10 @@ function createContextBudgetHandler(config3) {
|
|
|
36738
37313
|
const lastUserMessage = messages[lastUserMessageIndex];
|
|
36739
37314
|
if (!lastUserMessage?.parts)
|
|
36740
37315
|
return;
|
|
36741
|
-
const
|
|
36742
|
-
|
|
37316
|
+
const trackedAgents = config3.context_budget?.tracked_agents ?? [
|
|
37317
|
+
"architect"
|
|
37318
|
+
];
|
|
37319
|
+
if (baseAgent && !trackedAgents.includes(baseAgent))
|
|
36743
37320
|
return;
|
|
36744
37321
|
const textPartIndex = lastUserMessage.parts.findIndex((p) => p?.type === "text" && p.text !== undefined);
|
|
36745
37322
|
if (textPartIndex === -1)
|
|
@@ -36760,6 +37337,110 @@ function createContextBudgetHandler(config3) {
|
|
|
36760
37337
|
lastUserMessage.parts[textPartIndex].text = `${warningText}${originalText}`;
|
|
36761
37338
|
}
|
|
36762
37339
|
};
|
|
37340
|
+
return handler;
|
|
37341
|
+
}
|
|
37342
|
+
function identifyRemovableMessages(messages, priorities, preserveLastNTurns) {
|
|
37343
|
+
let turnCount = 0;
|
|
37344
|
+
const protectedIndices = new Set;
|
|
37345
|
+
for (let i2 = messages.length - 1;i2 >= 0 && turnCount < preserveLastNTurns * 2; i2--) {
|
|
37346
|
+
const role = messages[i2]?.info?.role;
|
|
37347
|
+
if (role === "user" || role === "assistant") {
|
|
37348
|
+
protectedIndices.add(i2);
|
|
37349
|
+
if (role === "user")
|
|
37350
|
+
turnCount++;
|
|
37351
|
+
}
|
|
37352
|
+
}
|
|
37353
|
+
let lastUserIdx = -1;
|
|
37354
|
+
let lastAssistantIdx = -1;
|
|
37355
|
+
for (let i2 = messages.length - 1;i2 >= 0; i2--) {
|
|
37356
|
+
const role = messages[i2]?.info?.role;
|
|
37357
|
+
if (role === "user" && lastUserIdx === -1) {
|
|
37358
|
+
lastUserIdx = i2;
|
|
37359
|
+
}
|
|
37360
|
+
if (role === "assistant" && lastAssistantIdx === -1) {
|
|
37361
|
+
lastAssistantIdx = i2;
|
|
37362
|
+
}
|
|
37363
|
+
if (lastUserIdx !== -1 && lastAssistantIdx !== -1)
|
|
37364
|
+
break;
|
|
37365
|
+
}
|
|
37366
|
+
if (lastUserIdx !== -1)
|
|
37367
|
+
protectedIndices.add(lastUserIdx);
|
|
37368
|
+
if (lastAssistantIdx !== -1)
|
|
37369
|
+
protectedIndices.add(lastAssistantIdx);
|
|
37370
|
+
const HIGH = MessagePriority.HIGH;
|
|
37371
|
+
const MEDIUM = MessagePriority.MEDIUM;
|
|
37372
|
+
const LOW = MessagePriority.LOW;
|
|
37373
|
+
const DISPOSABLE = MessagePriority.DISPOSABLE;
|
|
37374
|
+
const byPriority = [[], [], [], [], []];
|
|
37375
|
+
for (let i2 = 0;i2 < priorities.length; i2++) {
|
|
37376
|
+
const priority = priorities[i2];
|
|
37377
|
+
if (!protectedIndices.has(i2) && priority > HIGH) {
|
|
37378
|
+
byPriority[priority].push(i2);
|
|
37379
|
+
}
|
|
37380
|
+
}
|
|
37381
|
+
return [...byPriority[DISPOSABLE], ...byPriority[LOW], ...byPriority[MEDIUM]];
|
|
37382
|
+
}
|
|
37383
|
+
function applyObservationMasking(messages, toRemove) {
|
|
37384
|
+
let actualFreedTokens = 0;
|
|
37385
|
+
for (const idx of toRemove) {
|
|
37386
|
+
const msg = messages[idx];
|
|
37387
|
+
if (msg?.parts) {
|
|
37388
|
+
for (const part of msg.parts) {
|
|
37389
|
+
if (part.type === "text" && part.text) {
|
|
37390
|
+
const originalTokens = estimateTokens(part.text);
|
|
37391
|
+
const placeholder = `[Context pruned \u2014 message from turn ${idx}, ~${originalTokens} tokens freed. Use retrieve_summary if needed.]`;
|
|
37392
|
+
const maskedTokens = estimateTokens(placeholder);
|
|
37393
|
+
part.text = placeholder;
|
|
37394
|
+
actualFreedTokens += originalTokens - maskedTokens;
|
|
37395
|
+
}
|
|
37396
|
+
}
|
|
37397
|
+
}
|
|
37398
|
+
}
|
|
37399
|
+
return actualFreedTokens;
|
|
37400
|
+
}
|
|
37401
|
+
function extractMessageText2(msg) {
|
|
37402
|
+
if (!msg?.parts)
|
|
37403
|
+
return "";
|
|
37404
|
+
return msg.parts.filter((p) => p.type === "text" && p.text).map((p) => p.text).join(`
|
|
37405
|
+
`);
|
|
37406
|
+
}
|
|
37407
|
+
function extractToolName(text) {
|
|
37408
|
+
const match = text.match(/^(read_file|write|edit|apply_patch|task|bun|npm|git|bash|glob|grep|mkdir|cp|mv|rm)\b/i);
|
|
37409
|
+
return match?.[1];
|
|
37410
|
+
}
|
|
37411
|
+
function shouldMaskToolOutput(msg, index, totalMessages, recentWindowSize, threshold) {
|
|
37412
|
+
if (!isToolResult(msg))
|
|
37413
|
+
return false;
|
|
37414
|
+
const text = extractMessageText2(msg);
|
|
37415
|
+
if (text.includes("[Tool output masked") || text.includes("[Context pruned")) {
|
|
37416
|
+
return false;
|
|
37417
|
+
}
|
|
37418
|
+
const toolName = extractToolName(text);
|
|
37419
|
+
if (toolName && ["retrieve_summary", "task"].includes(toolName.toLowerCase())) {
|
|
37420
|
+
return false;
|
|
37421
|
+
}
|
|
37422
|
+
const age = totalMessages - 1 - index;
|
|
37423
|
+
return age > recentWindowSize || text.length > threshold;
|
|
37424
|
+
}
|
|
37425
|
+
function maskToolOutput(msg, threshold) {
|
|
37426
|
+
if (!msg?.parts)
|
|
37427
|
+
return 0;
|
|
37428
|
+
let freedTokens = 0;
|
|
37429
|
+
for (const part of msg.parts) {
|
|
37430
|
+
if (part.type === "text" && part.text) {
|
|
37431
|
+
if (part.text.includes("[Tool output masked") || part.text.includes("[Context pruned")) {
|
|
37432
|
+
continue;
|
|
37433
|
+
}
|
|
37434
|
+
const originalTokens = estimateTokens(part.text);
|
|
37435
|
+
const toolName = extractToolName(part.text) || "unknown";
|
|
37436
|
+
const excerpt = part.text.substring(0, 200).replace(/\n/g, " ");
|
|
37437
|
+
const placeholder = `[Tool output masked \u2014 ${toolName} returned ~${originalTokens} tokens. First 200 chars: "${excerpt}..." Use retrieve_summary if needed.]`;
|
|
37438
|
+
const maskedTokens = estimateTokens(placeholder);
|
|
37439
|
+
part.text = placeholder;
|
|
37440
|
+
freedTokens += originalTokens - maskedTokens;
|
|
37441
|
+
}
|
|
37442
|
+
}
|
|
37443
|
+
return freedTokens;
|
|
36763
37444
|
}
|
|
36764
37445
|
// src/hooks/delegation-gate.ts
|
|
36765
37446
|
function extractTaskLine(text) {
|
|
@@ -36988,6 +37669,12 @@ function isSourceCodePath(filePath) {
|
|
|
36988
37669
|
];
|
|
36989
37670
|
return !nonSourcePatterns.some((pattern) => pattern.test(normalized));
|
|
36990
37671
|
}
|
|
37672
|
+
function hasTraversalSegments(filePath) {
|
|
37673
|
+
if (!filePath)
|
|
37674
|
+
return false;
|
|
37675
|
+
const normalized = filePath.replace(/\\/g, "/");
|
|
37676
|
+
return normalized.startsWith("..") || normalized.includes("/../") || normalized.endsWith("/..");
|
|
37677
|
+
}
|
|
36991
37678
|
function isGateTool(toolName) {
|
|
36992
37679
|
const normalized = toolName.replace(/^[^:]+[:.]/, "");
|
|
36993
37680
|
const gateTools = [
|
|
@@ -37030,10 +37717,43 @@ function createGuardrailsHooks(config3) {
|
|
|
37030
37717
|
const inputArgsByCallID = new Map;
|
|
37031
37718
|
return {
|
|
37032
37719
|
toolBefore: async (input, output) => {
|
|
37033
|
-
|
|
37720
|
+
const currentSession = swarmState.agentSessions.get(input.sessionID);
|
|
37721
|
+
if (currentSession?.delegationActive) {} else if (isArchitect(input.sessionID) && isWriteTool(input.tool)) {
|
|
37034
37722
|
const args2 = output.args;
|
|
37035
37723
|
const targetPath = args2?.filePath ?? args2?.path ?? args2?.file ?? args2?.target;
|
|
37036
|
-
if (
|
|
37724
|
+
if (!targetPath && (input.tool === "apply_patch" || input.tool === "patch")) {
|
|
37725
|
+
const patchText = args2?.input ?? args2?.patch ?? (Array.isArray(args2?.cmd) ? args2.cmd[1] : undefined);
|
|
37726
|
+
if (typeof patchText === "string") {
|
|
37727
|
+
const patchPathPattern = /\*\*\*\s+(?:Update|Add|Delete)\s+File:\s*(.+)/gi;
|
|
37728
|
+
const diffPathPattern = /\+\+\+\s+b\/(.+)/gm;
|
|
37729
|
+
const paths = new Set;
|
|
37730
|
+
let match;
|
|
37731
|
+
while ((match = patchPathPattern.exec(patchText)) !== null) {
|
|
37732
|
+
paths.add(match[1].trim());
|
|
37733
|
+
}
|
|
37734
|
+
while ((match = diffPathPattern.exec(patchText)) !== null) {
|
|
37735
|
+
const p = match[1].trim();
|
|
37736
|
+
if (p !== "/dev/null")
|
|
37737
|
+
paths.add(p);
|
|
37738
|
+
}
|
|
37739
|
+
for (const p of paths) {
|
|
37740
|
+
if (isOutsideSwarmDir(p) && (isSourceCodePath(p) || hasTraversalSegments(p))) {
|
|
37741
|
+
const session2 = swarmState.agentSessions.get(input.sessionID);
|
|
37742
|
+
if (session2) {
|
|
37743
|
+
session2.architectWriteCount++;
|
|
37744
|
+
warn("Architect direct code edit detected via apply_patch", {
|
|
37745
|
+
tool: input.tool,
|
|
37746
|
+
sessionID: input.sessionID,
|
|
37747
|
+
targetPath: p,
|
|
37748
|
+
writeCount: session2.architectWriteCount
|
|
37749
|
+
});
|
|
37750
|
+
}
|
|
37751
|
+
break;
|
|
37752
|
+
}
|
|
37753
|
+
}
|
|
37754
|
+
}
|
|
37755
|
+
}
|
|
37756
|
+
if (typeof targetPath === "string" && isOutsideSwarmDir(targetPath) && (isSourceCodePath(targetPath) || hasTraversalSegments(targetPath))) {
|
|
37037
37757
|
const session2 = swarmState.agentSessions.get(input.sessionID);
|
|
37038
37758
|
if (session2) {
|
|
37039
37759
|
session2.architectWriteCount++;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "opencode-swarm",
|
|
3
|
-
"version": "6.
|
|
3
|
+
"version": "6.15.0",
|
|
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",
|