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 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
 
@@ -0,0 +1,5 @@
1
+ /**
2
+ * Handle /swarm analyze command.
3
+ * Returns a prompt that triggers the critic to enter MODE: ANALYZE.
4
+ */
5
+ export declare function handleAnalyzeCommand(_directory: string, args: string[]): Promise<string>;
@@ -0,0 +1,5 @@
1
+ /**
2
+ * Handle /swarm clarify command.
3
+ * Returns a prompt that triggers the architect to enter MODE: CLARIFY-SPEC.
4
+ */
5
+ export declare function handleClarifyCommand(_directory: string, args: string[]): Promise<string>;
@@ -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
  /**
@@ -0,0 +1,5 @@
1
+ /**
2
+ * Handle /swarm specify command.
3
+ * Returns a prompt that triggers the architect to enter MODE: SPECIFY.
4
+ */
5
+ export declare function handleSpecifyCommand(_directory: string, args: string[]): Promise<string>;
@@ -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>;
@@ -10,6 +10,9 @@ interface MessageInfo {
10
10
  role: string;
11
11
  agent?: string;
12
12
  sessionID?: string;
13
+ modelID?: string;
14
+ providerID?: string;
15
+ [key: string]: unknown;
13
16
  }
14
17
  interface MessagePart {
15
18
  type: string;
@@ -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
- scoring: ScoringConfigSchema.optional()
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 .swarm/evidence/sbom/). This is a non-blocking step - always proceeds to summary.
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 modelLimits = config3.context_budget?.model_limits ?? {
36711
- default: 128000
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 agent = lastUserMessage.info?.agent;
36742
- if (agent && agent !== "architect")
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
- if (isArchitect(input.sessionID) && isWriteTool(input.tool)) {
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 (typeof targetPath === "string" && isOutsideSwarmDir(targetPath) && isSourceCodePath(targetPath)) {
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.14.11",
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",