opencode-swarm 6.86.14 → 7.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/agents/architect.d.ts +1 -1
- package/dist/cli/index.js +726 -145
- package/dist/commands/close.d.ts +1 -0
- package/dist/commands/issue.d.ts +13 -0
- package/dist/commands/pr-review.d.ts +11 -0
- package/dist/commands/registry.d.ts +13 -1
- package/dist/config/plan-schema.d.ts +8 -0
- package/dist/config/schema.d.ts +6 -0
- package/dist/council/council-service.d.ts +10 -1
- package/dist/council/types.d.ts +36 -0
- package/dist/db/qa-gate-profile.d.ts +4 -1
- package/dist/git/branch.d.ts +20 -0
- package/dist/index.js +1113 -211
- package/dist/tools/set-qa-gates.d.ts +1 -0
- package/package.json +1 -1
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: "
|
|
36
|
+
version: "7.0.0",
|
|
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",
|
|
@@ -540,8 +540,8 @@ var init_constants = __esm(() => {
|
|
|
540
540
|
lint_spec: "validate .swarm/spec.md format and required fields",
|
|
541
541
|
get_approved_plan: "retrieve the last critic-approved immutable plan snapshot for baseline drift comparison",
|
|
542
542
|
repo_map: "query the repo code graph: importers, dependencies, blast radius, and localization context for structural awareness before refactoring",
|
|
543
|
-
get_qa_gate_profile: "retrieve the QA gate profile for the current plan: gates (reviewer, test_engineer, sme_enabled, critic_pre_plan, sast_enabled, council_mode, hallucination_guard, mutation_test, council_general_review), lock state, and profile hash. Read-only.",
|
|
544
|
-
set_qa_gates: "configure the QA gate profile for the current plan. Architect-only. Ratchet-tighter only — rejected once the profile is locked after critic approval. Supports: reviewer, test_engineer, sme_enabled, critic_pre_plan, sast_enabled, council_mode, hallucination_guard, mutation_test, council_general_review.",
|
|
543
|
+
get_qa_gate_profile: "retrieve the QA gate profile for the current plan: gates (reviewer, test_engineer, sme_enabled, critic_pre_plan, sast_enabled, council_mode, hallucination_guard, mutation_test, council_general_review, drift_check), lock state, and profile hash. Read-only.",
|
|
544
|
+
set_qa_gates: "configure the QA gate profile for the current plan. Architect-only. Ratchet-tighter only — rejected once the profile is locked after critic approval. Supports: reviewer, test_engineer, sme_enabled, critic_pre_plan, sast_enabled, council_mode, hallucination_guard, mutation_test, council_general_review, drift_check.",
|
|
545
545
|
req_coverage: "query requirement coverage status for tracked functional requirements"
|
|
546
546
|
};
|
|
547
547
|
for (const [agentName, tools] of Object.entries(AGENT_TOOL_MAP)) {
|
|
@@ -15330,12 +15330,15 @@ var init_schema = __esm(() => {
|
|
|
15330
15330
|
requireAllMembers: exports_external.boolean().default(false).describe("When true, submit_council_verdicts rejects if fewer than 5 member verdicts are provided. Equivalent to minimumMembers: 5."),
|
|
15331
15331
|
minimumMembers: exports_external.number().int().min(1).max(5).default(3).describe("Minimum distinct council member verdicts required for synthesis. Default 3. Set to 1 to disable quorum enforcement. requireAllMembers: true overrides this to 5 (stricter constraint wins)."),
|
|
15332
15332
|
escalateOnMaxRounds: exports_external.string().optional().describe("Optional webhook URL or handler name invoked when maxRounds is reached without APPROVE. Declared for forward compatibility; no behavior is implemented yet."),
|
|
15333
|
+
phaseConcernsAllowComplete: exports_external.boolean().default(true).describe("When true, a phase-level council CONCERNS verdict does NOT block phase completion — the advisory notes are logged as warnings and the phase proceeds. When false, CONCERNS blocks like REJECT. Default: true (CONCERNS is advisory)."),
|
|
15333
15334
|
general: GeneralCouncilConfigSchema.optional()
|
|
15334
15335
|
}).strict();
|
|
15335
15336
|
ParallelizationConfigSchema = exports_external.object({
|
|
15336
15337
|
enabled: exports_external.boolean().default(false),
|
|
15337
15338
|
maxConcurrentTasks: exports_external.number().int().min(1).max(64).default(1),
|
|
15338
15339
|
evidenceLockTimeoutMs: exports_external.number().int().min(1000).max(300000).default(60000),
|
|
15340
|
+
max_coders: exports_external.number().int().min(1).max(16).default(3),
|
|
15341
|
+
max_reviewers: exports_external.number().int().min(1).max(16).default(2),
|
|
15339
15342
|
stageB: exports_external.object({
|
|
15340
15343
|
parallel: exports_external.object({
|
|
15341
15344
|
enabled: exports_external.boolean().default(false)
|
|
@@ -15594,6 +15597,7 @@ var init_plan_schema = __esm(() => {
|
|
|
15594
15597
|
name: exports_external.string().min(1),
|
|
15595
15598
|
status: PhaseStatusSchema.default("pending"),
|
|
15596
15599
|
tasks: exports_external.array(TaskSchema).default([]),
|
|
15600
|
+
type: exports_external.enum(["code", "non-code"]).optional(),
|
|
15597
15601
|
required_agents: exports_external.array(exports_external.string()).optional()
|
|
15598
15602
|
});
|
|
15599
15603
|
PlanSchema = exports_external.object({
|
|
@@ -19928,7 +19932,8 @@ var init_qa_gate_profile = __esm(() => {
|
|
|
19928
19932
|
hallucination_guard: false,
|
|
19929
19933
|
sast_enabled: true,
|
|
19930
19934
|
mutation_test: false,
|
|
19931
|
-
council_general_review: false
|
|
19935
|
+
council_general_review: false,
|
|
19936
|
+
drift_check: true
|
|
19932
19937
|
};
|
|
19933
19938
|
});
|
|
19934
19939
|
|
|
@@ -39918,23 +39923,220 @@ function getCurrentBranch(cwd) {
|
|
|
39918
39923
|
const output = gitExec2(["rev-parse", "--abbrev-ref", "HEAD"], cwd);
|
|
39919
39924
|
return output.trim();
|
|
39920
39925
|
}
|
|
39921
|
-
function
|
|
39926
|
+
function hasUncommittedChanges(cwd) {
|
|
39927
|
+
const status = gitExec2(["status", "--porcelain"], cwd);
|
|
39928
|
+
return status.trim().length > 0;
|
|
39929
|
+
}
|
|
39930
|
+
function detectDefaultRemoteBranch(cwd) {
|
|
39931
|
+
try {
|
|
39932
|
+
const output = gitExec2(["symbolic-ref", "refs/remotes/origin/HEAD"], cwd);
|
|
39933
|
+
const trimmed = output.trim();
|
|
39934
|
+
if (trimmed.startsWith("refs/remotes/origin/")) {
|
|
39935
|
+
return trimmed.slice("refs/remotes/origin/".length);
|
|
39936
|
+
}
|
|
39937
|
+
} catch {}
|
|
39938
|
+
try {
|
|
39939
|
+
const output = gitExec2(["config", "init.defaultBranch"], cwd);
|
|
39940
|
+
const branch = output.trim();
|
|
39941
|
+
if (branch) {
|
|
39942
|
+
return branch;
|
|
39943
|
+
}
|
|
39944
|
+
} catch {}
|
|
39922
39945
|
try {
|
|
39923
39946
|
gitExec2(["rev-parse", "--verify", "origin/main"], cwd);
|
|
39924
|
-
return "
|
|
39947
|
+
return "main";
|
|
39948
|
+
} catch {}
|
|
39949
|
+
try {
|
|
39950
|
+
gitExec2(["rev-parse", "--verify", "origin/master"], cwd);
|
|
39951
|
+
return "master";
|
|
39925
39952
|
} catch {
|
|
39953
|
+
return null;
|
|
39954
|
+
}
|
|
39955
|
+
}
|
|
39956
|
+
function resetToRemoteBranch(cwd, options) {
|
|
39957
|
+
const warnings = [];
|
|
39958
|
+
const prunedBranches = [];
|
|
39959
|
+
try {
|
|
39960
|
+
const currentBranch = getCurrentBranch(cwd);
|
|
39961
|
+
const defaultRemoteBranch = detectDefaultRemoteBranch(cwd);
|
|
39962
|
+
if (!defaultRemoteBranch) {
|
|
39963
|
+
return {
|
|
39964
|
+
success: false,
|
|
39965
|
+
targetBranch: "",
|
|
39966
|
+
localBranch: currentBranch,
|
|
39967
|
+
message: "Could not detect default remote branch",
|
|
39968
|
+
alreadyAligned: false,
|
|
39969
|
+
prunedBranches: [],
|
|
39970
|
+
warnings: []
|
|
39971
|
+
};
|
|
39972
|
+
}
|
|
39973
|
+
const targetBranch = `origin/${defaultRemoteBranch}`;
|
|
39974
|
+
if (currentBranch === "HEAD") {
|
|
39975
|
+
return {
|
|
39976
|
+
success: false,
|
|
39977
|
+
targetBranch,
|
|
39978
|
+
localBranch: "HEAD",
|
|
39979
|
+
message: "Cannot reset: detached HEAD state",
|
|
39980
|
+
alreadyAligned: false,
|
|
39981
|
+
prunedBranches: [],
|
|
39982
|
+
warnings: []
|
|
39983
|
+
};
|
|
39984
|
+
}
|
|
39985
|
+
if (hasUncommittedChanges(cwd)) {
|
|
39986
|
+
return {
|
|
39987
|
+
success: false,
|
|
39988
|
+
targetBranch,
|
|
39989
|
+
localBranch: currentBranch,
|
|
39990
|
+
message: "Cannot reset: uncommitted changes in working tree",
|
|
39991
|
+
alreadyAligned: false,
|
|
39992
|
+
prunedBranches: [],
|
|
39993
|
+
warnings: []
|
|
39994
|
+
};
|
|
39995
|
+
}
|
|
39926
39996
|
try {
|
|
39927
|
-
gitExec2(["
|
|
39928
|
-
|
|
39929
|
-
|
|
39930
|
-
|
|
39997
|
+
const logOutput = gitExec2(["log", `${targetBranch}..HEAD`, "--oneline"], cwd);
|
|
39998
|
+
if (logOutput.trim().length > 0) {
|
|
39999
|
+
return {
|
|
40000
|
+
success: false,
|
|
40001
|
+
targetBranch,
|
|
40002
|
+
localBranch: currentBranch,
|
|
40003
|
+
message: "Cannot reset: unpushed commits",
|
|
40004
|
+
alreadyAligned: false,
|
|
40005
|
+
prunedBranches: [],
|
|
40006
|
+
warnings: []
|
|
40007
|
+
};
|
|
40008
|
+
}
|
|
40009
|
+
} catch {}
|
|
40010
|
+
try {
|
|
40011
|
+
gitExec2(["fetch", "--prune", "origin"], cwd);
|
|
40012
|
+
} catch (err2) {
|
|
40013
|
+
return {
|
|
40014
|
+
success: false,
|
|
40015
|
+
targetBranch,
|
|
40016
|
+
localBranch: currentBranch,
|
|
40017
|
+
message: `Fetch failed: ${err2 instanceof Error ? err2.message : String(err2)}`,
|
|
40018
|
+
alreadyAligned: false,
|
|
40019
|
+
prunedBranches: [],
|
|
40020
|
+
warnings: []
|
|
40021
|
+
};
|
|
39931
40022
|
}
|
|
40023
|
+
const headSha = gitExec2(["rev-parse", "HEAD"], cwd).trim();
|
|
40024
|
+
const remoteSha = gitExec2(["rev-parse", `${targetBranch}`], cwd).trim();
|
|
40025
|
+
if (headSha === remoteSha) {
|
|
40026
|
+
return {
|
|
40027
|
+
success: true,
|
|
40028
|
+
targetBranch,
|
|
40029
|
+
localBranch: currentBranch,
|
|
40030
|
+
message: "Already aligned with remote",
|
|
40031
|
+
alreadyAligned: true,
|
|
40032
|
+
prunedBranches: [],
|
|
40033
|
+
warnings: []
|
|
40034
|
+
};
|
|
40035
|
+
}
|
|
40036
|
+
try {
|
|
40037
|
+
gitExec2(["checkout", currentBranch], cwd);
|
|
40038
|
+
} catch (err2) {
|
|
40039
|
+
return {
|
|
40040
|
+
success: false,
|
|
40041
|
+
targetBranch,
|
|
40042
|
+
localBranch: currentBranch,
|
|
40043
|
+
message: `Checkout failed: ${err2 instanceof Error ? err2.message : String(err2)}`,
|
|
40044
|
+
alreadyAligned: false,
|
|
40045
|
+
prunedBranches: [],
|
|
40046
|
+
warnings: []
|
|
40047
|
+
};
|
|
40048
|
+
}
|
|
40049
|
+
let resetSucceeded = false;
|
|
40050
|
+
let lastError;
|
|
40051
|
+
for (let retry = 0;retry < 4; retry++) {
|
|
40052
|
+
if (retry > 0 && process.platform === "win32") {
|
|
40053
|
+
const endTime = Date.now() + 500;
|
|
40054
|
+
while (Date.now() < endTime) {}
|
|
40055
|
+
}
|
|
40056
|
+
try {
|
|
40057
|
+
gitExec2(["reset", "--hard", targetBranch], cwd);
|
|
40058
|
+
resetSucceeded = true;
|
|
40059
|
+
break;
|
|
40060
|
+
} catch (err2) {
|
|
40061
|
+
lastError = err2;
|
|
40062
|
+
}
|
|
40063
|
+
}
|
|
40064
|
+
if (!resetSucceeded) {
|
|
40065
|
+
return {
|
|
40066
|
+
success: false,
|
|
40067
|
+
targetBranch,
|
|
40068
|
+
localBranch: currentBranch,
|
|
40069
|
+
message: `Reset failed: ${lastError instanceof Error ? lastError.message : String(lastError)}`,
|
|
40070
|
+
alreadyAligned: false,
|
|
40071
|
+
prunedBranches: [],
|
|
40072
|
+
warnings: []
|
|
40073
|
+
};
|
|
40074
|
+
}
|
|
40075
|
+
if (options?.pruneBranches) {
|
|
40076
|
+
try {
|
|
40077
|
+
const mergedOutput = gitExec2(["branch", "--merged", targetBranch], cwd);
|
|
40078
|
+
const mergedLines = mergedOutput.split(`
|
|
40079
|
+
`);
|
|
40080
|
+
for (const line of mergedLines) {
|
|
40081
|
+
const trimmedLine = line.trim();
|
|
40082
|
+
if (!trimmedLine || trimmedLine.startsWith("*")) {
|
|
40083
|
+
continue;
|
|
40084
|
+
}
|
|
40085
|
+
try {
|
|
40086
|
+
gitExec2(["branch", "-d", trimmedLine], cwd);
|
|
40087
|
+
prunedBranches.push(trimmedLine);
|
|
40088
|
+
} catch {
|
|
40089
|
+
warnings.push(`Could not safely delete branch: ${trimmedLine}`);
|
|
40090
|
+
}
|
|
40091
|
+
}
|
|
40092
|
+
} catch (err2) {
|
|
40093
|
+
warnings.push(`Failed to get merged branches: ${err2 instanceof Error ? err2.message : String(err2)}`);
|
|
40094
|
+
}
|
|
40095
|
+
try {
|
|
40096
|
+
const branchVvOutput = gitExec2(["branch", "-vv"], cwd);
|
|
40097
|
+
const vvLines = branchVvOutput.split(`
|
|
40098
|
+
`);
|
|
40099
|
+
for (const line of vvLines) {
|
|
40100
|
+
const trimmedLine = line.trim();
|
|
40101
|
+
if (!trimmedLine || trimmedLine.startsWith("*")) {
|
|
40102
|
+
continue;
|
|
40103
|
+
}
|
|
40104
|
+
if (trimmedLine.includes(": gone]")) {
|
|
40105
|
+
const parts2 = trimmedLine.split(/\s+/);
|
|
40106
|
+
const branchName = parts2[0];
|
|
40107
|
+
try {
|
|
40108
|
+
gitExec2(["branch", "-d", branchName], cwd);
|
|
40109
|
+
prunedBranches.push(branchName);
|
|
40110
|
+
} catch {
|
|
40111
|
+
warnings.push(`Could not delete gone branch: ${branchName}`);
|
|
40112
|
+
}
|
|
40113
|
+
}
|
|
40114
|
+
}
|
|
40115
|
+
} catch (err2) {
|
|
40116
|
+
warnings.push(`Failed to prune gone branches: ${err2 instanceof Error ? err2.message : String(err2)}`);
|
|
40117
|
+
}
|
|
40118
|
+
}
|
|
40119
|
+
return {
|
|
40120
|
+
success: true,
|
|
40121
|
+
targetBranch,
|
|
40122
|
+
localBranch: currentBranch,
|
|
40123
|
+
message: "Successfully reset to remote branch",
|
|
40124
|
+
alreadyAligned: false,
|
|
40125
|
+
prunedBranches,
|
|
40126
|
+
warnings
|
|
40127
|
+
};
|
|
40128
|
+
} catch (err2) {
|
|
40129
|
+
return {
|
|
40130
|
+
success: false,
|
|
40131
|
+
targetBranch: "",
|
|
40132
|
+
localBranch: "",
|
|
40133
|
+
message: `Unexpected error: ${err2 instanceof Error ? err2.message : String(err2)}`,
|
|
40134
|
+
alreadyAligned: false,
|
|
40135
|
+
prunedBranches: [],
|
|
40136
|
+
warnings: []
|
|
40137
|
+
};
|
|
39932
40138
|
}
|
|
39933
40139
|
}
|
|
39934
|
-
function hasUncommittedChanges(cwd) {
|
|
39935
|
-
const status = gitExec2(["status", "--porcelain"], cwd);
|
|
39936
|
-
return status.trim().length > 0;
|
|
39937
|
-
}
|
|
39938
40140
|
var GIT_TIMEOUT_MS2 = 30000;
|
|
39939
40141
|
var init_branch = __esm(() => {
|
|
39940
40142
|
init_logger();
|
|
@@ -41588,9 +41790,28 @@ var init_write_retro = __esm(() => {
|
|
|
41588
41790
|
});
|
|
41589
41791
|
|
|
41590
41792
|
// src/commands/close.ts
|
|
41591
|
-
import { execFileSync } from "node:child_process";
|
|
41592
41793
|
import { promises as fs12 } from "node:fs";
|
|
41593
41794
|
import path18 from "node:path";
|
|
41795
|
+
function guaranteeAllPlansComplete(planData) {
|
|
41796
|
+
const closedPhaseIds = [];
|
|
41797
|
+
const closedTaskIds = [];
|
|
41798
|
+
for (const phase of planData.phases ?? []) {
|
|
41799
|
+
const wasComplete = phase.status === "complete" || phase.status === "completed" || phase.status === "closed";
|
|
41800
|
+
if (!wasComplete) {
|
|
41801
|
+
phase.status = "closed";
|
|
41802
|
+
closedPhaseIds.push(phase.id);
|
|
41803
|
+
}
|
|
41804
|
+
for (const task of phase.tasks ?? []) {
|
|
41805
|
+
const wasTaskDone = task.status === "completed" || task.status === "complete" || task.status === "closed";
|
|
41806
|
+
if (!wasTaskDone) {
|
|
41807
|
+
task.status = "closed";
|
|
41808
|
+
task.close_reason = "session_terminated";
|
|
41809
|
+
closedTaskIds.push(task.id);
|
|
41810
|
+
}
|
|
41811
|
+
}
|
|
41812
|
+
}
|
|
41813
|
+
return { closedPhaseIds, closedTaskIds };
|
|
41814
|
+
}
|
|
41594
41815
|
async function handleCloseCommand(directory, args2) {
|
|
41595
41816
|
const planPath = validateSwarmPath(directory, "plan.json");
|
|
41596
41817
|
const swarmDir = path18.join(directory, ".swarm");
|
|
@@ -41708,41 +41929,62 @@ async function handleCloseCommand(directory, args2) {
|
|
|
41708
41929
|
explicitLessons = lessonsText.split(`
|
|
41709
41930
|
`).map((line) => line.trim()).filter((line) => line.length > 0 && !line.startsWith("#"));
|
|
41710
41931
|
} catch {}
|
|
41932
|
+
const retroLessons = [];
|
|
41933
|
+
try {
|
|
41934
|
+
const evidenceDir = path18.join(swarmDir, "evidence");
|
|
41935
|
+
const evidenceEntries = await fs12.readdir(evidenceDir);
|
|
41936
|
+
const retroDirs = evidenceEntries.filter((e) => e.startsWith("retro-"));
|
|
41937
|
+
for (const retroDir of retroDirs) {
|
|
41938
|
+
const evidencePath = path18.join(evidenceDir, retroDir, "evidence.json");
|
|
41939
|
+
try {
|
|
41940
|
+
const content = await fs12.readFile(evidencePath, "utf-8");
|
|
41941
|
+
const parsed = JSON.parse(content);
|
|
41942
|
+
const entries = parsed.entries ?? [parsed];
|
|
41943
|
+
for (const entry of entries) {
|
|
41944
|
+
if (Array.isArray(entry.lessons_learned)) {
|
|
41945
|
+
for (const lesson of entry.lessons_learned) {
|
|
41946
|
+
if (typeof lesson === "string" && lesson.trim().length > 0) {
|
|
41947
|
+
retroLessons.push(lesson.trim());
|
|
41948
|
+
}
|
|
41949
|
+
}
|
|
41950
|
+
}
|
|
41951
|
+
}
|
|
41952
|
+
} catch {}
|
|
41953
|
+
}
|
|
41954
|
+
} catch {}
|
|
41955
|
+
const allLessons = [...new Set([...explicitLessons, ...retroLessons])];
|
|
41711
41956
|
let curationSucceeded = false;
|
|
41712
41957
|
try {
|
|
41713
|
-
await curateAndStoreSwarm(
|
|
41958
|
+
await curateAndStoreSwarm(allLessons, projectName, { phase_number: 0 }, directory, config3);
|
|
41714
41959
|
curationSucceeded = true;
|
|
41715
41960
|
} catch (error93) {
|
|
41716
41961
|
const msg = error93 instanceof Error ? error93.message : String(error93);
|
|
41717
41962
|
warnings.push(`Lessons curation failed: ${msg}`);
|
|
41718
41963
|
console.warn("[close-command] curateAndStoreSwarm error:", error93);
|
|
41719
41964
|
}
|
|
41720
|
-
if (curationSucceeded &&
|
|
41965
|
+
if (curationSucceeded && allLessons.length > 0) {
|
|
41721
41966
|
await fs12.unlink(lessonsFilePath).catch(() => {});
|
|
41722
41967
|
}
|
|
41723
|
-
if (planExists
|
|
41724
|
-
|
|
41725
|
-
|
|
41726
|
-
|
|
41727
|
-
|
|
41728
|
-
closedPhases.push(phase.id);
|
|
41729
|
-
}
|
|
41968
|
+
if (planExists) {
|
|
41969
|
+
const guaranteeResult = guaranteeAllPlansComplete(planData);
|
|
41970
|
+
for (const phaseId of guaranteeResult.closedPhaseIds) {
|
|
41971
|
+
if (!closedPhases.includes(phaseId)) {
|
|
41972
|
+
closedPhases.push(phaseId);
|
|
41730
41973
|
}
|
|
41731
|
-
|
|
41732
|
-
|
|
41733
|
-
|
|
41734
|
-
|
|
41735
|
-
closedTasks.push(task.id);
|
|
41736
|
-
}
|
|
41737
|
-
}
|
|
41974
|
+
}
|
|
41975
|
+
for (const taskId of guaranteeResult.closedTaskIds) {
|
|
41976
|
+
if (!closedTasks.includes(taskId)) {
|
|
41977
|
+
closedTasks.push(taskId);
|
|
41738
41978
|
}
|
|
41739
41979
|
}
|
|
41740
|
-
|
|
41741
|
-
|
|
41742
|
-
|
|
41743
|
-
|
|
41744
|
-
|
|
41745
|
-
|
|
41980
|
+
if (!planAlreadyDone || guaranteeResult.closedPhaseIds.length > 0 || guaranteeResult.closedTaskIds.length > 0) {
|
|
41981
|
+
try {
|
|
41982
|
+
await fs12.writeFile(planPath, JSON.stringify(planData, null, 2), "utf-8");
|
|
41983
|
+
} catch (error93) {
|
|
41984
|
+
const msg = error93 instanceof Error ? error93.message : String(error93);
|
|
41985
|
+
warnings.push(`Failed to persist terminal plan.json state: ${msg}`);
|
|
41986
|
+
console.warn("[close-command] Failed to write plan.json:", error93);
|
|
41987
|
+
}
|
|
41746
41988
|
}
|
|
41747
41989
|
}
|
|
41748
41990
|
const timestamp = new Date().toISOString().replace(/[:.]/g, "-");
|
|
@@ -41881,111 +42123,27 @@ async function handleCloseCommand(directory, args2) {
|
|
|
41881
42123
|
console.warn("[close-command] Failed to write context.md:", error93);
|
|
41882
42124
|
}
|
|
41883
42125
|
const pruneBranches = args2.includes("--prune-branches");
|
|
41884
|
-
const prunedBranches = [];
|
|
41885
|
-
const pruneErrors = [];
|
|
41886
42126
|
let gitAlignResult = "";
|
|
42127
|
+
const prunedBranches = [];
|
|
41887
42128
|
const isGit = isGitRepo2(directory);
|
|
41888
42129
|
if (isGit) {
|
|
41889
|
-
|
|
41890
|
-
|
|
41891
|
-
|
|
41892
|
-
|
|
41893
|
-
|
|
41894
|
-
} else if (hasUncommittedChanges(directory)) {
|
|
41895
|
-
gitAlignResult = "Skipped git alignment: uncommitted changes in worktree";
|
|
41896
|
-
warnings.push("Uncommitted changes detected. Commit or stash before aligning to main.");
|
|
41897
|
-
} else {
|
|
41898
|
-
const baseBranch = getDefaultBaseBranch(directory);
|
|
41899
|
-
const localBase = baseBranch.replace(/^origin\//, "");
|
|
41900
|
-
if (currentBranch === localBase) {
|
|
41901
|
-
try {
|
|
41902
|
-
execFileSync("git", ["fetch", "origin", localBase], {
|
|
41903
|
-
cwd: directory,
|
|
41904
|
-
encoding: "utf-8",
|
|
41905
|
-
timeout: 30000,
|
|
41906
|
-
stdio: ["pipe", "pipe", "pipe"]
|
|
41907
|
-
});
|
|
41908
|
-
const mergeBase = execFileSync("git", ["merge-base", "HEAD", baseBranch], {
|
|
41909
|
-
cwd: directory,
|
|
41910
|
-
encoding: "utf-8",
|
|
41911
|
-
timeout: 1e4,
|
|
41912
|
-
stdio: ["pipe", "pipe", "pipe"]
|
|
41913
|
-
}).trim();
|
|
41914
|
-
const headSha = execFileSync("git", ["rev-parse", "HEAD"], {
|
|
41915
|
-
cwd: directory,
|
|
41916
|
-
encoding: "utf-8",
|
|
41917
|
-
timeout: 1e4,
|
|
41918
|
-
stdio: ["pipe", "pipe", "pipe"]
|
|
41919
|
-
}).trim();
|
|
41920
|
-
if (mergeBase === headSha) {
|
|
41921
|
-
execFileSync("git", ["merge", "--ff-only", baseBranch], {
|
|
41922
|
-
cwd: directory,
|
|
41923
|
-
encoding: "utf-8",
|
|
41924
|
-
timeout: 30000,
|
|
41925
|
-
stdio: ["pipe", "pipe", "pipe"]
|
|
41926
|
-
});
|
|
41927
|
-
gitAlignResult = `Aligned to ${baseBranch} (fast-forward)`;
|
|
41928
|
-
} else {
|
|
41929
|
-
gitAlignResult = `On ${localBase} but cannot fast-forward to ${baseBranch} (diverged)`;
|
|
41930
|
-
warnings.push(`Local ${localBase} has diverged from ${baseBranch}. Manual merge/rebase needed.`);
|
|
41931
|
-
}
|
|
41932
|
-
} catch (fetchErr) {
|
|
41933
|
-
gitAlignResult = `Fetch from origin/${localBase} failed — remote may be unavailable`;
|
|
41934
|
-
warnings.push(`Git fetch failed: ${fetchErr instanceof Error ? fetchErr.message : String(fetchErr)}`);
|
|
41935
|
-
}
|
|
41936
|
-
} else {
|
|
41937
|
-
gitAlignResult = `On branch ${currentBranch}. Switch to ${localBase} manually when ready for a new swarm.`;
|
|
41938
|
-
}
|
|
41939
|
-
}
|
|
41940
|
-
} catch (gitError) {
|
|
41941
|
-
gitAlignResult = `Git alignment error: ${gitError instanceof Error ? gitError.message : String(gitError)}`;
|
|
42130
|
+
const alignResult = resetToRemoteBranch(directory, { pruneBranches });
|
|
42131
|
+
gitAlignResult = alignResult.message;
|
|
42132
|
+
prunedBranches.push(...alignResult.prunedBranches);
|
|
42133
|
+
if (!alignResult.success) {
|
|
42134
|
+
warnings.push(`Git alignment: ${alignResult.message}`);
|
|
41942
42135
|
}
|
|
41943
|
-
if (
|
|
41944
|
-
|
|
41945
|
-
|
|
41946
|
-
|
|
41947
|
-
|
|
41948
|
-
stdio: ["pipe", "pipe", "pipe"]
|
|
41949
|
-
});
|
|
41950
|
-
const goneBranches = branchOutput.split(`
|
|
41951
|
-
`).filter((line) => line.includes(": gone]")).map((line) => line.trim().replace(/^[*+]\s+/, "").split(/\s+/)[0]).filter(Boolean);
|
|
41952
|
-
for (const branch of goneBranches) {
|
|
41953
|
-
try {
|
|
41954
|
-
execFileSync("git", ["branch", "-d", branch], {
|
|
41955
|
-
cwd: directory,
|
|
41956
|
-
encoding: "utf-8",
|
|
41957
|
-
stdio: ["pipe", "pipe", "pipe"]
|
|
41958
|
-
});
|
|
41959
|
-
prunedBranches.push(branch);
|
|
41960
|
-
} catch {
|
|
41961
|
-
pruneErrors.push(branch);
|
|
41962
|
-
}
|
|
41963
|
-
}
|
|
41964
|
-
} catch {}
|
|
42136
|
+
if (alignResult.alreadyAligned) {
|
|
42137
|
+
gitAlignResult = `Already aligned with ${alignResult.targetBranch}`;
|
|
42138
|
+
}
|
|
42139
|
+
for (const w of alignResult.warnings) {
|
|
42140
|
+
warnings.push(w);
|
|
41965
42141
|
}
|
|
41966
42142
|
} else {
|
|
41967
42143
|
gitAlignResult = "Not a git repository — skipped git alignment";
|
|
41968
42144
|
}
|
|
41969
42145
|
const closeSummaryPath = validateSwarmPath(directory, "close-summary.md");
|
|
41970
42146
|
const finalizationType = isForced ? "Forced closure" : planAlreadyDone ? "Plan already terminal — cleanup only" : "Normal finalization";
|
|
41971
|
-
const actionsPerformed = [
|
|
41972
|
-
...!planAlreadyDone && inProgressPhases.length > 0 ? ["- Wrote retrospectives for in-progress phases"] : [],
|
|
41973
|
-
`- ${archiveResult}`,
|
|
41974
|
-
...cleanedFiles.length > 0 ? [
|
|
41975
|
-
`- Cleaned ${cleanedFiles.length} active-state file(s): ${cleanedFiles.join(", ")}`
|
|
41976
|
-
] : [],
|
|
41977
|
-
"- Reset context.md for next session",
|
|
41978
|
-
...configBackupsRemoved > 0 ? [`- Removed ${configBackupsRemoved} stale config backup file(s)`] : [],
|
|
41979
|
-
...swarmPlanFilesRemoved > 0 ? [
|
|
41980
|
-
`- Removed ${swarmPlanFilesRemoved} root-level SWARM_PLAN checkpoint artifact(s)`
|
|
41981
|
-
] : [],
|
|
41982
|
-
...prunedBranches.length > 0 ? [
|
|
41983
|
-
`- Pruned ${prunedBranches.length} stale local git branch(es): ${prunedBranches.join(", ")}`
|
|
41984
|
-
] : [],
|
|
41985
|
-
"- Cleared agent sessions and delegation chains",
|
|
41986
|
-
...planExists && !planAlreadyDone ? ["- Set non-completed phases/tasks to closed status"] : [],
|
|
41987
|
-
...gitAlignResult ? [`- Git: ${gitAlignResult}`] : []
|
|
41988
|
-
];
|
|
41989
42147
|
const summaryContent = [
|
|
41990
42148
|
"# Swarm Close Summary",
|
|
41991
42149
|
"",
|
|
@@ -41993,16 +42151,34 @@ async function handleCloseCommand(directory, args2) {
|
|
|
41993
42151
|
`**Closed:** ${new Date().toISOString()}`,
|
|
41994
42152
|
`**Finalization:** ${finalizationType}`,
|
|
41995
42153
|
"",
|
|
41996
|
-
|
|
41997
|
-
!planExists ? "_No plan — ad-hoc session_" : closedPhases.length > 0 ? closedPhases.map((id) => `- Phase ${id}`).join(`
|
|
41998
|
-
`) : "_No phases
|
|
42154
|
+
"## Retrospective",
|
|
42155
|
+
!planExists ? "_No plan — ad-hoc session_" : closedPhases.length > 0 ? closedPhases.map((id) => `- Phase ${id} closed`).join(`
|
|
42156
|
+
`) : "_No phases closed this run_",
|
|
42157
|
+
...closedTasks.length > 0 ? [
|
|
42158
|
+
"",
|
|
42159
|
+
`**Tasks marked closed:** ${closedTasks.length}`,
|
|
42160
|
+
...closedTasks.map((id) => `- ${id}`)
|
|
42161
|
+
] : [],
|
|
41999
42162
|
"",
|
|
42000
|
-
|
|
42001
|
-
|
|
42002
|
-
|
|
42163
|
+
"## Lessons Committed",
|
|
42164
|
+
allLessons.length > 0 ? `| # | Lesson |` : "_No lessons committed_",
|
|
42165
|
+
...allLessons.length > 0 ? ["| --- | --- |", ...allLessons.map((l, i2) => `| ${i2 + 1} | ${l} |`)] : [],
|
|
42003
42166
|
"",
|
|
42004
|
-
"##
|
|
42005
|
-
...
|
|
42167
|
+
"## Local Repo State",
|
|
42168
|
+
...gitAlignResult ? [`- **Git:** ${gitAlignResult}`] : ["- Git alignment skipped"],
|
|
42169
|
+
...prunedBranches.length > 0 ? [`- **Pruned branches:** ${prunedBranches.join(", ")}`] : [],
|
|
42170
|
+
`- **Archive:** ${archiveResult}`,
|
|
42171
|
+
...cleanedFiles.length > 0 ? [`- **Cleaned:** ${cleanedFiles.length} file(s)`] : [],
|
|
42172
|
+
"",
|
|
42173
|
+
"## Context",
|
|
42174
|
+
"- Reset context.md for next session",
|
|
42175
|
+
"- Cleared agent sessions and delegation chains",
|
|
42176
|
+
...configBackupsRemoved > 0 ? [`- Removed ${configBackupsRemoved} stale config backup file(s)`] : [],
|
|
42177
|
+
...swarmPlanFilesRemoved > 0 ? [
|
|
42178
|
+
`- Removed ${swarmPlanFilesRemoved} root-level SWARM_PLAN checkpoint artifact(s)`
|
|
42179
|
+
] : [],
|
|
42180
|
+
...planExists && !planAlreadyDone ? ["- Set non-completed phases/tasks to closed status"] : [],
|
|
42181
|
+
...curationSucceeded && allLessons.length > 0 ? [`- Committed ${allLessons.length} lesson(s) to knowledge store`] : [],
|
|
42006
42182
|
"",
|
|
42007
42183
|
...warnings.length > 0 ? ["## Warnings", ...warnings.map((w) => `- ${w}`), ""] : []
|
|
42008
42184
|
].join(`
|
|
@@ -42030,9 +42206,6 @@ async function handleCloseCommand(directory, args2) {
|
|
|
42030
42206
|
swarmState.fullAutoEnabledInConfig = preservedFullAutoFlag;
|
|
42031
42207
|
swarmState.curatorInitAgentNames = preservedCuratorInitNames;
|
|
42032
42208
|
swarmState.curatorPhaseAgentNames = preservedCuratorPhaseNames;
|
|
42033
|
-
if (pruneErrors.length > 0) {
|
|
42034
|
-
warnings.push(`Could not prune ${pruneErrors.length} branch(es) (unmerged or checked out): ${pruneErrors.join(", ")}`);
|
|
42035
|
-
}
|
|
42036
42209
|
const retroWarnings = warnings.filter((w) => w.includes("Retrospective write") || w.includes("retrospective write") || w.includes("Session retrospective"));
|
|
42037
42210
|
const otherWarnings = warnings.filter((w) => !w.includes("Retrospective write") && !w.includes("retrospective write") && !w.includes("Session retrospective"));
|
|
42038
42211
|
let warningMsg = "";
|
|
@@ -42050,16 +42223,19 @@ ${retroWarnings.map((w) => `- ${w}`).join(`
|
|
|
42050
42223
|
${otherWarnings.map((w) => `- ${w}`).join(`
|
|
42051
42224
|
`)}`;
|
|
42052
42225
|
}
|
|
42226
|
+
const lessonSummary = curationSucceeded && allLessons.length > 0 ? `
|
|
42227
|
+
|
|
42228
|
+
**Lessons Committed:** ${allLessons.length} lesson(s) committed to knowledge store` : "";
|
|
42053
42229
|
if (planAlreadyDone) {
|
|
42054
42230
|
return `✅ Session finalized. Plan was already in a terminal state — cleanup and archive applied.
|
|
42055
42231
|
|
|
42056
42232
|
**Archive:** ${archiveResult}
|
|
42057
|
-
**Git:** ${gitAlignResult}${warningMsg}`;
|
|
42233
|
+
**Git:** ${gitAlignResult}${lessonSummary}${warningMsg}`;
|
|
42058
42234
|
}
|
|
42059
42235
|
return `✅ Swarm finalized. ${closedPhases.length} phase(s) closed, ${closedTasks.length} incomplete task(s) marked closed.
|
|
42060
42236
|
|
|
42061
42237
|
**Archive:** ${archiveResult}
|
|
42062
|
-
**Git:** ${gitAlignResult}${warningMsg}`;
|
|
42238
|
+
**Git:** ${gitAlignResult}${lessonSummary}${warningMsg}`;
|
|
42063
42239
|
}
|
|
42064
42240
|
var ARCHIVE_ARTIFACTS, ACTIVE_STATE_TO_CLEAN;
|
|
42065
42241
|
var init_close = __esm(() => {
|
|
@@ -48185,6 +48361,216 @@ var init_history = __esm(() => {
|
|
|
48185
48361
|
init_history_service();
|
|
48186
48362
|
});
|
|
48187
48363
|
|
|
48364
|
+
// src/commands/issue.ts
|
|
48365
|
+
import { execSync as execSync2 } from "node:child_process";
|
|
48366
|
+
function sanitizeUrl(raw) {
|
|
48367
|
+
let urlStr = raw.trim();
|
|
48368
|
+
urlStr = urlStr.replace(/\[\s*MODE\s*:[^\]]*\]/gi, "");
|
|
48369
|
+
const fragmentIdx = urlStr.indexOf("#");
|
|
48370
|
+
if (fragmentIdx !== -1) {
|
|
48371
|
+
urlStr = urlStr.slice(0, fragmentIdx);
|
|
48372
|
+
}
|
|
48373
|
+
const queryIdx = urlStr.indexOf("?");
|
|
48374
|
+
if (queryIdx !== -1) {
|
|
48375
|
+
urlStr = urlStr.slice(0, queryIdx);
|
|
48376
|
+
}
|
|
48377
|
+
urlStr = urlStr.replace(/^[A-Za-z][A-Za-z0-9+.-]*:\/\/[^@/]+@/, "https://");
|
|
48378
|
+
if (urlStr.length > MAX_URL_LEN) {
|
|
48379
|
+
urlStr = urlStr.slice(0, MAX_URL_LEN);
|
|
48380
|
+
}
|
|
48381
|
+
return urlStr.trim();
|
|
48382
|
+
}
|
|
48383
|
+
function isPrivateHost(url3) {
|
|
48384
|
+
const host = url3.hostname.toLowerCase();
|
|
48385
|
+
if (host === "localhost" || host === "127.0.0.1" || host === "::1" || host === "0.0.0.0") {
|
|
48386
|
+
return true;
|
|
48387
|
+
}
|
|
48388
|
+
if (host.startsWith("localhost") || host === "localhost.com") {
|
|
48389
|
+
return true;
|
|
48390
|
+
}
|
|
48391
|
+
const ipv4Private = /^10\./;
|
|
48392
|
+
const ipv4172 = /^172\.(1[6-9]|2\d|3[0-1])\./;
|
|
48393
|
+
const ipv4192 = /^192\.168\./;
|
|
48394
|
+
const ipv6Private = /^fe80:/i;
|
|
48395
|
+
const ipv6Unique = /^f[cd][0-9a-f]{2}:/i;
|
|
48396
|
+
if (ipv4Private.test(host) || ipv4172.test(host) || ipv4192.test(host) || ipv6Private.test(host) || ipv6Unique.test(host)) {
|
|
48397
|
+
return true;
|
|
48398
|
+
}
|
|
48399
|
+
if (host.startsWith("::ffff:")) {
|
|
48400
|
+
const inner = host.slice(7);
|
|
48401
|
+
if (ipv4Private.test(inner) || ipv4172.test(inner) || ipv4192.test(inner)) {
|
|
48402
|
+
return true;
|
|
48403
|
+
}
|
|
48404
|
+
}
|
|
48405
|
+
return false;
|
|
48406
|
+
}
|
|
48407
|
+
function validateAndSanitizeUrl(rawUrl) {
|
|
48408
|
+
const sanitized = sanitizeUrl(rawUrl);
|
|
48409
|
+
if (!sanitized) {
|
|
48410
|
+
return { error: "Empty URL" };
|
|
48411
|
+
}
|
|
48412
|
+
if (!sanitized.startsWith("https://")) {
|
|
48413
|
+
return { error: "URL must use HTTPS scheme" };
|
|
48414
|
+
}
|
|
48415
|
+
try {
|
|
48416
|
+
const url3 = new URL(sanitized);
|
|
48417
|
+
const hostname5 = url3.hostname;
|
|
48418
|
+
if (/[\u0080-\u{10FFFF}]/u.test(hostname5)) {
|
|
48419
|
+
return { error: "Non-ASCII hostnames are not allowed" };
|
|
48420
|
+
}
|
|
48421
|
+
if (isPrivateHost(url3)) {
|
|
48422
|
+
return { error: "Private or localhost URLs are not allowed" };
|
|
48423
|
+
}
|
|
48424
|
+
const githubIssuePattern = /^https:\/\/github\.com\/([^/]+)\/([^/]+)\/issues\/([0-9]+)\/?$/;
|
|
48425
|
+
if (!githubIssuePattern.test(sanitized)) {
|
|
48426
|
+
return {
|
|
48427
|
+
error: "URL must be a GitHub issue URL (https://github.com/owner/repo/issues/N)"
|
|
48428
|
+
};
|
|
48429
|
+
}
|
|
48430
|
+
return { sanitized };
|
|
48431
|
+
} catch {
|
|
48432
|
+
return { error: "Invalid URL format" };
|
|
48433
|
+
}
|
|
48434
|
+
}
|
|
48435
|
+
function parseArgs2(args2) {
|
|
48436
|
+
const out2 = {
|
|
48437
|
+
plan: false,
|
|
48438
|
+
trace: false,
|
|
48439
|
+
noRepro: false,
|
|
48440
|
+
rest: []
|
|
48441
|
+
};
|
|
48442
|
+
for (const token of args2) {
|
|
48443
|
+
if (token === "--plan") {
|
|
48444
|
+
out2.plan = true;
|
|
48445
|
+
continue;
|
|
48446
|
+
}
|
|
48447
|
+
if (token === "--trace") {
|
|
48448
|
+
out2.trace = true;
|
|
48449
|
+
out2.plan = true;
|
|
48450
|
+
continue;
|
|
48451
|
+
}
|
|
48452
|
+
if (token === "--no-repro") {
|
|
48453
|
+
out2.noRepro = true;
|
|
48454
|
+
continue;
|
|
48455
|
+
}
|
|
48456
|
+
out2.rest.push(token);
|
|
48457
|
+
}
|
|
48458
|
+
return out2;
|
|
48459
|
+
}
|
|
48460
|
+
function parseIssueRef(input) {
|
|
48461
|
+
const urlMatch = input.match(/^https:\/\/github\.com\/([^/]+)\/([^/]+)\/issues\/(\d+)\/?$/i);
|
|
48462
|
+
if (urlMatch) {
|
|
48463
|
+
return {
|
|
48464
|
+
owner: urlMatch[1],
|
|
48465
|
+
repo: urlMatch[2],
|
|
48466
|
+
number: parseInt(urlMatch[3], 10)
|
|
48467
|
+
};
|
|
48468
|
+
}
|
|
48469
|
+
const shorthandMatch = input.match(/^([^/]+)\/([^#]+)#(\d+)$/);
|
|
48470
|
+
if (shorthandMatch) {
|
|
48471
|
+
return {
|
|
48472
|
+
owner: shorthandMatch[1],
|
|
48473
|
+
repo: shorthandMatch[2],
|
|
48474
|
+
number: parseInt(shorthandMatch[3], 10)
|
|
48475
|
+
};
|
|
48476
|
+
}
|
|
48477
|
+
const bareMatch = input.match(/^(\d+)$/);
|
|
48478
|
+
if (bareMatch) {
|
|
48479
|
+
const issueNumber = parseInt(bareMatch[1], 10);
|
|
48480
|
+
const remoteUrl = detectGitRemote();
|
|
48481
|
+
if (!remoteUrl) {
|
|
48482
|
+
return null;
|
|
48483
|
+
}
|
|
48484
|
+
const parsed = parseGitRemoteUrl(remoteUrl);
|
|
48485
|
+
if (!parsed) {
|
|
48486
|
+
return null;
|
|
48487
|
+
}
|
|
48488
|
+
return {
|
|
48489
|
+
owner: parsed.owner,
|
|
48490
|
+
repo: parsed.repo,
|
|
48491
|
+
number: issueNumber
|
|
48492
|
+
};
|
|
48493
|
+
}
|
|
48494
|
+
return null;
|
|
48495
|
+
}
|
|
48496
|
+
function detectGitRemote() {
|
|
48497
|
+
try {
|
|
48498
|
+
const remoteUrl = execSync2("git remote get-url origin", {
|
|
48499
|
+
encoding: "utf-8",
|
|
48500
|
+
stdio: ["pipe", "pipe", "pipe"],
|
|
48501
|
+
timeout: 5000
|
|
48502
|
+
}).trim();
|
|
48503
|
+
return remoteUrl || null;
|
|
48504
|
+
} catch {
|
|
48505
|
+
return null;
|
|
48506
|
+
}
|
|
48507
|
+
}
|
|
48508
|
+
function parseGitRemoteUrl(remoteUrl) {
|
|
48509
|
+
const httpsMatch = remoteUrl.match(/^https:\/\/github\.com\/([^/]+)\/([^/]+?)(?:\.git)?\/?$/i);
|
|
48510
|
+
if (httpsMatch) {
|
|
48511
|
+
return {
|
|
48512
|
+
owner: httpsMatch[1],
|
|
48513
|
+
repo: httpsMatch[2].replace(/\.git$/, "")
|
|
48514
|
+
};
|
|
48515
|
+
}
|
|
48516
|
+
const sshMatch = remoteUrl.match(/^git@github\.com:([^/]+)\/([^/]+?)(?:\.git)?$/i);
|
|
48517
|
+
if (sshMatch) {
|
|
48518
|
+
return {
|
|
48519
|
+
owner: sshMatch[1],
|
|
48520
|
+
repo: sshMatch[2].replace(/\.git$/, "")
|
|
48521
|
+
};
|
|
48522
|
+
}
|
|
48523
|
+
return null;
|
|
48524
|
+
}
|
|
48525
|
+
function handleIssueCommand(_directory, args2) {
|
|
48526
|
+
const parsed = parseArgs2(args2);
|
|
48527
|
+
const rawInput = parsed.rest.join(" ").trim();
|
|
48528
|
+
if (!rawInput) {
|
|
48529
|
+
return USAGE2;
|
|
48530
|
+
}
|
|
48531
|
+
const isFullUrl = /^https?:\/\//i.test(rawInput);
|
|
48532
|
+
const issueInfo = parseIssueRef(isFullUrl ? sanitizeUrl(rawInput) : rawInput);
|
|
48533
|
+
if (!issueInfo) {
|
|
48534
|
+
return `Error: Could not parse issue reference from "${rawInput}"
|
|
48535
|
+
|
|
48536
|
+
${USAGE2}`;
|
|
48537
|
+
}
|
|
48538
|
+
const issueUrl = `https://github.com/${issueInfo.owner}/${issueInfo.repo}/issues/${issueInfo.number}`;
|
|
48539
|
+
const result = validateAndSanitizeUrl(issueUrl);
|
|
48540
|
+
if ("error" in result) {
|
|
48541
|
+
return `Error: ${result.error}
|
|
48542
|
+
|
|
48543
|
+
${USAGE2}`;
|
|
48544
|
+
}
|
|
48545
|
+
const flags2 = [];
|
|
48546
|
+
if (parsed.plan)
|
|
48547
|
+
flags2.push("plan=true");
|
|
48548
|
+
if (parsed.trace)
|
|
48549
|
+
flags2.push("trace=true");
|
|
48550
|
+
if (parsed.noRepro)
|
|
48551
|
+
flags2.push("noRepro=true");
|
|
48552
|
+
const flagsStr = flags2.length > 0 ? ` ${flags2.join(" ")}` : "";
|
|
48553
|
+
return `[MODE: ISSUE_INGEST issue="${result.sanitized}"${flagsStr}]`;
|
|
48554
|
+
}
|
|
48555
|
+
var MAX_URL_LEN = 2048, USAGE2;
|
|
48556
|
+
var init_issue = __esm(() => {
|
|
48557
|
+
USAGE2 = [
|
|
48558
|
+
"Usage: /swarm issue <url|owner/repo#N|N> [--plan] [--trace] [--no-repro]",
|
|
48559
|
+
"",
|
|
48560
|
+
"Ingest a GitHub issue into the swarm workflow.",
|
|
48561
|
+
" /swarm issue https://github.com/owner/repo/issues/42",
|
|
48562
|
+
" /swarm issue owner/repo#42",
|
|
48563
|
+
" /swarm issue 42 --plan",
|
|
48564
|
+
" /swarm issue 42 --trace --no-repro",
|
|
48565
|
+
"",
|
|
48566
|
+
"Flags:",
|
|
48567
|
+
" --plan Transition to plan creation after spec generation",
|
|
48568
|
+
" --trace Run full fix-and-PR workflow (implies --plan)",
|
|
48569
|
+
" --no-repro Skip reproduction step"
|
|
48570
|
+
].join(`
|
|
48571
|
+
`);
|
|
48572
|
+
});
|
|
48573
|
+
|
|
48188
48574
|
// src/hooks/knowledge-migrator.ts
|
|
48189
48575
|
import { randomUUID as randomUUID3 } from "node:crypto";
|
|
48190
48576
|
import { existsSync as existsSync16, readFileSync as readFileSync11 } from "node:fs";
|
|
@@ -48693,6 +49079,192 @@ var init_plan = __esm(() => {
|
|
|
48693
49079
|
init_plan_service();
|
|
48694
49080
|
});
|
|
48695
49081
|
|
|
49082
|
+
// src/commands/pr-review.ts
|
|
49083
|
+
import { execSync as execSync3 } from "node:child_process";
|
|
49084
|
+
function sanitizeUrl2(raw) {
|
|
49085
|
+
let urlStr = raw.trim();
|
|
49086
|
+
urlStr = urlStr.replace(/\[\s*MODE\s*:[^\]]*\]/gi, "");
|
|
49087
|
+
const fragmentIdx = urlStr.indexOf("#");
|
|
49088
|
+
if (fragmentIdx !== -1) {
|
|
49089
|
+
urlStr = urlStr.slice(0, fragmentIdx);
|
|
49090
|
+
}
|
|
49091
|
+
const queryIdx = urlStr.indexOf("?");
|
|
49092
|
+
if (queryIdx !== -1) {
|
|
49093
|
+
urlStr = urlStr.slice(0, queryIdx);
|
|
49094
|
+
}
|
|
49095
|
+
urlStr = urlStr.replace(/^[A-Za-z][A-Za-z0-9+.-]*:\/\/[^@/]+@/, "https://");
|
|
49096
|
+
if (urlStr.length > MAX_URL_LEN2) {
|
|
49097
|
+
urlStr = urlStr.slice(0, MAX_URL_LEN2);
|
|
49098
|
+
}
|
|
49099
|
+
return urlStr.trim();
|
|
49100
|
+
}
|
|
49101
|
+
function isPrivateHost2(url3) {
|
|
49102
|
+
const host = url3.hostname.toLowerCase();
|
|
49103
|
+
if (host === "localhost" || host === "127.0.0.1" || host === "::1" || host === "0.0.0.0") {
|
|
49104
|
+
return true;
|
|
49105
|
+
}
|
|
49106
|
+
if (host.startsWith("localhost") || host === "localhost.com") {
|
|
49107
|
+
return true;
|
|
49108
|
+
}
|
|
49109
|
+
const ipv4Private = /^10\./;
|
|
49110
|
+
const ipv4172 = /^172\.(1[6-9]|2\d|3[0-1])\./;
|
|
49111
|
+
const ipv4192 = /^192\.168\./;
|
|
49112
|
+
const ipv6Private = /^fe80:/i;
|
|
49113
|
+
const ipv6Unique = /^f[cd][0-9a-f]{2}:/i;
|
|
49114
|
+
if (ipv4Private.test(host) || ipv4172.test(host) || ipv4192.test(host) || ipv6Private.test(host) || ipv6Unique.test(host)) {
|
|
49115
|
+
return true;
|
|
49116
|
+
}
|
|
49117
|
+
if (host.startsWith("::ffff:")) {
|
|
49118
|
+
const inner = host.slice(7);
|
|
49119
|
+
if (ipv4Private.test(inner) || ipv4172.test(inner) || ipv4192.test(inner)) {
|
|
49120
|
+
return true;
|
|
49121
|
+
}
|
|
49122
|
+
}
|
|
49123
|
+
return false;
|
|
49124
|
+
}
|
|
49125
|
+
function validateAndSanitizeUrl2(rawUrl) {
|
|
49126
|
+
const sanitized = sanitizeUrl2(rawUrl);
|
|
49127
|
+
if (!sanitized) {
|
|
49128
|
+
return { error: "Empty URL" };
|
|
49129
|
+
}
|
|
49130
|
+
if (!sanitized.startsWith("https://")) {
|
|
49131
|
+
return { error: "URL must use HTTPS scheme" };
|
|
49132
|
+
}
|
|
49133
|
+
try {
|
|
49134
|
+
const url3 = new URL(sanitized);
|
|
49135
|
+
const hostname5 = url3.hostname;
|
|
49136
|
+
if (/[\u0080-\u{10FFFF}]/u.test(hostname5)) {
|
|
49137
|
+
return { error: "Non-ASCII hostnames are not allowed" };
|
|
49138
|
+
}
|
|
49139
|
+
if (isPrivateHost2(url3)) {
|
|
49140
|
+
return { error: "Private or localhost URLs are not allowed" };
|
|
49141
|
+
}
|
|
49142
|
+
const githubPrPattern = /^https:\/\/github\.com\/([^/]+)\/([^/]+)\/pull\/([0-9]+)\/?$/;
|
|
49143
|
+
if (!githubPrPattern.test(sanitized)) {
|
|
49144
|
+
return {
|
|
49145
|
+
error: "URL must be a GitHub pull request URL (https://github.com/owner/repo/pull/N)"
|
|
49146
|
+
};
|
|
49147
|
+
}
|
|
49148
|
+
return { sanitized };
|
|
49149
|
+
} catch {
|
|
49150
|
+
return { error: "Invalid URL format" };
|
|
49151
|
+
}
|
|
49152
|
+
}
|
|
49153
|
+
function parseArgs3(args2) {
|
|
49154
|
+
const out2 = { council: false, rest: [] };
|
|
49155
|
+
for (const token of args2) {
|
|
49156
|
+
if (token === "--council") {
|
|
49157
|
+
out2.council = true;
|
|
49158
|
+
continue;
|
|
49159
|
+
}
|
|
49160
|
+
out2.rest.push(token);
|
|
49161
|
+
}
|
|
49162
|
+
return out2;
|
|
49163
|
+
}
|
|
49164
|
+
function parsePrRef(input) {
|
|
49165
|
+
const urlMatch = input.match(/^https:\/\/github\.com\/([^/]+)\/([^/]+)\/pull\/(\d+)\/?$/i);
|
|
49166
|
+
if (urlMatch) {
|
|
49167
|
+
return {
|
|
49168
|
+
owner: urlMatch[1],
|
|
49169
|
+
repo: urlMatch[2],
|
|
49170
|
+
number: parseInt(urlMatch[3], 10)
|
|
49171
|
+
};
|
|
49172
|
+
}
|
|
49173
|
+
const shorthandMatch = input.match(/^([^/]+)\/([^#]+)#(\d+)$/);
|
|
49174
|
+
if (shorthandMatch) {
|
|
49175
|
+
return {
|
|
49176
|
+
owner: shorthandMatch[1],
|
|
49177
|
+
repo: shorthandMatch[2],
|
|
49178
|
+
number: parseInt(shorthandMatch[3], 10)
|
|
49179
|
+
};
|
|
49180
|
+
}
|
|
49181
|
+
const bareMatch = input.match(/^(\d+)$/);
|
|
49182
|
+
if (bareMatch) {
|
|
49183
|
+
const prNumber = parseInt(bareMatch[1], 10);
|
|
49184
|
+
const remoteUrl = detectGitRemote2();
|
|
49185
|
+
if (!remoteUrl) {
|
|
49186
|
+
return null;
|
|
49187
|
+
}
|
|
49188
|
+
const parsed = parseGitRemoteUrl2(remoteUrl);
|
|
49189
|
+
if (!parsed) {
|
|
49190
|
+
return null;
|
|
49191
|
+
}
|
|
49192
|
+
return {
|
|
49193
|
+
owner: parsed.owner,
|
|
49194
|
+
repo: parsed.repo,
|
|
49195
|
+
number: prNumber
|
|
49196
|
+
};
|
|
49197
|
+
}
|
|
49198
|
+
return null;
|
|
49199
|
+
}
|
|
49200
|
+
function detectGitRemote2() {
|
|
49201
|
+
try {
|
|
49202
|
+
const remoteUrl = execSync3("git remote get-url origin", {
|
|
49203
|
+
encoding: "utf-8",
|
|
49204
|
+
stdio: ["pipe", "pipe", "pipe"],
|
|
49205
|
+
timeout: 5000
|
|
49206
|
+
}).trim();
|
|
49207
|
+
return remoteUrl || null;
|
|
49208
|
+
} catch {
|
|
49209
|
+
return null;
|
|
49210
|
+
}
|
|
49211
|
+
}
|
|
49212
|
+
function parseGitRemoteUrl2(remoteUrl) {
|
|
49213
|
+
const httpsMatch = remoteUrl.match(/^https:\/\/github\.com\/([^/]+)\/([^/]+?)(?:\.git)?\/?$/i);
|
|
49214
|
+
if (httpsMatch) {
|
|
49215
|
+
return {
|
|
49216
|
+
owner: httpsMatch[1],
|
|
49217
|
+
repo: httpsMatch[2].replace(/\.git$/, "")
|
|
49218
|
+
};
|
|
49219
|
+
}
|
|
49220
|
+
const sshMatch = remoteUrl.match(/^git@github\.com:([^/]+)\/([^/]+?)(?:\.git)?$/i);
|
|
49221
|
+
if (sshMatch) {
|
|
49222
|
+
return {
|
|
49223
|
+
owner: sshMatch[1],
|
|
49224
|
+
repo: sshMatch[2].replace(/\.git$/, "")
|
|
49225
|
+
};
|
|
49226
|
+
}
|
|
49227
|
+
return null;
|
|
49228
|
+
}
|
|
49229
|
+
function handlePrReviewCommand(_directory, args2) {
|
|
49230
|
+
const parsed = parseArgs3(args2);
|
|
49231
|
+
const rawInput = parsed.rest.join(" ").trim();
|
|
49232
|
+
if (!rawInput) {
|
|
49233
|
+
return USAGE3;
|
|
49234
|
+
}
|
|
49235
|
+
const isFullUrl = /^https?:\/\//i.test(rawInput);
|
|
49236
|
+
const prInfo = parsePrRef(isFullUrl ? sanitizeUrl2(rawInput) : rawInput);
|
|
49237
|
+
if (!prInfo) {
|
|
49238
|
+
return `Error: Could not parse PR reference from "${rawInput}"
|
|
49239
|
+
|
|
49240
|
+
${USAGE3}`;
|
|
49241
|
+
}
|
|
49242
|
+
const prUrl = `https://github.com/${prInfo.owner}/${prInfo.repo}/pull/${prInfo.number}`;
|
|
49243
|
+
const result = validateAndSanitizeUrl2(prUrl);
|
|
49244
|
+
if ("error" in result) {
|
|
49245
|
+
return `Error: ${result.error}
|
|
49246
|
+
|
|
49247
|
+
${USAGE3}`;
|
|
49248
|
+
}
|
|
49249
|
+
const councilFlag = parsed.council ? "council=true" : "council=false";
|
|
49250
|
+
return `[MODE: PR_REVIEW pr="${result.sanitized}" ${councilFlag}]`;
|
|
49251
|
+
}
|
|
49252
|
+
var MAX_URL_LEN2 = 2048, USAGE3;
|
|
49253
|
+
var init_pr_review = __esm(() => {
|
|
49254
|
+
USAGE3 = [
|
|
49255
|
+
"Usage: /swarm pr-review <url|owner/repo#N|N> [--council]",
|
|
49256
|
+
"",
|
|
49257
|
+
"Run a full swarm PR review on a GitHub pull request.",
|
|
49258
|
+
" /swarm pr-review https://github.com/owner/repo/pull/42",
|
|
49259
|
+
" /swarm pr-review owner/repo#42",
|
|
49260
|
+
" /swarm pr-review 42 --council",
|
|
49261
|
+
"",
|
|
49262
|
+
"Flags:",
|
|
49263
|
+
" --council Run adversarial council variant (all lanes assume work is wrong)"
|
|
49264
|
+
].join(`
|
|
49265
|
+
`);
|
|
49266
|
+
});
|
|
49267
|
+
|
|
48696
49268
|
// src/utils/path-security.ts
|
|
48697
49269
|
import * as fs17 from "node:fs";
|
|
48698
49270
|
import * as path30 from "node:path";
|
|
@@ -52653,7 +53225,8 @@ var init_qa_gates = __esm(() => {
|
|
|
52653
53225
|
"hallucination_guard",
|
|
52654
53226
|
"sast_enabled",
|
|
52655
53227
|
"mutation_test",
|
|
52656
|
-
"council_general_review"
|
|
53228
|
+
"council_general_review",
|
|
53229
|
+
"drift_check"
|
|
52657
53230
|
];
|
|
52658
53231
|
});
|
|
52659
53232
|
|
|
@@ -54333,8 +54906,10 @@ var init_registry = __esm(() => {
|
|
|
54333
54906
|
init_full_auto();
|
|
54334
54907
|
init_handoff();
|
|
54335
54908
|
init_history();
|
|
54909
|
+
init_issue();
|
|
54336
54910
|
init_knowledge();
|
|
54337
54911
|
init_plan();
|
|
54912
|
+
init_pr_review();
|
|
54338
54913
|
init_preflight();
|
|
54339
54914
|
init_promote();
|
|
54340
54915
|
init_qa_gates();
|
|
@@ -54489,11 +55064,23 @@ var init_registry = __esm(() => {
|
|
|
54489
55064
|
args: "<question> [--preset <name>] [--spec-review]",
|
|
54490
55065
|
details: "Triggers the architect to convene a configurable General Council: each member independently web-searches, answers, and engages in one structured deliberation round on disagreements; an optional moderator pass synthesizes the final answer. --preset <name> selects a member group from council.general.presets. --spec-review switches to single-pass advisory mode for spec review. Requires council.general.enabled: true and a search API key in opencode-swarm.json."
|
|
54491
55066
|
},
|
|
55067
|
+
"pr-review": {
|
|
55068
|
+
handler: async (ctx) => handlePrReviewCommand(ctx.directory, ctx.args),
|
|
55069
|
+
description: "Launch deep PR review with multi-lane analysis [url] [--council]",
|
|
55070
|
+
args: "<pr-url|owner/repo#N|N> [--council]",
|
|
55071
|
+
details: "Launches a structured PR review: reconstructs PR intent via obligation extraction cascade, runs 6 parallel explorer lanes (correctness, security, dependencies, docs-intent-vs-actual, tests, performance-architecture), validates findings through independent reviewer confirmation, applies critic challenge to HIGH/CRITICAL findings, synthesizes structured report. --council variant fires adversarial multi-model review. Supports full GitHub URL, owner/repo#N shorthand, or bare PR number (resolves against origin remote)."
|
|
55072
|
+
},
|
|
55073
|
+
issue: {
|
|
55074
|
+
handler: async (ctx) => handleIssueCommand(ctx.directory, ctx.args),
|
|
55075
|
+
description: "Ingest a GitHub issue into the swarm workflow [url] [--plan] [--trace] [--no-repro]",
|
|
55076
|
+
args: "<issue-url|owner/repo#N|N> [--plan] [--trace] [--no-repro]",
|
|
55077
|
+
details: "Triggers the architect to enter MODE: ISSUE_INGEST — ingests a GitHub issue, restructures it into a normalized intake note, localizes root cause through hypothesis-driven tracing, and outputs a resolution spec. --plan transitions to plan creation after spec generation. --trace runs the full fix-and-PR workflow (implies --plan). --no-repro skips the reproduction step. Supports full GitHub URL, owner/repo#N shorthand, or bare issue number (resolves against origin remote)."
|
|
55078
|
+
},
|
|
54492
55079
|
"qa-gates": {
|
|
54493
55080
|
handler: (ctx) => handleQaGatesCommand(ctx.directory, ctx.args, ctx.sessionID),
|
|
54494
55081
|
description: "View or modify QA gate profile for the current plan [enable|override <gate>...]",
|
|
54495
55082
|
args: "[show|enable|override] <gate>...",
|
|
54496
|
-
details: "show: display spec-level, session-override, and effective QA gates for the current plan. enable: persist gate(s) into the locked-once profile (architect; rejected after critic approval lock). override: session-only ratchet-tighter enable. Valid gates: reviewer, test_engineer, council_mode, sme_enabled, critic_pre_plan, hallucination_guard, sast_enabled, mutation_test, council_general_review."
|
|
55083
|
+
details: "show: display spec-level, session-override, and effective QA gates for the current plan. enable: persist gate(s) into the locked-once profile (architect; rejected after critic approval lock). override: session-only ratchet-tighter enable. Valid gates: reviewer, test_engineer, council_mode, sme_enabled, critic_pre_plan, hallucination_guard, sast_enabled, mutation_test, council_general_review, drift_check."
|
|
54497
55084
|
},
|
|
54498
55085
|
promote: {
|
|
54499
55086
|
handler: (ctx) => handlePromoteCommand(ctx.directory, ctx.args),
|
|
@@ -54709,7 +55296,7 @@ function buildQaGateSelectionDialogue(modeLabel) {
|
|
|
54709
55296
|
const leadIn = modeLabel === "BRAINSTORM" ? "Now ask the user which QA gates to enable for this plan — do not select on their behalf." : modeLabel === "SPECIFY" ? "Ask the user which QA gates to enable for this plan before suggesting the next step." : "No pending gate selection found in `.swarm/context.md`. Ask the user inline now.";
|
|
54710
55297
|
return `${leadIn}
|
|
54711
55298
|
|
|
54712
|
-
Present the
|
|
55299
|
+
Present the ten gates with their defaults (DEFAULT_QA_GATES) as a single user-facing question. Offer the user a one-shot choice: accept defaults, or customize. The ten gates are:
|
|
54713
55300
|
- reviewer (default: ON) — code review of coder output
|
|
54714
55301
|
- test_engineer (default: ON) — test verification of coder output
|
|
54715
55302
|
- sme_enabled (default: ON) — SME consultation during planning/clarification
|
|
@@ -54719,6 +55306,7 @@ Present the nine gates with their defaults (DEFAULT_QA_GATES) as a single user-f
|
|
|
54719
55306
|
- hallucination_guard (default: OFF) — when enabled, mandatory per-phase API/signature/claim/citation verification via critic_hallucination_verifier at PHASE-WRAP; phase_complete will REJECT phase completion unless .swarm/evidence/{phase}/hallucination-guard.json exists with an APPROVED verdict (recommended for claim-heavy or research-heavy work)
|
|
54720
55307
|
- mutation_test (default: OFF) — when enabled, runs mutation testing on source files touched this phase via generate_mutants + mutation_test + write_mutation_evidence at PHASE-WRAP; FAIL verdict blocks phase_complete; WARN is non-blocking (recommended for projects with coverage gaps or safety-critical code)
|
|
54721
55308
|
- council_general_review (default: OFF) — when enabled, MODE: SPECIFY runs convene_general_council on the draft spec before the critic-gate; multiple models each independently search the web, deliberate on disagreements, and a moderator synthesizes a final answer that the architect folds into the spec (recommended for novel architecture, unclear best practices, or high-risk design decisions). Requires council.general.enabled: true and a configured search API key.
|
|
55309
|
+
- drift_check (default: ON) — when enabled, mandatory per-phase drift verification via critic_drift_verifier at PHASE-WRAP; compares implemented changes against spec.md intent; hard-blocks phase_complete when spec.md exists and drift evidence is missing or REJECTED; advisory-only when no spec.md exists (recommended for all projects with a specification)
|
|
54722
55310
|
|
|
54723
55311
|
One question, one message, defaults pre-stated. Wait for the user's answer.`;
|
|
54724
55312
|
}
|
|
@@ -55464,6 +56052,7 @@ Do NOT call \`set_qa_gates\` yet — \`plan.json\` does not exist at this point.
|
|
|
55464
56052
|
- hallucination_guard: <true|false>
|
|
55465
56053
|
- mutation_test: <true|false>
|
|
55466
56054
|
- council_general_review: <true|false>
|
|
56055
|
+
- drift_check: <true|false>
|
|
55467
56056
|
- recorded_at: <ISO timestamp>
|
|
55468
56057
|
\`\`\`
|
|
55469
56058
|
MODE: PLAN applies these after \`save_plan\` succeeds via \`set_qa_gates\`.
|
|
@@ -55541,6 +56130,7 @@ Do NOT call \`set_qa_gates\` yet — \`plan.json\` does not exist at this point.
|
|
|
55541
56130
|
- hallucination_guard: <true|false>
|
|
55542
56131
|
- mutation_test: <true|false>
|
|
55543
56132
|
- council_general_review: <true|false>
|
|
56133
|
+
- drift_check: <true|false>
|
|
55544
56134
|
- recorded_at: <ISO timestamp>
|
|
55545
56135
|
\`\`\`
|
|
55546
56136
|
MODE: PLAN will read this section after \`save_plan\` succeeds and persist via \`set_qa_gates\`.
|
|
@@ -55772,6 +56362,61 @@ This mode is ADVISORY — it does NOT block any other workflow and does NOT modi
|
|
|
55772
56362
|
- If no moderator: present the structural \`synthesis\` markdown from the tool's return.
|
|
55773
56363
|
In either case, do NOT present the raw per-member JSON. Do NOT silently pick a winner among persisting disagreements — surface them honestly.
|
|
55774
56364
|
|
|
56365
|
+
### MODE: ISSUE_INGEST
|
|
56366
|
+
Activates when: user invokes \`/swarm issue <url>\`; OR architect receives \`[MODE: ISSUE_INGEST issue="<url>"]\` signal.
|
|
56367
|
+
|
|
56368
|
+
Purpose: ingest a GitHub issue, localize root cause, and produce a resolution spec. The issue URL points to a GitHub issue that describes a bug, feature request, or task to be resolved.
|
|
56369
|
+
|
|
56370
|
+
Flags parsed from signal:
|
|
56371
|
+
- \`plan=true\` → after spec generation, transition to MODE: PLAN (create implementation plan)
|
|
56372
|
+
- \`trace=true\` → after plan, delegate to swarm-implement skill for full fix-and-PR workflow (implies plan=true)
|
|
56373
|
+
- \`noRepro=true\` → skip reproduction verification step
|
|
56374
|
+
|
|
56375
|
+
#### Phase 1: INTAKE
|
|
56376
|
+
1. Fetch the issue body using the GitHub CLI (\`gh issue view <N> --repo <owner>/<repo> --json title,body,labels,assignees,comments\`) or web fetch.
|
|
56377
|
+
2. Parse the issue into a normalized **Intake Note** with four required fields:
|
|
56378
|
+
- **Observed behavior**: what the issue reports
|
|
56379
|
+
- **Expected behavior**: what should happen instead
|
|
56380
|
+
- **Reproduction steps**: how to trigger the issue (may be absent; flag with \`[NEEDS REPRO]\` if missing)
|
|
56381
|
+
- **Environment**: platform, version, configuration context
|
|
56382
|
+
3. If any required field is missing and cannot be inferred from context, flag as \`[NEEDS REPRO]\`.
|
|
56383
|
+
4. If \`--no-repro\` flag is set, skip reproduction verification and proceed with available information.
|
|
56384
|
+
5. Exit when the Intake Note is complete or all missing fields are flagged.
|
|
56385
|
+
|
|
56386
|
+
#### Phase 2: LOCALIZATION
|
|
56387
|
+
1. Delegate to \`mega_explorer\` to scan the codebase for code areas related to the issue's observed behavior.
|
|
56388
|
+
2. Build 2–5 candidate hypotheses for root cause, each with:
|
|
56389
|
+
- **Location**: file(s) and function(s) most likely responsible
|
|
56390
|
+
- **Confidence**: composite score (stack-trace match 0.4, recency 0.25, call-graph proximity 0.2, test-failure correlation 0.15)
|
|
56391
|
+
- **Falsifiability**: a specific test or observation that would disprove this hypothesis
|
|
56392
|
+
3. Validate top-3 hypotheses in parallel using targeted \`mega_sme\` consultations.
|
|
56393
|
+
4. Prune to a single root cause hypothesis with supporting evidence.
|
|
56394
|
+
5. Exit when a root cause is identified with ≥70% confidence, or when all hypotheses are exhausted (report ambiguity).
|
|
56395
|
+
|
|
56396
|
+
#### Phase 3: SPEC GENERATION
|
|
56397
|
+
0. Include a **Root Cause** section derived from Phase 2 localization results: concise statement of the identified root cause, location, and confidence score. Include a **Fix Strategy** section at product/behavior level (what the fix must accomplish, not how to implement it).
|
|
56398
|
+
1. Generate \`.swarm/spec.md\` using the same SPEC CONTENT RULES as MODE: SPECIFY:
|
|
56399
|
+
- WHAT users need and WHY — never HOW to implement
|
|
56400
|
+
- FR-### / SC-### numbering, Given/When/Then scenarios
|
|
56401
|
+
- No technology stack, APIs, or code structure
|
|
56402
|
+
- \`[NEEDS CLARIFICATION]\` markers (max 3)
|
|
56403
|
+
2. Cross-reference the spec against the issue's expected behavior to ensure alignment.
|
|
56404
|
+
3. If the issue is a bug: spec must describe the correct behavior, not the broken behavior.
|
|
56405
|
+
4. If the issue is a feature: spec must describe the user-facing outcome, not the implementation.
|
|
56406
|
+
5. QA GATE SELECTION: Ask user which QA gates to enable (same dialogue as MODE: SPECIFY). Write to \`.swarm/context.md\` under \`## Pending QA Gate Selection\`.
|
|
56407
|
+
|
|
56408
|
+
#### Phase 4: TRANSITION
|
|
56409
|
+
Based on flags:
|
|
56410
|
+
- No flags → report spec summary and suggest \`PLAN\` or \`CLARIFY-SPEC\`
|
|
56411
|
+
- \`plan=true\` → transition to MODE: PLAN using the generated spec
|
|
56412
|
+
- \`trace=true\` → transition to MODE: PLAN, then delegate to swarm-implement skill for full fix workflow
|
|
56413
|
+
|
|
56414
|
+
RULES:
|
|
56415
|
+
- One question per message in INTAKE dialogue (max 6 questions)
|
|
56416
|
+
- Hypotheses must be falsifiable — no unfalsifiable hypotheses
|
|
56417
|
+
- Spec must be independently testable — each FR must have a verification path
|
|
56418
|
+
- The issue URL is already sanitized by the issue command — do not re-sanitize
|
|
56419
|
+
|
|
55775
56420
|
### MODE: PLAN
|
|
55776
56421
|
|
|
55777
56422
|
SPEC GATE (soft — check before planning):
|
|
@@ -72786,7 +73431,7 @@ function deserializeAgentSession(s) {
|
|
|
72786
73431
|
for (const [key, win] of Object.entries(s.windows ?? {})) {
|
|
72787
73432
|
windows[key] = {
|
|
72788
73433
|
...win,
|
|
72789
|
-
transientRetryCount: win.transientRetryCount ?? 0
|
|
73434
|
+
transientRetryCount: "transientRetryCount" in win ? win.transientRetryCount ?? 0 : 0
|
|
72790
73435
|
};
|
|
72791
73436
|
}
|
|
72792
73437
|
return {
|
|
@@ -74773,7 +75418,8 @@ var COUNCIL_DEFAULTS = {
|
|
|
74773
75418
|
parallelTimeoutMs: 30000,
|
|
74774
75419
|
vetoPriority: true,
|
|
74775
75420
|
requireAllMembers: false,
|
|
74776
|
-
minimumMembers: 3
|
|
75421
|
+
minimumMembers: 3,
|
|
75422
|
+
phaseConcernsAllowComplete: true
|
|
74777
75423
|
};
|
|
74778
75424
|
|
|
74779
75425
|
// src/council/council-service.ts
|
|
@@ -78126,7 +78772,7 @@ async function executePhaseComplete(args2, workingDirectory, directory) {
|
|
|
78126
78772
|
}, null, 2);
|
|
78127
78773
|
}
|
|
78128
78774
|
if (hasActiveTurboMode(sessionID)) {
|
|
78129
|
-
console.warn(`[phase_complete] Turbo mode active — skipping completion-verify, drift-verifier, hallucination-guard,
|
|
78775
|
+
console.warn(`[phase_complete] Turbo mode active — skipping completion-verify, drift-verifier, hallucination-guard, mutation-gate, and phase-council gates for phase ${phase}`);
|
|
78130
78776
|
} else {
|
|
78131
78777
|
try {
|
|
78132
78778
|
const completionResultRaw = await executeCompletionVerify({ phase }, dir);
|
|
@@ -78148,88 +78794,138 @@ async function executePhaseComplete(args2, workingDirectory, directory) {
|
|
|
78148
78794
|
} catch (completionError) {
|
|
78149
78795
|
safeWarn(`[phase_complete] Completion verify error (non-blocking):`, completionError);
|
|
78150
78796
|
}
|
|
78797
|
+
let driftCheckEnabled = true;
|
|
78798
|
+
let driftHasSpecMd = false;
|
|
78151
78799
|
try {
|
|
78152
|
-
const
|
|
78153
|
-
|
|
78154
|
-
|
|
78800
|
+
const specMdPath = path81.join(dir, ".swarm", "spec.md");
|
|
78801
|
+
driftHasSpecMd = fs65.existsSync(specMdPath);
|
|
78802
|
+
const gatePlan = await loadPlan(dir);
|
|
78803
|
+
if (gatePlan) {
|
|
78804
|
+
const gatePlanId = `${gatePlan.swarm}-${gatePlan.title}`.replace(/[^a-zA-Z0-9-_]/g, "_");
|
|
78805
|
+
const gateProfile = getProfile(dir, gatePlanId);
|
|
78806
|
+
if (gateProfile) {
|
|
78807
|
+
const gateSession = sessionID ? swarmState.agentSessions.get(sessionID) : undefined;
|
|
78808
|
+
const gateOverrides = gateSession?.qaGateSessionOverrides ?? {};
|
|
78809
|
+
const gateEffective = getEffectiveGates(gateProfile, gateOverrides);
|
|
78810
|
+
driftCheckEnabled = gateEffective.drift_check === true;
|
|
78811
|
+
}
|
|
78812
|
+
}
|
|
78813
|
+
} catch (gateLoadError) {
|
|
78814
|
+
safeWarn(`[phase_complete] QA gate profile load error, drift_check defaults to enabled:`, gateLoadError);
|
|
78815
|
+
}
|
|
78816
|
+
if (!driftCheckEnabled) {
|
|
78817
|
+
console.info(`[phase_complete] drift_check disabled — skipping drift verification gate for phase ${phase}`);
|
|
78818
|
+
warnings.push(`drift_check gate is disabled. Drift verification was skipped for phase ${phase}.`);
|
|
78819
|
+
} else {
|
|
78820
|
+
let phaseType;
|
|
78155
78821
|
try {
|
|
78156
|
-
const
|
|
78157
|
-
|
|
78158
|
-
|
|
78159
|
-
|
|
78160
|
-
|
|
78161
|
-
|
|
78162
|
-
|
|
78163
|
-
|
|
78822
|
+
const planPath = path81.join(dir, ".swarm", "plan.json");
|
|
78823
|
+
if (fs65.existsSync(planPath)) {
|
|
78824
|
+
const planRaw = fs65.readFileSync(planPath, "utf-8");
|
|
78825
|
+
const plan = JSON.parse(planRaw);
|
|
78826
|
+
const targetPhase = plan.phases?.find((p) => p.id === phase);
|
|
78827
|
+
phaseType = targetPhase?.type;
|
|
78828
|
+
}
|
|
78829
|
+
} catch {}
|
|
78830
|
+
if (phaseType === "non-code") {
|
|
78831
|
+
console.info(`[phase_complete] Phase ${phase} annotated as 'non-code' — drift verification skipped.`);
|
|
78832
|
+
warnings.push(`Phase ${phase} is annotated as 'non-code'. Drift verification was skipped per phase type annotation.`);
|
|
78833
|
+
} else {
|
|
78834
|
+
try {
|
|
78835
|
+
const driftEvidencePath = path81.join(dir, ".swarm", "evidence", String(phase), "drift-verifier.json");
|
|
78836
|
+
let driftVerdictFound = false;
|
|
78837
|
+
let driftVerdictApproved = false;
|
|
78838
|
+
try {
|
|
78839
|
+
const driftEvidenceContent = fs65.readFileSync(driftEvidencePath, "utf-8");
|
|
78840
|
+
const driftEvidence = JSON.parse(driftEvidenceContent);
|
|
78841
|
+
const entries = driftEvidence.entries ?? [];
|
|
78842
|
+
for (const entry of entries) {
|
|
78843
|
+
if (typeof entry.type === "string" && entry.type.includes("drift") && typeof entry.verdict === "string") {
|
|
78844
|
+
driftVerdictFound = true;
|
|
78845
|
+
if (entry.verdict === "approved") {
|
|
78846
|
+
driftVerdictApproved = true;
|
|
78847
|
+
}
|
|
78848
|
+
if (entry.verdict === "rejected" || typeof entry.summary === "string" && entry.summary.includes("NEEDS_REVISION")) {
|
|
78849
|
+
return JSON.stringify({
|
|
78850
|
+
success: false,
|
|
78851
|
+
phase,
|
|
78852
|
+
status: "blocked",
|
|
78853
|
+
reason: "DRIFT_VERIFICATION_REJECTED",
|
|
78854
|
+
message: `Phase ${phase} cannot be completed: drift verifier returned verdict '${entry.verdict}'. Address the drift issues before completing the phase.`,
|
|
78855
|
+
agentsDispatched,
|
|
78856
|
+
agentsMissing: [],
|
|
78857
|
+
warnings: []
|
|
78858
|
+
}, null, 2);
|
|
78859
|
+
}
|
|
78860
|
+
}
|
|
78861
|
+
}
|
|
78862
|
+
} catch (readError) {
|
|
78863
|
+
if (readError.code !== "ENOENT") {
|
|
78864
|
+
safeWarn(`[phase_complete] Drift verifier evidence unreadable:`, readError);
|
|
78164
78865
|
}
|
|
78165
|
-
|
|
78866
|
+
driftVerdictFound = false;
|
|
78867
|
+
}
|
|
78868
|
+
if (!driftVerdictFound) {
|
|
78869
|
+
if (!driftHasSpecMd) {
|
|
78870
|
+
let incompleteTaskCount = 0;
|
|
78871
|
+
let planParseable = false;
|
|
78872
|
+
try {
|
|
78873
|
+
const planPath = path81.join(dir, ".swarm", "plan.json");
|
|
78874
|
+
if (fs65.existsSync(planPath)) {
|
|
78875
|
+
const planRaw = fs65.readFileSync(planPath, "utf-8");
|
|
78876
|
+
const plan = JSON.parse(planRaw);
|
|
78877
|
+
planParseable = true;
|
|
78878
|
+
const planPhase = plan.phases?.find((p) => p.id === phase);
|
|
78879
|
+
if (planPhase?.tasks) {
|
|
78880
|
+
incompleteTaskCount = planPhase.tasks.filter((t) => t.status !== "completed" && t.status !== "closed").length;
|
|
78881
|
+
}
|
|
78882
|
+
}
|
|
78883
|
+
} catch {}
|
|
78884
|
+
if (!planParseable) {
|
|
78885
|
+
warnings.push(`No spec.md found and drift verification evidence missing — consider running critic_drift_verifier before phase completion.`);
|
|
78886
|
+
} else if (incompleteTaskCount > 0) {
|
|
78887
|
+
warnings.push(`No spec.md found and drift verification evidence missing. Phase ${phase} has ${incompleteTaskCount} incomplete task(s) in plan.json — consider running critic_drift_verifier before phase completion.`);
|
|
78888
|
+
} else {
|
|
78889
|
+
warnings.push(`No spec.md found. Phase ${phase} tasks are all completed in plan.json. Drift verification was skipped.`);
|
|
78890
|
+
}
|
|
78891
|
+
} else {
|
|
78166
78892
|
return JSON.stringify({
|
|
78167
78893
|
success: false,
|
|
78168
78894
|
phase,
|
|
78169
78895
|
status: "blocked",
|
|
78170
|
-
reason: "
|
|
78171
|
-
message: `Phase ${phase} cannot be completed: drift verifier
|
|
78896
|
+
reason: "DRIFT_VERIFICATION_MISSING",
|
|
78897
|
+
message: `Phase ${phase} cannot be completed: drift_check is enabled and drift verifier evidence not found at .swarm/evidence/${phase}/drift-verifier.json. Run drift verification before completing the phase.`,
|
|
78172
78898
|
agentsDispatched,
|
|
78173
78899
|
agentsMissing: [],
|
|
78174
78900
|
warnings: []
|
|
78175
78901
|
}, null, 2);
|
|
78176
78902
|
}
|
|
78177
78903
|
}
|
|
78178
|
-
|
|
78179
|
-
|
|
78180
|
-
|
|
78181
|
-
|
|
78182
|
-
|
|
78183
|
-
|
|
78184
|
-
|
|
78185
|
-
|
|
78186
|
-
|
|
78187
|
-
|
|
78188
|
-
|
|
78189
|
-
let incompleteTaskCount = 0;
|
|
78190
|
-
let planPhaseFound = false;
|
|
78191
|
-
try {
|
|
78192
|
-
const planPath = validateSwarmPath(dir, "plan.json");
|
|
78193
|
-
const planRaw = fs65.readFileSync(planPath, "utf-8");
|
|
78194
|
-
const plan = JSON.parse(planRaw);
|
|
78195
|
-
const targetPhase = plan.phases.find((p) => p.id === phase);
|
|
78196
|
-
if (targetPhase) {
|
|
78197
|
-
planPhaseFound = true;
|
|
78198
|
-
incompleteTaskCount = targetPhase.tasks.filter((t) => t.status !== "completed").length;
|
|
78199
|
-
}
|
|
78200
|
-
} catch {}
|
|
78201
|
-
if (incompleteTaskCount > 0 || !planPhaseFound) {
|
|
78202
|
-
warnings.push(`No spec.md found and drift verification evidence missing. Phase ${phase} has ${incompleteTaskCount} incomplete task(s) in plan.json — consider running critic_drift_verifier before phase completion.`);
|
|
78203
|
-
} else {
|
|
78204
|
-
warnings.push(`No spec.md found. Phase ${phase} tasks are all completed in plan.json. Drift verification was skipped.`);
|
|
78904
|
+
if (!driftVerdictApproved && driftVerdictFound) {
|
|
78905
|
+
return JSON.stringify({
|
|
78906
|
+
success: false,
|
|
78907
|
+
phase,
|
|
78908
|
+
status: "blocked",
|
|
78909
|
+
reason: "DRIFT_VERIFICATION_REJECTED",
|
|
78910
|
+
message: `Phase ${phase} cannot be completed: drift verifier verdict is not approved.`,
|
|
78911
|
+
agentsDispatched,
|
|
78912
|
+
agentsMissing: [],
|
|
78913
|
+
warnings: []
|
|
78914
|
+
}, null, 2);
|
|
78205
78915
|
}
|
|
78206
|
-
}
|
|
78916
|
+
} catch (driftError) {
|
|
78207
78917
|
return JSON.stringify({
|
|
78208
78918
|
success: false,
|
|
78209
78919
|
phase,
|
|
78210
78920
|
status: "blocked",
|
|
78211
|
-
reason: "
|
|
78212
|
-
message: `Phase ${phase} cannot be completed: drift
|
|
78921
|
+
reason: "DRIFT_VERIFICATION_ERROR",
|
|
78922
|
+
message: `Phase ${phase} cannot be completed: drift verification encountered an error: ${driftError instanceof Error ? driftError.message : String(driftError)}. This is a hard block — resolve the error before completing the phase.`,
|
|
78213
78923
|
agentsDispatched,
|
|
78214
78924
|
agentsMissing: [],
|
|
78215
78925
|
warnings: []
|
|
78216
78926
|
}, null, 2);
|
|
78217
78927
|
}
|
|
78218
78928
|
}
|
|
78219
|
-
if (!driftVerdictApproved && driftVerdictFound) {
|
|
78220
|
-
return JSON.stringify({
|
|
78221
|
-
success: false,
|
|
78222
|
-
phase,
|
|
78223
|
-
status: "blocked",
|
|
78224
|
-
reason: "DRIFT_VERIFICATION_REJECTED",
|
|
78225
|
-
message: `Phase ${phase} cannot be completed: drift verifier verdict is not approved.`,
|
|
78226
|
-
agentsDispatched,
|
|
78227
|
-
agentsMissing: [],
|
|
78228
|
-
warnings: []
|
|
78229
|
-
}, null, 2);
|
|
78230
|
-
}
|
|
78231
|
-
} catch (driftError) {
|
|
78232
|
-
safeWarn(`[phase_complete] Drift verifier error (non-blocking):`, driftError);
|
|
78233
78929
|
}
|
|
78234
78930
|
try {
|
|
78235
78931
|
const plan = await loadPlan(dir);
|
|
@@ -78375,6 +79071,210 @@ async function executePhaseComplete(args2, workingDirectory, directory) {
|
|
|
78375
79071
|
} catch (mgError) {
|
|
78376
79072
|
safeWarn(`[phase_complete] Mutation gate error (non-blocking):`, mgError);
|
|
78377
79073
|
}
|
|
79074
|
+
let councilModeEnabled = false;
|
|
79075
|
+
try {
|
|
79076
|
+
const plan = await loadPlan(dir);
|
|
79077
|
+
if (plan) {
|
|
79078
|
+
const planId = `${plan.swarm}-${plan.title}`.replace(/[^a-zA-Z0-9-_]/g, "_");
|
|
79079
|
+
const profile = getProfile(dir, planId);
|
|
79080
|
+
if (profile) {
|
|
79081
|
+
const session2 = sessionID ? swarmState.agentSessions.get(sessionID) : undefined;
|
|
79082
|
+
const overrides = session2?.qaGateSessionOverrides ?? {};
|
|
79083
|
+
const effective = getEffectiveGates(profile, overrides);
|
|
79084
|
+
if (effective.council_mode === true) {
|
|
79085
|
+
councilModeEnabled = true;
|
|
79086
|
+
const pcPath = path81.join(dir, ".swarm", "evidence", String(phase), "phase-council.json");
|
|
79087
|
+
let pcVerdictFound = false;
|
|
79088
|
+
let _pcVerdict;
|
|
79089
|
+
let pcQuorumSize;
|
|
79090
|
+
let pcTimestamp;
|
|
79091
|
+
let pcPhaseNumber;
|
|
79092
|
+
try {
|
|
79093
|
+
const pcContent = fs65.readFileSync(pcPath, "utf-8");
|
|
79094
|
+
const pcBundle = JSON.parse(pcContent);
|
|
79095
|
+
for (const entry of pcBundle.entries ?? []) {
|
|
79096
|
+
if (typeof entry.type === "string" && entry.type === "phase-council" && typeof entry.verdict === "string") {
|
|
79097
|
+
pcVerdictFound = true;
|
|
79098
|
+
_pcVerdict = entry.verdict;
|
|
79099
|
+
pcQuorumSize = typeof entry.quorumSize === "number" ? entry.quorumSize : undefined;
|
|
79100
|
+
pcTimestamp = typeof entry.timestamp === "string" ? entry.timestamp : undefined;
|
|
79101
|
+
pcPhaseNumber = typeof entry.phase_number === "number" ? entry.phase_number : typeof entry.phase === "number" ? entry.phase : undefined;
|
|
79102
|
+
const now2 = new Date;
|
|
79103
|
+
const pcTime = pcTimestamp ? new Date(pcTimestamp) : null;
|
|
79104
|
+
if (!pcTime || Number.isNaN(pcTime.getTime())) {
|
|
79105
|
+
return JSON.stringify({
|
|
79106
|
+
success: false,
|
|
79107
|
+
phase,
|
|
79108
|
+
status: "blocked",
|
|
79109
|
+
reason: "PHASE_COUNCIL_INVALID_TIMESTAMP",
|
|
79110
|
+
message: `Phase ${phase} cannot be completed: phase council evidence has missing or invalid timestamp.`,
|
|
79111
|
+
agentsDispatched,
|
|
79112
|
+
agentsMissing: [],
|
|
79113
|
+
warnings: []
|
|
79114
|
+
}, null, 2);
|
|
79115
|
+
}
|
|
79116
|
+
const maxAge = 24 * 60 * 60 * 1000;
|
|
79117
|
+
if (pcTime.getTime() > now2.getTime()) {
|
|
79118
|
+
return JSON.stringify({
|
|
79119
|
+
success: false,
|
|
79120
|
+
phase,
|
|
79121
|
+
status: "blocked",
|
|
79122
|
+
reason: "PHASE_COUNCIL_FUTURE_TIMESTAMP",
|
|
79123
|
+
message: `Phase ${phase} cannot be completed: phase council evidence timestamp is in the future.`,
|
|
79124
|
+
agentsDispatched,
|
|
79125
|
+
agentsMissing: [],
|
|
79126
|
+
warnings: []
|
|
79127
|
+
}, null, 2);
|
|
79128
|
+
}
|
|
79129
|
+
if (now2.getTime() - pcTime.getTime() > maxAge) {
|
|
79130
|
+
return JSON.stringify({
|
|
79131
|
+
success: false,
|
|
79132
|
+
phase,
|
|
79133
|
+
status: "blocked",
|
|
79134
|
+
reason: "PHASE_COUNCIL_STALE_EVIDENCE",
|
|
79135
|
+
message: `Phase ${phase} cannot be completed: phase council evidence is older than 24 hours. Re-convene council for fresh review.`,
|
|
79136
|
+
agentsDispatched,
|
|
79137
|
+
agentsMissing: [],
|
|
79138
|
+
warnings: []
|
|
79139
|
+
}, null, 2);
|
|
79140
|
+
}
|
|
79141
|
+
if (entry.verdict === "REJECT" || entry.verdict === "reject") {
|
|
79142
|
+
const requiredFixes = entry.requiredFixes ?? entry.required_fixes ?? [];
|
|
79143
|
+
const fixesDetail = Array.isArray(requiredFixes) && requiredFixes.length > 0 ? `
|
|
79144
|
+
Required fixes: ${requiredFixes.map((f) => f.detail ?? JSON.stringify(f)).join("; ")}` : "";
|
|
79145
|
+
return JSON.stringify({
|
|
79146
|
+
success: false,
|
|
79147
|
+
phase,
|
|
79148
|
+
status: "blocked",
|
|
79149
|
+
reason: "PHASE_COUNCIL_REJECTED",
|
|
79150
|
+
message: `Phase ${phase} cannot be completed: phase council returned verdict 'REJECT'. Address the required fixes before completing the phase.${fixesDetail}`,
|
|
79151
|
+
agentsDispatched,
|
|
79152
|
+
agentsMissing: [],
|
|
79153
|
+
warnings: []
|
|
79154
|
+
}, null, 2);
|
|
79155
|
+
}
|
|
79156
|
+
if (entry.verdict === "CONCERNS" || entry.verdict === "concerns") {
|
|
79157
|
+
const phaseConcernsAllow = config3.council?.phaseConcernsAllowComplete ?? true;
|
|
79158
|
+
if (!phaseConcernsAllow) {
|
|
79159
|
+
const advisoryNotes = entry.advisoryNotes ?? entry.advisory_notes ?? [];
|
|
79160
|
+
const notesDetail = Array.isArray(advisoryNotes) && advisoryNotes.length > 0 ? `
|
|
79161
|
+
Advisory notes: ${advisoryNotes.join("; ")}` : "";
|
|
79162
|
+
return JSON.stringify({
|
|
79163
|
+
success: false,
|
|
79164
|
+
phase,
|
|
79165
|
+
status: "blocked",
|
|
79166
|
+
reason: "PHASE_COUNCIL_CONCERNS",
|
|
79167
|
+
message: `Phase ${phase} cannot be completed: phase council returned verdict 'CONCERNS'.${notesDetail}`,
|
|
79168
|
+
agentsDispatched,
|
|
79169
|
+
agentsMissing: [],
|
|
79170
|
+
warnings: []
|
|
79171
|
+
}, null, 2);
|
|
79172
|
+
}
|
|
79173
|
+
safeWarn(`[phase_complete] Phase council returned CONCERNS for phase ${phase} — proceeding (phaseConcernsAllowComplete is enabled)`, undefined);
|
|
79174
|
+
}
|
|
79175
|
+
if (entry.verdict !== "APPROVE" && entry.verdict !== "approve" && entry.verdict !== "CONCERNS" && entry.verdict !== "concerns") {
|
|
79176
|
+
return JSON.stringify({
|
|
79177
|
+
success: false,
|
|
79178
|
+
phase,
|
|
79179
|
+
status: "blocked",
|
|
79180
|
+
reason: "PHASE_COUNCIL_INVALID",
|
|
79181
|
+
message: `Phase ${phase} cannot be completed: phase council evidence contains unrecognized verdict '${entry.verdict}'. Expected one of: APPROVE, CONCERNS, REJECT.`,
|
|
79182
|
+
agentsDispatched,
|
|
79183
|
+
agentsMissing: [],
|
|
79184
|
+
warnings: []
|
|
79185
|
+
}, null, 2);
|
|
79186
|
+
}
|
|
79187
|
+
}
|
|
79188
|
+
}
|
|
79189
|
+
} catch (readErr) {
|
|
79190
|
+
if (readErr.code !== "ENOENT") {
|
|
79191
|
+
safeWarn(`[phase_complete] Phase council evidence unreadable:`, readErr);
|
|
79192
|
+
}
|
|
79193
|
+
pcVerdictFound = false;
|
|
79194
|
+
}
|
|
79195
|
+
if (!pcVerdictFound) {
|
|
79196
|
+
return JSON.stringify({
|
|
79197
|
+
success: false,
|
|
79198
|
+
phase,
|
|
79199
|
+
status: "blocked",
|
|
79200
|
+
reason: "PHASE_COUNCIL_REQUIRED",
|
|
79201
|
+
phase_council_required: true,
|
|
79202
|
+
message: `Phase ${phase} cannot be completed: council_mode is enabled and phase council evidence not found at .swarm/evidence/${phase}/phase-council.json. Convene a phase-level council (dispatch 5 members, collect verdicts, call submit_council_verdicts) before completing the phase.`,
|
|
79203
|
+
agentsDispatched,
|
|
79204
|
+
agentsMissing: [],
|
|
79205
|
+
warnings: [
|
|
79206
|
+
`Phase council required — convene 5 council members (critic, reviewer, sme, test_engineer, explorer) for holistic phase review. Call submit_council_verdicts to synthesize verdicts and write phase-council.json evidence.`
|
|
79207
|
+
]
|
|
79208
|
+
}, null, 2);
|
|
79209
|
+
}
|
|
79210
|
+
if (pcQuorumSize === undefined || typeof pcQuorumSize !== "number") {
|
|
79211
|
+
return JSON.stringify({
|
|
79212
|
+
success: false,
|
|
79213
|
+
phase,
|
|
79214
|
+
status: "blocked",
|
|
79215
|
+
reason: "PHASE_COUNCIL_MISSING_QUORUM",
|
|
79216
|
+
message: `Phase ${phase} cannot be completed: phase council evidence is missing quorumSize field.`,
|
|
79217
|
+
agentsDispatched,
|
|
79218
|
+
agentsMissing: [],
|
|
79219
|
+
warnings: []
|
|
79220
|
+
}, null, 2);
|
|
79221
|
+
}
|
|
79222
|
+
if (pcQuorumSize < 3) {
|
|
79223
|
+
return JSON.stringify({
|
|
79224
|
+
success: false,
|
|
79225
|
+
phase,
|
|
79226
|
+
status: "blocked",
|
|
79227
|
+
reason: "PHASE_COUNCIL_INSUFFICIENT_QUORUM",
|
|
79228
|
+
message: `Phase ${phase} cannot be completed: phase council quorum (${pcQuorumSize}) is below minimum (3). Re-convene council with sufficient members.`,
|
|
79229
|
+
agentsDispatched,
|
|
79230
|
+
agentsMissing: [],
|
|
79231
|
+
warnings: []
|
|
79232
|
+
}, null, 2);
|
|
79233
|
+
}
|
|
79234
|
+
if (pcPhaseNumber === undefined || typeof pcPhaseNumber !== "number") {
|
|
79235
|
+
return JSON.stringify({
|
|
79236
|
+
success: false,
|
|
79237
|
+
phase,
|
|
79238
|
+
status: "blocked",
|
|
79239
|
+
reason: "PHASE_COUNCIL_MISSING_PHASE",
|
|
79240
|
+
message: `Phase ${phase} cannot be completed: phase council evidence is missing phase_number field.`,
|
|
79241
|
+
agentsDispatched,
|
|
79242
|
+
agentsMissing: [],
|
|
79243
|
+
warnings: []
|
|
79244
|
+
}, null, 2);
|
|
79245
|
+
}
|
|
79246
|
+
if (pcPhaseNumber !== phase) {
|
|
79247
|
+
return JSON.stringify({
|
|
79248
|
+
success: false,
|
|
79249
|
+
phase,
|
|
79250
|
+
status: "blocked",
|
|
79251
|
+
reason: "PHASE_COUNCIL_PHASE_MISMATCH",
|
|
79252
|
+
message: `Phase ${phase} cannot be completed: phase council evidence is for phase ${pcPhaseNumber}, not phase ${phase}. Run council for the correct phase.`,
|
|
79253
|
+
agentsDispatched,
|
|
79254
|
+
agentsMissing: [],
|
|
79255
|
+
warnings: []
|
|
79256
|
+
}, null, 2);
|
|
79257
|
+
}
|
|
79258
|
+
}
|
|
79259
|
+
}
|
|
79260
|
+
}
|
|
79261
|
+
} catch (pcError) {
|
|
79262
|
+
if (councilModeEnabled) {
|
|
79263
|
+
warnings.push(`PHASE_COUNCIL_ERROR: ${String(pcError)}`);
|
|
79264
|
+
return JSON.stringify({
|
|
79265
|
+
success: false,
|
|
79266
|
+
phase,
|
|
79267
|
+
status: "blocked",
|
|
79268
|
+
reason: "PHASE_COUNCIL_ERROR",
|
|
79269
|
+
message: `Phase ${phase} cannot be completed: phase council gate encountered an error when council_mode was enabled. Error: ${String(pcError)}`,
|
|
79270
|
+
agentsDispatched,
|
|
79271
|
+
agentsMissing: [],
|
|
79272
|
+
warnings: [`PHASE_COUNCIL_ERROR: ${String(pcError)}`]
|
|
79273
|
+
}, null, 2);
|
|
79274
|
+
} else {
|
|
79275
|
+
safeWarn(`[phase_complete] Phase council gate error (non-blocking):`, pcError);
|
|
79276
|
+
}
|
|
79277
|
+
}
|
|
78378
79278
|
}
|
|
78379
79279
|
let knowledgeConfig;
|
|
78380
79280
|
try {
|
|
@@ -78559,7 +79459,7 @@ async function executePhaseComplete(args2, workingDirectory, directory) {
|
|
|
78559
79459
|
const lockTaskId = `phase-complete-${Date.now()}`;
|
|
78560
79460
|
const eventsFilePath = "events.jsonl";
|
|
78561
79461
|
let agentName = "phase-complete";
|
|
78562
|
-
for (const [, agent] of swarmState
|
|
79462
|
+
for (const [, agent] of swarmState?.activeAgent ?? []) {
|
|
78563
79463
|
agentName = agent;
|
|
78564
79464
|
break;
|
|
78565
79465
|
}
|
|
@@ -85717,7 +86617,8 @@ async function executeSetQaGates(args2, directory) {
|
|
|
85717
86617
|
"hallucination_guard",
|
|
85718
86618
|
"sast_enabled",
|
|
85719
86619
|
"mutation_test",
|
|
85720
|
-
"council_general_review"
|
|
86620
|
+
"council_general_review",
|
|
86621
|
+
"drift_check"
|
|
85721
86622
|
]) {
|
|
85722
86623
|
if (args2[key] !== undefined)
|
|
85723
86624
|
partial3[key] = args2[key];
|
|
@@ -85764,6 +86665,7 @@ var set_qa_gates = createSwarmTool({
|
|
|
85764
86665
|
sast_enabled: exports_external.boolean().optional().describe("Enable SAST scanning as a required QA gate."),
|
|
85765
86666
|
mutation_test: exports_external.boolean().optional().describe("Enable the mutation-testing gate (default: off). Requires mutation " + "tests to achieve a passing kill rate before phase completion; " + "WARN verdict allows advancement, FAIL blocks."),
|
|
85766
86667
|
council_general_review: exports_external.boolean().optional().describe("Enable the council_general_review gate (default: off). When on, " + "MODE: SPECIFY runs convene_general_council on the draft spec " + "before the critic-gate, folding multi-model deliberation into " + "the spec. Requires council.general.enabled and a search API key."),
|
|
86668
|
+
drift_check: exports_external.boolean().optional().describe("Enable drift verification gate (default: on). Blocks phase_complete " + "until drift-verifier.json has an approved verdict. When disabled, " + "drift verification is skipped entirely."),
|
|
85767
86669
|
project_type: exports_external.string().optional().describe('Project type label (e.g. "ts", "python"). Only applied when the profile is being created for the first time.')
|
|
85768
86670
|
},
|
|
85769
86671
|
execute: async (args2, directory) => {
|