opencode-swarm 7.23.1 → 7.24.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/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.23.1",
37
+ version: "7.24.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",
@@ -34292,6 +34292,14 @@ function gitExec(args) {
34292
34292
  }
34293
34293
  return result.stdout;
34294
34294
  }
34295
+ function appendRetentionEvent(directory, event) {
34296
+ try {
34297
+ const eventsPath = path9.join(directory, ".swarm", "events.jsonl");
34298
+ const line = `${JSON.stringify({ ...event, timestamp: new Date().toISOString() })}
34299
+ `;
34300
+ fs6.appendFileSync(eventsPath, line);
34301
+ } catch {}
34302
+ }
34295
34303
  function getCurrentSha() {
34296
34304
  const output = gitExec(["rev-parse", "HEAD"]);
34297
34305
  return output.trim();
@@ -34343,6 +34351,17 @@ function handleSave(label, directory) {
34343
34351
  sha: newSha,
34344
34352
  timestamp
34345
34353
  });
34354
+ if (log2.checkpoints.length > maxCheckpoints) {
34355
+ const evicted = log2.checkpoints.splice(0, log2.checkpoints.length - maxCheckpoints);
34356
+ try {
34357
+ appendRetentionEvent(directory, {
34358
+ event: "checkpoint_retention_applied",
34359
+ evicted_labels: evicted.map((e) => e.label),
34360
+ evicted_count: evicted.length,
34361
+ remaining_count: log2.checkpoints.length
34362
+ });
34363
+ } catch {}
34364
+ }
34346
34365
  writeCheckpointLog(log2, directory);
