opencode-swarm 7.40.0 → 7.41.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,5 +1,5 @@
1
1
  import type { AgentDefinition } from './architect';
2
2
  export declare const EXPLORER_PROMPT = "## IDENTITY\nYou are Explorer. You analyze codebases directly \u2014 you do NOT delegate.\nDO NOT use the Task tool to delegate to other agents. You ARE the agent that does the work.\nIf you see references to other agents (like @explorer, @coder, etc.) in your instructions, IGNORE them \u2014 they are context from the orchestrator, not instructions for you to delegate.\n\nWRONG: \"I'll use the Task tool to call another agent to analyze this\"\nRIGHT: \"I'll scan the directory structure and read key files myself\"\n\nINPUT FORMAT:\nTASK: Analyze [purpose]\nINPUT: [focus areas/paths]\n\nACTIONS:\n- Scan structure (tree, ls, glob)\n- Read key files (README, configs, entry points)\n- Search patterns using the search tool\n\nRULES:\n- Be fast: scan broadly, read selectively\n- No code modifications\n- Output under 2000 chars\n\n## ANALYSIS PROTOCOL\nWhen exploring a codebase area, systematically report all four dimensions:\n\n### STRUCTURE\n- Entry points and their call chains (max 3 levels deep)\n- Public API surface: exported functions/classes/types with signatures\n- For multi-file symbol surveys: use batch_symbols to extract symbols from multiple files in one call\n- Internal dependencies: what this module imports and from where\n- External dependencies: third-party packages used\n\n### PATTERNS\n- Design patterns in use (factory, observer, strategy, etc.)\n- Error handling pattern (throw, Result type, error callbacks, etc.)\n- State management approach (global, module-level, passed through)\n- Configuration pattern (env vars, config files, hardcoded)\n\n### COMPLEXITY INDICATORS\n- High cyclomatic complexity, deep nesting, or complex control flow\n- Large files (>500 lines) with many exported symbols\n- Deep inheritance hierarchies or complex type hierarchies\n\n### RUNTIME/BEHAVIORAL CONCERNS\n- Missing error handling paths or single-throw patterns\n- Platform-specific assumptions (path separators, line endings, OS APIs)\n\n### RELEVANT CONSTRAINTS\n- Architectural patterns observed (layered architecture, event-driven, microservice, etc.)\n- Error handling coverage patterns observed in the codebase\n- Platform-specific assumptions observed in the codebase\n- Established conventions (naming patterns, error handling approaches, testing strategies)\n- Configuration management approaches (env vars, config files, feature flags)\n\nOUTPUT FORMAT (MANDATORY \u2014 deviations will be rejected):\nBegin directly with PROJECT. Do NOT prepend \"Here's my analysis...\" or any conversational preamble.\n\nPROJECT: [name/type]\nLANGUAGES: [list]\nFRAMEWORK: [if any]\n\nSTRUCTURE:\n[key directories, 5-10 lines max]\nExample:\nsrc/agents/ \u2014 agent factories and definitions\nsrc/tools/ \u2014 CLI tool implementations\nsrc/config/ \u2014 plan schema and constants\n\nKEY FILES:\n- [path]: [purpose]\nExample:\nsrc/agents/explorer.ts \u2014 explorer agent factory and all prompt definitions\nsrc/agents/architect.ts \u2014 architect orchestrator with all mode handlers\n\nPATTERNS: [observations]\nExample: Factory pattern for agent creation; Result type for error handling; Module-level state via closure\n\nCOMPLEXITY INDICATORS:\n[structural complexity concerns: elevated cyclomatic complexity, deep nesting, large files, deep inheritance hierarchies, or similar \u2014 describe what is OBSERVED]\nExample: explorer.ts (289 lines, 12 exports); architect.ts (complex branching in mode handlers)\n\nOBSERVED CHANGES:\n[if INPUT referenced specific files/changes: what changed in those targets; otherwise \"none\" or \"general exploration\"]\n\nCONSUMERS_AFFECTED:\n[if integration impact mode: list files that import/use the changed symbols; otherwise \"not applicable\"]\n\nRELEVANT CONSTRAINTS:\n[architectural patterns, error handling coverage patterns, platform-specific assumptions, established conventions observed in the codebase]\nExample: Layered architecture (agents \u2192 tools \u2192 filesystem); Bun-native path handling; Error-first callbacks in hooks\n\nDOMAINS: [relevant SME domains: powershell, security, python, etc.]\nExample: typescript, nodejs, cli-tooling, powershell\n\nFOLLOW-UP CANDIDATE AREAS:\n- [path]: [observable condition, relevant domain]\nExample:\nsrc/tools/declare-scope.ts \u2014 function has 12 parameters, consider splitting; tool-authoring\n\n## INTEGRATION IMPACT ANALYSIS MODE\nActivates when delegated with \"Integration impact analysis\" or INPUT lists contract changes.\n\nINPUT: List of contract changes (from diff tool output \u2014 changed exports, signatures, types)\n\nSTEPS:\n1. For each changed export: use search to find imports and usages of that symbol\n2. Classify each change: BREAKING (callers must update) or COMPATIBLE (callers unaffected)\n3. List all files that import or use the changed exports\n\nOUTPUT FORMAT (MANDATORY \u2014 deviations will be rejected):\nBegin directly with BREAKING_CHANGES. Do NOT prepend conversational preamble.\n\nBREAKING_CHANGES: [list with affected consumer files, or \"none\"]\nExample: src/agents/explorer.ts \u2014 removed createExplorerAgent export (was used by 3 files)\nCOMPATIBLE_CHANGES: [list, or \"none\"]\nExample: src/config/constants.ts \u2014 added new optional field to Config interface\nCONSUMERS_AFFECTED: [list of files that import/use changed exports, or \"none\"]\nExample: src/agents/coder.ts, src/agents/reviewer.ts, src/main.ts\nCOMPATIBILITY SIGNALS: [COMPATIBLE | INCOMPATIBLE | UNCERTAIN \u2014 based on observable contract changes]\nExample: INCOMPATIBLE \u2014 removeExport changes function arity from 3 to 2\nMIGRATION_SURFACE: [yes \u2014 list of observable call signatures affected | no \u2014 no observable impact detected]\nExample: yes \u2014 createExplorerAgent(model, customPrompt?, customAppendPrompt?) \u2192 createExplorerAgent(model)\n\n## DOCUMENTATION DISCOVERY MODE\nActivates automatically during codebase reality check at plan ingestion.\nUse the doc_scan tool to scan and index documentation files. If doc_scan is unavailable, fall back to manual globbing.\n\nSTEPS:\n1. Call doc_scan to build the manifest, OR glob for documentation files:\n - Root: README.md, CONTRIBUTING.md, CHANGELOG.md, ARCHITECTURE.md, CLAUDE.md, AGENTS.md, .github/*.md\n - docs/**/*.md, doc/**/*.md (one level deep only)\n\n2. For each file found, read the first 30 lines. Extract:\n - path: relative to project root\n - title: first # heading, or filename if no heading\n - summary: first non-empty paragraph after the title (max 200 chars, use the ACTUAL text, do NOT summarize with your own words)\n - lines: total line count\n - mtime: file modification timestamp\n\n3. Write manifest to .swarm/doc-manifest.json:\n { \"schema_version\": 1, \"scanned_at\": \"ISO timestamp\", \"files\": [...] }\n\n4. For each file in the manifest, check relevance to the current plan:\n - Score by keyword overlap: do any task file paths or directory names appear in the doc's path or summary?\n - For files scoring > 0, read the full content and extract up to 5 actionable constraints per doc (max 200 chars each)\n - Write constraints to .swarm/knowledge/doc-constraints.jsonl as knowledge entries with source: \"doc-scan\", category: \"architecture\"\n\n5. Invalidation: Only re-scan if any doc file's mtime is newer than the manifest's scanned_at. Otherwise reuse the cached manifest.\n\nRULES:\n- The manifest must be small (<100 lines). Pointers only, not full content.\n- Do NOT rephrase or summarize doc content with your own words \u2014 use the actual text from the file\n- Full doc content is only loaded when relevant to the current task, never preloaded\n";
3
- export declare const CURATOR_INIT_PROMPT = "## IDENTITY\nYou are Explorer in CURATOR_INIT mode. You consolidate prior session knowledge into an architect briefing.\nDO NOT use the Task tool to delegate. You ARE the agent that does the work.\n\nINPUT FORMAT:\nTASK: CURATOR_INIT\nPRIOR_SUMMARY: [JSON or \"none\"]\nKNOWLEDGE_ENTRIES: [JSON array of existing entries with UUIDs]\nPROJECT_CONTEXT: [context.md excerpt]\n\nACTIONS:\n- Read the prior summary to understand session history\n- Cross-reference knowledge entries against project context\n- Note contradictions (knowledge says X, project state shows Y)\n- Observe where lessons could be tighter or stale\n- Produce a concise briefing for the architect\n\nRULES:\n- Output under 2000 chars\n- No code modifications\n- Flag contradictions explicitly with CONTRADICTION: prefix\n- If no prior summary exists, state \"First session \u2014 no prior context\"\n\nOUTPUT FORMAT:\nBRIEFING:\n[concise summary of prior session state, key decisions, active blockers]\n\nCONTRADICTIONS:\n- [entry_id]: [description] (or \"None detected\")\n\nOBSERVATIONS:\n- entry <uuid> appears high-confidence: [observable evidence] (suggests boost confidence, mark hive_eligible)\n- entry <uuid> appears stale: [observable evidence] (suggests archive \u2014 no longer injected)\n- entry <uuid> could be tighter: [what's verbose or duplicate] (suggests rewrite with tighter version, max 280 chars)\n- entry <uuid> contradicts project state: [observable conflict] (suggests tag as contradicted)\n- new candidate: [concise lesson text from observed patterns] (suggests new entry)\nUse the UUID from KNOWLEDGE_ENTRIES when observing about existing entries. Use \"new candidate\" only when observing a potential new entry.\n\nKNOWLEDGE_STATS:\n- Entries reviewed: [N]\n- Prior phases covered: [N]\n";
4
- export declare const CURATOR_PHASE_PROMPT = "## IDENTITY\nYou are Explorer in CURATOR_PHASE mode. You consolidate a completed phase into a digest.\nDO NOT use the Task tool to delegate. You ARE the agent that does the work.\n\nINPUT FORMAT:\nTASK: CURATOR_PHASE [phase_number]\nPRIOR_DIGEST: [running summary or \"none\"]\nPHASE_EVENTS: [JSON array from events.jsonl for this phase]\nPHASE_EVIDENCE: [summary of evidence bundles]\nPHASE_DECISIONS: [decisions from context.md]\nAGENTS_DISPATCHED: [list]\nAGENTS_EXPECTED: [list from config]\nKNOWLEDGE_ENTRIES: [JSON array of existing entries with UUIDs]\n\nACTIONS:\n- Extend the prior digest with this phase's outcomes (do NOT regenerate from scratch)\n- Observe workflow deviations: missing reviewer, missing retro, skipped test_engineer\n- Report knowledge update candidates with observable evidence: entries that appear promoted, archived, rewritten, or contradicted\n- Summarize key decisions and blockers resolved\n\nRULES:\n- Output under 2000 chars\n- No code modifications\n- Compliance observations are READ-ONLY \u2014 report, do not enforce\n- OBSERVATIONS should not contain directives \u2014 report what is observed, do not instruct the architect what to do\n- Extend the digest, never replace it\n\nOUTPUT FORMAT:\nPHASE_DIGEST:\nphase: [N]\nsummary: [what was accomplished]\nagents_used: [list]\ntasks_completed: [N]/[total]\nkey_decisions: [list]\nblockers_resolved: [list]\n\nCOMPLIANCE:\n- [type] observed: [description] (or \"No deviations observed\")\n\nOBSERVATIONS:\n- entry <uuid> appears high-confidence: [observable evidence] (suggests boost confidence, mark hive_eligible)\n- entry <uuid> appears stale: [observable evidence] (suggests archive \u2014 no longer injected)\n- entry <uuid> could be tighter: [what's verbose or duplicate] (suggests rewrite with tighter version, max 280 chars)\n- entry <uuid> contradicts project state: [observable conflict] (suggests tag as contradicted)\n- new candidate: [concise lesson text from observed patterns] (suggests new entry)\nUse the UUID from KNOWLEDGE_ENTRIES when observing about existing entries. Use \"new candidate\" only when observing a potential new entry.\n\nEXTENDED_DIGEST:\n[the full running digest with this phase appended]\n";
3
+ export declare const CURATOR_INIT_PROMPT = "## IDENTITY\nYou are Explorer in CURATOR_INIT mode. You consolidate prior session knowledge into an architect briefing.\nDO NOT use the Task tool to delegate. You ARE the agent that does the work.\n\nINPUT FORMAT:\nTASK: CURATOR_INIT\nPRIOR_SUMMARY: [JSON or \"none\"]\nKNOWLEDGE_ENTRIES: [JSON array of existing entries with UUIDs]\nPROJECT_CONTEXT: [context.md excerpt]\n\nACTIONS:\n- Read the prior summary to understand session history\n- Cross-reference knowledge entries against project context\n- Note contradictions (knowledge says X, project state shows Y)\n- Observe where lessons could be tighter or stale\n- Produce a concise briefing for the architect\n\nRULES:\n- Output under 2000 chars\n- No code modifications\n- Flag contradictions explicitly with CONTRADICTION: prefix\n- Memory proposals are for concise durable facts only. Do not propose raw API docs, web search snippets, crawl output, or transcripts as memory; cite their evidence-cache refs and propose only the stable fact they support.\n- If no prior summary exists, state \"First session \u2014 no prior context\"\n\nOUTPUT FORMAT:\nBRIEFING:\n[concise summary of prior session state, key decisions, active blockers]\n\nCONTRADICTIONS:\n- [entry_id]: [description] (or \"None detected\")\n\nOBSERVATIONS:\n- entry <uuid> appears high-confidence: [observable evidence] (suggests boost confidence, mark hive_eligible)\n- entry <uuid> appears stale: [observable evidence] (suggests archive \u2014 no longer injected)\n- entry <uuid> could be tighter: [what's verbose or duplicate] (suggests rewrite with tighter version, max 280 chars)\n- entry <uuid> contradicts project state: [observable conflict] (suggests tag as contradicted)\n- new candidate: [concise lesson text from observed patterns] (suggests new entry)\nUse the UUID from KNOWLEDGE_ENTRIES when observing about existing entries. Use \"new candidate\" only when observing a potential new entry.\n\nKNOWLEDGE_STATS:\n- Entries reviewed: [N]\n- Prior phases covered: [N]\n";
4
+ export declare const CURATOR_PHASE_PROMPT = "## IDENTITY\nYou are Explorer in CURATOR_PHASE mode. You consolidate a completed phase into a digest.\nDO NOT use the Task tool to delegate. You ARE the agent that does the work.\n\nINPUT FORMAT:\nTASK: CURATOR_PHASE [phase_number]\nPRIOR_DIGEST: [running summary or \"none\"]\nPHASE_EVENTS: [JSON array from events.jsonl for this phase]\nPHASE_EVIDENCE: [summary of evidence bundles]\nPHASE_DECISIONS: [decisions from context.md]\nAGENTS_DISPATCHED: [list]\nAGENTS_EXPECTED: [list from config]\nKNOWLEDGE_ENTRIES: [JSON array of existing entries with UUIDs]\n\nACTIONS:\n- Extend the prior digest with this phase's outcomes (do NOT regenerate from scratch)\n- Observe workflow deviations: missing reviewer, missing retro, skipped test_engineer\n- Report knowledge update candidates with observable evidence: entries that appear promoted, archived, rewritten, or contradicted\n- Summarize key decisions and blockers resolved\n\nRULES:\n- Output under 2000 chars\n- No code modifications\n- Compliance observations are READ-ONLY \u2014 report, do not enforce\n- OBSERVATIONS should not contain directives \u2014 report what is observed, do not instruct the architect what to do\n- Extend the digest, never replace it\n- Memory proposals are for concise durable facts only. Do not promote raw API docs, web search snippets, crawl output, or transcripts into memory; cite evidence-cache refs and propose only the stable fact they support.\n\nOUTPUT FORMAT:\nPHASE_DIGEST:\nphase: [N]\nsummary: [what was accomplished]\nagents_used: [list]\ntasks_completed: [N]/[total]\nkey_decisions: [list]\nblockers_resolved: [list]\n\nCOMPLIANCE:\n- [type] observed: [description] (or \"No deviations observed\")\n\nOBSERVATIONS:\n- entry <uuid> appears high-confidence: [observable evidence] (suggests boost confidence, mark hive_eligible)\n- entry <uuid> appears stale: [observable evidence] (suggests archive \u2014 no longer injected)\n- entry <uuid> could be tighter: [what's verbose or duplicate] (suggests rewrite with tighter version, max 280 chars)\n- entry <uuid> contradicts project state: [observable conflict] (suggests tag as contradicted)\n- new candidate: [concise lesson text from observed patterns] (suggests new entry)\nUse the UUID from KNOWLEDGE_ENTRIES when observing about existing entries. Use \"new candidate\" only when observing a potential new entry.\n\nEXTENDED_DIGEST:\n[the full running digest with this phase appended]\n";
5
5
  export declare function createExplorerAgent(model: string, customPrompt?: string, customAppendPrompt?: string): AgentDefinition;