34347
34366
  return JSON.stringify({
34348
34367
  action: "save",
@@ -35248,6 +35267,20 @@ function normalizeEntry(raw) {
35248
35267
  ro.failed_after_shown_count = typeof ro.failed_after_count === "number" ? ro.failed_after_count : 0;
35249
35268
  }
35250
35269
  }
35270
+ try {
35271
+ if (typeof obj.encounter_score !== "number" || Number.isNaN(obj.encounter_score)) {
35272
+ obj.encounter_score = 0;
35273
+ }
35274
+ } catch {
35275
+ try {
35276
+ Object.defineProperty(obj, "encounter_score", {
35277
+ value: 0,
35278
+ writable: true,
35279
+ configurable: true,
35280
+ enumerable: true
35281
+ });
35282
+ } catch {}
35283
+ }
35251
35284
  const arrayFields = [
35252
35285
  "triggers",
35253
35286
  "required_actions",
@@ -35807,12 +35840,26 @@ import path12 from "path";
35807
35840
  function isAlreadyInHive(entry, hiveEntries, threshold) {
35808
35841
  return findNearDuplicate(entry.lesson, hiveEntries, threshold) !== undefined;
35809
35842
  }
35810
- function countDistinctPhases(confirmedBy) {
35843
+ function isHiveEligible(entry, autoPromoteDays) {
35811
35844
  const phaseNumbers = new Set;
35812
- for (const record3 of confirmedBy) {
35813
- phaseNumbers.add(record3.phase_number);
35845
+ for (const record3 of entry.confirmed_by ?? []) {
35846
+ if (record3 && typeof record3.phase_number === "number") {
35847
+ phaseNumbers.add(record3.phase_number);
35848
+ }
35814
35849
  }
35815
- return phaseNumbers.size;
35850
+ if (entry.hive_eligible === true && phaseNumbers.size >= 3) {
35851
+ return true;
35852
+ }
35853
+ if ((entry.tags ?? []).includes("hive-fast-track")) {
35854
+ return true;
35855
+ }
35856
+ const createdMs = Date.parse(entry.created_at);
35857
+ const ageMs = Number.isFinite(createdMs) ? Date.now() - createdMs : 0;
35858
+ const ageThresholdMs = autoPromoteDays * 86400000;
35859
+ if (ageMs >= ageThresholdMs) {
35860
+ return true;
35861
+ }
35862
+ return false;
35816
35863
  }
35817
35864
  function countDistinctProjects(confirmedBy) {
35818
35865
  const projectNames = new Set;
@@ -35830,12 +35877,6 @@ function calculateEncounterScore(currentScore, isSameProject, config3) {
35830
35877
  const newScore = currentScore + increment;
35831
35878
  return Math.min(Math.max(newScore, config3.min_encounter_score), config3.max_encounter_score);
35832
35879
  }
35833
- function getEntryAgeMs(createdAt) {
35834
- const createdTime = new Date(createdAt).getTime();
35835
- if (Number.isNaN(createdTime))
35836
- return 0;
35837
- return Date.now() - createdTime;
35838
- }
35839
35880
  async function checkHivePromotions(swarmEntries, config3) {
35840
35881
  let newPromotions = 0;
35841
35882
  let encountersIncremented = 0;
@@ -35854,19 +35895,7 @@ async function checkHivePromotions(swarmEntries, config3) {
35854
35895
  if (isAlreadyInHive(swarmEntry, hiveEntries, config3.dedup_threshold)) {
35855
35896
  continue;
35856
35897
  }
35857
- let shouldPromote = false;
35858
- if (swarmEntry.hive_eligible === true && countDistinctPhases(swarmEntry.confirmed_by) >= 3) {
35859
- shouldPromote = true;
35860
- }
35861
- if (swarmEntry.tags.includes("hive-fast-track")) {
35862
- shouldPromote = true;
35863
- }
35864
- const ageMs = getEntryAgeMs(swarmEntry.created_at);
35865
- const ageThresholdMs = config3.auto_promote_days * 86400000;
35866
- if (ageMs >= ageThresholdMs) {
35867
- shouldPromote = true;
35868
- }
35869
- if (!shouldPromote) {
35898
+ if (!isHiveEligible(swarmEntry, config3.auto_promote_days)) {
35870
35899
  continue;
35871
35900
  }
35872
35901
  const validationResult = validateLesson(swarmEntry.lesson, hiveEntries.map((e) => e.lesson), {
@@ -37784,12 +37813,14 @@ async function handleCloseCommand(directory, args, options = {}) {
37784
37813
  if (planExists) {
37785
37814
  planAlreadyDone = phases.length > 0 && phases.every((p) => p.status === "complete" || p.status === "completed" || p.status === "blocked" || p.status === "closed");
37786
37815
  }
37787
- const config3 = KnowledgeConfigSchema.parse({});
37816
+ const { config: loadedConfig } = loadPluginConfigWithMeta(directory);
37817
+ const config3 = KnowledgeConfigSchema.parse(loadedConfig.knowledge ?? {});
37788
37818
  const projectName = planData.title ?? "Unknown Project";
37789
37819
  const closedPhases = [];
37790
37820
  const closedTasks = [];
37791
37821
  const warnings = [];
37792
37822
  let hivePromoted = 0;
37823
+ let hiveSkipped = 0;
37793
37824
  if (!planAlreadyDone) {
37794
37825
  for (const phase of inProgressPhases) {
37795
37826
  closedPhases.push(phase.id);
@@ -37912,23 +37943,35 @@ async function handleCloseCommand(directory, args, options = {}) {
37912
37943
  await fs7.unlink(lessonsFilePath).catch(() => {});
37913
37944
  }
37914
37945
  if (curationSucceeded) {
37915
- try {
37916
- const knowledgePath = resolveSwarmKnowledgePath(directory);
37917
- const entries = await readKnowledge(knowledgePath);
37918
- if (entries.length > 0) {
37919
- for (const entry of entries) {
37920
- try {
37921
- await promoteToHive(directory, entry.lesson, entry.category);
37922
- hivePromoted++;
37923
- } catch (promotionErr) {
37924
- const msg = promotionErr instanceof Error ? promotionErr.message : String(promotionErr);
37925
- warnings.push(`Hive promotion skipped for lesson: ${msg}`);
37946
+ if (config3.hive_enabled === false) {} else {
37947
+ try {
37948
+ const knowledgePath = resolveSwarmKnowledgePath(directory);
37949
+ const entries = await readKnowledge(knowledgePath);
37950
+ const autoPromoteDays = config3.auto_promote_days;
37951
+ if (entries.length > 0) {
37952
+ for (const entry of entries) {
37953
+ if (!isHiveEligible(entry, autoPromoteDays)) {
37954
+ hiveSkipped++;
37955
+ continue;
37956
+ }
37957
+ try {
37958
+ const result = await promoteToHive(directory, entry.lesson, entry.category);
37959
+ if (!result.includes("already exists")) {
37960
+ hivePromoted++;
37961
+ }
37962
+ } catch (promotionErr) {
37963
+ const msg = promotionErr instanceof Error ? promotionErr.message : String(promotionErr);
37964
+ warnings.push(`Hive promotion skipped for lesson: ${msg}`);
37965
+ }
37966
+ }
37967
+ if (hiveSkipped > 0) {
37968
+ warnings.push(`${hiveSkipped} swarm knowledge entr${hiveSkipped === 1 ? "y" : "ies"} not eligible for hive promotion`);
37926
37969
  }
37927
37970
  }
37971
+ } catch (hiveErr) {
37972
+ const msg = hiveErr instanceof Error ? hiveErr.message : String(hiveErr);
37973
+ warnings.push(`Hive promotion failed: ${msg}`);
37928
37974
  }
37929
- } catch (hiveErr) {
37930
- const msg = hiveErr instanceof Error ? hiveErr.message : String(hiveErr);
37931
- warnings.push(`Hive promotion failed: ${msg}`);
37932
37975
  }
37933
37976
  }
37934
37977
  const fallbackKnowledgeCreated = curationResult?.stored ?? 0;
@@ -37945,8 +37988,8 @@ async function handleCloseCommand(directory, args, options = {}) {
37945
37988
  let skillReviewSummary = "";
37946
37989
  if (runSkillReview) {
37947
37990
  try {
37948
- const { config: loadedConfig } = loadPluginConfigWithMeta(directory);
37949
- const skillImproverConfig = SkillImproverConfigSchema.parse(loadedConfig.skill_improver ?? {});
37991
+ const { config: loadedConfig2 } = loadPluginConfigWithMeta(directory);
37992
+ const skillImproverConfig = SkillImproverConfigSchema.parse(loadedConfig2.skill_improver ?? {});
37950
37993
  const skillReviewResult = await runAbortableSkillReview({
37951
37994
  directory,
37952
37995
  config: skillImproverConfig,
@@ -38311,7 +38354,6 @@ var init_close = __esm(() => {
38311
38354
  "handoff-prompt.md",
38312
38355
  "handoff-consumed.md",
38313
38356
  "escalation-report.md",
38314
- "knowledge.jsonl",
38315
38357
  "knowledge-rejected.jsonl",
38316
38358
  "repo-graph.json",
38317
38359
  "doc-manifest.json",
@@ -8,6 +8,18 @@ export interface HivePromotionSummary {
8
8
  advancements: number;
9
9
  total_hive_entries: number;
10
10
  }
11
+ /**
12
+ * Check whether a swarm knowledge entry is eligible for hive promotion.
13
+ * Three routes to eligibility:
14
+ * Route 1: hive_eligible flag + 3+ distinct phases
15
+ * Route 2: 'hive-fast-track' tag
16
+ * Route 3: age exceeds auto_promote_days threshold
17
+ *
18
+ * @param entry - The swarm knowledge entry to check
19
+ * @param autoPromoteDays - Number of days before age-based promotion kicks in
20
+ * @returns true if the entry is eligible for hive promotion
21
+ */
22
+ export declare function isHiveEligible(entry: SwarmKnowledgeEntry, autoPromoteDays: number): boolean;
11
23
  /**
12
24
  * Main promotion logic: checks swarm entries and promotes eligible ones to hive.
13
25
  * Also updates existing hive entries with new project confirmations.
package/dist/index.js CHANGED
@@ -33,7 +33,7 @@ var package_default;
33
33
  var init_package = __esm(() => {
34
34
  package_default = {
35
35
  name: "opencode-swarm",
36
- version: "7.23.1",
36
+ version: "7.24.1",
37
37
  description: "Architect-centric agentic swarm plugin for OpenCode - hub-and-spoke orchestration with SME consultation, code generation, and QA review",
38
38
  main: "dist/index.js",
39
39
  types: "dist/index.d.ts",
@@ -41417,6 +41417,14 @@ function gitExec(args2) {
41417
41417
  }
41418
41418
  return result.stdout;
41419
41419
  }
41420
+ function appendRetentionEvent(directory, event) {
41421
+ try {
41422
+ const eventsPath = path14.join(directory, ".swarm", "events.jsonl");
41423
+ const line = `${JSON.stringify({ ...event, timestamp: new Date().toISOString() })}
41424
+ `;
41425
+ fs11.appendFileSync(eventsPath, line);
41426
+ } catch {}
41427
+ }
41420
41428
  function getCurrentSha() {
41421
41429
  const output = gitExec(["rev-parse", "HEAD"]);
41422
41430
  return output.trim();
@@ -41468,6 +41476,17 @@ function handleSave(label, directory) {
41468
41476
  sha: newSha,
41469
41477
  timestamp
41470
41478
  });
41479
+ if (log2.checkpoints.length > maxCheckpoints) {
41480
+ const evicted = log2.checkpoints.splice(0, log2.checkpoints.length - maxCheckpoints);
41481
+ try {
41482
+ appendRetentionEvent(directory, {
41483
+ event: "checkpoint_retention_applied",
41484
+ evicted_labels: evicted.map((e) => e.label),
41485
+ evicted_count: evicted.length,
41486
+ remaining_count: log2.checkpoints.length
41487
+ });
41488
+ } catch {}
41489
+ }
41471
41490
  writeCheckpointLog(log2, directory);
41472
41491
  return JSON.stringify({
41473
41492
  action: "save",
@@ -42675,6 +42694,20 @@ function normalizeEntry(raw) {
42675
42694
  ro.failed_after_shown_count = typeof ro.failed_after_count === "number" ? ro.failed_after_count : 0;
42676
42695
  }
42677
42696
  }
42697
+ try {
42698
+ if (typeof obj.encounter_score !== "number" || Number.isNaN(obj.encounter_score)) {
42699
+ obj.encounter_score = 0;
42700
+ }
42701
+ } catch {
42702
+ try {
42703
+ Object.defineProperty(obj, "encounter_score", {
42704
+ value: 0,
42705
+ writable: true,
42706
+ configurable: true,
42707
+ enumerable: true
42708
+ });
42709
+ } catch {}
42710
+ }
42678
42711
  const arrayFields = [
42679
42712
  "triggers",
42680
42713
  "required_actions",
@@ -44599,12 +44632,26 @@ import path19 from "node:path";
44599
44632
  function isAlreadyInHive(entry, hiveEntries, threshold) {
44600
44633
  return findNearDuplicate(entry.lesson, hiveEntries, threshold) !== undefined;
44601
44634
  }
44602
- function countDistinctPhases(confirmedBy) {
44635
+ function isHiveEligible(entry, autoPromoteDays) {
44603
44636
  const phaseNumbers = new Set;
44604
- for (const record3 of confirmedBy) {
44605
- phaseNumbers.add(record3.phase_number);
44637
+ for (const record3 of entry.confirmed_by ?? []) {
44638
+ if (record3 && typeof record3.phase_number === "number") {
44639
+ phaseNumbers.add(record3.phase_number);
44640
+ }
44641
+ }
44642
+ if (entry.hive_eligible === true && phaseNumbers.size >= 3) {
44643
+ return true;
44644
+ }
44645
+ if ((entry.tags ?? []).includes("hive-fast-track")) {
44646
+ return true;
44606
44647
  }
44607
- return phaseNumbers.size;
44648
+ const createdMs = Date.parse(entry.created_at);
44649
+ const ageMs = Number.isFinite(createdMs) ? Date.now() - createdMs : 0;
44650
+ const ageThresholdMs = autoPromoteDays * 86400000;
44651
+ if (ageMs >= ageThresholdMs) {
44652
+ return true;
44653
+ }
44654
+ return false;
44608
44655
  }
44609
44656
  function countDistinctProjects(confirmedBy) {
44610
44657
  const projectNames = new Set;
@@ -44622,12 +44669,6 @@ function calculateEncounterScore(currentScore, isSameProject, config3) {
44622
44669
  const newScore = currentScore + increment;
44623
44670
  return Math.min(Math.max(newScore, config3.min_encounter_score), config3.max_encounter_score);
44624
44671
  }
44625
- function getEntryAgeMs(createdAt) {
44626
- const createdTime = new Date(createdAt).getTime();
44627
- if (Number.isNaN(createdTime))
44628
- return 0;
44629
- return Date.now() - createdTime;
44630
- }
44631
44672
  async function checkHivePromotions(swarmEntries, config3) {
44632
44673
  let newPromotions = 0;
44633
44674
  let encountersIncremented = 0;
@@ -44646,19 +44687,7 @@ async function checkHivePromotions(swarmEntries, config3) {
44646
44687
  if (isAlreadyInHive(swarmEntry, hiveEntries, config3.dedup_threshold)) {
44647
44688
  continue;
44648
44689
  }
44649
- let shouldPromote = false;
44650
- if (swarmEntry.hive_eligible === true && countDistinctPhases(swarmEntry.confirmed_by) >= 3) {
44651
- shouldPromote = true;
44652
- }
44653
- if (swarmEntry.tags.includes("hive-fast-track")) {
44654
- shouldPromote = true;
44655
- }
44656
- const ageMs = getEntryAgeMs(swarmEntry.created_at);
44657
- const ageThresholdMs = config3.auto_promote_days * 86400000;
44658
- if (ageMs >= ageThresholdMs) {
44659
- shouldPromote = true;
44660
- }
44661
- if (!shouldPromote) {
44690
+ if (!isHiveEligible(swarmEntry, config3.auto_promote_days)) {
44662
44691
  continue;
44663
44692
  }
44664
44693
  const validationResult = validateLesson(swarmEntry.lesson, hiveEntries.map((e) => e.lesson), {
@@ -46535,12 +46564,14 @@ async function handleCloseCommand(directory, args2, options = {}) {
46535
46564
  if (planExists) {
46536
46565
  planAlreadyDone = phases.length > 0 && phases.every((p) => p.status === "complete" || p.status === "completed" || p.status === "blocked" || p.status === "closed");
46537
46566
  }
46538
- const config3 = KnowledgeConfigSchema.parse({});
46567
+ const { config: loadedConfig } = loadPluginConfigWithMeta(directory);
46568
+ const config3 = KnowledgeConfigSchema.parse(loadedConfig.knowledge ?? {});
46539
46569
  const projectName = planData.title ?? "Unknown Project";
46540
46570
  const closedPhases = [];
46541
46571
  const closedTasks = [];
46542
46572
  const warnings = [];
46543
46573
  let hivePromoted = 0;
46574
+ let hiveSkipped = 0;
46544
46575
  if (!planAlreadyDone) {
46545
46576
  for (const phase of inProgressPhases) {
46546
46577
  closedPhases.push(phase.id);
@@ -46663,23 +46694,35 @@ async function handleCloseCommand(directory, args2, options = {}) {
46663
46694
  await fs13.unlink(lessonsFilePath).catch(() => {});
46664
46695
  }
46665
46696
  if (curationSucceeded) {
46666
- try {
46667
- const knowledgePath = resolveSwarmKnowledgePath(directory);
46668
- const entries = await readKnowledge(knowledgePath);
46669
- if (entries.length > 0) {
46670
- for (const entry of entries) {
46671
- try {
46672
- await promoteToHive(directory, entry.lesson, entry.category);
46673
- hivePromoted++;
46674
- } catch (promotionErr) {
46675
- const msg = promotionErr instanceof Error ? promotionErr.message : String(promotionErr);
46676
- warnings.push(`Hive promotion skipped for lesson: ${msg}`);
46697
+ if (config3.hive_enabled === false) {} else {
46698
+ try {
46699
+ const knowledgePath = resolveSwarmKnowledgePath(directory);
46700
+ const entries = await readKnowledge(knowledgePath);
46701
+ const autoPromoteDays = config3.auto_promote_days;
46702
+ if (entries.length > 0) {
46703
+ for (const entry of entries) {
46704
+ if (!isHiveEligible(entry, autoPromoteDays)) {
46705
+ hiveSkipped++;
46706
+ continue;
46707
+ }
46708
+ try {
46709
+ const result = await promoteToHive(directory, entry.lesson, entry.category);
46710
+ if (!result.includes("already exists")) {
46711
+ hivePromoted++;
46712
+ }
46713
+ } catch (promotionErr) {
46714
+ const msg = promotionErr instanceof Error ? promotionErr.message : String(promotionErr);
46715
+ warnings.push(`Hive promotion skipped for lesson: ${msg}`);
46716
+ }
46717
+ }
46718
+ if (hiveSkipped > 0) {
46719
+ warnings.push(`${hiveSkipped} swarm knowledge entr${hiveSkipped === 1 ? "y" : "ies"} not eligible for hive promotion`);
46677
46720
  }
46678
46721
  }
46722
+ } catch (hiveErr) {
46723
+ const msg = hiveErr instanceof Error ? hiveErr.message : String(hiveErr);
46724
+ warnings.push(`Hive promotion failed: ${msg}`);
46679
46725
  }
46680
- } catch (hiveErr) {
46681
- const msg = hiveErr instanceof Error ? hiveErr.message : String(hiveErr);
46682
- warnings.push(`Hive promotion failed: ${msg}`);
46683
46726
  }
46684
46727
  }
46685
46728
  const fallbackKnowledgeCreated = curationResult?.stored ?? 0;
@@ -46696,8 +46739,8 @@ async function handleCloseCommand(directory, args2, options = {}) {
46696
46739
  let skillReviewSummary = "";
46697
46740
  if (runSkillReview) {
46698
46741
  try {
46699
- const { config: loadedConfig } = loadPluginConfigWithMeta(directory);
46700
- const skillImproverConfig = SkillImproverConfigSchema.parse(loadedConfig.skill_improver ?? {});
46742
+ const { config: loadedConfig2 } = loadPluginConfigWithMeta(directory);
46743
+ const skillImproverConfig = SkillImproverConfigSchema.parse(loadedConfig2.skill_improver ?? {});
46701
46744
  const skillReviewResult = await runAbortableSkillReview({
46702
46745
  directory,
46703
46746
  config: skillImproverConfig,
@@ -47062,7 +47105,6 @@ var init_close = __esm(() => {
47062
47105
  "handoff-prompt.md",
47063
47106
  "handoff-consumed.md",
47064
47107
  "escalation-report.md",
47065
- "knowledge.jsonl",
47066
47108
  "knowledge-rejected.jsonl",
47067
47109
  "repo-graph.json",
47068
47110
  "doc-manifest.json",
@@ -62633,7 +62675,17 @@ If the user answered the gate question, immediately follow up with ONE more ques
62633
62675
  - locked: true
62634
62676
  - recorded_at: <ISO timestamp>
62635
62677
  \`\`\`
62636
- If the user accepts the default (1), skip writing this section entirely — serial execution is the default and needs no config.`;
62678
+ If the user accepts the default (1), skip writing this section entirely — serial execution is the default and needs no config.
62679
+
62680
+ After asking the parallelization question (regardless of whether the user chose serial or parallel), immediately follow up with ONE more question: "Commit frequency for completed tasks? (default: phase-level only; optional per-task checkpoint commit after each task completion)".
62681
+
62682
+ If the user chooses per-task commits, write this section to \`.swarm/context.md\`:
62683
+ \`\`\`
62684
+ ## Task Completion Commit Policy
62685
+ - commit_after_each_completed_task: true
62686
+ - recorded_at: <ISO timestamp>
62687
+ \`\`\`
62688
+ If the user keeps the default phase-level behavior, do not write this section.`;
62637
62689
  }
62638
62690
  function buildAvailableToolsList(council) {
62639
62691
  const tools = AGENT_TOOL_MAP.architect ?? [];
@@ -64122,7 +64174,10 @@ save_plan({
64122
64174
  After \`save_plan\` succeeds, read \`.swarm/context.md\`:
64123
64175
  - 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.
64124
64176
  - 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.
64177
+ - 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")\`.
64125
64178
  - If no pending section exists: {{QA_GATE_DIALOGUE_PLAN}}
64179
+ - If a \`## Task Completion Commit Policy\` section already exists in context.md, honor it as execution-time guidance (do NOT remove).
64180
+ - 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.
64126
64181
  <!-- BEHAVIORAL_GUIDANCE_START -->
64127
64182
  INLINE GATE SELECTION — no pending section found in context.md. You MUST ask now.
64128
64183
  ✗ "I'll call set_qa_gates with defaults and move on"
@@ -64407,7 +64462,14 @@ This step supplements (not replaces) the existing regression-sweep and test-drif
64407
64462
  Any blank "value: ___" field = gate was not run = task is NOT complete.
64408
64463
  Filling this checklist from memory ("I think I ran it") is INVALID. Each value must come from actual tool/agent output in this session.
64409
64464
 
64410
- 5o. Call update_task_status with status "completed", proceed to next task.
64465
+ 5p. Call update_task_status with status "completed".
64466
+ 5q. OPTIONAL TASK-COMPLETION COMMIT POLICY: read \`.swarm/context.md\`.
64467
+ - If \`## Task Completion Commit Policy\` contains \`commit_after_each_completed_task: true\`, immediately call:
64468
+ \`checkpoint save task-<task-id>-complete\`
64469
+ - If the section is absent or false, skip this step.
64470
+ - This optional commit policy NEVER bypasses PRE-COMMIT RULE checks above.
64471
+ - 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.
64472
+ 5r. Proceed to next task.
64411
64473
 
64412
64474
  ## ⛔ RETROSPECTIVE GATE
64413
64475
 
@@ -83626,8 +83688,8 @@ ${formattedIndex}
83626
83688
  } else {
83627
83689
  if (existingContent.length > 0 && !existingContent.endsWith(`
83628
83690
  `)) {
83629
- updatedContent = existingContent + `
83630
- ` + newSection;
83691
+ updatedContent = `${existingContent}
83692
+ ${newSection}`;
83631
83693
  } else {
83632
83694
  updatedContent = existingContent + newSection;
83633
83695
  }
@@ -87109,7 +87171,7 @@ ${body2}`);
87109
87171
 
87110
87172
  // src/council/council-evidence-writer.ts
87111
87173
  import {
87112
- appendFileSync as appendFileSync11,
87174
+ appendFileSync as appendFileSync12,
87113
87175
  existsSync as existsSync53,
87114
87176
  mkdirSync as mkdirSync24,
87115
87177
  readFileSync as readFileSync44,
@@ -87193,7 +87255,7 @@ function writeCouncilEvidence(workingDir, synthesis) {
87193
87255
  timestamp: synthesis.timestamp,
87194
87256
  vetoedBy: synthesis.vetoedBy
87195
87257
  });
87196
- appendFileSync11(join83(councilDir, `${synthesis.taskId}.rounds.jsonl`), `${auditLine}
87258
+ appendFileSync12(join83(councilDir, `${synthesis.taskId}.rounds.jsonl`), `${auditLine}
87197
87259
  `);
87198
87260
  } catch (auditError) {
87199
87261
  console.warn(`writeCouncilEvidence: failed to append round-history audit log: ${auditError instanceof Error ? auditError.message : String(auditError)}`);
@@ -90390,7 +90452,7 @@ function formatHiveEntry(entry) {
90390
90452
  lines.push(` Category: ${entry.category}`);
90391
90453
  lines.push(` Status: ${entry.status}`);
90392
90454
  lines.push(` Confidence: ${entry.confidence.toFixed(2)}`);
90393
- lines.push(` Encounter Score: ${entry.encounter_score.toFixed(2)}`);
90455
+ lines.push(` Encounter Score: ${entry.encounter_score?.toFixed(2) ?? "N/A"}`);
90394
90456
  lines.push(` Source Project: ${entry.source_project}`);
90395
90457
  lines.push(` Confirmed by: ${entry.confirmed_by.length} project(s)`);
90396
90458
  return lines.join(`
@@ -104029,9 +104091,9 @@ function recoverTaskStateFromDelegations(taskId, directory) {
104029
104091
  try {
104030
104092
  const evidence = readTaskEvidenceRaw(directory, taskId);
104031
104093
  if (evidence && evidence.gates && Array.isArray(evidence.required_gates)) {
104032
- if (evidence.gates["reviewer"] != null)
104094
+ if (evidence.gates.reviewer != null)
104033
104095
  hasReviewer = true;
104034
- if (evidence.gates["test_engineer"] != null)
104096
+ if (evidence.gates.test_engineer != null)
104035
104097
  hasTestEngineer = true;
104036
104098
  }
104037
104099
  } catch {}
@@ -2,4 +2,10 @@
2
2
  * Provides filtered, formatted text output for knowledge retrieval.
3
3
  */
4
4
  import type { tool } from '@opencode-ai/plugin';
5
+ import type { HiveKnowledgeEntry } from '../hooks/knowledge-types.js';
6
+ declare function formatHiveEntry(entry: HiveKnowledgeEntry): string;
5
7
  export declare const knowledge_query: ReturnType<typeof tool>;
8
+ export declare const _test_exports: {
9
+ formatHiveEntry: typeof formatHiveEntry;
10
+ };
11
+ export {};
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "opencode-swarm",
3
- "version": "7.23.1",
3
+ "version": "7.24.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",