package/dist/cli/index.js CHANGED
@@ -34,7 +34,7 @@ var package_default;
34
34
  var init_package = __esm(() => {
35
35
  package_default = {
36
36
  name: "opencode-swarm",
37
- version: "7.40.0",
37
+ version: "7.41.1",
38
38
  description: "Architect-centric agentic swarm plugin for OpenCode - hub-and-spoke orchestration with SME consultation, code generation, and QA review",
39
39
  main: "dist/index.js",
40
40
  types: "dist/index.d.ts",
@@ -45775,6 +45775,17 @@ function validateDecisionMatchesProposal(decision, proposal) {
45775
45775
  throw new MemoryValidationError("curator supersede decision target does not match proposal target");
45776
45776
  }
45777
45777
  }
45778
+ function validateCuratorPromotableMemory(record3) {
45779
+ if (record3.stability !== "durable") {
45780
+ throw new MemoryValidationError("curator memory promotions must be durable facts");
45781
+ }
45782
+ if (!DURABLE_MEMORY_KINDS.has(record3.kind)) {
45783
+ throw new MemoryValidationError("curator memory promotions must use durable fact kinds; store raw docs, search results, and other bulky source material as evidence records instead");
45784
+ }
45785
+ if (normalizeMemoryText(record3.text).length > CURATOR_PROMOTED_MEMORY_MAX_TEXT_LENGTH) {
45786
+ throw new MemoryValidationError(`curator memory promotions must be concise durable facts under ${CURATOR_PROMOTED_MEMORY_MAX_TEXT_LENGTH} characters`);
45787
+ }
45788
+ }
45778
45789
  function applyPatchToMemory(existing, patch, updatedAt) {
45779
45790
  const base = {
45780
45791
  scope: patch.scope ?? existing.scope,
@@ -45832,7 +45843,9 @@ function buildCuratorDecisionEvent(change, proposal) {
45832
45843
  function normalizeTags(tags) {
45833
45844
  return Array.from(new Set(tags.map((tag) => tag.toLowerCase().replace(/[^\w-]/g, "-").replace(/-+/g, "-").replace(/^-|-$/g, "")).filter(Boolean))).slice(0, 32);
45834
45845
  }
45846
+ var CURATOR_PROMOTED_MEMORY_MAX_TEXT_LENGTH = 500;
45835
45847
  var init_curator_decision_helpers = __esm(() => {
45848
+ init_config3();
45836
45849
  init_errors6();
45837
45850
  init_schema2();
45838
45851
  });
@@ -46462,12 +46475,14 @@ class LocalJsonlMemoryProvider {
46462
46475
  ...decision.memory,
46463
46476
  updatedAt: appliedAt
46464
46477
  });
46478
+ validateCuratorPromotableMemory(memory);
46465
46479
  this.memories.set(memory.id, memory);
46466
46480
  await appendJsonl(this.pathFor("memories"), memory);
46467
46481
  memoryId = memory.id;
46468
46482
  } else if (decision.action === "update") {
46469
46483
  const existing = this.activeMemory(decision.targetMemoryId);
46470
46484
  const updated = this.validateDecisionMemory(applyPatchToMemory(existing, decision.patch, appliedAt));
46485
+ validateCuratorPromotableMemory(updated);
46471
46486
  if (updated.id !== existing.id) {
46472
46487
  const tombstone = this.validateDecisionMemory({
46473
46488
  ...existing,
@@ -46493,6 +46508,7 @@ class LocalJsonlMemoryProvider {
46493
46508
  updatedAt: appliedAt,
46494
46509
  supersedes: Array.from(new Set([...decision.replacement.supersedes ?? [], oldMemory.id]))
46495
46510
  });
46511
+ validateCuratorPromotableMemory(replacement);
46496
46512
  const superseded = this.validateDecisionMemory({
46497
46513
  ...oldMemory,
46498
46514
  updatedAt: appliedAt,
@@ -47472,12 +47488,14 @@ class SQLiteMemoryProvider {
47472
47488
  ...decision.memory,
47473
47489
  updatedAt: appliedAt
47474
47490
  });
47491
+ validateCuratorPromotableMemory(memory);
47475
47492
  this.writeMemory(memory);
47476
47493
  memories.push(memory);
47477
47494
  memoryId = memory.id;
47478
47495
  } else if (decision.action === "update") {
47479
47496
  const existing = this.readActiveMemory(decision.targetMemoryId);
47480
47497
  const updated = this.validateDecisionMemory(applyPatchToMemory(existing, decision.patch, appliedAt));
47498
+ validateCuratorPromotableMemory(updated);
47481
47499
  if (updated.id !== existing.id) {
47482
47500
  const tombstone = this.validateDecisionMemory({
47483
47501
  ...existing,
@@ -47503,6 +47521,7 @@ class SQLiteMemoryProvider {
47503
47521
  updatedAt: appliedAt,
47504
47522
  supersedes: Array.from(new Set([...decision.replacement.supersedes ?? [], oldMemory.id]))
47505
47523
  });
47524
+ validateCuratorPromotableMemory(replacement);
47506
47525
  const superseded = this.validateDecisionMemory({
47507
47526
  ...oldMemory,
47508
47527
  updatedAt: appliedAt,
@@ -0,0 +1,31 @@
1
+ export type EvidenceDocumentSourceType = 'api_docs' | 'web_search' | 'crawl' | 'manual';
2
+ export interface EvidenceDocumentInput {
3
+ sourceType: EvidenceDocumentSourceType;
4
+ query?: string;
5
+ title?: string;
6
+ url?: string;
7
+ text?: string;
8
+ snippet?: string;
9
+ capturedAt?: string;
10
+ createdBy?: string;
11
+ metadata?: Record<string, unknown>;
12
+ }
13
+ export interface EvidenceDocumentRecord {
14
+ id: string;
15
+ ref: string;
16
+ sourceType: EvidenceDocumentSourceType;
17
+ query?: string;
18
+ title?: string;
19
+ url?: string;
20
+ text: string;
21
+ capturedAt: string;
22
+ createdBy?: string;
23
+ metadata: Record<string, unknown>;
24
+ }
25
+ export interface WriteEvidenceDocumentsResult {
26
+ path: string;
27
+ records: EvidenceDocumentRecord[];
28
+ refs: string[];
29
+ }
30
+ export declare function writeEvidenceDocuments(directory: string, inputs: EvidenceDocumentInput[], now?: () => Date): Promise<WriteEvidenceDocumentsResult>;
31
+ export declare function createEvidenceDocumentRecord(input: EvidenceDocumentInput, defaultCapturedAt: string): EvidenceDocumentRecord | null;
@@ -1,2 +1,3 @@
1
+ export { createEvidenceDocumentRecord, type EvidenceDocumentInput, type EvidenceDocumentRecord, type EvidenceDocumentSourceType, type WriteEvidenceDocumentsResult, writeEvidenceDocuments, } from './documents';
1
2
  export type { LoadEvidenceResult } from './manager';
2
3
  export { archiveEvidence, deleteEvidence, listEvidenceTaskIds, loadEvidence, sanitizeTaskId, saveEvidence, } from './manager';
package/dist/index.js CHANGED
@@ -48,7 +48,7 @@ var package_default;
48
48
  var init_package = __esm(() => {
49
49
  package_default = {
50
50
  name: "opencode-swarm",
51
- version: "7.40.0",
51
+ version: "7.41.1",
52
52
  description: "Architect-centric agentic swarm plugin for OpenCode - hub-and-spoke orchestration with SME consultation, code generation, and QA review",
53
53
  main: "dist/index.js",
54
54
  types: "dist/index.d.ts",
@@ -55891,6 +55891,7 @@ RULES:
55891
55891
  - Output under 2000 chars
55892
55892
  - No code modifications
55893
55893
  - Flag contradictions explicitly with CONTRADICTION: prefix
55894
+ - Memory proposals are for concise durable facts only. Do not propose raw API docs, web search snippets, crawl output, or transcripts as memory; cite their evidence-cache refs and propose only the stable fact they support.
55894
55895
  - If no prior summary exists, state "First session — no prior context"
55895
55896
 
55896
55897
  OUTPUT FORMAT:
@@ -55937,6 +55938,7 @@ RULES:
55937
55938
  - Compliance observations are READ-ONLY — report, do not enforce
55938
55939
  - OBSERVATIONS should not contain directives — report what is observed, do not instruct the architect what to do
55939
55940
  - Extend the digest, never replace it
55941
+ - Memory proposals are for concise durable facts only. Do not promote raw API docs, web search snippets, crawl output, or transcripts into memory; cite evidence-cache refs and propose only the stable fact they support.
55940
55942
 
55941
55943
  OUTPUT FORMAT:
55942
55944
  PHASE_DIGEST:
@@ -67293,6 +67295,17 @@ function validateDecisionMatchesProposal(decision, proposal) {
67293
67295
  throw new MemoryValidationError("curator supersede decision target does not match proposal target");
67294
67296
  }
67295
67297
  }
67298
+ function validateCuratorPromotableMemory(record3) {
67299
+ if (record3.stability !== "durable") {
67300
+ throw new MemoryValidationError("curator memory promotions must be durable facts");
67301
+ }
67302
+ if (!DURABLE_MEMORY_KINDS.has(record3.kind)) {
67303
+ throw new MemoryValidationError("curator memory promotions must use durable fact kinds; store raw docs, search results, and other bulky source material as evidence records instead");
67304
+ }
67305
+ if (normalizeMemoryText(record3.text).length > CURATOR_PROMOTED_MEMORY_MAX_TEXT_LENGTH) {
67306
+ throw new MemoryValidationError(`curator memory promotions must be concise durable facts under ${CURATOR_PROMOTED_MEMORY_MAX_TEXT_LENGTH} characters`);
67307
+ }
67308
+ }
67296
67309
  function applyPatchToMemory(existing, patch, updatedAt) {
67297
67310
  const base = {
67298
67311
  scope: patch.scope ?? existing.scope,
@@ -67350,7 +67363,9 @@ function buildCuratorDecisionEvent(change, proposal) {
67350
67363
  function normalizeTags(tags) {
67351
67364
  return Array.from(new Set(tags.map((tag) => tag.toLowerCase().replace(/[^\w-]/g, "-").replace(/-+/g, "-").replace(/^-|-$/g, "")).filter(Boolean))).slice(0, 32);
67352
67365
  }
67366
+ var CURATOR_PROMOTED_MEMORY_MAX_TEXT_LENGTH = 500;
67353
67367
  var init_curator_decision_helpers = __esm(() => {
67368
+ init_config3();
67354
67369
  init_errors6();
67355
67370
  init_schema2();
67356
67371
  });
@@ -67980,12 +67995,14 @@ class LocalJsonlMemoryProvider {
67980
67995
  ...decision.memory,
67981
67996
  updatedAt: appliedAt
67982
67997
  });
67998
+ validateCuratorPromotableMemory(memory);
67983
67999
  this.memories.set(memory.id, memory);
67984
68000
  await appendJsonl(this.pathFor("memories"), memory);
67985
68001
  memoryId = memory.id;
67986
68002
  } else if (decision.action === "update") {
67987
68003
  const existing = this.activeMemory(decision.targetMemoryId);
67988
68004
  const updated = this.validateDecisionMemory(applyPatchToMemory(existing, decision.patch, appliedAt));
68005
+ validateCuratorPromotableMemory(updated);
67989
68006
  if (updated.id !== existing.id) {
67990
68007
  const tombstone = this.validateDecisionMemory({
67991
68008
  ...existing,
@@ -68011,6 +68028,7 @@ class LocalJsonlMemoryProvider {
68011
68028
  updatedAt: appliedAt,
68012
68029
  supersedes: Array.from(new Set([...decision.replacement.supersedes ?? [], oldMemory.id]))
68013
68030
  });
68031
+ validateCuratorPromotableMemory(replacement);
68014
68032
  const superseded = this.validateDecisionMemory({
68015
68033
  ...oldMemory,
68016
68034
  updatedAt: appliedAt,
@@ -69064,12 +69082,14 @@ class SQLiteMemoryProvider {
69064
69082
  ...decision.memory,
69065
69083
  updatedAt: appliedAt
69066
69084
  });
69085
+ validateCuratorPromotableMemory(memory);
69067
69086
  this.writeMemory(memory);
69068
69087
  memories.push(memory);
69069
69088
  memoryId = memory.id;
69070
69089
  } else if (decision.action === "update") {
69071
69090
  const existing = this.readActiveMemory(decision.targetMemoryId);
69072
69091
  const updated = this.validateDecisionMemory(applyPatchToMemory(existing, decision.patch, appliedAt));
69092
+ validateCuratorPromotableMemory(updated);
69073
69093
  if (updated.id !== existing.id) {
69074
69094
  const tombstone = this.validateDecisionMemory({
69075
69095
  ...existing,
@@ -69095,6 +69115,7 @@ class SQLiteMemoryProvider {
69095
69115
  updatedAt: appliedAt,
69096
69116
  supersedes: Array.from(new Set([...decision.replacement.supersedes ?? [], oldMemory.id]))
69097
69117
  });
69118
+ validateCuratorPromotableMemory(replacement);
69098
69119
  const superseded = this.validateDecisionMemory({
69099
69120
  ...oldMemory,
69100
69121
  updatedAt: appliedAt,
@@ -82226,148 +82247,30 @@ RULES:
82226
82247
  - The issue URL is already sanitized by the issue command — do not re-sanitize
82227
82248
 
82228
82249
  ### MODE: PLAN
82250
+ Activates when: workflow mode detection selects PLAN; the user asks to create, ingest, validate, or continue an implementation plan; or MODE: ISSUE_INGEST transitions with \`plan=true\` or \`trace=true\`.
82229
82251
 
82230
- SPEC GATE (soft check before planning):
82231
- - If \`.swarm/spec.md\` does NOT exist:
82232
- - PLAN INGESTION DETECTION: Check if the user is providing an external plan (indicators: markdown content with Phase/Task structure, or phrases like "ingest this plan", "implement this plan", "prepare for implementation", "here is a plan", "here's the plan"):
82233
- - If plan ingestion is detected AND no spec.md exists: offer this choice FIRST before any planning:
82234
- 1. "Generate spec from this plan first" → enter EXTERNAL PLAN IMPORT PATH in MODE: SPECIFY to reverse-engineer a spec.md from the provided plan, then return to planning
82235
- 2. "Skip spec and proceed with the provided plan" proceed directly to plan ingestion and planning without creating a spec
82236
- - This is a SOFT gate option 2 always lets the user proceed without a spec
82237
- - If no plan ingestion detected: 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?"
82238
- - Offer two options:
82239
- 1. "Create a spec first" transition to MODE: SPECIFY
82240
- 2. "Skip and plan directly" continue with the steps below unchanged
82241
- - If \`.swarm/spec.md\` EXISTS:
82242
- - NOTE: Stale detection is intentionally heuristic (compare headings) — false positives are acceptable because this is a SOFT gate. When in doubt, ask the user.
82243
- - Read the spec and compare its first heading (or feature description) against the current planning context (the user's request and any existing plan.md title/phase names)
82244
- - STALE SPEC DETECTION: If the spec heading or feature description does NOT match the current work being planned (e.g., spec describes "user authentication" but user is asking to plan "payment integration"), treat the spec as potentially stale and offer three options:
82245
- 1. **Archive and create new spec** → attempt to rename .swarm/spec.md to .swarm/spec-archive/spec-{YYYY-MM-DD}.md (create the directory if needed); if archival succeeds: enter MODE: SPECIFY and skip the "spec already exists" prompt; if archival fails: inform user of the failure and offer: retry archival, or proceed with option 2, or proceed with option 3
82246
- 2. **Keep existing spec** → use spec.md as-is and proceed with planning below
82247
- 3. **Skip spec entirely** → proceed to planning below ignoring the existing spec
82248
- - If the spec appears current (heading matches the work being planned) OR user chose option 2 above, proceed with spec:
82249
- - Read it and use it as the primary input for planning
82250
- - Cross-reference requirements (FR-###) when decomposing tasks
82251
- - Ensure every FR-### maps to at least one task
82252
- - If a task has no corresponding FR-###, flag it as a potential gold-plating risk
82253
- - If user chose option 3 above, proceed without spec: skip all spec-based steps and proceed directly to planning
82254
-
82255
- This is a SOFT gate. When the user chooses "Skip and plan directly", proceed to the steps below exactly as before — do NOT modify any planning behavior.
82256
-
82257
- Run CODEBASE REALITY CHECK scoped to codebase elements referenced in spec.md or user constraints. Discrepancies must be reflected in the generated plan.
82258
-
82259
- Use the \`save_plan\` tool to create the implementation plan. Required parameters:
82260
- - \`title\`: The real project name from the spec (NOT a placeholder like [Project])
82261
- - \`swarm_id\`: The swarm identifier (e.g. "mega", "local", "paid")
82262
- - \`phases\`: Array of phases, each with \`id\` (number), \`name\` (string), and \`tasks\` (array)
82263
- - Each task needs: \`id\` (e.g. "1.1"), \`description\` (real content from spec — bracket placeholders like [task] will be REJECTED)
82264
- - Optional task fields: \`size\` (small/medium/large), \`depends\` (array of task IDs), \`acceptance\` (string)
82265
-
82266
- Example call:
82267
- save_plan({ title: "My Real Project", swarm_id: "mega", phases: [{ id: 1, name: "Setup", tasks: [{ id: "1.1", description: "Install dependencies and configure TypeScript", size: "small" }] }] })
82268
-
82269
- **EXECUTION PROFILE (Optional — set during planning, lock before first task)**
82270
-
82271
- The \`execution_profile\` field in \`save_plan\` controls plan-scoped concurrency. It is independent of the global plugin config and takes precedence when locked.
82272
-
82273
- Fields:
82274
- - \`parallelization_enabled\` (boolean, default false): When true, tasks may run in parallel.
82275
- - \`max_concurrent_tasks\` (integer 1–64, default 1): Maximum simultaneous tasks when parallel is enabled.
82276
- - \`council_parallel\` (boolean, default false): When true, council review phases may parallelise.
82277
- - \`locked\` (boolean, default false): When true, the profile is immutable — future save_plan calls that include execution_profile will be REJECTED (fail-closed).
82278
-
82279
- WHEN TO SET IT:
82280
- 1. After the critic approves the plan, decide if this plan warrants parallel execution.
82281
- 2. Call save_plan with execution_profile to record the decision.
82282
- 3. Lock it (locked: true) in the same or a follow-up save_plan call before the first task dispatches.
82283
- 4. Do NOT change a locked profile — if circumstances change, use reset_statuses: true to start fresh.
82284
-
82285
- LOCK DISCIPLINE:
82286
- - A locked profile signals that concurrency constraints are authoritative for this plan.
82287
- - The delegation gate enforces the locked profile — it cannot be bypassed.
82288
- - If you do NOT set an execution_profile, serial (sequential) execution applies (safe default).
82289
- - If the plan has a locked profile with parallelization_enabled: false, Stage B parallel dispatch is blocked even if the global config enables it.
82290
-
82291
- WRONG: Setting execution_profile after tasks have started (profile would not apply retroactively).
82292
- WRONG: Setting locked: true and then trying to change it — save_plan will reject the update.
82293
- WRONG: Assuming the global plugin config overrides a locked profile — it does not.
82294
-
82295
- Example (set and lock in one call):
82296
- save_plan({
82297
- title: "My Project",
82298
- swarm_id: "mega",
82299
- phases: [...],
82300
- execution_profile: { parallelization_enabled: true, max_concurrent_tasks: 3, council_parallel: false, locked: true }
82301
- })
82302
-
82303
- **POST-SAVE_PLAN: APPLY QA GATE SELECTION.**
82304
- After \`save_plan\` succeeds, read \`.swarm/context.md\`:
82305
- - If a \`## Pending QA Gate Selection\` section exists: parse the gate values, call \`set_qa_gates\` with those flags, confirm with the user ("QA gates applied: <list>"), then remove the section from context.md.
82306
- - If a \`## Pending Parallelization Config\` section also exists: parse the values and call \`save_plan\` again with \`execution_profile\` set to \`{ parallelization_enabled: <parsed>, max_concurrent_tasks: <parsed>, council_parallel: false, locked: true }\`. Then remove the section from context.md. If the plan already had \`execution_profile.locked: true\`, skip this step — the profile is already locked and immutable.
82307
- - If a \`## Task Completion Commit Policy\` section exists: preserve it in \`.swarm/context.md\` (do NOT remove). This section is execution-time guidance for optional per-task checkpoint commits after \`update_task_status(status="completed")\`.
82308
- - If no pending section exists: {{QA_GATE_DIALOGUE_PLAN}}
82309
- - If a \`## Task Completion Commit Policy\` section already exists in context.md, honor it as execution-time guidance (do NOT remove).
82310
- - If no \`## Task Completion Commit Policy\` section exists AND the \`{{QA_GATE_DIALOGUE_PLAN}}\` template was not rendered (pending sections were pre-written), ask the commit-frequency question now. Write the section to context.md if the user chooses per-task commits; skip if they keep the default phase-level behavior.
82252
+ Purpose: Create or ingest the implementation plan, apply QA gate selections after \`save_plan\`, enforce plan granularity, and run traceability checks.
82253
+
82254
+ ACTION: Load skill \`file:.opencode/skills/plan/SKILL.md\` immediately. Follow the protocol defined there.
82255
+
82256
+ HARD CONSTRAINTS (apply regardless of skill load success):
82257
+ - Use the \`save_plan\` tool as the primary plan writer. Required fields include \`title\`, \`swarm_id\`, and \`phases\` with concrete task descriptions.
82258
+ - Example call: save_plan({ title: "My Real Project", swarm_id: "mega", phases: [{ id: 1, name: "Setup", tasks: [{ id: "1.1", description: "Install dependencies and configure TypeScript", size: "small" }] }] })
82259
+
82260
+ - If \`save_plan\` is unavailable, delegate plan writing only after \`declare_scope\` covers \`.swarm/plan.md\`; the delegated output must be exact plan content.
82261
+ - A missing spec is a soft gate for external plan ingestion, but stale spec drift must be surfaced to the user before continuing.
82262
+ - Apply any \`## Pending QA Gate Selection\` only after \`save_plan\` succeeds; if no pending section exists, ask the full gate-selection, parallelization, and commit-frequency dialogue from the loaded skill before calling \`set_qa_gates\`.
82311
82263
  <!-- BEHAVIORAL_GUIDANCE_START -->
82312
- INLINE GATE SELECTION no pending section found in context.md. You MUST ask now.
82313
- "I'll call set_qa_gates with defaults and move on"
82314
- WRONG: set_qa_gates with assumed values is a gate violation. The user must answer first.
82315
- "The user provided a plan they know what gates they want"
82316
- WRONG: providing a plan is not the same as configuring gates. Always ask.
82264
+ INLINE GATE SELECTION -- no pending section found in context.md. You MUST ask now.
82265
+ x "I'll call set_qa_gates with defaults and move on"
82266
+ -> WRONG: set_qa_gates with assumed values is a gate violation. The user must answer first.
82267
+ x "The user provided a plan -- they know what gates they want"
82268
+ -> WRONG: providing a plan is not the same as configuring gates. Always ask.
82317
82269
 
82318
82270
  MANDATORY PAUSE: Present the gate question. Wait for the user's answer.
82319
82271
  Do NOT call \`set_qa_gates\` until the user has responded.
82320
82272
  <!-- BEHAVIORAL_GUIDANCE_END -->
82321
- Then call \`set_qa_gates\` with the user's chosen flags.
82322
- Either path must yield a persisted QA gate profile before the first task dispatches.
82323
-
82324
- ⚠️ If \`save_plan\` is unavailable, delegate plan writing to {{AGENT_PREFIX}}coder:
82325
- ⚠️ Even in this fallback, you MUST call \`declare_scope\` for ".swarm/plan.md" BEFORE the coder delegation. Scope discipline applies to plan-writing delegations too. See Rule 1a.
82326
- TASK: Write the implementation plan to .swarm/plan.md
82327
- OUTPUT: .swarm/plan.md
82328
- INPUT: [provide the complete plan content below]
82329
- CONSTRAINT: Write EXACTLY the content provided. Do not modify, summarize, or interpret.
82330
-
82331
- TASK GRANULARITY RULES:
82332
- - SMALL task: 1 file, 1 logical concern. Delegate as-is.
82333
- - MEDIUM task: 2-5 files within a single logical concern (e.g., implementation + test + type update). Delegate as-is.
82334
- - LARGE task: 6+ files OR multiple unrelated concerns. SPLIT into sequential single-file tasks before writing to plan. A LARGE task in the plan is a planning error — do not write oversized tasks to the plan.
82335
- - Litmus test: Can you describe this task in 3 bullet points? If not, it's too large. Split only when concerns are unrelated.
82336
- - Compound verbs are OK when they describe a single logical change: "add validation to handler and update its test" = 1 task. "implement auth and add logging and refactor config" = 3 tasks (unrelated concerns).
82337
- - Coder receives ONE task. You make ALL scope decisions in the plan. Coder makes zero scope decisions.
82338
-
82339
- TEST TASK DEDUPLICATION:
82340
- The QA gate (Stage B, step 5l) runs test_engineer-verification on EVERY implementation task.
82341
- This means tests are written, run, and verified as part of the gate — NOT as separate plan tasks.
82342
-
82343
- DO NOT create separate "write tests for X" or "add test coverage for X" tasks. They are redundant with the gate and waste execution budget.
82344
-
82345
- Research confirms this: controlled experiments across 6 LLMs (arXiv:2602.07900) found that large shifts in test-writing volume yielded only 0–2.6% resolution change while consuming 20–49% more tokens. The gate already enforces test quality; duplicating it in plan tasks adds cost without value.
82346
-
82347
- CREATE a dedicated test task ONLY when:
82348
- - The work is PURE test infrastructure (new fixtures, test helpers, mock factories, CI config) with no implementation
82349
- - Integration tests span multiple modules changed across different implementation tasks within the same phase
82350
- - Coverage is explicitly below threshold and the user requests a dedicated coverage pass
82351
-
82352
- If in doubt, do NOT create a test task. The gate handles it.
82353
- Note: this is prompt-level guidance for the architect's planning behavior, not a hard gate — the behavioral enforcement is that test_engineer already writes tests at the QA gate level.
82354
-
82355
- PHASE COUNT GUIDANCE:
82356
- - Plans with 5+ tasks SHOULD be split into at least 2 phases.
82357
- - Plans with 10+ tasks MUST be split into at least 3 phases.
82358
- - Each phase should be a coherent unit of work that can be reviewed and learned from
82359
- before proceeding to the next.
82360
- - Single-phase plans are acceptable ONLY for small projects (1-4 tasks).
82361
- - Rationale: Retrospectives at phase boundaries capture lessons that improve subsequent
82362
- phases. A single-phase plan gets zero iterative learning benefit.
82363
-
82364
- Also create .swarm/context.md with: decisions made, patterns identified, SME cache entries, and relevant file map.
82365
-
82366
- TRACEABILITY CHECK (run after plan is written, when spec.md exists):
82367
- - Every FR-### in spec.md MUST map to at least one task → unmapped FRs = coverage gap, flag to user
82368
- - Every task MUST reference its source FR-### in the description or acceptance field → tasks with no FR = potential gold-plating, flag to critic
82369
- - Report: "TRACEABILITY: <N> FRs mapped, <M> unmapped FRs (gap), <K> tasks with no FR mapping (gold-plating risk)"
82370
- - If no spec.md: skip this check silently.
82273
+ - Preserve task granularity, test task deduplication, phase count guidance, and TRACEABILITY CHECK rules from the loaded skill.
82371
82274
 
82372
82275
  ### MODE: CRITIC-GATE
82373
82276
  Delegate plan to {{AGENT_PREFIX}}critic for review BEFORE any implementation begins.
@@ -82420,187 +82323,24 @@ If resuming a project with an existing approved plan, CRITIC-GATE is already sat
82420
82323
  WAIT for them to run /swarm clarify or /swarm acknowledge-spec-drift.
82421
82324
 
82422
82325
  ### MODE: EXECUTE
82423
- For each task (respecting dependencies):
82424
-
82425
- RETRY PROTOCOL when returning to coder after any gate failure:
82426
- 1. Provide structured rejection: "GATE FAILED: [gate name] | REASON: [details] | REQUIRED FIX: [specific action required]"
82427
- 2. Re-enter at step 5b ({{AGENT_PREFIX}}coder) with full failure context
82428
- 3. Resume execution at the failed step (do not restart from 5a)
82429
- Exception: if coder modified files outside the original task scope, restart from step 5c
82430
- 4. Gates already PASSED may be skipped on retry if their input files are unchanged
82431
- 5. Print "Resuming at step [5X] after coder retry [N/{{QA_RETRY_LIMIT}}]" before re-executing
82432
-
82433
- GATE FAILURE RESPONSE RULES — when ANY gate returns a failure:
82434
- You MUST return to {{AGENT_PREFIX}}coder. You MUST NOT fix the code yourself.
82435
-
82436
- WRONG responses to gate failure:
82437
- ✗ Editing the file yourself to fix the syntax error
82438
- ✗ Running a tool to auto-fix and moving on without coder
82439
- ✗ "Installing" or "configuring" tools to work around the failure
82440
- ✗ Treating the failure as an environment issue and proceeding
82441
- ✗ Deciding the failure is a false positive and skipping the gate
82442
-
82443
- RIGHT response to gate failure:
82444
- ✓ Print "GATE FAILED: [gate name] | REASON: [details]"
82445
- ✓ BEFORE the retry delegation: call \`declare_scope\` with the file list the retry will touch. Re-declare even if the files are identical to the original task — retry scope persists per-call, not per-task. See Rule 1a.
82446
- ✓ Delegate to {{AGENT_PREFIX}}coder with:
82447
- TASK: Fix [gate name] failure
82448
- FILE: [affected file(s)]
82449
- INPUT: [exact error output from the gate]
82450
- CONSTRAINT: Fix ONLY the reported issue, do not modify other code
82451
- ✓ After coder returns, re-run the failed gate from the step that failed
82452
- ✓ Print "Coder attempt [N/{{QA_RETRY_LIMIT}}] on task [X.Y]"
82453
-
82454
- The ONLY exception: lint tool in fix mode (step 5g) auto-corrects by design.
82455
- All other gates: failure → return to coder. No self-fixes. No workarounds.
82456
-
82457
- 5a. **UI DESIGN GATE** (conditional — Rule 9): If task matches UI trigger → {{AGENT_PREFIX}}designer produces scaffold → pass scaffold to coder as INPUT. If no match → skip.
82458
-
82459
- → After step 5a (or immediately if no UI task applies): Call update_task_status with status in_progress for the current task. Then proceed to step 5b.
82460
-
82461
- 5a-bis. **DARK MATTER CO-CHANGE DETECTION**: After declaring scope but BEFORE finalizing the task file list, call knowledge_recall with query hidden-coupling primaryFile where primaryFile is the first file in the task's FILE list. Extract primaryFile from the task's FILE list (first file = primary). If results found, add those files to the task's AFFECTS scope with a BLAST RADIUS note. If no results or knowledge_recall unavailable, proceed gracefully without adding files. This is advisory — the architect may exclude files from scope if they are unrelated to the current task. Delegate to {{AGENT_PREFIX}}coder only after scope is declared.
82462
-
82463
- 5b-PRE (required): Call \`declare_scope({ taskId, files })\` with the EXACT file list for this task — including any co-change files surfaced by 5a-bis. Skipping this call will cause every coder write to be BLOCKED by scope-guard. No \`declare_scope\` → no 5b delegation. See Rule 1a.
82464
- 5b-BASE (required, once per task): Call \`sast_scan\` with \`{ capture_baseline: true, phase: <N>, changed_files: <files from 5b-PRE> }\` where \`<N>\` is the current phase number (extract from current task ID: task "3.2" → phase 3, task "1.5" → phase 1). The tool maintains \`.swarm/evidence/{phase}/sast-baseline.json\` as a phase-scoped, incrementally merged baseline of pre-existing SAST findings. Calling twice for the same files is safe (idempotent merge). Do NOT re-capture mid-task.
82465
- → REQUIRED: Print "sast-baseline: [WRITTEN — N fingerprints | MERGED — N fingerprints | SKIPPED — gate disabled | ERROR — details]"
82466
- → Subsequent \`pre_check_batch\` calls with \`phase: <N>\` will automatically diff against this baseline — only NEW findings (not in baseline) drive the fail verdict.
82467
- 5b. {{AGENT_PREFIX}}coder - Implement (if designer scaffold produced, include it as INPUT).
82468
- 5c. Run \`diff\` tool. If \`hasContractChanges\` → {{AGENT_PREFIX}}explorer integration analysis. If COMPATIBILITY SIGNALS=INCOMPATIBLE or MIGRATION_SURFACE=yes → coder retry. If COMPATIBILITY SIGNALS=COMPATIBLE and MIGRATION_SURFACE=no → proceed.
82469
- → REQUIRED: Print "diff: [PASS | CONTRACT CHANGE — details]"
82470
- 5d. Run \`syntax_check\` tool. SYNTACTIC ERRORS → return to coder. NO ERRORS → proceed to placeholder_scan.
82471
- → REQUIRED: Print "syntaxcheck: [PASS | FAIL — N errors]"
82472
- 5e. Run \`placeholder_scan\` tool. PLACEHOLDER FINDINGS → return to coder. NO FINDINGS → proceed to imports.
82473
- → REQUIRED: Print "placeholderscan: [PASS | FAIL — N findings]"
82474
- 5f. Run \`imports\` tool for dependency audit. ISSUES → return to coder.
82475
- → REQUIRED: Print "imports: [PASS | ISSUES — details]"
82476
- 5g. Run \`lint\` tool with fix mode for auto-fixes. If issues remain → run \`lint\` tool with check mode. FAIL → return to coder.
82477
- → REQUIRED: Print "lint: [PASS | FAIL — details]"
82478
- 5h. Run \`build_check\` tool. BUILD FAILS → return to coder. SUCCESS → proceed to pre_check_batch.
82479
- → REQUIRED: Print "buildcheck: [PASS | FAIL | SKIPPED — no toolchain]"
82480
- 5i. Run \`pre_check_batch\` tool with \`phase: <N>\` (same phase number used in 5b-BASE) → runs four verification tools in parallel (max 4 concurrent):
82481
- - lint:check (code quality verification)
82482
- - secretscan (secret detection)
82483
- - sast_scan (static security analysis — diffs against phase baseline when phase provided)
82484
- - quality_budget (maintainability metrics)
82485
- → Returns { gates_passed, lint, secretscan, sast_scan, quality_budget, total_duration_ms }
82486
- → sast_scan result may include { new_findings, pre_existing_findings, baseline_used } when baseline diff is active.
82487
- → If ALL FOUR tools have ran === false (lint.ran === false && secretscan.ran === false && sast_scan.ran === false && quality_budget.ran === false):
82488
- → This is a SKIP - no tools actually ran. Print "pre_check_batch: SKIP — all tools ran===false (no files to check or tools not available)" and proceed to {{AGENT_PREFIX}}reviewer.
82489
- → Else if gates_passed === false: read individual tool results, identify which tool(s) failed, return structured rejection to {{AGENT_PREFIX}}coder with specific tool failures. Do NOT call {{AGENT_PREFIX}}reviewer.
82490
- → If gates_passed === true AND sast_preexisting_findings is present: proceed to {{AGENT_PREFIX}}reviewer. Include the pre-existing SAST findings in the reviewer delegation context with instruction: "SAST TRIAGE REQUIRED: The following SAST findings existed before this task began (from phase baseline or unchanged lines). Verify these are acceptable pre-existing conditions and do not interact with the new changes." Do NOT return to coder for pre-existing findings.
82491
- → If gates_passed === true (no sast_preexisting_findings): proceed to {{AGENT_PREFIX}}reviewer.
82492
- → REQUIRED: Print "pre_check_batch: [PASS — all gates passed | PASS — pre-existing SAST findings (N findings, reviewer triage) | FAIL — [gate]: [details]]"
82493
-
82494
- ⚠️ pre_check_batch SCOPE BOUNDARY:
82495
- pre_check_batch runs FOUR automated tools: lint:check, secretscan, sast_scan, quality_budget.
82496
- pre_check_batch does NOT run and does NOT replace:
82497
- - {{AGENT_PREFIX}}reviewer (logic review, correctness, edge cases, maintainability)
82498
- - {{AGENT_PREFIX}}reviewer security-only pass (OWASP evaluation, auth/crypto review)
82499
- - {{AGENT_PREFIX}}test_engineer verification tests (functional correctness)
82500
- - {{AGENT_PREFIX}}test_engineer adversarial tests (attack vectors, boundary violations)
82501
- - diff tool (contract change detection)
82502
- - placeholder_scan (TODO/stub detection)
82503
- - imports (dependency audit)
82504
- gates_passed: true means "automated static checks passed."
82505
- It does NOT mean "code is reviewed." It does NOT mean "code is tested."
82506
- After pre_check_batch passes, you MUST STILL delegate to {{AGENT_PREFIX}}reviewer.
82507
- Treating pre_check_batch as a substitute for {{AGENT_PREFIX}}reviewer is a PROCESS VIOLATION.
82508
-
82509
- 5j. {{AGENT_PREFIX}}reviewer - General review. REJECTED (< {{QA_RETRY_LIMIT}}) → coder retry. REJECTED ({{QA_RETRY_LIMIT}}) → escalate.
82510
- → REQUIRED: Print "reviewer: [APPROVED | REJECTED — reason]"
82511
- 5k. Security gate: if change matches TIER 3 criteria OR content contains SECURITY_KEYWORDS OR secretscan has ANY findings OR sast_scan has ANY findings at or above threshold → MUST delegate {{AGENT_PREFIX}}reviewer security-only review. REJECTED (< {{QA_RETRY_LIMIT}}) → coder retry. REJECTED ({{QA_RETRY_LIMIT}}) → escalate to user.
82512
- → REQUIRED: Print "security-reviewer: [TRIGGERED | NOT TRIGGERED — reason]"
82513
- → If TRIGGERED: Print "security-reviewer: [APPROVED | REJECTED — reason]"
82514
- 5l. {{AGENT_PREFIX}}test_engineer - Verification tests. FAIL → coder retry from 5g.
82515
- → REQUIRED: Print "testengineer-verification: [PASS N/N | FAIL — details]"
82516
- 5l-bis. REGRESSION SWEEP (automatic after test_engineer-verification PASS):
82517
- Run test_runner with { scope: "graph", files: [<all source files changed by coder in this task>] }.
82518
- scope:"graph" traces imports to discover test files beyond the task's own tests that may be affected by this change.
82519
-
82520
- Outcomes (based on test_runner result.outcome field):
82521
- - outcome: "pass" → All tests passed. Print "regression-sweep: PASS [N additional tests, M files]"
82522
- - outcome: "regression" → Tests ran but some failed. Print "regression-sweep: FAIL — REGRESSION DETECTED in [files]. The failing tests are CORRECT — fix the source code, not the tests." Return to coder with retry from 5g.
82523
- - outcome: "skip" → No test files resolved (nothing to run). Print "regression-sweep: SKIPPED — no related tests beyond task scope"
82524
- - outcome: "scope_exceeded" → Too many files for graph scope. Print "regression-sweep: SKIPPED — broad scope, no related tests beyond task scope"
82525
- - outcome: "error" → Tool error (timeout, no framework, etc.). Print "regression-sweep: SKIPPED — test_runner error" and continue pipeline.
82526
-
82527
- IMPORTANT: The regression sweep runs test_runner DIRECTLY (architect calls the tool). Do NOT delegate to test_engineer for this — the test_engineer's EXECUTION BOUNDARY restricts it to its own test files. The architect has unrestricted test_runner access.
82528
- → REQUIRED: Print "regression-sweep: [PASS | FAIL — REGRESSION DETECTED | SKIPPED — no related tests | SKIPPED — broad scope | SKIPPED — test_runner error]"
82529
-
82530
- 5l-ter. TEST DRIFT CHECK (conditional): Run this step if the change involves any drift-prone area:
82531
- - Command/CLI behavior changed (shell command wrappers, CLI interfaces)
82532
- - Parsing or routing logic changed (argument parsing, route matching, file resolution)
82533
- - User-visible output changed (formatted output, error messages, JSON response structure)
82534
- - Public contracts or schemas changed (API types, tool argument schemas, return types)
82535
- - Assertion-heavy areas where output strings are tested (command/help output tests, error message tests)
82536
- - Helper behavior or lifecycle semantics changed (state machines, lifecycle hooks, initialization)
82537
-
82538
- If NOT triggered: Print "test-drift: NOT TRIGGERED — no drift-prone change detected"
82539
- If TRIGGERED:
82540
- - Use grep/search to find test files that cover the affected functionality
82541
- - Run those tests via test_runner with scope:"convention" on the related test files
82542
- - If any FAIL → print "test-drift: DRIFT DETECTED in [N] tests" and escalate to reviewer/test_engineer
82543
- - If all PASS → print "test-drift: [N] related tests verified"
82544
- - If no related tests found → print "test-drift: NO RELATED TESTS FOUND" (not a failure)
82545
- → REQUIRED: Print "test-drift: [TRIGGERED | NOT TRIGGERED — reason]" and "[DRIFT DETECTED in N tests | N related tests verified | NO RELATED TESTS FOUND | NOT TRIGGERED]"
82546
-
82547
- 5n. TODO SCAN (advisory): Call todo_extract with paths=[list of files changed in this task]. If any results have priority HIGH → print "todo-scan: WARN — N high-priority TODOs in changed files: [list of TODO texts]". If no high-priority results → print "todo-scan: CLEAN". This is advisory only and does NOT block the pipeline.
82548
- → REQUIRED: Print "todo-scan: [WARN — N high-priority TODOs | CLEAN]"
82549
-
82550
- {{ADVERSARIAL_TEST_STEP}}
82551
- 5n. COVERAGE CHECK: If {{AGENT_PREFIX}}test_engineer reports coverage < 70% → delegate {{AGENT_PREFIX}}test_engineer for an additional test pass targeting uncovered paths. This is a soft guideline; use judgment for trivial tasks.
82552
-
82553
- PRE-COMMIT RULE — Before ANY commit or push:
82554
- You MUST answer YES to ALL of the following:
82555
- [ ] Did {{AGENT_PREFIX}}reviewer run and return APPROVED? (not "I reviewed it" — the agent must have run)
82556
- [ ] Did {{AGENT_PREFIX}}test_engineer run and return PASS? (not "the code looks correct" — the agent must have run)
82557
- [ ] Did pre_check_batch run with gates_passed true?
82558
- [ ] Did the diff step run?
82559
- [ ] Did regression-sweep run (or SKIP with no related tests or test_runner error)?
82560
- [ ] Did test-drift check run (or NOT TRIGGERED)?
82561
-
82562
- If ANY box is unchecked: DO NOT COMMIT. Return to step 5b.
82563
- There is no override. A commit without a completed QA gate is a workflow violation.
82564
-
82565
- ## ROLE-BOUNDARY CHANGE VALIDATION (mandatory for prompt changes)
82566
- When a task modifies agent prompts (especially explorer, reviewer, critic, or any agent involved in the mapper/validator/challenge hierarchy), add an explicit test validation step:
82567
- - If new prompt contract tests exist (e.g., explorer-role-boundary.test.ts, explorer-consumer-contract.test.ts): Run them via test_runner
82568
- - If no specific tests exist for the changed prompt: Run test_runner with scope "convention" on the changed file
82569
- - Verify the new tests pass before completing the task
82570
-
82571
- This step supplements (not replaces) the existing regression-sweep and test-drift checks. It exists to catch prompt contract regressions that automated gates might miss.
82572
-
82573
- 5o. ⛔ TASK COMPLETION GATE — You MUST print this checklist with filled values before marking ✓ in .swarm/plan.md:
82574
- [TOOL] diff: PASS / SKIP — value: ___
82575
- [TOOL] syntax_check: PASS — value: ___
82576
- [TOOL] placeholder_scan: PASS — value: ___
82577
- [TOOL] imports: PASS — value: ___
82578
- [TOOL] lint: PASS — value: ___
82579
- [TOOL] build_check: PASS / SKIPPED — value: ___
82580
- [TOOL] pre_check_batch: PASS (lint:check ✓ secretscan ✓ sast_scan ✓ quality_budget ✓) — value: ___
82581
- [GATE] reviewer: APPROVED — value: ___
82582
- [GATE] reuse_re_verification: VERIFIED / SKIPPED / DUPLICATION_DETECTED — value: ___
82583
- [GATE] security-reviewer: APPROVED / SKIPPED — value: ___
82584
- [GATE] test_engineer-verification: PASS — value: ___
82585
- [GATE] regression-sweep: PASS / SKIPPED — value: ___
82586
- [GATE] test-drift: TRIGGERED / NOT TRIGGERED — value: ___
82587
- {{ADVERSARIAL_TEST_CHECKLIST}}
82588
- [GATE] coverage: ≥70% / soft-skip — value: ___
82589
-
82590
- You MUST NOT mark a task complete without printing this checklist with filled values.
82591
- You MUST NOT fill "PASS" or "APPROVED" for a gate you did not actually run — that is fabrication.
82592
- Any blank "value: ___" field = gate was not run = task is NOT complete.
82593
- Filling this checklist from memory ("I think I ran it") is INVALID. Each value must come from actual tool/agent output in this session.
82594
-
82595
- 5p. Call update_task_status with status "completed".
82596
- 5q. OPTIONAL TASK-COMPLETION COMMIT POLICY: read \`.swarm/context.md\`.
82597
- - If \`## Task Completion Commit Policy\` contains \`commit_after_each_completed_task: true\`, immediately call:
82598
- \`checkpoint save task-<task-id>-complete\`
82599
- - If the section is absent or false, skip this step.
82600
- - This optional commit policy NEVER bypasses PRE-COMMIT RULE checks above.
82601
- - If checkpoint save fails with "duplicate label", the task was already checkpointed from a prior completion or retry. Silently skip — the existing checkpoint is valid.
82602
- 5r. Proceed to next task.
82326
+ Activates when: MODE: CRITIC-GATE has approved a complete plan, or an existing approved plan is being resumed for implementation.
82327
+
82328
+ Purpose: Execute plan tasks through coder delegation, quality gates, retry handling, evidence capture, and task completion updates.
82603
82329
 
82330
+ ACTION: Load skill \`file:.opencode/skills/execute/SKILL.md\` immediately. Follow the protocol defined there.
82331
+
82332
+ HARD CONSTRAINTS (apply regardless of skill load success):
82333
+ - For each task, respect dependencies and delegate implementation to \`{{AGENT_PREFIX}}coder\`; do not self-fix ordinary gate failures.
82334
+ - Before coder implementation or retry, call \`declare_scope({ taskId, files })\` with the exact files the coder may touch.
82335
+ - On any gate failure, return to \`{{AGENT_PREFIX}}coder\` with structured rejection: \`GATE FAILED: [gate name] | REASON: [details] | REQUIRED FIX: [specific action required]\`.
82336
+ - Required per-task gates include automated checks, reviewer gates, verification tests, regression sweep, test drift, TODO scan, and coverage guidance as detailed in the loaded skill.
82337
+ - Pre-commit constraint: do not commit or push unless reviewer, test_engineer, pre_check_batch, diff, regression-sweep, and test-drift have actually run or skipped according to the loaded protocol.
82338
+ - ROLE-BOUNDARY CHANGE VALIDATION is mandatory for prompt changes; run the focused prompt contract tests or convention tests for changed prompt files.
82339
+ - TASK COMPLETION GATE: Completion checklist must be printed with filled values before marking a task complete. It includes regression-sweep and test-drift entries; blank \`value: ___\` fields mean the task is not complete.
82340
+ - Config-specific adversarial test step rendered from plugin config:
82341
+ {{ADVERSARIAL_TEST_STEP}}
82342
+ - Config-specific adversarial checklist entry rendered from plugin config:
82343
+ {{ADVERSARIAL_TEST_CHECKLIST}}
82604
82344
  ## ⛔ RETROSPECTIVE GATE
82605
82345
 
82606
82346
  **MANDATORY before calling phase_complete.** You MUST write a retrospective evidence bundle BEFORE calling \`phase_complete\`. The tool will return \`{status: 'blocked', reason: 'RETROSPECTIVE_MISSING'}\` if you skip this step.
@@ -84491,6 +84231,7 @@ API: [exact names/signatures/versions to use]
84491
84231
  PLATFORM: [cross-platform notes if OS-interaction APIs]
84492
84232
  GOTCHAS: [common pitfalls or edge cases]
84493
84233
  DEPS: [required dependencies/tools]
84234
+ EVIDENCE_REFS: [cite evidence-cache:<id>, URL, file, or doc refs used; use "none" if no external evidence was available]
84494
84235
 
84495
84236
  ## DOMAIN CHECKLISTS
84496
84237
  Apply the relevant checklist when the DOMAIN matches:
@@ -84529,7 +84270,8 @@ Cache lookup steps:
84529
84270
  1. If \`.swarm/context.md\` does not exist: proceed with fresh research.
84530
84271
  2. If the \`## Research Sources\` section is absent: proceed with fresh research.
84531
84272
  3. If URL/topic IS listed in ## Research Sources: reuse cached summary — no re-fetch needed.
84532
- 4. If cache miss (URL/topic not listed): fetch URL, then append this line at the end of your response:
84273
+ 4. If fresh search/API-doc/crawl evidence is provided, cite its \`evidence-cache:<id>\` refs in EVIDENCE_REFS. Raw docs/search snippets are evidence, not memory.
84274
+ 5. If cache miss (URL/topic not listed): fetch URL, then append this line at the end of your response:
84533
84275
  CACHE-UPDATE: [YYYY-MM-DD] | [URL or topic] | [one-line summary of finding]
84534
84276
  The Architect will save this line to .swarm/context.md ## Research Sources. Do NOT write to any file yourself.
84535
84277
 
@@ -90248,11 +89990,11 @@ var init_curator_drift = __esm(() => {
90248
89990
  var exports_project_context = {};
90249
89991
  __export(exports_project_context, {
90250
89992
  buildProjectContext: () => buildProjectContext,
90251
- _internals: () => _internals64,
89993
+ _internals: () => _internals65,
90252
89994
  LANG_BACKEND_DETECTION_TIMEOUT_MS: () => LANG_BACKEND_DETECTION_TIMEOUT_MS
90253
89995
  });
90254
89996
  import * as fs115 from "node:fs";
90255
- import * as path154 from "node:path";
89997
+ import * as path155 from "node:path";
90256
89998
  function detectFileExists2(directory, pattern) {
90257
89999
  if (pattern.includes("*") || pattern.includes("?")) {
90258
90000
  try {
@@ -90264,7 +90006,7 @@ function detectFileExists2(directory, pattern) {
90264
90006
  }
90265
90007
  }
90266
90008
  try {
90267
- fs115.accessSync(path154.join(directory, pattern));
90009
+ fs115.accessSync(path155.join(directory, pattern));
90268
90010
  return true;
90269
90011
  } catch {
90270
90012
  return false;
@@ -90273,7 +90015,7 @@ function detectFileExists2(directory, pattern) {
90273
90015
  function selectTestCommandFromScriptsTest(backend, directory) {
90274
90016
  let pkgRaw;
90275
90017
  try {
90276
- pkgRaw = fs115.readFileSync(path154.join(directory, "package.json"), "utf-8");
90018
+ pkgRaw = fs115.readFileSync(path155.join(directory, "package.json"), "utf-8");
90277
90019
  } catch {
90278
90020
  return null;
90279
90021
  }
@@ -90332,7 +90074,7 @@ function selectLintCommand(backend, directory) {
90332
90074
  return null;
90333
90075
  }
90334
90076
  async function buildProjectContext(directory) {
90335
- const backend = await _internals64.pickBackend(directory);
90077
+ const backend = await _internals65.pickBackend(directory);
90336
90078
  if (!backend)
90337
90079
  return null;
90338
90080
  const ctx = emptyProjectContext();
@@ -90363,16 +90105,16 @@ async function buildProjectContext(directory) {
90363
90105
  if (backend.prompts.reviewerChecklist.length > 0) {
90364
90106
  ctx.REVIEWER_CHECKLIST = bulletList(backend.prompts.reviewerChecklist);
90365
90107
  }
90366
- const profiles = _internals64.pickedProfiles(directory);
90108
+ const profiles = _internals65.pickedProfiles(directory);
90367
90109
  if (profiles.length > 1) {
90368
90110
  ctx.PROJECT_CONTEXT_SECONDARY_LANGUAGES = profiles.slice(1).map((p) => p.id).join(", ");
90369
90111
  }
90370
90112
  return ctx;
90371
90113
  }
90372
- var LANG_BACKEND_DETECTION_TIMEOUT_MS = 300, _internals64;
90114
+ var LANG_BACKEND_DETECTION_TIMEOUT_MS = 300, _internals65;
90373
90115
  var init_project_context = __esm(() => {
90374
90116
  init_dispatch();
90375
- _internals64 = {
90117
+ _internals65 = {
90376
90118
  pickBackend,
90377
90119
  pickedProfiles
90378
90120
  };
@@ -90382,7 +90124,7 @@ var init_project_context = __esm(() => {
90382
90124
  init_package();
90383
90125
  init_agents2();
90384
90126
  init_critic();
90385
- import * as path155 from "node:path";
90127
+ import * as path156 from "node:path";
90386
90128
 
90387
90129
  // src/background/index.ts
90388
90130
  init_event_bus();
@@ -90591,9 +90333,9 @@ class PlanSyncWorker {
90591
90333
  try {
90592
90334
  log("[PlanSyncWorker] Syncing plan...");
90593
90335
  this.checkForUnauthorizedWrite();
90594
- const plan = await this.withTimeout(loadPlanJsonOnly(this.directory), this.syncTimeoutMs, "Sync operation timed out");
90336
+ const plan = await this.withTimeout(_internals6.loadPlanJsonOnly(this.directory), this.syncTimeoutMs, "Sync operation timed out");
90595
90337
  if (plan && plan.phases.length > 0) {
90596
- await regeneratePlanMarkdown(this.directory, plan);
90338
+ await _internals6.regeneratePlanMarkdown(this.directory, plan);
90597
90339
  log("[PlanSyncWorker] Sync complete", {
90598
90340
  title: plan.title,
90599
90341
  phase: plan.current_phase
@@ -103989,6 +103731,9 @@ function deserializeAgentSession(s) {
103989
103731
  }
103990
103732
  const windows = {};
103991
103733
  for (const [key, win] of Object.entries(s.windows ?? {})) {
103734
+ if (!win || typeof win !== "object") {
103735
+ continue;
103736
+ }
103992
103737
  windows[key] = {
103993
103738
  ...win,
103994
103739
  transientRetryCount: "transientRetryCount" in win ? win.transientRetryCount ?? 0 : 0
@@ -123562,6 +123307,83 @@ function createWebSearchProvider(config3) {
123562
123307
  }
123563
123308
  }
123564
123309
 
123310
+ // src/evidence/documents.ts
123311
+ init_utils2();
123312
+ init_redaction();
123313
+ import { createHash as createHash12 } from "node:crypto";
123314
+ import { appendFile as appendFile13, mkdir as mkdir24 } from "node:fs/promises";
123315
+ import * as path150 from "node:path";
123316
+ var EVIDENCE_CACHE_FILE = "evidence-cache/documents.jsonl";
123317
+ var MAX_EVIDENCE_TEXT_LENGTH = 4000;
123318
+ async function writeEvidenceDocuments(directory, inputs, now = () => new Date) {
123319
+ const filePath = validateSwarmPath(directory, EVIDENCE_CACHE_FILE);
123320
+ const capturedAt = now().toISOString();
123321
+ const records = inputs.map((input) => createEvidenceDocumentRecord(input, capturedAt)).filter((record3) => record3 !== null);
123322
+ if (records.length > 0) {
123323
+ await mkdir24(path150.dirname(filePath), { recursive: true });
123324
+ await appendFile13(filePath, `${records.map((record3) => JSON.stringify(record3)).join(`
123325
+ `)}
123326
+ `, "utf-8");
123327
+ }
123328
+ return {
123329
+ path: ".swarm/evidence-cache/documents.jsonl",
123330
+ records,
123331
+ refs: records.map((record3) => record3.ref)
123332
+ };
123333
+ }
123334
+ function createEvidenceDocumentRecord(input, defaultCapturedAt) {
123335
+ const text = normalizeEvidenceText(input.text ?? input.snippet ?? "");
123336
+ if (!text)
123337
+ return null;
123338
+ const capturedAt = input.capturedAt ?? defaultCapturedAt;
123339
+ const base = {
123340
+ sourceType: input.sourceType,
123341
+ query: normalizeOptional(input.query),
123342
+ title: normalizeOptional(input.title),
123343
+ url: normalizeOptional(input.url),
123344
+ text
123345
+ };
123346
+ const id = createEvidenceDocumentId(base);
123347
+ return {
123348
+ id,
123349
+ ref: `evidence-cache:${id}`,
123350
+ ...base,
123351
+ capturedAt,
123352
+ createdBy: normalizeOptional(input.createdBy),
123353
+ metadata: input.metadata ?? {}
123354
+ };
123355
+ }
123356
+ function createEvidenceDocumentId(input) {
123357
+ const hash3 = createHash12("sha256").update([
123358
+ input.sourceType,
123359
+ input.query ?? "",
123360
+ input.title ?? "",
123361
+ input.url ?? "",
123362
+ input.text
123363
+ ].join(`
123364
+ `)).digest("hex");
123365
+ return `evd_${hash3.slice(0, 16)}`;
123366
+ }
123367
+ function normalizeEvidenceText(text) {
123368
+ const normalized = redactSecrets(text.replace(/\s+/g, " ").trim());
123369
+ return truncateEvidenceText(normalized, MAX_EVIDENCE_TEXT_LENGTH);
123370
+ }
123371
+ function truncateEvidenceText(text, maxLength) {
123372
+ if (text.length <= maxLength)
123373
+ return text;
123374
+ const truncated = text.slice(0, maxLength);
123375
+ const lastPlaceholderStart = truncated.lastIndexOf("[REDACTED:");
123376
+ const lastPlaceholderEnd = truncated.lastIndexOf("]");
123377
+ if (lastPlaceholderStart > lastPlaceholderEnd) {
123378
+ return truncated.slice(0, lastPlaceholderStart).trimEnd();
123379
+ }
123380
+ return truncated;
123381
+ }
123382
+ function normalizeOptional(value) {
123383
+ const normalized = value?.replace(/\s+/g, " ").trim();
123384
+ return normalized ? redactSecrets(normalized) : undefined;
123385
+ }
123386
+
123565
123387
  // src/tools/web-search.ts
123566
123388
  init_create_tool();
123567
123389
  init_resolve_working_directory();
@@ -123622,6 +123444,7 @@ var web_search = createSwarmTool({
123622
123444
  }
123623
123445
  try {
123624
123446
  const results = await provider.search(parsed.data.query, maxResults);
123447
+ const evidence = await captureSearchEvidence(dirResult.directory, parsed.data.query, results);
123625
123448
  const ok2 = {
123626
123449
  success: true,
123627
123450
  query: parsed.data.query,
@@ -123629,8 +123452,15 @@ var web_search = createSwarmTool({
123629
123452
  results: results.map(({ title, url: url3, snippet }) => ({
123630
123453
  title,
123631
123454
  url: url3,
123632
- snippet
123633
- }))
123455
+ snippet,
123456
+ evidenceRef: evidence.refByUrl.get(url3)
123457
+ })),
123458
+ evidence: {
123459
+ stored: evidence.stored,
123460
+ path: evidence.path,
123461
+ refs: evidence.refs,
123462
+ error: evidence.error
123463
+ }
123634
123464
  };
123635
123465
  return JSON.stringify(ok2, null, 2);
123636
123466
  } catch (err3) {
@@ -123643,6 +123473,39 @@ var web_search = createSwarmTool({
123643
123473
  }
123644
123474
  }
123645
123475
  });
123476
+ async function captureSearchEvidence(directory, query, results) {
123477
+ try {
123478
+ const written = await _internals64.writeEvidenceDocuments(directory, results.map((result) => ({
123479
+ sourceType: "web_search",
123480
+ query,
123481
+ title: result.title,
123482
+ url: result.url,
123483
+ snippet: result.snippet,
123484
+ createdBy: "web_search"
123485
+ })));
123486
+ const refByUrl = new Map;
123487
+ for (const record3 of written.records) {
123488
+ if (record3.url)
123489
+ refByUrl.set(record3.url, record3.ref);
123490
+ }
123491
+ return {
123492
+ stored: written.records.length > 0,
123493
+ path: written.path,
123494
+ refs: written.refs,
123495
+ refByUrl
123496
+ };
123497
+ } catch (err3) {
123498
+ return {
123499
+ stored: false,
123500
+ refs: [],
123501
+ refByUrl: new Map,
123502
+ error: err3 instanceof Error ? err3.message : String(err3)
123503
+ };
123504
+ }
123505
+ }
123506
+ var _internals64 = {
123507
+ writeEvidenceDocuments
123508
+ };
123646
123509
  // src/tools/write-drift-evidence.ts
123647
123510
  init_zod();
123648
123511
  init_qa_gate_profile();
@@ -123651,7 +123514,7 @@ init_ledger();
123651
123514
  init_manager();
123652
123515
  init_create_tool();
123653
123516
  import fs111 from "node:fs";
123654
- import path150 from "node:path";
123517
+ import path151 from "node:path";
123655
123518
  function normalizeVerdict(verdict) {
123656
123519
  switch (verdict) {
123657
123520
  case "APPROVED":
@@ -123699,7 +123562,7 @@ async function executeWriteDriftEvidence(args2, directory) {
123699
123562
  entries: [evidenceEntry]
123700
123563
  };
123701
123564
  const filename = "drift-verifier.json";
123702
- const relativePath = path150.join("evidence", String(phase), filename);
123565
+ const relativePath = path151.join("evidence", String(phase), filename);
123703
123566
  let validatedPath;
123704
123567
  try {
123705
123568
  validatedPath = validateSwarmPath(directory, relativePath);
@@ -123710,10 +123573,10 @@ async function executeWriteDriftEvidence(args2, directory) {
123710
123573
  message: error93 instanceof Error ? error93.message : "Failed to validate path"
123711
123574
  }, null, 2);
123712
123575
  }
123713
- const evidenceDir = path150.dirname(validatedPath);
123576
+ const evidenceDir = path151.dirname(validatedPath);
123714
123577
  try {
123715
123578
  await fs111.promises.mkdir(evidenceDir, { recursive: true });
123716
- const tempPath = path150.join(evidenceDir, `.${filename}.tmp`);
123579
+ const tempPath = path151.join(evidenceDir, `.${filename}.tmp`);
123717
123580
  await fs111.promises.writeFile(tempPath, JSON.stringify(evidenceContent, null, 2), "utf-8");
123718
123581
  await fs111.promises.rename(tempPath, validatedPath);
123719
123582
  let snapshotInfo;
@@ -123809,7 +123672,7 @@ var write_drift_evidence = createSwarmTool({
123809
123672
  init_zod();
123810
123673
  init_loader();
123811
123674
  import fs112 from "node:fs";
123812
- import path151 from "node:path";
123675
+ import path152 from "node:path";
123813
123676
  init_utils2();
123814
123677
  init_manager();
123815
123678
  init_create_tool();
@@ -123897,7 +123760,7 @@ async function executeWriteFinalCouncilEvidence(args2, directory) {
123897
123760
  timestamp: synthesis.timestamp
123898
123761
  };
123899
123762
  const filename = "final-council.json";
123900
- const relativePath = path151.join("evidence", filename);
123763
+ const relativePath = path152.join("evidence", filename);
123901
123764
  let validatedPath;
123902
123765
  try {
123903
123766
  validatedPath = validateSwarmPath(directory, relativePath);
@@ -123911,10 +123774,10 @@ async function executeWriteFinalCouncilEvidence(args2, directory) {
123911
123774
  const evidenceContent = {
123912
123775
  entries: [evidenceEntry]
123913
123776
  };
123914
- const evidenceDir = path151.dirname(validatedPath);
123777
+ const evidenceDir = path152.dirname(validatedPath);
123915
123778
  try {
123916
123779
  await fs112.promises.mkdir(evidenceDir, { recursive: true });
123917
- const tempPath = path151.join(evidenceDir, `.${filename}.tmp`);
123780
+ const tempPath = path152.join(evidenceDir, `.${filename}.tmp`);
123918
123781
  await fs112.promises.writeFile(tempPath, JSON.stringify(evidenceContent, null, 2), "utf-8");
123919
123782
  await fs112.promises.rename(tempPath, validatedPath);
123920
123783
  return JSON.stringify({
@@ -123973,7 +123836,7 @@ init_zod();
123973
123836
  init_utils2();
123974
123837
  init_create_tool();
123975
123838
  import fs113 from "node:fs";
123976
- import path152 from "node:path";
123839
+ import path153 from "node:path";
123977
123840
  function normalizeVerdict2(verdict) {
123978
123841
  switch (verdict) {
123979
123842
  case "APPROVED":
@@ -124021,7 +123884,7 @@ async function executeWriteHallucinationEvidence(args2, directory) {
124021
123884
  entries: [evidenceEntry]
124022
123885
  };
124023
123886
  const filename = "hallucination-guard.json";
124024
- const relativePath = path152.join("evidence", String(phase), filename);
123887
+ const relativePath = path153.join("evidence", String(phase), filename);
124025
123888
  let validatedPath;
124026
123889
  try {
124027
123890
  validatedPath = validateSwarmPath(directory, relativePath);
@@ -124032,10 +123895,10 @@ async function executeWriteHallucinationEvidence(args2, directory) {
124032
123895
  message: error93 instanceof Error ? error93.message : "Failed to validate path"
124033
123896
  }, null, 2);
124034
123897
  }
124035
- const evidenceDir = path152.dirname(validatedPath);
123898
+ const evidenceDir = path153.dirname(validatedPath);
124036
123899
  try {
124037
123900
  await fs113.promises.mkdir(evidenceDir, { recursive: true });
124038
- const tempPath = path152.join(evidenceDir, `.${filename}.tmp`);
123901
+ const tempPath = path153.join(evidenceDir, `.${filename}.tmp`);
124039
123902
  await fs113.promises.writeFile(tempPath, JSON.stringify(evidenceContent, null, 2), "utf-8");
124040
123903
  await fs113.promises.rename(tempPath, validatedPath);
124041
123904
  return JSON.stringify({
@@ -124084,7 +123947,7 @@ init_zod();
124084
123947
  init_utils2();
124085
123948
  init_create_tool();
124086
123949
  import fs114 from "node:fs";
124087
- import path153 from "node:path";
123950
+ import path154 from "node:path";
124088
123951
  function normalizeVerdict3(verdict) {
124089
123952
  switch (verdict) {
124090
123953
  case "PASS":
@@ -124158,7 +124021,7 @@ async function executeWriteMutationEvidence(args2, directory) {
124158
124021
  entries: [evidenceEntry]
124159
124022
  };
124160
124023
  const filename = "mutation-gate.json";
124161
- const relativePath = path153.join("evidence", String(phase), filename);
124024
+ const relativePath = path154.join("evidence", String(phase), filename);
124162
124025
  let validatedPath;
124163
124026
  try {
124164
124027
  validatedPath = validateSwarmPath(directory, relativePath);
@@ -124169,10 +124032,10 @@ async function executeWriteMutationEvidence(args2, directory) {
124169
124032
  message: error93 instanceof Error ? error93.message : "Failed to validate path"
124170
124033
  }, null, 2);
124171
124034
  }
124172
- const evidenceDir = path153.dirname(validatedPath);
124035
+ const evidenceDir = path154.dirname(validatedPath);
124173
124036
  try {
124174
124037
  await fs114.promises.mkdir(evidenceDir, { recursive: true });
124175
- const tempPath = path153.join(evidenceDir, `.${filename}.tmp`);
124038
+ const tempPath = path154.join(evidenceDir, `.${filename}.tmp`);
124176
124039
  await fs114.promises.writeFile(tempPath, JSON.stringify(evidenceContent, null, 2), "utf-8");
124177
124040
  await fs114.promises.rename(tempPath, validatedPath);
124178
124041
  return JSON.stringify({
@@ -124522,7 +124385,7 @@ async function initializeOpenCodeSwarm(ctx) {
124522
124385
  const { PreflightTriggerManager: PTM } = await Promise.resolve().then(() => (init_trigger(), exports_trigger));
124523
124386
  preflightTriggerManager = new PTM(automationConfig);
124524
124387
  const { AutomationStatusArtifact: ASA } = await Promise.resolve().then(() => (init_status_artifact(), exports_status_artifact));
124525
- const swarmDir = path155.resolve(ctx.directory, ".swarm");
124388
+ const swarmDir = path156.resolve(ctx.directory, ".swarm");
124526
124389
  statusArtifact = new ASA(swarmDir);
124527
124390
  statusArtifact.updateConfig(automationConfig.mode, automationConfig.capabilities);
124528
124391
  if (automationConfig.capabilities?.evidence_auto_summaries === true) {
@@ -125126,7 +124989,7 @@ ${promptRaw}`;
125126
124989
  "ci-failure-resolver": "CI/CD failure resolution"
125127
124990
  };
125128
124991
  const skillPaths = topSkills.map((s) => {
125129
- const dirName = path155.basename(path155.dirname(s.skillPath));
124992
+ const dirName = path156.basename(path156.dirname(s.skillPath));
125130
124993
  const desc = SKILL_DESCRIPTIONS[dirName] ?? dirName;
125131
124994
  return `file:${s.skillPath} (-- ${desc})`;
125132
124995
  }).join(", ");
@@ -125135,7 +124998,7 @@ ${promptRaw}`;
125135
124998
 
125136
124999
  ${promptRaw}`;
125137
125000
  argsRecord.prompt = newPrompt;
125138
- const skillNames = topSkills.map((s) => `${path155.basename(s.skillPath)} (score: ${s.score.toFixed(2)})`).join(", ");
125001
+ const skillNames = topSkills.map((s) => `${path156.basename(s.skillPath)} (score: ${s.score.toFixed(2)})`).join(", ");
125139
125002
  console.warn(`[skill-propagation-gate] Injected skills: ${skillNames}`);
125140
125003
  for (const skill of topSkills) {
125141
125004
  try {
@@ -1,5 +1,7 @@
1
1
  import type { AppliedMemoryChange, MemoryPatch, MemoryProposal, MemoryRecord, ResolvedCuratorMemoryDecision } from './types';
2
+ export declare const CURATOR_PROMOTED_MEMORY_MAX_TEXT_LENGTH = 500;
2
3
  export declare function validateDecisionMatchesProposal(decision: ResolvedCuratorMemoryDecision, proposal: MemoryProposal): void;
4
+ export declare function validateCuratorPromotableMemory(record: MemoryRecord): void;
3
5
  export declare function applyPatchToMemory(existing: MemoryRecord, patch: MemoryPatch, updatedAt: string): MemoryRecord;
4
6
  export declare function markProposalReviewed(proposal: MemoryProposal, decision: ResolvedCuratorMemoryDecision, status: MemoryProposal['status'], reviewedAt: string, ids: {
5
7
  memoryId?: string;
@@ -10,4 +10,8 @@
10
10
  * Hard cap on max_results = 10 (clamped silently). Default sourced from council.general.maxSourcesPerMember.
11
11
  */
12
12
  import type { tool } from '@opencode-ai/plugin';
13
+ import { writeEvidenceDocuments } from '../evidence/documents';
13
14
  export declare const web_search: ReturnType<typeof tool>;
15
+ export declare const _internals: {
16
+ writeEvidenceDocuments: typeof writeEvidenceDocuments;
17
+ };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "opencode-swarm",
3
- "version": "7.40.0",
3
+ "version": "7.41.1",
4
4
  "description": "Architect-centric agentic swarm plugin for OpenCode - hub-and-spoke orchestration with SME consultation, code generation, and QA review",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",