fifony 0.1.26 → 0.1.27-next.84df008

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.
Files changed (36) hide show
  1. package/README.md +152 -129
  2. package/app/dist/assets/{KeyboardShortcutsHelp-lTNj9GiT.js → KeyboardShortcutsHelp-By0KcDhZ.js} +1 -1
  3. package/app/dist/assets/OnboardingWizard-5cz7Onsu.js +1 -0
  4. package/app/dist/assets/{analytics.lazy-C42PFRzr.js → analytics.lazy-ArFwnOEn.js} +1 -1
  5. package/app/dist/assets/{createLucideIcon-BWC-guQt.js → createLucideIcon-DgMTp0yx.js} +1 -1
  6. package/app/dist/assets/index-59O8esMr.js +45 -0
  7. package/app/dist/assets/{index-Qr2OPvRO.css → index-DuBwUsuf.css} +1 -1
  8. package/app/dist/assets/vendor-D-IqxHHu.js +9 -0
  9. package/app/dist/dinofffaur.webp +0 -0
  10. package/app/dist/index.html +4 -4
  11. package/app/dist/service-worker.js +1 -1
  12. package/dist/agent/run-local.js +57 -144
  13. package/dist/agent-KMXNVDRO.js +74 -0
  14. package/dist/chunk-FJR4ALEN.js +847 -0
  15. package/dist/{chunk-2F3Q2MAG.js → chunk-HSGUPFTV.js} +1224 -611
  16. package/dist/chunk-O5AEQXUV.js +311 -0
  17. package/dist/chunk-OONOOWNC.js +123 -0
  18. package/dist/{chunk-NFHVAIPW.js → chunk-UYCDOH6S.js} +380 -795
  19. package/dist/chunk-XENKNHFS.js +295 -0
  20. package/dist/cli.js +6 -4
  21. package/dist/issue-runner-JJAFMHKV.js +15 -0
  22. package/dist/{issue-state-machine-OWABY5S2.js → issue-state-machine-ACMUJSXC.js} +5 -3
  23. package/dist/issues-VDFXBK3N.js +40 -0
  24. package/dist/mcp/server.js +23 -121
  25. package/dist/queue-workers-U47CVPTO.js +23 -0
  26. package/dist/scheduler-MEXEDV4M.js +21 -0
  27. package/dist/{store-WN47MDT5.js → store-AG6LLYJ7.js} +7 -5
  28. package/dist/workspace-474CCKTW.js +44 -0
  29. package/package.json +6 -6
  30. package/app/dist/assets/OnboardingWizard-B6LlJR9B.js +0 -1
  31. package/app/dist/assets/index-fVSxs9d5.js +0 -43
  32. package/app/dist/assets/vendor-BTlTWMUF.js +0 -9
  33. package/dist/chunk-AMOGDOM7.js +0 -796
  34. package/dist/chunk-IA7IMQ5F.js +0 -91
  35. package/dist/issue-runner-DA4IDLKX.js +0 -13
  36. package/dist/queue-workers-JIH5ZMNQ.js +0 -20
@@ -1,36 +1,13 @@
1
1
  import {
2
- areQueueWorkersActive,
3
- enqueueForExecution,
4
- enqueueForPlanning,
5
- enqueueForReview
6
- } from "./chunk-IA7IMQ5F.js";
7
- import {
8
- ADAPTERS,
9
2
  ISSUE_STATE_MACHINE_ID,
10
- applyCapabilityMetadata,
11
- buildExecutionPayload,
12
- buildFullPlanPrompt,
13
- buildProviderBasePrompt,
14
- buildTurnPrompt,
15
- cleanWorkspace,
16
- computeCapabilityCounts,
17
- computeDiffStats,
18
3
  computeMetrics,
19
- describeRoutingSignals,
20
- detectAvailableProviders,
21
- discoverModels,
22
- ensureWorktreeCommitted,
23
4
  executeTransition,
24
5
  findIssueStateMachineTransitionPath,
25
- getCapabilityRoutingOptions,
26
6
  getDirtyEventIds,
27
7
  getDirtyIssueIds,
28
- getEffectiveAgentProviders,
29
8
  getIssueStateMachinePlugin,
30
9
  getMetrics,
31
- getProviderDefaultCommand,
32
10
  hasDirtyState,
33
- hydrateIssuePathsFromWorkspace,
34
11
  issueStateMachineConfig,
35
12
  markAllEventsDirty,
36
13
  markAllIssuePlansDirty,
@@ -38,22 +15,65 @@ import {
38
15
  markEventDirty,
39
16
  markIssueDirty,
40
17
  markIssuePlanDirty,
18
+ setFsmEventEmitter,
19
+ setIssueResourceStateApi,
20
+ setIssueStateMachinePlugin,
21
+ snapshotAndClearDirtyEventIds,
22
+ snapshotAndClearDirtyIssueIds,
23
+ snapshotAndClearDirtyIssuePlanIds
24
+ } from "./chunk-FJR4ALEN.js";
25
+ import {
26
+ ADAPTERS,
27
+ buildExecutionPayload,
28
+ buildFullPlanPrompt,
29
+ buildProviderBasePrompt,
30
+ buildRetryContext,
31
+ buildTurnPrompt,
32
+ cleanWorkspace,
33
+ computeDiffStats,
34
+ detectAvailableProviders,
35
+ discoverModels,
36
+ ensureWorktreeCommitted,
37
+ getEffectiveAgentProviders,
38
+ getProviderDefaultCommand,
39
+ hydrateIssuePathsFromWorkspace,
41
40
  mergeWorkspace,
42
41
  normalizeAgentProvider,
43
42
  parseDiffStats,
44
43
  prepareWorkspace,
45
- pushWorktreeBranch,
46
44
  readCodexConfig,
47
45
  resolveAgentCommand,
48
46
  runCommandWithTimeout,
49
- runHook,
50
- setFsmEventEmitter,
51
- setIssueResourceStateApi,
52
- setIssueStateMachinePlugin,
53
- snapshotAndClearDirtyEventIds,
54
- snapshotAndClearDirtyIssueIds,
55
- snapshotAndClearDirtyIssuePlanIds
56
- } from "./chunk-NFHVAIPW.js";
47
+ runHook
48
+ } from "./chunk-UYCDOH6S.js";
49
+ import {
50
+ appendFileTail,
51
+ clamp,
52
+ debugBoot,
53
+ extractJsonObjects,
54
+ fail,
55
+ idToSafePath,
56
+ isoWeek,
57
+ normalizeState,
58
+ now,
59
+ parseEnvNumber,
60
+ parseIntArg,
61
+ parseIssueState,
62
+ parsePositiveIntEnv,
63
+ renderPrompt,
64
+ repairTruncatedJson,
65
+ toBooleanValue,
66
+ toNumberValue,
67
+ toStringArray,
68
+ toStringValue,
69
+ withRetryBackoff
70
+ } from "./chunk-O5AEQXUV.js";
71
+ import {
72
+ enqueue
73
+ } from "./chunk-XENKNHFS.js";
74
+ import {
75
+ logger
76
+ } from "./chunk-DVU3CXWA.js";
57
77
  import {
58
78
  ALLOWED_STATES,
59
79
  ATTACHMENTS_ROOT,
@@ -81,42 +101,18 @@ import {
81
101
  STATE_ROOT,
82
102
  TARGET_ROOT,
83
103
  TERMINAL_STATES,
84
- WORKSPACE_ROOT,
85
- appendFileTail,
86
- clamp,
87
- debugBoot,
88
- extractJsonObjects,
89
- fail,
90
- idToSafePath,
91
- inferCapabilityPaths,
92
- isoWeek,
93
- normalizeState,
94
- now,
95
- parseEnvNumber,
96
- parseIntArg,
97
- parseIssueState,
98
- parsePositiveIntEnv,
99
- renderPrompt,
100
- repairTruncatedJson,
101
- resolveTaskCapabilities,
102
- toNumberValue,
103
- toStringArray,
104
- toStringValue,
105
- withRetryBackoff
106
- } from "./chunk-AMOGDOM7.js";
107
- import {
108
- logger
109
- } from "./chunk-DVU3CXWA.js";
104
+ WORKSPACE_ROOT
105
+ } from "./chunk-OONOOWNC.js";
110
106
 
111
107
  // src/agents/issue-runner.ts
112
108
  import {
113
- existsSync as existsSync13,
114
- mkdirSync as mkdirSync7,
115
- readFileSync as readFileSync10,
116
- writeFileSync as writeFileSync11
109
+ existsSync as existsSync14,
110
+ mkdirSync as mkdirSync8,
111
+ readFileSync as readFileSync11,
112
+ writeFileSync as writeFileSync12
117
113
  } from "fs";
118
- import { join as join16 } from "path";
119
- import { execSync as execSync4 } from "child_process";
114
+ import { join as join17 } from "path";
115
+ import { execSync as execSync5 } from "child_process";
120
116
 
121
117
  // src/domains/tokens.ts
122
118
  var EMPTY = { inputTokens: 0, outputTokens: 0, totalTokens: 0 };
@@ -290,19 +286,19 @@ function getAnalytics(topN = 20) {
290
286
  }
291
287
 
292
288
  // src/domains/project.ts
293
- import { execFileSync, spawn as spawn3 } from "child_process";
289
+ import { execFileSync as execFileSync2, spawn as spawn3 } from "child_process";
294
290
  import { createHash } from "crypto";
295
291
  import {
296
- existsSync as existsSync12,
297
- mkdirSync as mkdirSync6,
292
+ existsSync as existsSync13,
293
+ mkdirSync as mkdirSync7,
298
294
  mkdtempSync as mkdtempSync4,
299
- readdirSync as readdirSync3,
300
- readFileSync as readFileSync9,
295
+ readdirSync as readdirSync4,
296
+ readFileSync as readFileSync10,
301
297
  rmSync as rmSync5,
302
- writeFileSync as writeFileSync10
298
+ writeFileSync as writeFileSync11
303
299
  } from "fs";
304
300
  import { homedir as homedir3, tmpdir as tmpdir4 } from "os";
305
- import { basename as basename4, dirname as dirname2, join as join15, relative as relativePath, resolve as resolve2 } from "path";
301
+ import { basename as basename4, dirname as dirname2, join as join16, relative as relativePath, resolve as resolve2 } from "path";
306
302
  import { env as env3 } from "process";
307
303
 
308
304
  // src/persistence/plugins/api-runtime-context.ts
@@ -322,8 +318,8 @@ function getApiRuntimeContextOrThrow() {
322
318
 
323
319
  // src/persistence/plugins/api-server.ts
324
320
  import {
325
- existsSync as existsSync11,
326
- readFileSync as readFileSync8
321
+ existsSync as existsSync12,
322
+ readFileSync as readFileSync9
327
323
  } from "fs";
328
324
 
329
325
  // src/persistence/resources/runtime-state.resource.ts
@@ -417,6 +413,31 @@ function extractTokenUsage(output, jsonObj) {
417
413
  };
418
414
  }
419
415
  }
416
+ const stats = jsonObj.stats;
417
+ const geminiModels = stats?.models ?? null;
418
+ if (geminiModels && typeof geminiModels === "object") {
419
+ let totalInput = 0, totalOutput = 0, primaryModel = "", maxTokens = 0;
420
+ for (const [model, data] of Object.entries(geminiModels)) {
421
+ const tokens = data?.tokens;
422
+ if (!tokens) continue;
423
+ const inp = Number(tokens.input || 0) + Number(tokens.cached || 0);
424
+ const out = Number(tokens.candidates || 0);
425
+ totalInput += inp;
426
+ totalOutput += out;
427
+ if (inp + out > maxTokens) {
428
+ maxTokens = inp + out;
429
+ primaryModel = model;
430
+ }
431
+ }
432
+ if (totalInput > 0 || totalOutput > 0) {
433
+ return {
434
+ inputTokens: totalInput,
435
+ outputTokens: totalOutput,
436
+ totalTokens: totalInput + totalOutput,
437
+ model: primaryModel || void 0
438
+ };
439
+ }
440
+ }
420
441
  const usage = jsonObj.usage;
421
442
  if (usage && typeof usage === "object") {
422
443
  const inp = Number(usage.input_tokens) || 0;
@@ -466,6 +487,15 @@ function tryParseJsonOutput(output) {
466
487
  }
467
488
  }
468
489
  if (obj.status) return obj;
490
+ if (typeof obj.response === "string") {
491
+ try {
492
+ const inner = JSON.parse(obj.response);
493
+ if (inner && typeof inner === "object" && !Array.isArray(inner)) {
494
+ return inner;
495
+ }
496
+ } catch {
497
+ }
498
+ }
469
499
  }
470
500
  } catch {
471
501
  }
@@ -764,24 +794,25 @@ async function loadAgentSessionSnapshotsForIssue(issue, providers, pipeline, _wo
764
794
 
765
795
  // src/agents/agent-pipeline.ts
766
796
  import {
767
- writeFileSync as writeFileSync2
797
+ mkdirSync as mkdirSync2,
798
+ writeFileSync as writeFileSync3
768
799
  } from "fs";
769
- import { join as join5 } from "path";
800
+ import { join as join6 } from "path";
770
801
 
771
802
  // src/agents/adapters/index.ts
772
803
  import { writeFileSync } from "fs";
773
804
  import { join as join3 } from "path";
774
- async function compileExecution(issue, provider, config, workspacePath, skillContext) {
805
+ async function compileExecution(issue, provider, config, workspacePath, skillContext, capabilitiesManifest) {
775
806
  const plan = issue.plan;
776
807
  if (!plan?.steps?.length) return null;
777
808
  const adapter = ADAPTERS[provider.provider];
778
809
  if (!adapter) return null;
779
810
  const payload = buildExecutionPayload(issue, provider, plan, workspacePath);
780
- const compiled = await adapter.compile(issue, provider, plan, config, workspacePath, skillContext);
811
+ const compiled = await adapter.compile(issue, provider, plan, config, workspacePath, skillContext, capabilitiesManifest);
781
812
  compiled.payload = payload;
782
813
  return compiled;
783
814
  }
784
- async function compileReview(issue, reviewer, workspacePath, diffSummary) {
815
+ async function compileReview(issue, reviewer, workspacePath, diffSummary, config) {
785
816
  const plan = issue.plan;
786
817
  const prompt = await renderPrompt("compile-review", {
787
818
  issueIdentifier: issue.identifier,
@@ -794,7 +825,7 @@ async function compileReview(issue, reviewer, workspacePath, diffSummary) {
794
825
  diffSummary
795
826
  });
796
827
  const adapter = ADAPTERS[reviewer.provider];
797
- const command = adapter ? adapter.buildReviewCommand(reviewer) : reviewer.command;
828
+ const command = adapter ? adapter.buildReviewCommand(reviewer, config) : reviewer.command;
798
829
  return { prompt, command };
799
830
  }
800
831
  function buildExecutionAudit(provider, compiled, issue, durationMs, result) {
@@ -863,32 +894,186 @@ function persistExecutionAudit(workspacePath, audit) {
863
894
  }
864
895
 
865
896
  // src/agents/skills.ts
866
- import { existsSync as existsSync3, readdirSync, readFileSync as readFileSync3 } from "fs";
897
+ import { existsSync as existsSync4, readdirSync, readFileSync as readFileSync4 } from "fs";
867
898
  import { homedir } from "os";
868
- import { join as join4, resolve } from "path";
899
+ import { join as join5, resolve } from "path";
900
+
901
+ // src/agents/catalog.ts
902
+ import { existsSync as existsSync3, mkdirSync, readFileSync as readFileSync3, writeFileSync as writeFileSync2 } from "fs";
903
+ import { join as join4 } from "path";
904
+ function parseFrontmatter(content) {
905
+ const match = content.match(/^---\r?\n([\s\S]*?)\r?\n---/);
906
+ if (!match) return {};
907
+ const result = {};
908
+ for (const line of match[1].split("\n")) {
909
+ const idx = line.indexOf(":");
910
+ if (idx === -1) continue;
911
+ const key = line.slice(0, idx).trim();
912
+ const value = line.slice(idx + 1).trim().replace(/^["']|["']$/g, "");
913
+ if (key) result[key] = value;
914
+ }
915
+ return result;
916
+ }
917
+ function loadAgentCatalog() {
918
+ const entries = [];
919
+ try {
920
+ const repos = listReferenceRepositories();
921
+ for (const repo of repos) {
922
+ if (!repo.present || !repo.synced) continue;
923
+ const artifacts = collectArtifacts(repo.path, repo.id).filter((a) => a.kind === "agent");
924
+ for (const artifact of artifacts) {
925
+ try {
926
+ const content = readFileSync3(artifact.sourcePath, "utf8");
927
+ const fm = parseFrontmatter(content);
928
+ entries.push({
929
+ name: artifact.targetName,
930
+ displayName: fm.name || artifact.targetName,
931
+ description: fm.description || "",
932
+ emoji: fm.emoji || "\u{1F916}",
933
+ domains: fm.domains ? fm.domains.split(",").map((d) => d.trim()).filter(Boolean) : [],
934
+ source: repo.id,
935
+ content
936
+ });
937
+ } catch (err) {
938
+ logger.warn({ err, path: artifact.sourcePath }, "Failed to read agent file");
939
+ }
940
+ }
941
+ }
942
+ } catch (error) {
943
+ logger.error({ err: error }, "Failed to load agent catalog from repositories");
944
+ }
945
+ return entries;
946
+ }
947
+ function loadSkillCatalog() {
948
+ return [];
949
+ }
950
+ function filterByDomains(catalog, domains) {
951
+ const domainSet = new Set(domains.map((d) => d.toLowerCase().trim()));
952
+ if (domainSet.size === 0) return catalog;
953
+ const scored = catalog.map((entry) => {
954
+ const matchCount = entry.domains.filter((d) => domainSet.has(d.toLowerCase())).length;
955
+ return { entry, matchCount };
956
+ });
957
+ return scored.filter((item) => item.matchCount > 0).sort((a, b) => b.matchCount - a.matchCount).map((item) => item.entry);
958
+ }
959
+ function installAgents(targetRoot, agentNames, catalog) {
960
+ const result = { installed: [], skipped: [], errors: [] };
961
+ const catalogMap = new Map(catalog.map((entry) => [entry.name, entry]));
962
+ const agentsDir = join4(targetRoot, ".claude", "agents");
963
+ try {
964
+ mkdirSync(agentsDir, { recursive: true });
965
+ } catch (error) {
966
+ logger.error({ err: error, path: agentsDir }, "Failed to create agents directory");
967
+ result.errors.push({ name: "_directory", error: `Failed to create ${agentsDir}` });
968
+ return result;
969
+ }
970
+ for (const name of agentNames) {
971
+ const entry = catalogMap.get(name);
972
+ if (!entry) {
973
+ result.errors.push({ name, error: "Agent not found in catalog" });
974
+ continue;
975
+ }
976
+ const filePath = join4(agentsDir, `${name}.md`);
977
+ if (existsSync3(filePath)) {
978
+ result.skipped.push(name);
979
+ continue;
980
+ }
981
+ try {
982
+ writeFileSync2(filePath, entry.content, "utf8");
983
+ result.installed.push(name);
984
+ logger.info({ agent: name, path: filePath }, "Agent installed");
985
+ } catch (error) {
986
+ result.errors.push({
987
+ name,
988
+ error: error instanceof Error ? error.message : String(error)
989
+ });
990
+ }
991
+ }
992
+ return result;
993
+ }
994
+ function installSkills(targetRoot, skillNames, catalog) {
995
+ const result = { installed: [], skipped: [], errors: [] };
996
+ const catalogMap = new Map(catalog.map((entry) => [entry.name, entry]));
997
+ const skillsDir = join4(targetRoot, ".claude", "skills");
998
+ try {
999
+ mkdirSync(skillsDir, { recursive: true });
1000
+ } catch (error) {
1001
+ logger.error({ err: error, path: skillsDir }, "Failed to create skills directory");
1002
+ result.errors.push({ name: "_directory", error: `Failed to create ${skillsDir}` });
1003
+ return result;
1004
+ }
1005
+ for (const name of skillNames) {
1006
+ const entry = catalogMap.get(name);
1007
+ if (!entry) {
1008
+ result.errors.push({ name, error: "Skill not found in catalog" });
1009
+ continue;
1010
+ }
1011
+ const skillDir = join4(skillsDir, name);
1012
+ const filePath = join4(skillDir, "SKILL.md");
1013
+ if (existsSync3(filePath)) {
1014
+ result.skipped.push(name);
1015
+ continue;
1016
+ }
1017
+ try {
1018
+ mkdirSync(skillDir, { recursive: true });
1019
+ if (entry.installType === "bundled" && entry.content) {
1020
+ writeFileSync2(filePath, entry.content, "utf8");
1021
+ } else {
1022
+ const referenceContent = [
1023
+ `# ${entry.displayName}`,
1024
+ "",
1025
+ entry.description,
1026
+ "",
1027
+ `**Source**: ${entry.source}`,
1028
+ entry.url ? `**URL**: ${entry.url}` : "",
1029
+ "",
1030
+ `> This skill references an external resource. Install it from the source above.`
1031
+ ].filter(Boolean).join("\n");
1032
+ writeFileSync2(filePath, referenceContent, "utf8");
1033
+ }
1034
+ result.installed.push(name);
1035
+ logger.info({ skill: name, path: filePath, type: entry.installType }, "Skill installed");
1036
+ } catch (error) {
1037
+ result.errors.push({
1038
+ name,
1039
+ error: error instanceof Error ? error.message : String(error)
1040
+ });
1041
+ }
1042
+ }
1043
+ return result;
1044
+ }
1045
+
1046
+ // src/agents/skills.ts
869
1047
  function discoverSkills(workspacePath) {
870
1048
  const home = homedir();
871
- const codePath = existsSync3(join4(workspacePath, "worktree")) ? join4(workspacePath, "worktree") : workspacePath;
1049
+ const codePath = existsSync4(join5(workspacePath, "worktree")) ? join5(workspacePath, "worktree") : workspacePath;
872
1050
  const searchPaths = [
873
1051
  resolve(codePath, ".codex", "skills"),
874
1052
  resolve(codePath, ".claude", "skills"),
875
- join4(home, ".codex", "skills"),
876
- join4(home, ".claude", "skills")
1053
+ join5(home, ".codex", "skills"),
1054
+ join5(home, ".claude", "skills")
877
1055
  ];
878
1056
  const seen = /* @__PURE__ */ new Set();
879
1057
  const skills = [];
880
1058
  for (const basePath of searchPaths) {
881
- if (!existsSync3(basePath)) continue;
1059
+ if (!existsSync4(basePath)) continue;
882
1060
  for (const entry of readdirSync(basePath, { withFileTypes: true })) {
883
1061
  if (!entry.isDirectory()) continue;
884
1062
  if (seen.has(entry.name)) continue;
885
- const skillFile = join4(basePath, entry.name, "SKILL.md");
886
- if (!existsSync3(skillFile)) continue;
1063
+ const skillFile = join5(basePath, entry.name, "SKILL.md");
1064
+ if (!existsSync4(skillFile)) continue;
887
1065
  try {
888
- const content = readFileSync3(skillFile, "utf8").trim();
1066
+ const content = readFileSync4(skillFile, "utf8").trim();
889
1067
  if (content) {
890
1068
  seen.add(entry.name);
891
- skills.push({ name: entry.name, content });
1069
+ const fm = parseFrontmatter(content);
1070
+ skills.push({
1071
+ name: entry.name,
1072
+ content,
1073
+ description: fm.description || void 0,
1074
+ whenToUse: fm.when_to_use || fm.whenToUse || void 0,
1075
+ avoidIf: fm.avoid_if || fm.avoidIf || void 0
1076
+ });
892
1077
  }
893
1078
  } catch {
894
1079
  }
@@ -906,8 +1091,141 @@ ${skill.content}`
906
1091
 
907
1092
  ${sections.join("\n\n")}`;
908
1093
  }
1094
+ function extractFirstLine(content) {
1095
+ for (const line of content.split("\n")) {
1096
+ const trimmed = line.replace(/^#+\s*/, "").trim();
1097
+ if (trimmed && !trimmed.startsWith("---")) return trimmed;
1098
+ }
1099
+ return "";
1100
+ }
1101
+ function discoverAgents(workspacePath) {
1102
+ const home = homedir();
1103
+ const codePath = existsSync4(join5(workspacePath, "worktree")) ? join5(workspacePath, "worktree") : workspacePath;
1104
+ const searchPaths = [
1105
+ resolve(codePath, ".claude", "agents"),
1106
+ resolve(codePath, ".codex", "agents"),
1107
+ join5(home, ".claude", "agents"),
1108
+ join5(home, ".codex", "agents")
1109
+ ];
1110
+ const seen = /* @__PURE__ */ new Set();
1111
+ const agents = [];
1112
+ for (const basePath of searchPaths) {
1113
+ if (!existsSync4(basePath)) continue;
1114
+ for (const entry of readdirSync(basePath, { withFileTypes: true })) {
1115
+ if (!entry.isFile() || !entry.name.endsWith(".md")) continue;
1116
+ const name = entry.name.replace(/\.md$/, "");
1117
+ if (seen.has(name)) continue;
1118
+ try {
1119
+ const content = readFileSync4(join5(basePath, entry.name), "utf8").trim();
1120
+ if (content) {
1121
+ seen.add(name);
1122
+ const fm = parseFrontmatter(content);
1123
+ agents.push({
1124
+ name,
1125
+ description: fm.description || extractFirstLine(content),
1126
+ whenToUse: fm.when_to_use || fm.whenToUse || void 0,
1127
+ avoidIf: fm.avoid_if || fm.avoidIf || void 0
1128
+ });
1129
+ }
1130
+ } catch {
1131
+ }
1132
+ }
1133
+ }
1134
+ return agents;
1135
+ }
1136
+ function discoverCommands(workspacePath) {
1137
+ const home = homedir();
1138
+ const codePath = existsSync4(join5(workspacePath, "worktree")) ? join5(workspacePath, "worktree") : workspacePath;
1139
+ const searchPaths = [
1140
+ resolve(codePath, ".claude", "commands"),
1141
+ resolve(codePath, ".codex", "commands"),
1142
+ join5(home, ".claude", "commands"),
1143
+ join5(home, ".codex", "commands")
1144
+ ];
1145
+ const seen = /* @__PURE__ */ new Set();
1146
+ const commands = [];
1147
+ for (const basePath of searchPaths) {
1148
+ if (!existsSync4(basePath)) continue;
1149
+ for (const entry of readdirSync(basePath, { withFileTypes: true })) {
1150
+ if (!entry.isFile() || !entry.name.endsWith(".md")) continue;
1151
+ const name = entry.name.replace(/\.md$/, "");
1152
+ if (seen.has(name)) continue;
1153
+ try {
1154
+ const content = readFileSync4(join5(basePath, entry.name), "utf8").trim();
1155
+ if (content) {
1156
+ seen.add(name);
1157
+ commands.push({ name, description: extractFirstLine(content) });
1158
+ }
1159
+ } catch {
1160
+ }
1161
+ }
1162
+ }
1163
+ return commands;
1164
+ }
1165
+ var MAX_CAPABILITIES_ITEMS = 40;
1166
+ function buildCapabilitiesManifest(skills, agents, commands) {
1167
+ if (skills.length === 0 && agents.length === 0 && commands.length === 0) return "";
1168
+ const sections = ["## Your Capabilities"];
1169
+ let itemCount = 0;
1170
+ if (commands.length > 0) {
1171
+ sections.push("");
1172
+ sections.push("### Slash Commands");
1173
+ sections.push("You have these commands available. Invoke with `/command-name`:");
1174
+ const show = commands.slice(0, MAX_CAPABILITIES_ITEMS);
1175
+ for (const cmd of show) {
1176
+ sections.push(`- \`/${cmd.name}\`${cmd.description ? ` \u2014 ${cmd.description}` : ""}`);
1177
+ itemCount++;
1178
+ }
1179
+ if (commands.length > show.length) {
1180
+ sections.push(`- ...and ${commands.length - show.length} more available`);
1181
+ }
1182
+ }
1183
+ if (skills.length > 0) {
1184
+ const remaining = Math.max(5, MAX_CAPABILITIES_ITEMS - itemCount);
1185
+ sections.push("");
1186
+ sections.push("### Skills");
1187
+ sections.push("Specialized procedures available for this workspace:");
1188
+ const show = skills.slice(0, remaining);
1189
+ for (const skill of show) {
1190
+ let line = `- **${skill.name}**`;
1191
+ if (skill.description) line += ` \u2014 ${skill.description}`;
1192
+ if (skill.whenToUse) line += ` (Use when: ${skill.whenToUse})`;
1193
+ sections.push(line);
1194
+ itemCount++;
1195
+ }
1196
+ if (skills.length > show.length) {
1197
+ sections.push(`- ...and ${skills.length - show.length} more available`);
1198
+ }
1199
+ }
1200
+ if (agents.length > 0) {
1201
+ const remaining = Math.max(5, MAX_CAPABILITIES_ITEMS - itemCount);
1202
+ sections.push("");
1203
+ sections.push("### Subagents");
1204
+ sections.push("You can delegate to these specialist agents via the Agent tool:");
1205
+ const show = agents.slice(0, remaining);
1206
+ for (const agent of show) {
1207
+ let line = `- **${agent.name}**`;
1208
+ if (agent.description) line += ` \u2014 ${agent.description}`;
1209
+ if (agent.whenToUse) line += ` (Use when: ${agent.whenToUse})`;
1210
+ if (agent.avoidIf) line += ` (Avoid if: ${agent.avoidIf})`;
1211
+ sections.push(line);
1212
+ }
1213
+ if (agents.length > show.length) {
1214
+ sections.push(`- ...and ${agents.length - show.length} more available`);
1215
+ }
1216
+ }
1217
+ sections.push("");
1218
+ sections.push("When a task matches a capability above, USE IT instead of doing everything manually.");
1219
+ return sections.join("\n");
1220
+ }
909
1221
 
910
1222
  // src/agents/agent-pipeline.ts
1223
+ function resolveOutputFileName(role, planVersion, attempt, turn) {
1224
+ if (role === "planner") {
1225
+ return `plan.v${planVersion}.t${turn}.stdout.log`;
1226
+ }
1227
+ return `${role === "reviewer" ? "review" : "execute"}.v${planVersion}a${attempt}.t${turn}.stdout.log`;
1228
+ }
911
1229
  async function runAgentSession(state, issue, provider, cycle, workspacePath, basePromptText, basePromptFile) {
912
1230
  const maxTurns = clamp(state.config.maxTurns, 1, 16);
913
1231
  const attempt = issue.attempts + 1;
@@ -919,7 +1237,7 @@ async function runAgentSession(state, issue, provider, cycle, workspacePath, bas
919
1237
  let nextPrompt = session.nextPrompt;
920
1238
  let lastCode = session.lastCode;
921
1239
  let lastOutput = session.lastOutput;
922
- const resultFile = join5(workspacePath, `result-${provider.role}-${provider.provider}.json`);
1240
+ const resultFile = join6(workspacePath, `result-${provider.role}-${provider.provider}.json`);
923
1241
  if (session.status === "done" && session.turns.length > 0) {
924
1242
  logger.debug({ issueId: issue.id, identifier: issue.identifier, provider: provider.provider, role: provider.role }, "[Agent] Session already completed, returning cached result");
925
1243
  return { success: true, blocked: false, continueRequested: false, code: session.lastCode, output: session.lastOutput, turns: session.turns.length };
@@ -936,14 +1254,23 @@ Agent requested additional turns beyond configured limit (${maxTurns}).`;
936
1254
  const compactedOutput = previousOutput.length > maxOutputChars ? `[...${previousOutput.length - maxOutputChars} chars truncated...]
937
1255
  ${previousOutput.slice(-maxOutputChars)}` : previousOutput;
938
1256
  const turnPrompt = await buildTurnPrompt(issue, basePromptText, compactedOutput, turnIndex, maxTurns, nextPrompt);
939
- const turnPromptFile = turnIndex === 1 ? basePromptFile : join5(workspacePath, `turn-${String(turnIndex).padStart(2, "0")}.md`);
940
- if (turnIndex > 1) writeFileSync2(turnPromptFile, `${turnPrompt}
1257
+ const turnPromptFile = turnIndex === 1 ? basePromptFile : join6(workspacePath, `turn-${String(turnIndex).padStart(2, "0")}.md`);
1258
+ if (turnIndex > 1) writeFileSync3(turnPromptFile, `${turnPrompt}
941
1259
  `, "utf8");
942
1260
  session.status = "running";
943
1261
  session.lastPrompt = turnPrompt;
944
1262
  session.lastPromptFile = turnPromptFile;
945
1263
  session.maxTurns = maxTurns;
946
1264
  await persistAgentSessionState(sessionKey, issue, provider, cycle, session);
1265
+ const outputsDir = join6(workspacePath, "outputs");
1266
+ mkdirSync2(outputsDir, { recursive: true });
1267
+ const outputFileName = resolveOutputFileName(
1268
+ provider.role,
1269
+ issue.planVersion ?? 1,
1270
+ provider.role === "planner" ? 0 : issue.executeAttempt ?? 1,
1271
+ turnIndex
1272
+ );
1273
+ const outputFilePath = join6(outputsDir, outputFileName);
947
1274
  logger.info({ issueId: issue.id, identifier: issue.identifier, turn: turnIndex, maxTurns, provider: provider.provider, role: provider.role, cycle, command: provider.command.slice(0, 120) }, "[Agent] Spawning agent command");
948
1275
  const turnStartedAt = now();
949
1276
  const turnEnv = {
@@ -967,7 +1294,7 @@ ${previousOutput.slice(-maxOutputChars)}` : previousOutput;
967
1294
  await runHook(state.config.beforeRunHook, workspacePath, issue, "before_run", turnEnv);
968
1295
  }
969
1296
  addEvent(state, issue.id, "runner", `Turn ${turnIndex}/${maxTurns} started for ${issue.identifier}.`);
970
- const turnResult = await runCommandWithTimeout(provider.command, workspacePath, issue, state.config, turnPrompt, turnPromptFile, turnEnv);
1297
+ const turnResult = await runCommandWithTimeout(provider.command, workspacePath, issue, state.config, turnPrompt, turnPromptFile, turnEnv, outputFilePath);
971
1298
  if (state.config.afterRunHook) {
972
1299
  await runHook(state.config.afterRunHook, workspacePath, issue, "after_run", {
973
1300
  ...turnEnv,
@@ -1056,10 +1383,13 @@ async function runAgentPipeline(state, issue, workspacePath, basePromptText, bas
1056
1383
  const executorIndex = providers.findIndex((provider) => provider.role === "executor");
1057
1384
  const skills = discoverSkills(workspacePath);
1058
1385
  const skillContext = buildSkillContext(skills);
1386
+ const agents = discoverAgents(workspacePath);
1387
+ const commands = discoverCommands(workspacePath);
1388
+ const capabilitiesManifest = buildCapabilitiesManifest(skills, agents, commands);
1059
1389
  if (skillContext) {
1060
- writeFileSync2(join5(workspacePath, "skills.md"), skillContext, "utf8");
1390
+ writeFileSync3(join6(workspacePath, "skills.md"), skillContext, "utf8");
1061
1391
  }
1062
- const compiled = await compileExecution(issue, activeProvider, state.config, workspacePath, skillContext);
1392
+ const compiled = await compileExecution(issue, activeProvider, state.config, workspacePath, skillContext, capabilitiesManifest);
1063
1393
  let providerPrompt;
1064
1394
  let effectiveProvider = activeProvider;
1065
1395
  if (compiled) {
@@ -1073,16 +1403,24 @@ async function runAgentPipeline(state, issue, workspacePath, basePromptText, bas
1073
1403
  `Plan compiled for ${compiled.meta.adapter}: effort=${compiled.meta.reasoningEffort}, skills=[${compiled.meta.skillsActivated.join(",")}], subagents=[${compiled.meta.subagentsRequested.join(",")}].`
1074
1404
  );
1075
1405
  if (Object.keys(compiled.env).length > 0) {
1076
- const envFile = join5(workspacePath, ".compiled-env.sh");
1406
+ const envFile = join6(workspacePath, ".compiled-env.sh");
1077
1407
  const envLines = Object.entries(compiled.env).map(([k, v]) => `export ${k}=${JSON.stringify(v)}`).join("\n");
1078
- writeFileSync2(envFile, envLines, "utf8");
1408
+ writeFileSync3(envFile, envLines, "utf8");
1079
1409
  }
1080
1410
  } else {
1081
- providerPrompt = await buildProviderBasePrompt(activeProvider, issue, basePromptText, workspacePath, skillContext);
1411
+ providerPrompt = await buildProviderBasePrompt(activeProvider, issue, basePromptText, workspacePath, skillContext, capabilitiesManifest);
1082
1412
  }
1083
1413
  if (!effectiveProvider.command.trim()) {
1084
1414
  throw new Error(`No command configured for provider ${effectiveProvider.provider} (${effectiveProvider.role}).`);
1085
1415
  }
1416
+ if (issue.attempts > 0) {
1417
+ const retryCtx = buildRetryContext(issue);
1418
+ if (retryCtx) {
1419
+ providerPrompt = `${providerPrompt}
1420
+
1421
+ ${retryCtx}`;
1422
+ }
1423
+ }
1086
1424
  pipeline.history.push(`[${now()}] Running ${effectiveProvider.role}:${effectiveProvider.provider} in cycle ${pipeline.cycle}${compiled ? ` [${compiled.meta.adapter} adapter]` : ""}.`);
1087
1425
  await persistAgentPipelineState(pipelineFile, pipeline);
1088
1426
  const result = await runAgentSession(state, issue, effectiveProvider, pipeline.cycle, workspacePath, providerPrompt, basePromptFile);
@@ -1206,7 +1544,6 @@ function applyPlanUsage(issue, usage) {
1206
1544
  }
1207
1545
  function applyPlanSuggestions(issue, plan) {
1208
1546
  if (plan.suggestedPaths?.length && !issue.paths?.length) issue.paths = plan.suggestedPaths;
1209
- if (plan.suggestedLabels?.length && !issue.labels?.length) issue.labels = plan.suggestedLabels;
1210
1547
  if (plan.suggestedEffort && !issue.effort) issue.effort = plan.suggestedEffort;
1211
1548
  }
1212
1549
  async function mutateIssueState(state, c, updater) {
@@ -1240,30 +1577,11 @@ function createS3dbEventStore(state) {
1240
1577
  };
1241
1578
  }
1242
1579
 
1243
- // src/persistence/s3queue-adapter.ts
1244
- function createS3QueueAdapter() {
1245
- return {
1246
- async enqueueForPlanning(issue) {
1247
- return enqueueForPlanning(issue);
1248
- },
1249
- async enqueueForExecution(issue) {
1250
- return enqueueForExecution(issue);
1251
- },
1252
- async enqueueForReview(issue) {
1253
- return enqueueForReview(issue);
1254
- },
1255
- isActive() {
1256
- return areQueueWorkersActive();
1257
- }
1258
- };
1259
- }
1260
-
1261
1580
  // src/persistence/container.ts
1262
1581
  var _container = null;
1263
1582
  function createContainer(state) {
1264
1583
  const issueRepository = createS3dbIssueRepository(state);
1265
1584
  const eventStore = createS3dbEventStore(state);
1266
- const queuePort = createS3QueueAdapter();
1267
1585
  const persistencePort = {
1268
1586
  persistState: (s) => persistState(s),
1269
1587
  loadState: async () => null
@@ -1271,7 +1589,6 @@ function createContainer(state) {
1271
1589
  const container = {
1272
1590
  issueRepository,
1273
1591
  eventStore,
1274
- queuePort,
1275
1592
  persistencePort
1276
1593
  };
1277
1594
  _container = container;
@@ -1286,19 +1603,19 @@ function getContainer() {
1286
1603
  }
1287
1604
 
1288
1605
  // src/commands/create-issue.command.ts
1289
- import { existsSync as existsSync4, mkdirSync, renameSync } from "fs";
1290
- import { basename, join as join6 } from "path";
1606
+ import { existsSync as existsSync5, mkdirSync as mkdirSync3, renameSync } from "fs";
1607
+ import { basename, join as join7 } from "path";
1291
1608
  async function createIssueCommand(input, deps) {
1292
1609
  const { payload, state } = input;
1293
1610
  const issue = createIssueFromPayload(payload, state.issues, state.config.defaultBranch);
1294
1611
  const tempImages = Array.isArray(payload.images) ? payload.images : [];
1295
1612
  if (tempImages.length) {
1296
- const issueAttachDir = join6(ATTACHMENTS_ROOT, issue.id);
1297
- mkdirSync(issueAttachDir, { recursive: true });
1613
+ const issueAttachDir = join7(ATTACHMENTS_ROOT, issue.id);
1614
+ mkdirSync3(issueAttachDir, { recursive: true });
1298
1615
  const finalPaths = [];
1299
1616
  for (const tempPath of tempImages) {
1300
- if (typeof tempPath === "string" && existsSync4(tempPath)) {
1301
- const dest = join6(issueAttachDir, basename(tempPath));
1617
+ if (typeof tempPath === "string" && existsSync5(tempPath)) {
1618
+ const dest = join7(issueAttachDir, basename(tempPath));
1302
1619
  try {
1303
1620
  renameSync(tempPath, dest);
1304
1621
  finalPaths.push(dest);
@@ -1317,13 +1634,13 @@ async function createIssueCommand(input, deps) {
1317
1634
  }
1318
1635
  await deps.persistencePort.persistState(state);
1319
1636
  if (issue.state === "Planning") {
1320
- deps.queuePort.enqueueForPlanning(issue).catch(() => {
1637
+ enqueue(issue, "plan").catch(() => {
1321
1638
  });
1322
1639
  } else if (issue.state === "Queued" || issue.state === "Running") {
1323
- deps.queuePort.enqueueForExecution(issue).catch(() => {
1640
+ enqueue(issue, "execute").catch(() => {
1324
1641
  });
1325
1642
  } else if (issue.state === "Reviewing") {
1326
- deps.queuePort.enqueueForReview(issue).catch(() => {
1643
+ enqueue(issue, "review").catch(() => {
1327
1644
  });
1328
1645
  }
1329
1646
  return { issue };
@@ -1503,17 +1820,12 @@ var issues_resource_default = {
1503
1820
  identifier: "string|required",
1504
1821
  title: "string|required",
1505
1822
  description: "string|optional",
1506
- priority: "number|required",
1507
1823
  state: "string|required",
1508
1824
  branchName: "string|optional",
1509
1825
  url: "string|optional",
1510
1826
  assigneeId: "string|optional",
1511
1827
  labels: "json|required",
1512
1828
  paths: "json|optional",
1513
- inferredPaths: "json|optional",
1514
- capabilityCategory: "string|optional",
1515
- capabilityOverlays: "json|optional",
1516
- capabilityRationale: "json|optional",
1517
1829
  blockedBy: "json|required",
1518
1830
  assignedToWorker: "boolean|required",
1519
1831
  createdAt: "datetime|required",
@@ -1561,10 +1873,6 @@ var issues_resource_default = {
1561
1873
  },
1562
1874
  partitions: {
1563
1875
  byState: { fields: { state: "string" } },
1564
- byCapabilityCategory: { fields: { capabilityCategory: "string" } },
1565
- byStateAndCapability: {
1566
- fields: { state: "string", capabilityCategory: "string" }
1567
- },
1568
1876
  byTerminalWeek: { fields: { terminalWeek: "string" } }
1569
1877
  },
1570
1878
  asyncPartitions: true,
@@ -1801,7 +2109,6 @@ function broadcastToWebSocketClients(message) {
1801
2109
  type: "state:delta",
1802
2110
  seq: broadcastSeq,
1803
2111
  metrics: message.metrics,
1804
- capabilities: message.capabilities,
1805
2112
  updatedAt: message.updatedAt,
1806
2113
  issuesDelta: changedIssues,
1807
2114
  issuesRemoved: removedIds,
@@ -1835,7 +2142,6 @@ function makeWebSocketConfig(state) {
1835
2142
  seq: broadcastSeq,
1836
2143
  timestamp: now(),
1837
2144
  metrics: computeMetrics(state.issues),
1838
- capabilities: computeCapabilityCounts(state.issues),
1839
2145
  issues: state.issues,
1840
2146
  events: state.events.slice(0, 50)
1841
2147
  }));
@@ -1864,7 +2170,7 @@ var shuttingDown = false;
1864
2170
  function isShuttingDown() {
1865
2171
  return shuttingDown;
1866
2172
  }
1867
- function installGracefulShutdown(state, running) {
2173
+ function installGracefulShutdown(state) {
1868
2174
  const handler = async (signal) => {
1869
2175
  if (shuttingDown) {
1870
2176
  logger.warn(`Received ${signal} again, forcing exit.`);
@@ -1875,7 +2181,7 @@ function installGracefulShutdown(state, running) {
1875
2181
  const container = getContainer();
1876
2182
  container.eventStore.addEvent(void 0, "info", `Graceful shutdown initiated (${signal}).`);
1877
2183
  for (const issue of state.issues) {
1878
- if (running.has(issue.id) && (issue.state === "Running" || issue.state === "Reviewing")) {
2184
+ if (issue.state === "Running" || issue.state === "Reviewing") {
1879
2185
  try {
1880
2186
  await transitionIssueCommand({ issue, target: "Queued", note: `Interrupted by ${signal} \u2014 queued for resume on next start.`, fallbackToLocal: true }, container);
1881
2187
  } catch {
@@ -1907,7 +2213,7 @@ function installGracefulShutdown(state, running) {
1907
2213
  }
1908
2214
  function analyzeParallelizability(issues) {
1909
2215
  const todo = issues.filter(
1910
- (issue) => issue.state === "Planned" && issue.assignedToWorker && issue.blockedBy.length === 0
2216
+ (issue) => issue.state === "PendingApproval" && issue.assignedToWorker && issue.blockedBy.length === 0
1911
2217
  );
1912
2218
  if (todo.length === 0) {
1913
2219
  return {
@@ -1917,7 +2223,7 @@ function analyzeParallelizability(issues) {
1917
2223
  groups: []
1918
2224
  };
1919
2225
  }
1920
- const getIssuePaths = (issue) => /* @__PURE__ */ new Set([...issue.paths ?? [], ...issue.inferredPaths ?? []]);
2226
+ const getIssuePaths = (issue) => /* @__PURE__ */ new Set([...issue.paths ?? []]);
1921
2227
  const hasPathOverlap = (a, b) => {
1922
2228
  const pathsA = getIssuePaths(a);
1923
2229
  const pathsB = getIssuePaths(b);
@@ -1986,6 +2292,9 @@ async function ensureNotStale(state, staleTimeoutMs) {
1986
2292
  if (pidDead) {
1987
2293
  logger.info({ issueId: issue.id, identifier: issue.identifier, state: issue.state, pid: agentStatus.pid?.pid }, "[Scheduler] PID dead \u2014 silently recovering to Queued");
1988
2294
  issue.startedAt = void 0;
2295
+ issue.lastError = `Agent process died unexpectedly (PID ${agentStatus.pid.pid}).`;
2296
+ issue.lastFailedPhase = "crash";
2297
+ issue.attempts = (issue.attempts ?? 0) + 1;
1989
2298
  container.issueRepository.markDirty(issue.id);
1990
2299
  await transitionIssueCommand({ issue, target: "Queued", note: `Agent process died (PID ${agentStatus.pid.pid}) \u2014 auto-recovering.` }, container);
1991
2300
  container.eventStore.addEvent(issue.id, "info", `Issue ${issue.identifier} agent process died (PID ${agentStatus.pid.pid}), silently recovered to Queued.`);
@@ -2011,8 +2320,8 @@ function hasTerminalQueue(state) {
2011
2320
  // src/agents/providers-usage.ts
2012
2321
  import { execFile } from "child_process";
2013
2322
  import { promisify } from "util";
2014
- import { existsSync as existsSync5, readFileSync as readFileSync4, readdirSync as readdirSync2, realpathSync } from "fs";
2015
- import { join as join7, dirname } from "path";
2323
+ import { existsSync as existsSync6, readFileSync as readFileSync5, readdirSync as readdirSync2, realpathSync } from "fs";
2324
+ import { join as join8, dirname } from "path";
2016
2325
  import { homedir as homedir2 } from "os";
2017
2326
  import { env } from "process";
2018
2327
  var execFileAsync = promisify(execFile);
@@ -2039,24 +2348,24 @@ function resolveCodexHomeCandidates() {
2039
2348
  ]);
2040
2349
  const candidates = [...homePaths, ...direct].filter(Boolean).flatMap((candidate) => {
2041
2350
  if (candidate.endsWith("/.codex") || candidate.endsWith("/codex")) return [candidate];
2042
- return [join7(candidate, ".codex"), join7(candidate, "codex")];
2351
+ return [join8(candidate, ".codex"), join8(candidate, "codex")];
2043
2352
  });
2044
2353
  return [...new Set(candidates)];
2045
2354
  }
2046
2355
  function resolveCodexDir() {
2047
2356
  for (const candidate of resolveCodexHomeCandidates()) {
2048
- if (existsSync5(candidate)) {
2357
+ if (existsSync6(candidate)) {
2049
2358
  return candidate;
2050
2359
  }
2051
2360
  }
2052
2361
  return null;
2053
2362
  }
2054
2363
  function findLatestCodexDb(codexDir) {
2055
- const explicit = join7(codexDir, "state_5.sqlite");
2056
- if (existsSync5(explicit)) return explicit;
2364
+ const explicit = join8(codexDir, "state_5.sqlite");
2365
+ if (existsSync6(explicit)) return explicit;
2057
2366
  const candidates = readdirSync2(codexDir).filter((name) => name.startsWith("state_") && name.endsWith(".sqlite")).sort().reverse();
2058
2367
  if (candidates.length === 0) return null;
2059
- return join7(codexDir, candidates[0]);
2368
+ return join8(codexDir, candidates[0]);
2060
2369
  }
2061
2370
  function computeNextMonday() {
2062
2371
  const now2 = /* @__PURE__ */ new Date();
@@ -2093,10 +2402,10 @@ var CLAUDE_PLAN_LIMITS = {
2093
2402
  };
2094
2403
  async function collectClaudeUsage() {
2095
2404
  const home = homedir2();
2096
- const claudeDir = join7(home, ".claude");
2097
- if (!existsSync5(claudeDir)) return null;
2405
+ const claudeDir = join8(home, ".claude");
2406
+ if (!existsSync6(claudeDir)) return null;
2098
2407
  const available = await whichExists("claude");
2099
- const projectsDir = join7(claudeDir, "projects");
2408
+ const projectsDir = join8(claudeDir, "projects");
2100
2409
  let totalInputTokens = 0;
2101
2410
  let totalOutputTokens = 0;
2102
2411
  let totalSessions = 0;
@@ -2110,12 +2419,12 @@ async function collectClaudeUsage() {
2110
2419
  const todayMs = todayStart.getTime();
2111
2420
  const weekStart = computeWeekStart();
2112
2421
  const weekMs = weekStart.getTime();
2113
- if (existsSync5(projectsDir)) {
2422
+ if (existsSync6(projectsDir)) {
2114
2423
  try {
2115
2424
  const projectDirs = readdirSync2(projectsDir, { withFileTypes: true });
2116
2425
  for (const dir of projectDirs) {
2117
2426
  if (!dir.isDirectory()) continue;
2118
- const projectPath = join7(projectsDir, dir.name);
2427
+ const projectPath = join8(projectsDir, dir.name);
2119
2428
  let sessionFiles;
2120
2429
  try {
2121
2430
  sessionFiles = readdirSync2(projectPath).filter((f) => f.endsWith(".jsonl"));
@@ -2123,10 +2432,10 @@ async function collectClaudeUsage() {
2123
2432
  continue;
2124
2433
  }
2125
2434
  for (const file of sessionFiles) {
2126
- const filePath = join7(projectPath, file);
2435
+ const filePath = join8(projectPath, file);
2127
2436
  let content;
2128
2437
  try {
2129
- content = readFileSync4(filePath, "utf8");
2438
+ content = readFileSync5(filePath, "utf8");
2130
2439
  } catch {
2131
2440
  continue;
2132
2441
  }
@@ -2181,10 +2490,10 @@ async function collectClaudeUsage() {
2181
2490
  let plan = "pro";
2182
2491
  let resetInfo = "Weekly reset (every Monday 00:00 UTC)";
2183
2492
  let currentModel = "";
2184
- const settingsPath = join7(claudeDir, "settings.json");
2185
- if (existsSync5(settingsPath)) {
2493
+ const settingsPath = join8(claudeDir, "settings.json");
2494
+ if (existsSync6(settingsPath)) {
2186
2495
  try {
2187
- const settings = JSON.parse(readFileSync4(settingsPath, "utf8"));
2496
+ const settings = JSON.parse(readFileSync5(settingsPath, "utf8"));
2188
2497
  if (settings.plan === "max" || settings.plan === "max5x") {
2189
2498
  plan = settings.plan;
2190
2499
  resetInfo = `Plan: ${settings.plan.toUpperCase()} \u2014 Weekly token limit resets every Monday 00:00 UTC`;
@@ -2220,11 +2529,11 @@ async function collectCodexUsage() {
2220
2529
  if (!codexDir) return null;
2221
2530
  const available = await whichExists("codex");
2222
2531
  const models = [];
2223
- const modelsCachePath = join7(codexDir, "models_cache.json");
2532
+ const modelsCachePath = join8(codexDir, "models_cache.json");
2224
2533
  let currentModel = "";
2225
- if (existsSync5(modelsCachePath)) {
2534
+ if (existsSync6(modelsCachePath)) {
2226
2535
  try {
2227
- const cache = JSON.parse(readFileSync4(modelsCachePath, "utf8"));
2536
+ const cache = JSON.parse(readFileSync5(modelsCachePath, "utf8"));
2228
2537
  for (const m of cache.models || []) {
2229
2538
  models.push({
2230
2539
  slug: m.slug,
@@ -2235,10 +2544,10 @@ async function collectCodexUsage() {
2235
2544
  } catch {
2236
2545
  }
2237
2546
  }
2238
- const configPath = join7(codexDir, "config.toml");
2239
- if (existsSync5(configPath)) {
2547
+ const configPath = join8(codexDir, "config.toml");
2548
+ if (existsSync6(configPath)) {
2240
2549
  try {
2241
- const configContent = readFileSync4(configPath, "utf8");
2550
+ const configContent = readFileSync5(configPath, "utf8");
2242
2551
  const modelMatch = configContent.match(/^model\s*=\s*"([^"]+)"/m);
2243
2552
  if (modelMatch) currentModel = modelMatch[1];
2244
2553
  } catch {
@@ -2327,9 +2636,9 @@ async function collectGeminiUsage() {
2327
2636
  try {
2328
2637
  const { stdout: binPath } = await execFileAsync("which", ["gemini"], { encoding: "utf8", timeout: 3e3 });
2329
2638
  const realBin = realpathSync(binPath.trim());
2330
- const modelsPath = join7(dirname(dirname(realBin)), "node_modules", "@google", "gemini-cli-core", "dist", "src", "config", "models.js");
2331
- if (existsSync5(modelsPath)) {
2332
- const content = readFileSync4(modelsPath, "utf8");
2639
+ const modelsPath = join8(dirname(dirname(realBin)), "node_modules", "@google", "gemini-cli-core", "dist", "src", "config", "models.js");
2640
+ if (existsSync6(modelsPath)) {
2641
+ const content = readFileSync5(modelsPath, "utf8");
2333
2642
  const regex = /export const ([A-Z0-9_]+)\s*=\s*'(gemini-[^']+)';/g;
2334
2643
  const seen = /* @__PURE__ */ new Set();
2335
2644
  let m;
@@ -2347,10 +2656,10 @@ async function collectGeminiUsage() {
2347
2656
  } catch {
2348
2657
  }
2349
2658
  let currentModel = "";
2350
- const settingsPath = join7(homedir2(), ".gemini", "settings.json");
2351
- if (existsSync5(settingsPath)) {
2659
+ const settingsPath = join8(homedir2(), ".gemini", "settings.json");
2660
+ if (existsSync6(settingsPath)) {
2352
2661
  try {
2353
- const settings = JSON.parse(readFileSync4(settingsPath, "utf8"));
2662
+ const settings = JSON.parse(readFileSync5(settingsPath, "utf8"));
2354
2663
  if (typeof settings.model === "string" && settings.model.trim()) {
2355
2664
  currentModel = settings.model.trim();
2356
2665
  }
@@ -2394,10 +2703,10 @@ async function collectProvidersUsage() {
2394
2703
  }
2395
2704
 
2396
2705
  // src/routes/state.ts
2397
- import { existsSync as existsSync7, mkdirSync as mkdirSync2, writeFileSync as writeFileSync3 } from "fs";
2706
+ import { existsSync as existsSync8, mkdirSync as mkdirSync4, writeFileSync as writeFileSync4 } from "fs";
2398
2707
  import { randomUUID } from "crypto";
2399
- import { execSync as execSync2 } from "child_process";
2400
- import { basename as basename2, extname, join as join8 } from "path";
2708
+ import { execSync as execSync3 } from "child_process";
2709
+ import { basename as basename2, extname, join as join9 } from "path";
2401
2710
 
2402
2711
  // src/commands/approve-plan.command.ts
2403
2712
  async function approvePlanCommand(input, deps) {
@@ -2406,7 +2715,7 @@ async function approvePlanCommand(input, deps) {
2406
2715
  throw new Error(`Cannot approve issue in state ${issue.state}. Must be in Planning.`);
2407
2716
  }
2408
2717
  await transitionIssueCommand(
2409
- { issue, target: "Planned", note: `Plan approved for ${issue.identifier}. Ready for execution.` },
2718
+ { issue, target: "PendingApproval", note: `Plan approved for ${issue.identifier}. Ready for execution.` },
2410
2719
  deps
2411
2720
  );
2412
2721
  await transitionIssueCommand(
@@ -2418,8 +2727,8 @@ async function approvePlanCommand(input, deps) {
2418
2727
  // src/commands/execute-issue.command.ts
2419
2728
  async function executeIssueCommand(input, deps) {
2420
2729
  const { issue } = input;
2421
- if (issue.state !== "Planned") {
2422
- throw new Error(`Cannot execute issue in state ${issue.state}. Must be in Planned.`);
2730
+ if (issue.state !== "PendingApproval") {
2731
+ throw new Error(`Cannot execute issue in state ${issue.state}. Must be in PendingApproval.`);
2423
2732
  }
2424
2733
  await transitionIssueCommand(
2425
2734
  { issue, target: "Queued", note: `Execution requested for ${issue.identifier}.` },
@@ -2460,21 +2769,63 @@ async function replanIssueCommand(input, deps) {
2460
2769
  }
2461
2770
 
2462
2771
  // src/commands/merge-workspace.command.ts
2463
- import { existsSync as existsSync6 } from "fs";
2772
+ import { existsSync as existsSync7 } from "fs";
2464
2773
  import { execSync } from "child_process";
2774
+
2775
+ // src/domains/validation.ts
2776
+ import { execFile as execFile2 } from "child_process";
2777
+ async function runValidationGate(issue, config) {
2778
+ if (!config.testCommand) return null;
2779
+ const cwd = issue.worktreePath ?? issue.workspacePath;
2780
+ if (!cwd) {
2781
+ logger.warn({ issueId: issue.id }, "[Validation] No workspace path \u2014 skipping gate");
2782
+ return null;
2783
+ }
2784
+ const command = config.testCommand;
2785
+ logger.info({ issueId: issue.id, command, cwd }, "[Validation] Running validation gate");
2786
+ return new Promise((resolve3) => {
2787
+ const child = execFile2("sh", ["-c", command], {
2788
+ cwd,
2789
+ encoding: "utf8",
2790
+ timeout: 3e5,
2791
+ maxBuffer: 2 * 1024 * 1024
2792
+ }, (err, stdout, stderr) => {
2793
+ const combined = (stdout || "") + (stderr || "");
2794
+ if (!err) {
2795
+ logger.info({ issueId: issue.id }, "[Validation] Gate passed");
2796
+ resolve3({
2797
+ passed: true,
2798
+ output: combined.slice(-2048),
2799
+ command,
2800
+ ranAt: (/* @__PURE__ */ new Date()).toISOString()
2801
+ });
2802
+ return;
2803
+ }
2804
+ logger.warn({ issueId: issue.id, exitCode: err.code }, "[Validation] Gate failed");
2805
+ resolve3({
2806
+ passed: false,
2807
+ output: combined.slice(-2048) || String(err).slice(0, 2048),
2808
+ command,
2809
+ ranAt: (/* @__PURE__ */ new Date()).toISOString()
2810
+ });
2811
+ });
2812
+ });
2813
+ }
2814
+
2815
+ // src/commands/merge-workspace.command.ts
2465
2816
  async function mergeWorkspaceCommand(input, deps) {
2466
2817
  const { issue, state } = input;
2467
- if (!["Done", "Reviewing", "Reviewed"].includes(issue.state)) {
2468
- throw new Error(`Issue ${issue.identifier} is in state ${issue.state}. Merge is only allowed in Reviewing, Reviewed, or Done state.`);
2818
+ if (!["Approved", "Reviewing", "PendingDecision"].includes(issue.state)) {
2819
+ throw new Error(`Issue ${issue.identifier} is in state ${issue.state}. Merge is only allowed in Reviewing, PendingDecision, or Approved state.`);
2469
2820
  }
2470
- if (issue.state === "Reviewing" || issue.state === "Reviewed") {
2821
+ if (issue.state === "Reviewing" || issue.state === "PendingDecision") {
2471
2822
  await transitionIssueCommand(
2472
- { issue, target: "Done", note: "Approved and merged by user." },
2823
+ { issue, target: "Approved", note: "Approved and merged by user." },
2473
2824
  deps
2474
2825
  );
2475
2826
  }
2476
2827
  const wp = issue.worktreePath ?? issue.workspacePath;
2477
- if (!wp || !existsSync6(wp)) {
2828
+ if (!wp || !existsSync7(wp)) {
2478
2829
  throw new Error("No workspace found for this issue.");
2479
2830
  }
2480
2831
  if (issue.branchName && issue.baseBranch) {
@@ -2498,12 +2849,20 @@ async function mergeWorkspaceCommand(input, deps) {
2498
2849
  }
2499
2850
  } catch {
2500
2851
  }
2852
+ const validation = await runValidationGate(issue, state.config);
2853
+ if (validation) {
2854
+ issue.validationResult = validation;
2855
+ if (!validation.passed) {
2856
+ throw new Error(`Validation gate failed (${validation.command}): ${validation.output.slice(0, 500)}`);
2857
+ }
2858
+ }
2501
2859
  const result = mergeWorkspace(issue);
2502
2860
  issue.mergeResult = {
2503
2861
  copied: result.copied.length,
2504
2862
  deleted: result.deleted.length,
2505
2863
  skipped: result.skipped.length,
2506
- conflicts: result.conflicts.length
2864
+ conflicts: result.conflicts.length,
2865
+ conflictFiles: result.conflicts.length > 0 ? result.conflicts : void 0
2507
2866
  };
2508
2867
  if (result.conflicts.length > 0) {
2509
2868
  deps.eventStore.addEvent(issue.id, "error", `Merge conflicts: ${result.conflicts.join(", ")}`);
@@ -2527,6 +2886,139 @@ async function mergeWorkspaceCommand(input, deps) {
2527
2886
  return result;
2528
2887
  }
2529
2888
 
2889
+ // src/commands/push-workspace.command.ts
2890
+ import { execFileSync, execSync as execSync2 } from "child_process";
2891
+ function isGhAvailable() {
2892
+ try {
2893
+ execFileSync("gh", ["--version"], { stdio: "pipe", timeout: 5e3 });
2894
+ return true;
2895
+ } catch {
2896
+ return false;
2897
+ }
2898
+ }
2899
+ function getCompareUrl(branchName, baseBranch) {
2900
+ try {
2901
+ const remote = execSync2("git remote get-url origin", { cwd: TARGET_ROOT, encoding: "utf8", stdio: "pipe" }).trim();
2902
+ const cleanRemote = remote.replace(/\.git$/, "");
2903
+ return `${cleanRemote}/compare/${baseBranch}...${branchName}`;
2904
+ } catch {
2905
+ return `(branch pushed: ${branchName})`;
2906
+ }
2907
+ }
2908
+ function findExistingPr(branchName) {
2909
+ try {
2910
+ const result = execFileSync(
2911
+ "gh",
2912
+ ["pr", "view", branchName, "--json", "url,state", "--jq", 'select(.state == "OPEN") | .url'],
2913
+ { cwd: TARGET_ROOT, encoding: "utf8", stdio: "pipe", timeout: 15e3 }
2914
+ ).trim();
2915
+ return result || null;
2916
+ } catch {
2917
+ return null;
2918
+ }
2919
+ }
2920
+ function createPr(branchName, baseBranch, title, body) {
2921
+ return execFileSync(
2922
+ "gh",
2923
+ ["pr", "create", "--head", branchName, "--base", baseBranch, "--title", title, "--body", body],
2924
+ { cwd: TARGET_ROOT, encoding: "utf8", stdio: "pipe", timeout: 3e4 }
2925
+ ).trim();
2926
+ }
2927
+ async function pushWorkspaceCommand(input, deps) {
2928
+ const { issue, state } = input;
2929
+ if (!["Approved", "Reviewing", "PendingDecision"].includes(issue.state)) {
2930
+ throw new Error(`Issue ${issue.identifier} is in state ${issue.state}. Push is only allowed in Reviewing, PendingDecision, or Approved state.`);
2931
+ }
2932
+ if (!issue.branchName || !issue.baseBranch || !issue.worktreePath) {
2933
+ throw new Error(`Issue ${issue.identifier} has no git worktree \u2014 cannot push.`);
2934
+ }
2935
+ if (issue.state === "Reviewing" || issue.state === "PendingDecision") {
2936
+ await transitionIssueCommand(
2937
+ { issue, target: "Approved", note: "Approved and pushed by user." },
2938
+ deps
2939
+ );
2940
+ }
2941
+ ensureWorktreeCommitted(issue);
2942
+ const validation = await runValidationGate(issue, state.config);
2943
+ if (validation) {
2944
+ issue.validationResult = validation;
2945
+ if (!validation.passed) {
2946
+ throw new Error(`Validation gate failed (${validation.command}): ${validation.output.slice(0, 500)}`);
2947
+ }
2948
+ }
2949
+ computeDiffStats(issue);
2950
+ const planSummary = issue.plan?.summary ?? issue.title;
2951
+ let diffStat = "";
2952
+ try {
2953
+ diffStat = execSync2(
2954
+ `git diff --stat "${issue.baseBranch}"..."${issue.branchName}"`,
2955
+ { cwd: TARGET_ROOT, encoding: "utf8", maxBuffer: 512e3, timeout: 1e4, stdio: "pipe" }
2956
+ ).trim();
2957
+ } catch {
2958
+ }
2959
+ const body = `## Summary
2960
+ ${planSummary}
2961
+
2962
+ ## Diff Stats
2963
+ \`\`\`
2964
+ ${diffStat || "No diff stats available"}
2965
+ \`\`\`
2966
+
2967
+ *Automated by fifony*`;
2968
+ execSync2(`git push -u origin "${issue.branchName}"`, { cwd: TARGET_ROOT, stdio: "pipe" });
2969
+ const prBase = state.config.prBaseBranch || issue.baseBranch;
2970
+ const ghAvailable = isGhAvailable();
2971
+ let prUrl;
2972
+ if (!ghAvailable) {
2973
+ prUrl = getCompareUrl(issue.branchName, prBase);
2974
+ logger.info({ issueId: issue.id, prUrl }, "[Push] gh CLI not available \u2014 using compare URL");
2975
+ } else {
2976
+ const existingUrl = findExistingPr(issue.branchName);
2977
+ if (existingUrl) {
2978
+ prUrl = existingUrl;
2979
+ logger.info({ issueId: issue.id, prUrl }, "[Push] Existing open PR found");
2980
+ } else {
2981
+ try {
2982
+ prUrl = createPr(issue.branchName, prBase, issue.title, body);
2983
+ logger.info({ issueId: issue.id, prUrl }, "[Push] PR created");
2984
+ } catch (err) {
2985
+ const ghError = (err.stderr || err.stdout || String(err)).toString().slice(0, 500);
2986
+ logger.error({ issueId: issue.id, ghError }, "[Push] gh pr create failed");
2987
+ prUrl = getCompareUrl(issue.branchName, prBase);
2988
+ deps.eventStore.addEvent(issue.id, "error", `gh pr create failed: ${ghError}. Branch was pushed \u2014 use the compare URL to create the PR manually.`);
2989
+ }
2990
+ }
2991
+ }
2992
+ issue.prUrl = prUrl;
2993
+ if (!issue.mergedReason) issue.mergedReason = "Pushed to origin and PR created.";
2994
+ await transitionIssueCommand(
2995
+ { issue, target: "Merged", note: `Branch ${issue.branchName} pushed. PR: ${prUrl}` },
2996
+ deps
2997
+ );
2998
+ deps.eventStore.addEvent(issue.id, "merge", `PR created: ${prUrl}`);
2999
+ await deps.persistencePort.persistState(state);
3000
+ return { prUrl, ghAvailable };
3001
+ }
3002
+
3003
+ // src/commands/retry-execution.command.ts
3004
+ async function retryExecutionCommand(input, deps) {
3005
+ const { issue, note } = input;
3006
+ if (issue.state !== "Blocked") {
3007
+ throw new Error(
3008
+ `retryExecutionCommand requires Blocked state, got ${issue.state}. Use replanIssueCommand for re-planning or the generic /retry endpoint for other states.`
3009
+ );
3010
+ }
3011
+ await transitionIssueCommand(
3012
+ { issue, target: "Queued", note: note ?? `Retry execution for ${issue.identifier} (attempt ${issue.attempts + 1}).` },
3013
+ deps
3014
+ );
3015
+ deps.eventStore.addEvent(
3016
+ issue.id,
3017
+ "manual",
3018
+ `Execution retry requested for ${issue.identifier} \u2014 re-queued from Blocked.`
3019
+ );
3020
+ }
3021
+
2530
3022
  // src/routes/state.ts
2531
3023
  function getStateQuery(state, showAll = false) {
2532
3024
  let issues = state.issues;
@@ -2544,7 +3036,6 @@ function getStateQuery(state, showAll = false) {
2544
3036
  return {
2545
3037
  ...state,
2546
3038
  issues,
2547
- capabilities: computeCapabilityCounts(issues),
2548
3039
  metrics: computeMetrics(issues),
2549
3040
  _filter: showAll ? "all" : "recent",
2550
3041
  _totalIssues: state.issues.length
@@ -2629,7 +3120,7 @@ function registerStateRoutes(app, state) {
2629
3120
  );
2630
3121
  if (issue.plan?.steps?.length) {
2631
3122
  await transitionIssueCommand(
2632
- { issue, target: "Planned", note: "Existing plan found." },
3123
+ { issue, target: "PendingApproval", note: "Existing plan found." },
2633
3124
  container
2634
3125
  );
2635
3126
  await transitionIssueCommand(
@@ -2638,11 +3129,26 @@ function registerStateRoutes(app, state) {
2638
3129
  );
2639
3130
  }
2640
3131
  } else if (issue.state === "Blocked") {
3132
+ await retryExecutionCommand(
3133
+ { issue, note: "Manual retry from Blocked." },
3134
+ container
3135
+ );
3136
+ } else if (issue.state === "Approved") {
2641
3137
  await transitionIssueCommand(
2642
- { issue, target: "Queued", note: "Manual retry from Blocked." },
3138
+ { issue, target: "Planning", note: "Requeued for rework after merge conflicts." },
2643
3139
  container
2644
3140
  );
2645
- } else if (issue.state === "Planned") {
3141
+ if (issue.plan?.steps?.length) {
3142
+ await transitionIssueCommand(
3143
+ { issue, target: "PendingApproval", note: "Existing plan found." },
3144
+ container
3145
+ );
3146
+ await transitionIssueCommand(
3147
+ { issue, target: "Queued", note: "Auto-queued for rework." },
3148
+ container
3149
+ );
3150
+ }
3151
+ } else if (issue.state === "PendingApproval") {
2646
3152
  await transitionIssueCommand(
2647
3153
  { issue, target: "Queued", note: "Manual retry \u2014 queued for execution." },
2648
3154
  container
@@ -2691,6 +3197,10 @@ function registerStateRoutes(app, state) {
2691
3197
  const issue = findIssue(state, issueId);
2692
3198
  if (!issue) return c.json({ ok: false, error: "Issue not found." }, 404);
2693
3199
  const container = getContainer();
3200
+ if (state.config.mergeMode === "push-pr") {
3201
+ const result2 = await pushWorkspaceCommand({ issue, state }, container);
3202
+ return c.json({ ok: true, prUrl: result2.prUrl, ghAvailable: result2.ghAvailable });
3203
+ }
2694
3204
  const result = await mergeWorkspaceCommand({ issue, state }, container);
2695
3205
  return c.json({ ok: true, ...result });
2696
3206
  } catch (error) {
@@ -2699,17 +3209,51 @@ function registerStateRoutes(app, state) {
2699
3209
  return c.json({ ok: false, error: String(error) }, 500);
2700
3210
  }
2701
3211
  });
3212
+ app.get("/api/issues/:id/merge-preview", async (c) => {
3213
+ logger.info({ issueId: parseIssue(c) }, "[API] GET /api/issues/:id/merge-preview");
3214
+ try {
3215
+ const issueId = parseIssue(c);
3216
+ if (!issueId) return c.json({ ok: false, error: "Issue id is required." }, 400);
3217
+ const issue = findIssue(state, issueId);
3218
+ if (!issue) return c.json({ ok: false, error: "Issue not found." }, 404);
3219
+ const { dryMerge } = await import("./workspace-474CCKTW.js");
3220
+ const result = dryMerge(issue);
3221
+ return c.json({ ok: true, ...result });
3222
+ } catch (error) {
3223
+ logger.error(`Failed to preview merge for ${parseIssue(c) || "<unknown>"}: ${String(error)}`);
3224
+ return c.json({ ok: false, error: String(error) }, 500);
3225
+ }
3226
+ });
3227
+ app.post("/api/issues/:id/rebase", async (c) => {
3228
+ logger.info({ issueId: parseIssue(c) }, "[API] POST /api/issues/:id/rebase");
3229
+ try {
3230
+ const issueId = parseIssue(c);
3231
+ if (!issueId) return c.json({ ok: false, error: "Issue id is required." }, 400);
3232
+ const issue = findIssue(state, issueId);
3233
+ if (!issue) return c.json({ ok: false, error: "Issue not found." }, 404);
3234
+ const { rebaseWorktree } = await import("./workspace-474CCKTW.js");
3235
+ const result = rebaseWorktree(issue);
3236
+ if (result.success) {
3237
+ addEvent(state, issue.id, "info", `Branch ${issue.branchName} rebased onto ${issue.baseBranch}.`);
3238
+ }
3239
+ await persistState(state);
3240
+ return c.json({ ok: true, ...result });
3241
+ } catch (error) {
3242
+ logger.error(`Failed to rebase for ${parseIssue(c) || "<unknown>"}: ${String(error)}`);
3243
+ return c.json({ ok: false, error: String(error) }, 500);
3244
+ }
3245
+ });
2702
3246
  app.post("/api/issues/:id/try", async (c) => {
2703
3247
  logger.info({ issueId: parseIssue(c) }, "[API] POST /api/issues/:id/try");
2704
3248
  return mutateIssueState(state, c, async (issue) => {
2705
- if (!["Reviewing", "Reviewed"].includes(issue.state)) {
3249
+ if (!["Reviewing", "PendingDecision"].includes(issue.state)) {
2706
3250
  throw new Error(`Cannot apply test for issue in state ${issue.state}.`);
2707
3251
  }
2708
3252
  if (!issue.branchName) {
2709
3253
  throw new Error("No branch name found for this issue.");
2710
3254
  }
2711
3255
  try {
2712
- execSync2(
3256
+ execSync3(
2713
3257
  `git merge --squash "${issue.branchName}"`,
2714
3258
  { encoding: "utf8", cwd: TARGET_ROOT, stdio: "pipe", timeout: 3e4 }
2715
3259
  );
@@ -2724,8 +3268,8 @@ function registerStateRoutes(app, state) {
2724
3268
  logger.info({ issueId: parseIssue(c) }, "[API] POST /api/issues/:id/revert-try");
2725
3269
  return mutateIssueState(state, c, async (issue) => {
2726
3270
  try {
2727
- execSync2("git reset --hard HEAD", { cwd: TARGET_ROOT, stdio: "pipe", timeout: 15e3 });
2728
- execSync2("git clean -fd", { cwd: TARGET_ROOT, stdio: "pipe", timeout: 15e3 });
3271
+ execSync3("git reset --hard HEAD", { cwd: TARGET_ROOT, stdio: "pipe", timeout: 15e3 });
3272
+ execSync3("git clean -fd", { cwd: TARGET_ROOT, stdio: "pipe", timeout: 15e3 });
2729
3273
  } catch (err) {
2730
3274
  const msg = err.stderr || err.stdout || String(err);
2731
3275
  throw new Error(`git reset/clean failed: ${msg}`);
@@ -2736,7 +3280,7 @@ function registerStateRoutes(app, state) {
2736
3280
  app.post("/api/issues/:id/rollback", async (c) => {
2737
3281
  logger.info({ issueId: parseIssue(c) }, "[API] POST /api/issues/:id/rollback");
2738
3282
  return mutateIssueState(state, c, async (issue) => {
2739
- if (!["Reviewing", "Reviewed", "Done"].includes(issue.state)) {
3283
+ if (!["Reviewing", "PendingDecision", "Approved"].includes(issue.state)) {
2740
3284
  throw new Error(`Cannot rollback issue in state ${issue.state}. Must be in Reviewing, Reviewed, or Done.`);
2741
3285
  }
2742
3286
  if (issue.workspacePath) {
@@ -2766,15 +3310,15 @@ function registerStateRoutes(app, state) {
2766
3310
  if (!Array.isArray(payload.files) || payload.files.length === 0) {
2767
3311
  return c.json({ ok: false, error: "No files provided." }, 400);
2768
3312
  }
2769
- const issueAttachDir = join8(ATTACHMENTS_ROOT, issue.id);
2770
- mkdirSync2(issueAttachDir, { recursive: true });
3313
+ const issueAttachDir = join9(ATTACHMENTS_ROOT, issue.id);
3314
+ mkdirSync4(issueAttachDir, { recursive: true });
2771
3315
  const newPaths = [];
2772
3316
  for (const file of payload.files) {
2773
3317
  if (typeof file.data !== "string" || !file.name) continue;
2774
3318
  const safeExt = extname(file.name).replace(/[^a-z0-9.]/gi, "").slice(0, 10) || ".bin";
2775
3319
  const safeName = `${randomUUID()}${safeExt}`;
2776
- const dest = join8(issueAttachDir, safeName);
2777
- writeFileSync3(dest, Buffer.from(file.data, "base64"));
3320
+ const dest = join9(issueAttachDir, safeName);
3321
+ writeFileSync4(dest, Buffer.from(file.data, "base64"));
2778
3322
  newPaths.push(dest);
2779
3323
  }
2780
3324
  issue.images = [...issue.images ?? [], ...newPaths];
@@ -2794,8 +3338,8 @@ function registerStateRoutes(app, state) {
2794
3338
  const filename = c.req.param?.("filename") ?? c.req.params?.filename ?? "";
2795
3339
  if (!filename) return c.json({ ok: false, error: "Filename is required." }, 400);
2796
3340
  const safeName = basename2(filename);
2797
- const filePath = join8(ATTACHMENTS_ROOT, issueId, safeName);
2798
- if (!existsSync7(filePath)) return c.json({ ok: false, error: "Image not found." }, 404);
3341
+ const filePath = join9(ATTACHMENTS_ROOT, issueId, safeName);
3342
+ if (!existsSync8(filePath)) return c.json({ ok: false, error: "Image not found." }, 404);
2799
3343
  const ext = extname(safeName).toLowerCase();
2800
3344
  const mimeMap = {
2801
3345
  ".png": "image/png",
@@ -2806,8 +3350,8 @@ function registerStateRoutes(app, state) {
2806
3350
  ".svg": "image/svg+xml"
2807
3351
  };
2808
3352
  const mime = mimeMap[ext] ?? "application/octet-stream";
2809
- const { readFileSync: readFileSync11 } = await import("fs");
2810
- const data = readFileSync11(filePath);
3353
+ const { readFileSync: readFileSync12 } = await import("fs");
3354
+ const data = readFileSync12(filePath);
2811
3355
  return new Response(data, { headers: { "Content-Type": mime, "Cache-Control": "private, max-age=86400" } });
2812
3356
  } catch (error) {
2813
3357
  return c.json({ ok: false, error: error instanceof Error ? error.message : String(error) }, 500);
@@ -2819,7 +3363,7 @@ function registerStateRoutes(app, state) {
2819
3363
  const issue = findIssue(state, issueId);
2820
3364
  if (!issue) return c.json({ ok: false, error: "Issue not found." }, 404);
2821
3365
  try {
2822
- const { getIssueTransitionHistory } = await import("./issue-state-machine-OWABY5S2.js");
3366
+ const { getIssueTransitionHistory } = await import("./issue-state-machine-ACMUJSXC.js");
2823
3367
  const limit = parseInt(c.req.query("limit") ?? "50", 10);
2824
3368
  const offset = parseInt(c.req.query("offset") ?? "0", 10);
2825
3369
  const transitions = await getIssueTransitionHistory(issue.id, { limit, offset });
@@ -2830,7 +3374,7 @@ function registerStateRoutes(app, state) {
2830
3374
  });
2831
3375
  app.get("/api/state-machine/transitions", async (c) => {
2832
3376
  try {
2833
- const { getStateMachineTransitions } = await import("./issue-state-machine-OWABY5S2.js");
3377
+ const { getStateMachineTransitions } = await import("./issue-state-machine-ACMUJSXC.js");
2834
3378
  return c.json({ ok: true, transitions: getStateMachineTransitions() });
2835
3379
  } catch (error) {
2836
3380
  return c.json({ ok: false, error: error instanceof Error ? error.message : String(error) }, 500);
@@ -2838,7 +3382,7 @@ function registerStateRoutes(app, state) {
2838
3382
  });
2839
3383
  app.get("/api/state-machine/visualize", async (c) => {
2840
3384
  try {
2841
- const { visualizeStateMachine } = await import("./issue-state-machine-OWABY5S2.js");
3385
+ const { visualizeStateMachine } = await import("./issue-state-machine-ACMUJSXC.js");
2842
3386
  const dot = visualizeStateMachine();
2843
3387
  if (!dot) return c.json({ ok: false, error: "Visualization not available." }, 404);
2844
3388
  return c.json({ ok: true, dot });
@@ -2923,8 +3467,8 @@ async function recoverPlanningSession() {
2923
3467
  }
2924
3468
 
2925
3469
  // src/agents/planning/plan-generator.ts
2926
- import { writeFileSync as writeFileSync5 } from "fs";
2927
- import { join as join10 } from "path";
3470
+ import { writeFileSync as writeFileSync6 } from "fs";
3471
+ import { join as join11 } from "path";
2928
3472
  import { mkdtempSync, rmSync as rmSync2 } from "fs";
2929
3473
  import { tmpdir } from "os";
2930
3474
 
@@ -2982,10 +3526,9 @@ function tryBuildPlan(parsed) {
2982
3526
  })) : void 0,
2983
3527
  validation: toStringArray(parsed.validation),
2984
3528
  deliverables: toStringArray(parsed.deliverables),
2985
- executionStrategy: parsed.executionStrategy || parsed.execution_strategy || void 0,
2986
- toolingDecision: parsed.toolingDecision || parsed.tooling_decision || void 0,
2987
3529
  suggestedPaths: toStringArray(parsed.suggestedPaths || parsed.suggested_paths || parsed.suggestedFilePaths || parsed.filePaths || parsed.file_paths || parsed.paths),
2988
- suggestedLabels: toStringArray(parsed.suggestedLabels || parsed.suggested_labels || parsed.labels),
3530
+ suggestedSkills: toStringArray(parsed.suggestedSkills || parsed.suggested_skills),
3531
+ suggestedAgents: toStringArray(parsed.suggestedAgents || parsed.suggested_agents),
2989
3532
  suggestedEffort: parsed.suggestedEffort || parsed.suggested_effort || parsed.effortSuggestion || parsed.effort_suggestion || parsed.effort || { default: "medium" },
2990
3533
  provider: "",
2991
3534
  createdAt: now()
@@ -3096,28 +3639,27 @@ function extractPlanTokenUsage(raw) {
3096
3639
  }
3097
3640
 
3098
3641
  // src/agents/planning/planning-prompts.ts
3099
- import { mkdirSync as mkdirSync3, writeFileSync as writeFileSync4 } from "fs";
3642
+ import { mkdirSync as mkdirSync5, writeFileSync as writeFileSync5 } from "fs";
3100
3643
  import { spawn } from "child_process";
3101
- import { join as join9 } from "path";
3644
+ import { join as join10 } from "path";
3102
3645
 
3103
3646
  // src/agents/planning/planning-schema.ts
3104
3647
  var STEP_SCHEMA = {
3105
3648
  type: "object",
3106
3649
  additionalProperties: false,
3107
- required: ["step", "action", "files", "details", "ownerType", "doneWhen"],
3650
+ required: ["step", "action", "files", "details", "doneWhen"],
3108
3651
  properties: {
3109
3652
  step: { type: "number" },
3110
3653
  action: { type: "string" },
3111
3654
  files: { type: "array", items: { type: "string" } },
3112
3655
  details: { type: "string" },
3113
- ownerType: { type: "string", enum: ["human", "agent", "skill", "subagent", "tool"] },
3114
3656
  doneWhen: { type: "string" }
3115
3657
  }
3116
3658
  };
3117
3659
  var PLAN_JSON_SCHEMA = JSON.stringify({
3118
3660
  type: "object",
3119
3661
  additionalProperties: false,
3120
- required: ["summary", "steps", "phases", "estimatedComplexity", "suggestedPaths", "suggestedLabels", "assumptions", "constraints", "unknowns", "successCriteria", "executionStrategy", "toolingDecision", "risks", "validation", "deliverables", "suggestedEffort"],
3662
+ required: ["summary", "steps", "estimatedComplexity", "suggestedPaths", "suggestedEffort"],
3121
3663
  properties: {
3122
3664
  summary: { type: "string" },
3123
3665
  estimatedComplexity: { type: "string", enum: ["trivial", "low", "medium", "high"] },
@@ -3125,16 +3667,8 @@ var PLAN_JSON_SCHEMA = JSON.stringify({
3125
3667
  constraints: { type: "array", items: { type: "string" } },
3126
3668
  unknowns: { type: "array", items: { type: "object", additionalProperties: false, properties: { question: { type: "string" }, whyItMatters: { type: "string" }, howToResolve: { type: "string" } }, required: ["question", "whyItMatters", "howToResolve"] } },
3127
3669
  successCriteria: { type: "array", items: { type: "string" } },
3128
- executionStrategy: { type: "object", additionalProperties: false, required: ["approach", "whyThisApproach", "alternativesConsidered"], properties: { approach: { type: "string" }, whyThisApproach: { type: "string" }, alternativesConsidered: { type: "array", items: { type: "string" } } } },
3129
- toolingDecision: { type: "object", additionalProperties: false, required: ["shouldUseSkills", "skillsToUse", "shouldUseSubagents", "subagentsToUse", "decisionSummary"], properties: {
3130
- shouldUseSkills: { type: "boolean" },
3131
- skillsToUse: { type: "array", items: { type: "object", additionalProperties: false, properties: { name: { type: "string" }, why: { type: "string" } }, required: ["name", "why"] } },
3132
- shouldUseSubagents: { type: "boolean" },
3133
- subagentsToUse: { type: "array", items: { type: "object", additionalProperties: false, properties: { name: { type: "string" }, role: { type: "string" }, why: { type: "string" } }, required: ["name", "role", "why"] } },
3134
- decisionSummary: { type: "string" }
3135
- } },
3136
3670
  steps: { type: "array", items: STEP_SCHEMA },
3137
- phases: { type: "array", items: { type: "object", additionalProperties: false, required: ["phaseName", "goal", "tasks", "dependencies", "outputs"], properties: {
3671
+ phases: { type: "array", items: { type: "object", additionalProperties: false, required: ["phaseName", "goal", "tasks"], properties: {
3138
3672
  phaseName: { type: "string" },
3139
3673
  goal: { type: "string" },
3140
3674
  tasks: { type: "array", items: STEP_SCHEMA },
@@ -3145,8 +3679,9 @@ var PLAN_JSON_SCHEMA = JSON.stringify({
3145
3679
  validation: { type: "array", items: { type: "string" } },
3146
3680
  deliverables: { type: "array", items: { type: "string" } },
3147
3681
  suggestedPaths: { type: "array", items: { type: "string" } },
3148
- suggestedLabels: { type: "array", items: { type: "string" } },
3149
- suggestedEffort: { type: "object", additionalProperties: false, required: ["default", "planner", "executor", "reviewer"], properties: { default: { type: "string" }, planner: { type: "string" }, executor: { type: "string" }, reviewer: { type: "string" } } }
3682
+ suggestedSkills: { type: "array", items: { type: "string" } },
3683
+ suggestedAgents: { type: "array", items: { type: "string" } },
3684
+ suggestedEffort: { type: "object", additionalProperties: false, required: ["default"], properties: { default: { type: "string" }, planner: { type: "string" }, executor: { type: "string" }, reviewer: { type: "string" } } }
3150
3685
  }
3151
3686
  });
3152
3687
  var PLAN_SCHEMA_OBJECT = JSON.parse(PLAN_JSON_SCHEMA);
@@ -3166,6 +3701,9 @@ var SETTING_ID_AGENT_COMMAND = "runtime.agentCommand";
3166
3701
  var SETTING_ID_DEFAULT_EFFORT = "runtime.defaultEffort";
3167
3702
  var SETTING_ID_DETECTED_PROVIDERS = "providers.detected";
3168
3703
  var SETTING_ID_WORKFLOW_CONFIG = "runtime.workflowConfig";
3704
+ var SETTING_ID_TEST_COMMAND = "runtime.testCommand";
3705
+ var SETTING_ID_MERGE_MODE = "runtime.mergeMode";
3706
+ var SETTING_ID_PR_BASE_BRANCH = "runtime.prBaseBranch";
3169
3707
  async function loadRuntimeSettings() {
3170
3708
  return loadPersistedSettings();
3171
3709
  }
@@ -3181,7 +3719,10 @@ var RUNTIME_CONFIG_SETTING_IDS = /* @__PURE__ */ new Set([
3181
3719
  SETTING_ID_MAX_CONCURRENT_BY_STATE,
3182
3720
  SETTING_ID_AGENT_PROVIDER,
3183
3721
  SETTING_ID_AGENT_COMMAND,
3184
- SETTING_ID_DEFAULT_EFFORT
3722
+ SETTING_ID_DEFAULT_EFFORT,
3723
+ SETTING_ID_TEST_COMMAND,
3724
+ SETTING_ID_MERGE_MODE,
3725
+ SETTING_ID_PR_BASE_BRANCH
3185
3726
  ]);
3186
3727
  var VALID_REASONING_EFFORTS = /* @__PURE__ */ new Set(["low", "medium", "high", "extra-high"]);
3187
3728
  function parseIntegerSetting(value) {
@@ -3240,7 +3781,10 @@ function buildRuntimeConfigSettings(config, source) {
3240
3781
  { id: SETTING_ID_MAX_CONCURRENT_BY_STATE, scope: "runtime", value: config.maxConcurrentByState, source, updatedAt },
3241
3782
  { id: SETTING_ID_AGENT_PROVIDER, scope: "runtime", value: config.agentProvider, source, updatedAt },
3242
3783
  { id: SETTING_ID_AGENT_COMMAND, scope: "runtime", value: config.agentCommand, source, updatedAt },
3243
- { id: SETTING_ID_DEFAULT_EFFORT, scope: "runtime", value: config.defaultEffort, source, updatedAt }
3784
+ { id: SETTING_ID_DEFAULT_EFFORT, scope: "runtime", value: config.defaultEffort, source, updatedAt },
3785
+ { id: SETTING_ID_TEST_COMMAND, scope: "runtime", value: config.testCommand ?? "", source, updatedAt },
3786
+ { id: SETTING_ID_MERGE_MODE, scope: "runtime", value: config.mergeMode ?? "local", source, updatedAt },
3787
+ { id: SETTING_ID_PR_BASE_BRANCH, scope: "runtime", value: config.prBaseBranch ?? "", source, updatedAt }
3244
3788
  ];
3245
3789
  }
3246
3790
  function applyPersistedSettings(config, settings) {
@@ -3324,10 +3868,28 @@ function applyPersistedSettings(config, settings) {
3324
3868
  agentCommandOverridden = true;
3325
3869
  break;
3326
3870
  }
3327
- case SETTING_ID_DEFAULT_EFFORT: {
3328
- const parsed = sanitizeDefaultEffort(setting.value);
3329
- if (parsed) {
3330
- nextConfig.defaultEffort = parsed;
3871
+ case SETTING_ID_DEFAULT_EFFORT: {
3872
+ const parsed = sanitizeDefaultEffort(setting.value);
3873
+ if (parsed) {
3874
+ nextConfig.defaultEffort = parsed;
3875
+ }
3876
+ break;
3877
+ }
3878
+ case SETTING_ID_TEST_COMMAND: {
3879
+ if (typeof setting.value === "string") {
3880
+ nextConfig.testCommand = setting.value.trim() || void 0;
3881
+ }
3882
+ break;
3883
+ }
3884
+ case SETTING_ID_MERGE_MODE: {
3885
+ if (setting.value === "local" || setting.value === "push-pr") {
3886
+ nextConfig.mergeMode = setting.value;
3887
+ }
3888
+ break;
3889
+ }
3890
+ case SETTING_ID_PR_BASE_BRANCH: {
3891
+ if (typeof setting.value === "string" && setting.value.trim()) {
3892
+ nextConfig.prBaseBranch = setting.value.trim();
3331
3893
  }
3332
3894
  break;
3333
3895
  }
@@ -3435,11 +3997,19 @@ async function persistWorkflowConfig(config) {
3435
3997
 
3436
3998
  // src/agents/planning/planning-prompts.ts
3437
3999
  async function buildPlanPrompt(title, description, fast = false, images) {
4000
+ const skills = discoverSkills(TARGET_ROOT);
4001
+ const agents = discoverAgents(TARGET_ROOT);
4002
+ const commands = discoverCommands(TARGET_ROOT);
4003
+ const hasCapabilities = skills.length > 0 || agents.length > 0 || commands.length > 0;
3438
4004
  return renderPrompt("issue-planner", {
3439
4005
  title,
3440
4006
  description: description || "(none provided)",
3441
4007
  fast,
3442
- images: images?.length ? images : void 0
4008
+ images: images?.length ? images : void 0,
4009
+ availableCapabilities: hasCapabilities,
4010
+ availableSkills: skills.map((s) => ({ name: s.name, description: s.description || "", whenToUse: s.whenToUse || "" })),
4011
+ availableAgents: agents.map((a) => ({ name: a.name, description: a.description || "", whenToUse: a.whenToUse || "", avoidIf: a.avoidIf || "" })),
4012
+ availableCommands: commands.map((c) => ({ name: c.name }))
3443
4013
  });
3444
4014
  }
3445
4015
  async function buildRefinePrompt(title, description, currentPlan, feedback) {
@@ -3458,11 +4028,11 @@ function getPlanCommand(provider, model, imagePaths) {
3458
4028
  }
3459
4029
  function savePlanDebugFiles(slug, prompt, output) {
3460
4030
  try {
3461
- const debugDir = join9(STATE_ROOT, "debug");
3462
- mkdirSync3(debugDir, { recursive: true });
4031
+ const debugDir = join10(STATE_ROOT, "debug");
4032
+ mkdirSync5(debugDir, { recursive: true });
3463
4033
  const ts = (/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-").slice(0, 19);
3464
- writeFileSync4(join9(debugDir, `plan-${slug}-${ts}-prompt.md`), prompt, "utf8");
3465
- if (output) writeFileSync4(join9(debugDir, `plan-${slug}-${ts}-output.txt`), output, "utf8");
4034
+ writeFileSync5(join10(debugDir, `plan-${slug}-${ts}-prompt.md`), prompt, "utf8");
4035
+ if (output) writeFileSync5(join10(debugDir, `plan-${slug}-${ts}-output.txt`), output, "utf8");
3466
4036
  } catch {
3467
4037
  }
3468
4038
  }
@@ -3620,9 +4190,9 @@ async function generatePlan(title, description, config, _workflowDefinition, opt
3620
4190
  const command = getPlanCommand(preferred, planStageModel, images);
3621
4191
  if (!command) throw new Error(`No command configured for provider ${preferred}.`);
3622
4192
  logger.debug({ provider: preferred, model: planStageModel, command: command.slice(0, 120) }, "[Planner] Provider selected for plan generation");
3623
- const tempDir = mkdtempSync(join10(tmpdir(), "fifony-plan-"));
3624
- const promptFile = join10(tempDir, "fifony-plan-prompt.md");
3625
- writeFileSync5(promptFile, `${prompt}
4193
+ const tempDir = mkdtempSync(join11(tmpdir(), "fifony-plan-"));
4194
+ const promptFile = join11(tempDir, "fifony-plan-prompt.md");
4195
+ writeFileSync6(promptFile, `${prompt}
3626
4196
  `, "utf8");
3627
4197
  let lastProgressPersist = 0;
3628
4198
  const PROGRESS_INTERVAL_MS = 2e3;
@@ -3699,8 +4269,8 @@ async function generatePlan(title, description, config, _workflowDefinition, opt
3699
4269
  }
3700
4270
 
3701
4271
  // src/agents/planning/plan-refiner.ts
3702
- import { writeFileSync as writeFileSync6 } from "fs";
3703
- import { join as join11 } from "path";
4272
+ import { writeFileSync as writeFileSync7 } from "fs";
4273
+ import { join as join12 } from "path";
3704
4274
  import { mkdtempSync as mkdtempSync2, rmSync as rmSync3 } from "fs";
3705
4275
  import { tmpdir as tmpdir2 } from "os";
3706
4276
  async function refinePlan(issue, feedback, config, _workflowDefinition) {
@@ -3713,9 +4283,9 @@ async function refinePlan(issue, feedback, config, _workflowDefinition) {
3713
4283
  {
3714
4284
  const command = getPlanCommand(preferred, planStageModel);
3715
4285
  if (!command) throw new Error(`No command configured for provider ${preferred}.`);
3716
- const tempDir = mkdtempSync2(join11(tmpdir2(), "fifony-refine-"));
3717
- const promptFile = join11(tempDir, "fifony-refine-prompt.md");
3718
- writeFileSync6(promptFile, `${prompt}
4286
+ const tempDir = mkdtempSync2(join12(tmpdir2(), "fifony-refine-"));
4287
+ const promptFile = join12(tempDir, "fifony-refine-prompt.md");
4288
+ writeFileSync7(promptFile, `${prompt}
3719
4289
  `, "utf8");
3720
4290
  const output = await runPlanningProcess({
3721
4291
  command,
@@ -3836,10 +4406,10 @@ function refinePlanInBackground(issue, feedback, config, _workflowDefinition, ca
3836
4406
 
3837
4407
  // src/agents/planning/issue-enhancer.ts
3838
4408
  import { env as env2 } from "process";
3839
- import { existsSync as existsSync8, mkdtempSync as mkdtempSync3, readFileSync as readFileSync5, rmSync as rmSync4, writeFileSync as writeFileSync7 } from "fs";
4409
+ import { existsSync as existsSync9, mkdtempSync as mkdtempSync3, readFileSync as readFileSync6, rmSync as rmSync4, writeFileSync as writeFileSync8 } from "fs";
3840
4410
  import { spawn as spawn2 } from "child_process";
3841
4411
  import { tmpdir as tmpdir3 } from "os";
3842
- import { join as join12 } from "path";
4412
+ import { join as join13 } from "path";
3843
4413
  function getProviderCommand(provider, config) {
3844
4414
  return resolveAgentCommand(provider, config.agentCommand || "", "", "");
3845
4415
  }
@@ -3908,22 +4478,22 @@ function parseCandidate(raw, expectedField) {
3908
4478
  return "";
3909
4479
  }
3910
4480
  function readProviderOutput(resultFile, fallback) {
3911
- if (existsSync8(resultFile)) {
4481
+ if (existsSync9(resultFile)) {
3912
4482
  try {
3913
- return readFileSync5(resultFile, "utf8").trim();
4483
+ return readFileSync6(resultFile, "utf8").trim();
3914
4484
  } catch {
3915
4485
  }
3916
4486
  }
3917
4487
  return fallback;
3918
4488
  }
3919
4489
  async function runProviderCommand(command, provider, prompt, title, description, field, timeoutMs, images) {
3920
- const tempDir = mkdtempSync3(join12(tmpdir3(), "fifony-enhance-"));
3921
- const promptFile = join12(tempDir, "fifony-enhance-prompt.md");
3922
- const issuePayloadFile = join12(tempDir, "fifony-issue.json");
3923
- const resultFile = join12(tempDir, "fifony-result.txt");
3924
- writeFileSync7(promptFile, `${prompt}
4490
+ const tempDir = mkdtempSync3(join13(tmpdir3(), "fifony-enhance-"));
4491
+ const promptFile = join13(tempDir, "fifony-enhance-prompt.md");
4492
+ const issuePayloadFile = join13(tempDir, "fifony-issue.json");
4493
+ const resultFile = join13(tempDir, "fifony-result.txt");
4494
+ writeFileSync8(promptFile, `${prompt}
3925
4495
  `, "utf8");
3926
- writeFileSync7(issuePayloadFile, JSON.stringify({ title, description, field }, null, 2), "utf8");
4496
+ writeFileSync8(issuePayloadFile, JSON.stringify({ title, description, field }, null, 2), "utf8");
3927
4497
  let effectiveCommand = command;
3928
4498
  if (provider === "codex" && images?.length) {
3929
4499
  const imageFlags = images.map((p) => `--image "${p}"`).join(" ");
@@ -4140,7 +4710,6 @@ function registerPlanRoutes(app, state) {
4140
4710
  applyUsage: (iss, usage) => applyPlanUsage(iss, usage),
4141
4711
  applySuggestions: (iss, plan) => {
4142
4712
  if (plan.suggestedPaths?.length) iss.paths = plan.suggestedPaths;
4143
- if (plan.suggestedLabels?.length) iss.labels = plan.suggestedLabels;
4144
4713
  if (plan.suggestedEffort) iss.effort = plan.suggestedEffort;
4145
4714
  }
4146
4715
  });
@@ -4322,7 +4891,7 @@ function registerAnalyticsRoutes(app) {
4322
4891
  try {
4323
4892
  const context2 = getApiRuntimeContextOrThrow();
4324
4893
  const doneIssues = context2.state.issues.filter(
4325
- (i) => (i.state === "Done" || i.state === "Merged") && i.completedAt
4894
+ (i) => (i.state === "Approved" || i.state === "Merged") && i.completedAt
4326
4895
  );
4327
4896
  const msToDay = (ms) => ms / (1e3 * 60 * 60 * 24);
4328
4897
  const avg = (arr) => arr.length ? arr.reduce((a, b) => a + b, 0) / arr.length : null;
@@ -4379,149 +4948,59 @@ function registerScanningRoutes(app, state) {
4379
4948
  });
4380
4949
  }
4381
4950
 
4382
- // src/agents/catalog.ts
4383
- import { existsSync as existsSync9, mkdirSync as mkdirSync4, readFileSync as readFileSync6, writeFileSync as writeFileSync8 } from "fs";
4384
- import { join as join13 } from "path";
4385
- function parseFrontmatter(content) {
4386
- const match = content.match(/^---\r?\n([\s\S]*?)\r?\n---/);
4387
- if (!match) return {};
4388
- const result = {};
4389
- for (const line of match[1].split("\n")) {
4390
- const idx = line.indexOf(":");
4391
- if (idx === -1) continue;
4392
- const key = line.slice(0, idx).trim();
4393
- const value = line.slice(idx + 1).trim().replace(/^["']|["']$/g, "");
4394
- if (key) result[key] = value;
4395
- }
4396
- return result;
4397
- }
4398
- function loadAgentCatalog() {
4399
- const entries = [];
4400
- try {
4401
- const repos = listReferenceRepositories();
4402
- for (const repo of repos) {
4403
- if (!repo.present || !repo.synced) continue;
4404
- const artifacts = collectArtifacts(repo.path, repo.id).filter((a) => a.kind === "agent");
4405
- for (const artifact of artifacts) {
4406
- try {
4407
- const content = readFileSync6(artifact.sourcePath, "utf8");
4408
- const fm = parseFrontmatter(content);
4409
- entries.push({
4410
- name: artifact.targetName,
4411
- displayName: fm.name || artifact.targetName,
4412
- description: fm.description || "",
4413
- emoji: fm.emoji || "\u{1F916}",
4414
- domains: fm.domains ? fm.domains.split(",").map((d) => d.trim()).filter(Boolean) : [],
4415
- source: repo.id,
4416
- content
4417
- });
4418
- } catch (err) {
4419
- logger.warn({ err, path: artifact.sourcePath }, "Failed to read agent file");
4420
- }
4421
- }
4422
- }
4423
- } catch (error) {
4424
- logger.error({ err: error }, "Failed to load agent catalog from repositories");
4425
- }
4426
- return entries;
4427
- }
4428
- function loadSkillCatalog() {
4429
- return [];
4430
- }
4431
- function filterByDomains(catalog, domains) {
4432
- const domainSet = new Set(domains.map((d) => d.toLowerCase().trim()));
4433
- if (domainSet.size === 0) return catalog;
4434
- const scored = catalog.map((entry) => {
4435
- const matchCount = entry.domains.filter((d) => domainSet.has(d.toLowerCase())).length;
4436
- return { entry, matchCount };
4437
- });
4438
- return scored.filter((item) => item.matchCount > 0).sort((a, b) => b.matchCount - a.matchCount).map((item) => item.entry);
4439
- }
4440
- function installAgents(targetRoot, agentNames, catalog) {
4441
- const result = { installed: [], skipped: [], errors: [] };
4442
- const catalogMap = new Map(catalog.map((entry) => [entry.name, entry]));
4443
- const agentsDir = join13(targetRoot, ".claude", "agents");
4444
- try {
4445
- mkdirSync4(agentsDir, { recursive: true });
4446
- } catch (error) {
4447
- logger.error({ err: error, path: agentsDir }, "Failed to create agents directory");
4448
- result.errors.push({ name: "_directory", error: `Failed to create ${agentsDir}` });
4449
- return result;
4450
- }
4451
- for (const name of agentNames) {
4452
- const entry = catalogMap.get(name);
4453
- if (!entry) {
4454
- result.errors.push({ name, error: "Agent not found in catalog" });
4455
- continue;
4456
- }
4457
- const filePath = join13(agentsDir, `${name}.md`);
4458
- if (existsSync9(filePath)) {
4459
- result.skipped.push(name);
4460
- continue;
4461
- }
4462
- try {
4463
- writeFileSync8(filePath, entry.content, "utf8");
4464
- result.installed.push(name);
4465
- logger.info({ agent: name, path: filePath }, "Agent installed");
4466
- } catch (error) {
4467
- result.errors.push({
4468
- name,
4469
- error: error instanceof Error ? error.message : String(error)
4470
- });
4471
- }
4472
- }
4473
- return result;
4474
- }
4475
- function installSkills(targetRoot, skillNames, catalog) {
4476
- const result = { installed: [], skipped: [], errors: [] };
4477
- const catalogMap = new Map(catalog.map((entry) => [entry.name, entry]));
4478
- const skillsDir = join13(targetRoot, ".claude", "skills");
4479
- try {
4480
- mkdirSync4(skillsDir, { recursive: true });
4481
- } catch (error) {
4482
- logger.error({ err: error, path: skillsDir }, "Failed to create skills directory");
4483
- result.errors.push({ name: "_directory", error: `Failed to create ${skillsDir}` });
4484
- return result;
4485
- }
4486
- for (const name of skillNames) {
4487
- const entry = catalogMap.get(name);
4488
- if (!entry) {
4489
- result.errors.push({ name, error: "Skill not found in catalog" });
4490
- continue;
4491
- }
4492
- const skillDir = join13(skillsDir, name);
4493
- const filePath = join13(skillDir, "SKILL.md");
4494
- if (existsSync9(filePath)) {
4495
- result.skipped.push(name);
4496
- continue;
4497
- }
4498
- try {
4499
- mkdirSync4(skillDir, { recursive: true });
4500
- if (entry.installType === "bundled" && entry.content) {
4501
- writeFileSync8(filePath, entry.content, "utf8");
4502
- } else {
4503
- const referenceContent = [
4504
- `# ${entry.displayName}`,
4505
- "",
4506
- entry.description,
4507
- "",
4508
- `**Source**: ${entry.source}`,
4509
- entry.url ? `**URL**: ${entry.url}` : "",
4510
- "",
4511
- `> This skill references an external resource. Install it from the source above.`
4512
- ].filter(Boolean).join("\n");
4513
- writeFileSync8(filePath, referenceContent, "utf8");
4514
- }
4515
- result.installed.push(name);
4516
- logger.info({ skill: name, path: filePath, type: entry.installType }, "Skill installed");
4517
- } catch (error) {
4518
- result.errors.push({
4519
- name,
4520
- error: error instanceof Error ? error.message : String(error)
4521
- });
4522
- }
4951
+ // src/agents/claude-md-manager.ts
4952
+ import { existsSync as existsSync10, readFileSync as readFileSync7, writeFileSync as writeFileSync9 } from "fs";
4953
+ import { join as join14 } from "path";
4954
+ var BLOCK_START = "<!-- FIFONY:START \u2014 managed by fifony, do not edit manually -->";
4955
+ var BLOCK_END = "<!-- FIFONY:END -->";
4956
+ var BLOCK_PATTERN = /<!-- FIFONY:START[^>]*-->[\s\S]*?<!-- FIFONY:END -->/;
4957
+ function buildManagedBlock(skills, agents, commands) {
4958
+ const lines = [
4959
+ BLOCK_START,
4960
+ "## Fifony \u2014 Installed Capabilities",
4961
+ "",
4962
+ "This workspace has fifony-managed agents and skills installed.",
4963
+ ""
4964
+ ];
4965
+ if (commands.length > 0) {
4966
+ lines.push(`**Commands**: ${commands.map((c) => `/${c.name}`).join(", ")}`);
4967
+ }
4968
+ if (skills.length > 0) {
4969
+ lines.push(`**Skills**: ${skills.map((s) => s.name).join(", ")}`);
4970
+ }
4971
+ if (agents.length > 0) {
4972
+ lines.push(`**Agents**: ${agents.map((a) => a.name).join(", ")}`);
4973
+ }
4974
+ lines.push("");
4975
+ lines.push("Use these capabilities when working on tasks. For details:");
4976
+ lines.push("- Skills: `.claude/skills/*/SKILL.md`");
4977
+ lines.push("- Agents: `.claude/agents/*.md`");
4978
+ lines.push("- Commands: `.claude/commands/*.md`");
4979
+ lines.push(BLOCK_END);
4980
+ return lines.join("\n");
4981
+ }
4982
+ function updateClaudeMdManagedBlock(targetRoot, skills, agents, commands) {
4983
+ if (skills.length === 0 && agents.length === 0 && commands.length === 0) return;
4984
+ const claudeMdPath = join14(targetRoot, "CLAUDE.md");
4985
+ const newBlock = buildManagedBlock(skills, agents, commands);
4986
+ let existing = "";
4987
+ if (existsSync10(claudeMdPath)) {
4988
+ existing = readFileSync7(claudeMdPath, "utf8");
4989
+ }
4990
+ let updated;
4991
+ if (BLOCK_PATTERN.test(existing)) {
4992
+ updated = existing.replace(BLOCK_PATTERN, newBlock);
4993
+ } else if (existing) {
4994
+ updated = `${existing.trimEnd()}
4995
+
4996
+ ${newBlock}
4997
+ `;
4998
+ } else {
4999
+ updated = `${newBlock}
5000
+ `;
4523
5001
  }
4524
- return result;
5002
+ if (updated === existing) return;
5003
+ writeFileSync9(claudeMdPath, updated, "utf8");
4525
5004
  }
4526
5005
 
4527
5006
  // src/routes/catalog.ts
@@ -4545,6 +5024,10 @@ function registerCatalogRoutes(app) {
4545
5024
  }
4546
5025
  const catalog = loadAgentCatalog();
4547
5026
  const result = installAgents(TARGET_ROOT, agentNames, catalog);
5027
+ try {
5028
+ updateClaudeMdManagedBlock(TARGET_ROOT, discoverSkills(TARGET_ROOT), discoverAgents(TARGET_ROOT), discoverCommands(TARGET_ROOT));
5029
+ } catch {
5030
+ }
4548
5031
  return c.json({ ok: true, ...result });
4549
5032
  } catch (error) {
4550
5033
  logger.error({ err: error }, "Failed to install agents");
@@ -4560,6 +5043,10 @@ function registerCatalogRoutes(app) {
4560
5043
  }
4561
5044
  const catalog = loadSkillCatalog();
4562
5045
  const result = installSkills(TARGET_ROOT, skillNames, catalog);
5046
+ try {
5047
+ updateClaudeMdManagedBlock(TARGET_ROOT, discoverSkills(TARGET_ROOT), discoverAgents(TARGET_ROOT), discoverCommands(TARGET_ROOT));
5048
+ } catch {
5049
+ }
4563
5050
  return c.json({ ok: true, ...result });
4564
5051
  } catch (error) {
4565
5052
  logger.error({ err: error }, "Failed to install skills");
@@ -4622,6 +5109,12 @@ function registerReferenceRepositoryRoutes(app) {
4622
5109
  dryRun: payload?.dryRun === true,
4623
5110
  importToGlobal: payload?.global === true
4624
5111
  });
5112
+ if (!payload?.dryRun) {
5113
+ try {
5114
+ updateClaudeMdManagedBlock(TARGET_ROOT, discoverSkills(TARGET_ROOT), discoverAgents(TARGET_ROOT), discoverCommands(TARGET_ROOT));
5115
+ } catch {
5116
+ }
5117
+ }
4625
5118
  return c.json({
4626
5119
  ok: true,
4627
5120
  ...summary
@@ -4636,35 +5129,34 @@ function registerReferenceRepositoryRoutes(app) {
4636
5129
  }
4637
5130
 
4638
5131
  // src/routes/misc.ts
4639
- import { execSync as execSync3 } from "child_process";
5132
+ import { execSync as execSync4 } from "child_process";
4640
5133
  import {
4641
5134
  appendFileSync,
4642
5135
  closeSync,
4643
- existsSync as existsSync10,
4644
- mkdirSync as mkdirSync5,
5136
+ existsSync as existsSync11,
5137
+ mkdirSync as mkdirSync6,
4645
5138
  openSync,
4646
- readFileSync as readFileSync7,
5139
+ readdirSync as readdirSync3,
5140
+ readFileSync as readFileSync8,
4647
5141
  readSync,
4648
5142
  statSync,
4649
- writeFileSync as writeFileSync9
5143
+ writeFileSync as writeFileSync10
4650
5144
  } from "fs";
4651
5145
  import { randomUUID as randomUUID2 } from "crypto";
4652
- import { extname as extname2, join as join14 } from "path";
5146
+ import { basename as basename3, extname as extname2, join as join15 } from "path";
4653
5147
  function registerMiscRoutes(app, state) {
4654
5148
  app.post("/api/issues/:id/push", async (c) => {
4655
5149
  const issueId = parseIssue(c);
4656
5150
  if (!issueId) return c.json({ ok: false, error: "Issue id is required." }, 400);
4657
5151
  const issue = findIssue(state, issueId);
4658
5152
  if (!issue) return c.json({ ok: false, error: "Issue not found." }, 404);
4659
- if (issue.state !== "Done") {
4660
- return c.json({ ok: false, error: `Issue ${issue.identifier} must be in Done state to push. Current state: ${issue.state}.` }, 409);
5153
+ if (!["Approved", "Reviewing", "PendingDecision"].includes(issue.state)) {
5154
+ return c.json({ ok: false, error: `Issue ${issue.identifier} must be in Approved, Reviewing, or PendingDecision state to push. Current state: ${issue.state}.` }, 409);
4661
5155
  }
4662
5156
  try {
4663
- const prUrl = pushWorktreeBranch(issue);
4664
- issue.mergedAt = (/* @__PURE__ */ new Date()).toISOString();
4665
- addEvent(state, issue.id, "merge", `Branch ${issue.branchName} pushed to origin. PR: ${prUrl}`);
4666
- await persistState(state);
4667
- return c.json({ ok: true, prUrl });
5157
+ const container = getContainer();
5158
+ const result = await pushWorkspaceCommand({ issue, state }, container);
5159
+ return c.json({ ok: true, prUrl: result.prUrl, ghAvailable: result.ghAvailable });
4668
5160
  } catch (error) {
4669
5161
  logger.error({ err: error }, `[API] Failed to push branch for ${issueId}`);
4670
5162
  return c.json({ ok: false, error: error instanceof Error ? error.message : String(error) }, 500);
@@ -4687,7 +5179,7 @@ function registerMiscRoutes(app, state) {
4687
5179
  const wp = issue.workspacePath;
4688
5180
  const liveLog = wp ? `${wp}/live-output.log` : null;
4689
5181
  let lastSize = 0;
4690
- if (liveLog && existsSync10(liveLog)) {
5182
+ if (liveLog && existsSync11(liveLog)) {
4691
5183
  try {
4692
5184
  const stat = statSync(liveLog);
4693
5185
  lastSize = stat.size;
@@ -4715,7 +5207,7 @@ function registerMiscRoutes(app, state) {
4715
5207
  return;
4716
5208
  }
4717
5209
  const logPath = currentIssue.workspacePath ? `${currentIssue.workspacePath}/live-output.log` : null;
4718
- if (logPath && existsSync10(logPath)) {
5210
+ if (logPath && existsSync11(logPath)) {
4719
5211
  try {
4720
5212
  const stat = statSync(logPath);
4721
5213
  if (stat.size > lastSize) {
@@ -4770,7 +5262,7 @@ function registerMiscRoutes(app, state) {
4770
5262
  const liveLog = wp ? `${wp}/live-output.log` : null;
4771
5263
  let logTail = "";
4772
5264
  let logSize = 0;
4773
- if (liveLog && existsSync10(liveLog)) {
5265
+ if (liveLog && existsSync11(liveLog)) {
4774
5266
  try {
4775
5267
  const stat = statSync(liveLog);
4776
5268
  logSize = stat.size;
@@ -4810,13 +5302,13 @@ function registerMiscRoutes(app, state) {
4810
5302
  const issue = findIssue(state, issueId);
4811
5303
  if (!issue) return c.json({ ok: false, error: "Issue not found." }, 404);
4812
5304
  const wp = issue.workspacePath;
4813
- if (!wp || !existsSync10(wp)) {
5305
+ if (!wp || !existsSync11(wp)) {
4814
5306
  return c.json({ ok: true, files: [], diff: "", message: "No workspace found." });
4815
5307
  }
4816
5308
  let raw = "";
4817
5309
  if (issue.branchName && issue.baseBranch) {
4818
5310
  try {
4819
- raw = execSync3(
5311
+ raw = execSync4(
4820
5312
  `git diff --no-color "${issue.baseBranch}"..."${issue.branchName}"`,
4821
5313
  { encoding: "utf8", maxBuffer: 4 * 1024 * 1024, timeout: 15e3, cwd: TARGET_ROOT, stdio: "pipe" }
4822
5314
  );
@@ -4824,11 +5316,11 @@ function registerMiscRoutes(app, state) {
4824
5316
  raw = err.stdout || "";
4825
5317
  }
4826
5318
  } else {
4827
- if (!existsSync10(SOURCE_ROOT)) {
5319
+ if (!existsSync11(SOURCE_ROOT)) {
4828
5320
  return c.json({ ok: true, files: [], diff: "", message: "Source root not found." });
4829
5321
  }
4830
5322
  try {
4831
- raw = execSync3(
5323
+ raw = execSync4(
4832
5324
  `git diff --no-index --no-color -- "${SOURCE_ROOT}" "${wp}"`,
4833
5325
  { encoding: "utf8", maxBuffer: 4 * 1024 * 1024, timeout: 15e3 }
4834
5326
  );
@@ -4878,7 +5370,7 @@ function registerMiscRoutes(app, state) {
4878
5370
  try {
4879
5371
  const isGit = (() => {
4880
5372
  try {
4881
- execSync3("git rev-parse --git-dir", { cwd: TARGET_ROOT, stdio: "pipe" });
5373
+ execSync4("git rev-parse --git-dir", { cwd: TARGET_ROOT, stdio: "pipe" });
4882
5374
  return true;
4883
5375
  } catch {
4884
5376
  return false;
@@ -4887,14 +5379,14 @@ function registerMiscRoutes(app, state) {
4887
5379
  if (!isGit) return c.json({ isGit: false, branch: null, hasCommits: false });
4888
5380
  const branch = (() => {
4889
5381
  try {
4890
- return execSync3("git rev-parse --abbrev-ref HEAD", { cwd: TARGET_ROOT, encoding: "utf8", stdio: "pipe" }).trim();
5382
+ return execSync4("git rev-parse --abbrev-ref HEAD", { cwd: TARGET_ROOT, encoding: "utf8", stdio: "pipe" }).trim();
4891
5383
  } catch {
4892
5384
  return null;
4893
5385
  }
4894
5386
  })();
4895
5387
  const hasCommits = (() => {
4896
5388
  try {
4897
- execSync3("git rev-parse HEAD", { cwd: TARGET_ROOT, stdio: "pipe" });
5389
+ execSync4("git rev-parse HEAD", { cwd: TARGET_ROOT, stdio: "pipe" });
4898
5390
  return true;
4899
5391
  } catch {
4900
5392
  return false;
@@ -4907,9 +5399,9 @@ function registerMiscRoutes(app, state) {
4907
5399
  });
4908
5400
  app.post("/api/git/init", async (c) => {
4909
5401
  try {
4910
- execSync3("git init", { cwd: TARGET_ROOT, stdio: "pipe" });
4911
- execSync3('git commit --allow-empty -m "Initial commit"', { cwd: TARGET_ROOT, stdio: "pipe" });
4912
- const branch = execSync3("git rev-parse --abbrev-ref HEAD", { cwd: TARGET_ROOT, encoding: "utf8", stdio: "pipe" }).trim();
5402
+ execSync4("git init", { cwd: TARGET_ROOT, stdio: "pipe" });
5403
+ execSync4('git commit --allow-empty -m "Initial commit"', { cwd: TARGET_ROOT, stdio: "pipe" });
5404
+ const branch = execSync4("git rev-parse --abbrev-ref HEAD", { cwd: TARGET_ROOT, encoding: "utf8", stdio: "pipe" }).trim();
4913
5405
  state.config.defaultBranch = branch;
4914
5406
  await persistState(state);
4915
5407
  return c.json({ ok: true, branch });
@@ -4923,7 +5415,7 @@ function registerMiscRoutes(app, state) {
4923
5415
  if (!branchName || !/^[a-zA-Z0-9/_.-]+$/.test(branchName)) {
4924
5416
  return c.json({ ok: false, error: "Invalid branch name." }, 400);
4925
5417
  }
4926
- execSync3(`git checkout -b "${branchName}"`, { cwd: TARGET_ROOT, stdio: "pipe" });
5418
+ execSync4(`git checkout -b "${branchName}"`, { cwd: TARGET_ROOT, stdio: "pipe" });
4927
5419
  state.config.defaultBranch = branchName;
4928
5420
  await persistState(state);
4929
5421
  return c.json({ ok: true, defaultBranch: branchName });
@@ -4931,6 +5423,26 @@ function registerMiscRoutes(app, state) {
4931
5423
  return c.json({ ok: false, error: error instanceof Error ? error.message : String(error) }, 500);
4932
5424
  }
4933
5425
  });
5426
+ app.post("/api/git/switch", async (c) => {
5427
+ try {
5428
+ const { branchName } = await c.req.json();
5429
+ if (!branchName || !/^[a-zA-Z0-9/_.-]+$/.test(branchName)) {
5430
+ return c.json({ ok: false, error: "Invalid branch name." }, 400);
5431
+ }
5432
+ let created = false;
5433
+ try {
5434
+ execSync4(`git checkout "${branchName}"`, { cwd: TARGET_ROOT, stdio: "pipe" });
5435
+ } catch {
5436
+ execSync4(`git checkout -b "${branchName}"`, { cwd: TARGET_ROOT, stdio: "pipe" });
5437
+ created = true;
5438
+ }
5439
+ state.config.defaultBranch = branchName;
5440
+ await persistState(state);
5441
+ return c.json({ ok: true, defaultBranch: branchName, created });
5442
+ } catch (error) {
5443
+ return c.json({ ok: false, error: error instanceof Error ? error.message : String(error) }, 500);
5444
+ }
5445
+ });
4934
5446
  app.get("/api/events/feed", async (c) => {
4935
5447
  const since = c.req.query("since");
4936
5448
  const issueId = c.req.query("issueId");
@@ -4944,11 +5456,11 @@ function registerMiscRoutes(app, state) {
4944
5456
  });
4945
5457
  app.get("/api/gitignore/status", async (c) => {
4946
5458
  try {
4947
- const gitignorePath = join14(TARGET_ROOT, ".gitignore");
4948
- if (!existsSync10(gitignorePath)) {
5459
+ const gitignorePath = join15(TARGET_ROOT, ".gitignore");
5460
+ if (!existsSync11(gitignorePath)) {
4949
5461
  return c.json({ exists: false, hasFifony: false });
4950
5462
  }
4951
- const content = readFileSync7(gitignorePath, "utf-8");
5463
+ const content = readFileSync8(gitignorePath, "utf-8");
4952
5464
  const lines = content.split("\n").map((l) => l.trim());
4953
5465
  const hasFifony = lines.some((l) => l === ".fifony" || l === ".fifony/" || l === "/.fifony" || l === "/.fifony/");
4954
5466
  return c.json({ exists: true, hasFifony });
@@ -4959,12 +5471,12 @@ function registerMiscRoutes(app, state) {
4959
5471
  });
4960
5472
  app.post("/api/gitignore/add", async (c) => {
4961
5473
  try {
4962
- const gitignorePath = join14(TARGET_ROOT, ".gitignore");
4963
- if (!existsSync10(gitignorePath)) {
4964
- writeFileSync9(gitignorePath, "# Fifony state directory\n.fifony/\n", "utf-8");
5474
+ const gitignorePath = join15(TARGET_ROOT, ".gitignore");
5475
+ if (!existsSync11(gitignorePath)) {
5476
+ writeFileSync10(gitignorePath, "# Fifony state directory\n.fifony/\n", "utf-8");
4965
5477
  return c.json({ ok: true, created: true });
4966
5478
  }
4967
- const content = readFileSync7(gitignorePath, "utf-8");
5479
+ const content = readFileSync8(gitignorePath, "utf-8");
4968
5480
  const lines = content.split("\n").map((l) => l.trim());
4969
5481
  const hasFifony = lines.some((l) => l === ".fifony" || l === ".fifony/" || l === "/.fifony" || l === "/.fifony/");
4970
5482
  if (hasFifony) {
@@ -4981,6 +5493,51 @@ function registerMiscRoutes(app, state) {
4981
5493
  return c.json({ ok: false, error: "Failed to update .gitignore" }, 500);
4982
5494
  }
4983
5495
  });
5496
+ app.get("/api/issues/:id/outputs", async (c) => {
5497
+ try {
5498
+ const issueId = parseIssue(c);
5499
+ if (!issueId) return c.json({ ok: false, error: "Issue id is required." }, 400);
5500
+ const issue = findIssue(state, issueId);
5501
+ if (!issue) return c.json({ ok: false, error: "Issue not found." }, 404);
5502
+ const wp = issue.workspacePath;
5503
+ if (!wp) return c.json({ ok: true, files: [] });
5504
+ const outputsDir = join15(wp, "outputs");
5505
+ if (!existsSync11(outputsDir)) return c.json({ ok: true, files: [] });
5506
+ const entries = readdirSync3(outputsDir).filter((f) => f.endsWith(".stdout.log")).map((f) => {
5507
+ try {
5508
+ const s = statSync(join15(outputsDir, f));
5509
+ return { name: f, size: s.size };
5510
+ } catch {
5511
+ return { name: f, size: 0 };
5512
+ }
5513
+ });
5514
+ return c.json({ ok: true, files: entries });
5515
+ } catch (error) {
5516
+ return c.json({ ok: false, error: error instanceof Error ? error.message : String(error) }, 500);
5517
+ }
5518
+ });
5519
+ app.get("/api/issues/:id/outputs/:filename", async (c) => {
5520
+ try {
5521
+ const issueId = parseIssue(c);
5522
+ if (!issueId) return c.json({ ok: false, error: "Issue id is required." }, 400);
5523
+ const issue = findIssue(state, issueId);
5524
+ if (!issue) return c.json({ ok: false, error: "Issue not found." }, 404);
5525
+ const filename = c.req.param?.("filename") ?? c.req.params?.filename ?? "";
5526
+ if (!filename) return c.json({ ok: false, error: "Filename is required." }, 400);
5527
+ const safeName = basename3(filename);
5528
+ if (safeName !== filename || !safeName.endsWith(".stdout.log")) {
5529
+ return c.json({ ok: false, error: "Invalid filename." }, 400);
5530
+ }
5531
+ const wp = issue.workspacePath;
5532
+ if (!wp) return c.json({ ok: false, error: "No workspace found." }, 404);
5533
+ const filePath = join15(wp, "outputs", safeName);
5534
+ if (!existsSync11(filePath)) return c.json({ ok: false, error: "Output file not found." }, 404);
5535
+ const content = readFileSync8(filePath, "utf8");
5536
+ return new Response(content, { headers: { "Content-Type": "text/plain; charset=utf-8" } });
5537
+ } catch (error) {
5538
+ return c.json({ ok: false, error: error instanceof Error ? error.message : String(error) }, 500);
5539
+ }
5540
+ });
4984
5541
  app.post("/api/attachments/upload", async (c) => {
4985
5542
  try {
4986
5543
  const payload = await c.req.json();
@@ -4988,15 +5545,15 @@ function registerMiscRoutes(app, state) {
4988
5545
  return c.json({ ok: false, error: "No files provided." }, 400);
4989
5546
  }
4990
5547
  const uploadId = randomUUID2();
4991
- const uploadDir = join14(ATTACHMENTS_ROOT, "temp", uploadId);
4992
- mkdirSync5(uploadDir, { recursive: true });
5548
+ const uploadDir = join15(ATTACHMENTS_ROOT, "temp", uploadId);
5549
+ mkdirSync6(uploadDir, { recursive: true });
4993
5550
  const paths = [];
4994
5551
  for (const file of payload.files) {
4995
5552
  if (typeof file.data !== "string" || !file.name) continue;
4996
5553
  const safeExt = extname2(file.name).replace(/[^a-z0-9.]/gi, "").slice(0, 10) || ".bin";
4997
5554
  const safeName = `${randomUUID2()}${safeExt}`;
4998
- const dest = join14(uploadDir, safeName);
4999
- writeFileSync9(dest, Buffer.from(file.data, "base64"));
5555
+ const dest = join15(uploadDir, safeName);
5556
+ writeFileSync10(dest, Buffer.from(file.data, "base64"));
5000
5557
  paths.push(dest);
5001
5558
  }
5002
5559
  return c.json({ ok: true, paths, uploadId });
@@ -5051,10 +5608,10 @@ async function startApiServer(state, port) {
5051
5608
  }
5052
5609
  setApiRuntimeContext(state);
5053
5610
  const serveTextFile = (filePath, contentType, cacheControl = "no-cache") => {
5054
- if (!existsSync11(filePath)) {
5611
+ if (!existsSync12(filePath)) {
5055
5612
  return new Response("Not found", { status: 404 });
5056
5613
  }
5057
- return new Response(readFileSync8(filePath), {
5614
+ return new Response(readFileSync9(filePath), {
5058
5615
  headers: {
5059
5616
  "content-type": contentType,
5060
5617
  "cache-control": cacheControl
@@ -5062,10 +5619,10 @@ async function startApiServer(state, port) {
5062
5619
  });
5063
5620
  };
5064
5621
  const serveAppShell = () => {
5065
- if (!existsSync11(FRONTEND_INDEX)) {
5622
+ if (!existsSync12(FRONTEND_INDEX)) {
5066
5623
  return new Response("Not found", { status: 404 });
5067
5624
  }
5068
- const html = readFileSync8(FRONTEND_INDEX, "utf8").replace('href="/assets/manifest.webmanifest"', 'href="/manifest.webmanifest"').replaceAll('href="/assets/icon.svg"', 'href="/icon.svg"');
5625
+ const html = readFileSync9(FRONTEND_INDEX, "utf8").replace('href="/assets/manifest.webmanifest"', 'href="/manifest.webmanifest"').replaceAll('href="/assets/icon.svg"', 'href="/icon.svg"');
5069
5626
  return new Response(html, {
5070
5627
  headers: {
5071
5628
  "content-type": "text/html; charset=utf-8",
@@ -5086,6 +5643,9 @@ async function startApiServer(state, port) {
5086
5643
  port,
5087
5644
  host: "0.0.0.0",
5088
5645
  versionPrefix: false,
5646
+ metrics: {
5647
+ logLevel: "info"
5648
+ },
5089
5649
  // HTTP + WebSocket on the same port via listeners
5090
5650
  listeners: [{
5091
5651
  bind: { host: "0.0.0.0", port },
@@ -5124,6 +5684,7 @@ async function startApiServer(state, port) {
5124
5684
  "GET /onboarding": () => serveAppShell(),
5125
5685
  "GET /kanban": () => serveAppShell(),
5126
5686
  "GET /issues": () => serveAppShell(),
5687
+ "GET /analytics": () => serveAppShell(),
5127
5688
  "GET /agents": () => serveAppShell(),
5128
5689
  "GET /settings": () => serveAppShell(),
5129
5690
  "GET /settings/general": () => serveAppShell(),
@@ -5433,7 +5994,6 @@ async function persistState(state) {
5433
5994
  broadcastToWebSocketClients({
5434
5995
  type: "state:update",
5435
5996
  metrics: state.metrics,
5436
- capabilities: computeCapabilityCounts(state.issues),
5437
5997
  issues: state.issues,
5438
5998
  events: state.events.slice(0, 50),
5439
5999
  updatedAt: state.updatedAt
@@ -5577,12 +6137,6 @@ async function closeStateStore() {
5577
6137
 
5578
6138
  // src/domains/project.ts
5579
6139
  var SETTING_ID_PROJECT_NAME = "system.projectName";
5580
- var LEGACY_PROJECT_SETTING_IDS = [
5581
- "runtime.projectName",
5582
- "ui.projectName",
5583
- "projectName",
5584
- "project.name"
5585
- ];
5586
6140
  function normalizeProjectName(value) {
5587
6141
  return typeof value === "string" ? value.trim().replace(/\s+/g, " ") : "";
5588
6142
  }
@@ -5592,14 +6146,7 @@ function detectProjectName(targetRoot) {
5592
6146
  return normalizeProjectName(basename4(normalizedPath));
5593
6147
  }
5594
6148
  function readSavedProjectName(settings) {
5595
- const settingIds = [SETTING_ID_PROJECT_NAME, ...LEGACY_PROJECT_SETTING_IDS];
5596
- for (const id of settingIds) {
5597
- const value = normalizeProjectName(settings.find((setting) => setting.id === id)?.value);
5598
- if (value) {
5599
- return value;
5600
- }
5601
- }
5602
- return "";
6149
+ return normalizeProjectName(settings.find((s) => s.id === SETTING_ID_PROJECT_NAME)?.value);
5603
6150
  }
5604
6151
  function buildQueueTitle(projectName) {
5605
6152
  const normalizedProjectName = normalizeProjectName(projectName);
@@ -5617,7 +6164,7 @@ function resolveProjectMetadata(settings, targetRoot) {
5617
6164
  };
5618
6165
  }
5619
6166
  function scanProjectFiles(targetRoot) {
5620
- const check = (rel) => existsSync12(join15(targetRoot, rel));
6167
+ const check = (rel) => existsSync13(join16(targetRoot, rel));
5621
6168
  const files = {
5622
6169
  claudeMd: check("CLAUDE.md"),
5623
6170
  claudeDir: check(".claude"),
@@ -5638,10 +6185,10 @@ function scanProjectFiles(targetRoot) {
5638
6185
  };
5639
6186
  const existingAgents = [];
5640
6187
  for (const agentDir of [".claude/agents", ".codex/agents"]) {
5641
- const fullPath = join15(targetRoot, agentDir);
5642
- if (!existsSync12(fullPath)) continue;
6188
+ const fullPath = join16(targetRoot, agentDir);
6189
+ if (!existsSync13(fullPath)) continue;
5643
6190
  try {
5644
- const entries = readdirSync3(fullPath);
6191
+ const entries = readdirSync4(fullPath);
5645
6192
  for (const entry of entries) {
5646
6193
  if (entry.endsWith(".md")) {
5647
6194
  existingAgents.push(basename4(entry, ".md"));
@@ -5652,13 +6199,13 @@ function scanProjectFiles(targetRoot) {
5652
6199
  }
5653
6200
  const existingSkills = [];
5654
6201
  for (const skillDir of [".claude/skills", ".codex/skills"]) {
5655
- const fullPath = join15(targetRoot, skillDir);
5656
- if (!existsSync12(fullPath)) continue;
6202
+ const fullPath = join16(targetRoot, skillDir);
6203
+ if (!existsSync13(fullPath)) continue;
5657
6204
  try {
5658
- const entries = readdirSync3(fullPath);
6205
+ const entries = readdirSync4(fullPath);
5659
6206
  for (const entry of entries) {
5660
- const skillFile = join15(fullPath, entry, "SKILL.md");
5661
- if (existsSync12(skillFile)) {
6207
+ const skillFile = join16(fullPath, entry, "SKILL.md");
6208
+ if (existsSync13(skillFile)) {
5662
6209
  existingSkills.push(entry);
5663
6210
  }
5664
6211
  }
@@ -5666,20 +6213,20 @@ function scanProjectFiles(targetRoot) {
5666
6213
  }
5667
6214
  }
5668
6215
  let readmeExcerpt = "";
5669
- const readmePath = join15(targetRoot, "README.md");
5670
- if (existsSync12(readmePath)) {
6216
+ const readmePath = join16(targetRoot, "README.md");
6217
+ if (existsSync13(readmePath)) {
5671
6218
  try {
5672
- const content = readFileSync9(readmePath, "utf8");
6219
+ const content = readFileSync10(readmePath, "utf8");
5673
6220
  readmeExcerpt = content.slice(0, 200).trim();
5674
6221
  } catch {
5675
6222
  }
5676
6223
  }
5677
6224
  let packageName = "";
5678
6225
  let packageDescription = "";
5679
- const pkgPath = join15(targetRoot, "package.json");
5680
- if (existsSync12(pkgPath)) {
6226
+ const pkgPath = join16(targetRoot, "package.json");
6227
+ if (existsSync13(pkgPath)) {
5681
6228
  try {
5682
- const pkg = JSON.parse(readFileSync9(pkgPath, "utf8"));
6229
+ const pkg = JSON.parse(readFileSync10(pkgPath, "utf8"));
5683
6230
  packageName = typeof pkg.name === "string" ? pkg.name : "";
5684
6231
  packageDescription = typeof pkg.description === "string" ? pkg.description : "";
5685
6232
  } catch {
@@ -5721,39 +6268,39 @@ function buildFallbackAnalysis(targetRoot) {
5721
6268
  let description = "";
5722
6269
  let readmeExcerpt = "";
5723
6270
  for (const readmeFile of ["README.md", "README.rst", "README.txt", "README"]) {
5724
- const p = join15(targetRoot, readmeFile);
5725
- if (existsSync12(p)) {
6271
+ const p = join16(targetRoot, readmeFile);
6272
+ if (existsSync13(p)) {
5726
6273
  try {
5727
- readmeExcerpt = readFileSync9(p, "utf8").slice(0, 300).trim();
6274
+ readmeExcerpt = readFileSync10(p, "utf8").slice(0, 300).trim();
5728
6275
  break;
5729
6276
  } catch {
5730
6277
  }
5731
6278
  }
5732
6279
  }
5733
- const pkgPath = join15(targetRoot, "package.json");
5734
- if (existsSync12(pkgPath)) {
6280
+ const pkgPath = join16(targetRoot, "package.json");
6281
+ if (existsSync13(pkgPath)) {
5735
6282
  try {
5736
- const pkg = JSON.parse(readFileSync9(pkgPath, "utf8"));
6283
+ const pkg = JSON.parse(readFileSync10(pkgPath, "utf8"));
5737
6284
  const name = typeof pkg.name === "string" ? pkg.name : "";
5738
6285
  const desc = typeof pkg.description === "string" ? pkg.description : "";
5739
6286
  if (desc) description = name ? `${name}: ${desc}` : desc;
5740
6287
  } catch {
5741
6288
  }
5742
6289
  }
5743
- const cargoPath = join15(targetRoot, "Cargo.toml");
5744
- if (!description && existsSync12(cargoPath)) {
6290
+ const cargoPath = join16(targetRoot, "Cargo.toml");
6291
+ if (!description && existsSync13(cargoPath)) {
5745
6292
  try {
5746
- const content = readFileSync9(cargoPath, "utf8");
6293
+ const content = readFileSync10(cargoPath, "utf8");
5747
6294
  const descMatch = content.match(/^description\s*=\s*"([^"]+)"/m);
5748
6295
  const nameMatch = content.match(/^name\s*=\s*"([^"]+)"/m);
5749
6296
  if (descMatch) description = nameMatch ? `${nameMatch[1]}: ${descMatch[1]}` : descMatch[1];
5750
6297
  } catch {
5751
6298
  }
5752
6299
  }
5753
- const pyprojectPath = join15(targetRoot, "pyproject.toml");
5754
- if (!description && existsSync12(pyprojectPath)) {
6300
+ const pyprojectPath = join16(targetRoot, "pyproject.toml");
6301
+ if (!description && existsSync13(pyprojectPath)) {
5755
6302
  try {
5756
- const content = readFileSync9(pyprojectPath, "utf8");
6303
+ const content = readFileSync10(pyprojectPath, "utf8");
5757
6304
  const descMatch = content.match(/^description\s*=\s*"([^"]+)"/m);
5758
6305
  const nameMatch = content.match(/^name\s*=\s*"([^"]+)"/m);
5759
6306
  if (descMatch) description = nameMatch ? `${nameMatch[1]}: ${descMatch[1]}` : descMatch[1];
@@ -5766,7 +6313,7 @@ function buildFallbackAnalysis(targetRoot) {
5766
6313
  let language = "unknown";
5767
6314
  const stack = [];
5768
6315
  for (const [file, signal] of Object.entries(BUILD_FILE_SIGNALS)) {
5769
- if (existsSync12(join15(targetRoot, file))) {
6316
+ if (existsSync13(join16(targetRoot, file))) {
5770
6317
  if (language === "unknown" && signal.language !== "unknown") {
5771
6318
  language = signal.language;
5772
6319
  }
@@ -5849,7 +6396,7 @@ function isBlockedProjectAnalysisResponse(analysis) {
5849
6396
  var ANALYSIS_CACHE_TTL_MS = 24 * 60 * 60 * 1e3;
5850
6397
  function computeProjectHash(targetRoot) {
5851
6398
  const buildFiles = Object.keys(BUILD_FILE_SIGNALS);
5852
- const found = buildFiles.filter((f) => existsSync12(join15(targetRoot, f))).sort();
6399
+ const found = buildFiles.filter((f) => existsSync13(join16(targetRoot, f))).sort();
5853
6400
  return createHash("sha256").update(found.join(",")).digest("hex").slice(0, 16);
5854
6401
  }
5855
6402
  async function loadCachedAnalysis(targetRoot) {
@@ -5901,10 +6448,10 @@ async function analyzeProjectWithCli(provider, targetRoot, options) {
5901
6448
  );
5902
6449
  return buildFallbackAnalysis(targetRoot);
5903
6450
  }
5904
- const tempDir = mkdtempSync4(join15(tmpdir4(), "fifony-scan-"));
5905
- const promptFile = join15(tempDir, "fifony-scan-prompt.txt");
6451
+ const tempDir = mkdtempSync4(join16(tmpdir4(), "fifony-scan-"));
6452
+ const promptFile = join16(tempDir, "fifony-scan-prompt.txt");
5906
6453
  const analysisPrompt = await renderPrompt("project-analysis");
5907
- writeFileSync10(promptFile, analysisPrompt, "utf8");
6454
+ writeFileSync11(promptFile, analysisPrompt, "utf8");
5908
6455
  const processEnv = {};
5909
6456
  for (const [key, value] of Object.entries(env3)) {
5910
6457
  if (typeof value === "string") processEnv[key] = value;
@@ -6022,6 +6569,12 @@ var DEFAULT_REFERENCE_REPOSITORIES = [
6022
6569
  name: "pbakaus/impeccable",
6023
6570
  url: "https://github.com/pbakaus/impeccable.git",
6024
6571
  description: "Frontend polish and impeccable-style quality workflows."
6572
+ },
6573
+ {
6574
+ id: "everything-claude-code",
6575
+ name: "affaan-m/everything-claude-code",
6576
+ url: "https://github.com/affaan-m/everything-claude-code.git",
6577
+ description: "28 specialized agents, 116 skills, and 59 commands \u2014 agent harness performance system."
6025
6578
  }
6026
6579
  ];
6027
6580
  var REPOSITORY_ROOT = resolve2(homedir3(), ".fifony", "repositories");
@@ -6046,7 +6599,7 @@ var REFERENCE_REPOSITORY_PARSERS = {
6046
6599
  impeccable: collectImpeccableArtifacts
6047
6600
  };
6048
6601
  function runGit(args, cwd) {
6049
- return execFileSync("git", args, {
6602
+ return execFileSync2("git", args, {
6050
6603
  cwd,
6051
6604
  encoding: "utf8",
6052
6605
  stdio: "pipe",
@@ -6073,7 +6626,7 @@ function uniqueSuffix(base, used) {
6073
6626
  }
6074
6627
  function collectDirectoryEntries(path) {
6075
6628
  try {
6076
- return readdirSync3(path, { withFileTypes: true });
6629
+ return readdirSync4(path, { withFileTypes: true });
6077
6630
  } catch {
6078
6631
  return [];
6079
6632
  }
@@ -6099,7 +6652,7 @@ function isMarkdownFile(value, expectedName) {
6099
6652
  function isReferenceFrontMatterFile(filePath) {
6100
6653
  let source;
6101
6654
  try {
6102
- source = readFileSync9(filePath, "utf8");
6655
+ source = readFileSync10(filePath, "utf8");
6103
6656
  } catch {
6104
6657
  return false;
6105
6658
  }
@@ -6121,10 +6674,10 @@ function collectAgentArtifacts(agentsDir, usedNames, out) {
6121
6674
  const parent = slugify(basename4(dirname2(agentsDir)));
6122
6675
  const entries = collectDirectoryEntries(agentsDir);
6123
6676
  for (const entry of entries) {
6124
- const itemPath = join15(agentsDir, entry.name);
6677
+ const itemPath = join16(agentsDir, entry.name);
6125
6678
  if (entry.isDirectory()) {
6126
- const nestedAgentSpec = join15(itemPath, "AGENT.md");
6127
- if (existsSync12(nestedAgentSpec)) {
6679
+ const nestedAgentSpec = join16(itemPath, "AGENT.md");
6680
+ if (existsSync13(nestedAgentSpec)) {
6128
6681
  const name2 = uniqueSuffix(`${parent}__${slugify(entry.name)}`, usedNames);
6129
6682
  out.push({ kind: "agent", sourcePath: nestedAgentSpec, targetName: name2 });
6130
6683
  }
@@ -6145,10 +6698,10 @@ function collectSkillArtifacts(skillsDir, usedNames, out) {
6145
6698
  const parent = slugify(basename4(dirname2(skillsDir)));
6146
6699
  const entries = collectDirectoryEntries(skillsDir);
6147
6700
  for (const entry of entries) {
6148
- const itemPath = join15(skillsDir, entry.name);
6701
+ const itemPath = join16(skillsDir, entry.name);
6149
6702
  if (entry.isDirectory()) {
6150
- const skillFile = join15(itemPath, "SKILL.md");
6151
- if (existsSync12(skillFile)) {
6703
+ const skillFile = join16(itemPath, "SKILL.md");
6704
+ if (existsSync13(skillFile)) {
6152
6705
  const name = uniqueSuffix(`${parent}__${slugify(entry.name)}`, usedNames);
6153
6706
  out.push({ kind: "skill", sourcePath: skillFile, targetName: name });
6154
6707
  }
@@ -6175,7 +6728,7 @@ function collectStandardArtifacts(repoPath) {
6175
6728
  for (const entry of entries) {
6176
6729
  if (!entry.isDirectory()) continue;
6177
6730
  if (SKIP_DIRS.has(entry.name)) continue;
6178
- const childPath = join15(state.path, entry.name);
6731
+ const childPath = join16(state.path, entry.name);
6179
6732
  if (entry.name === "agents") {
6180
6733
  collectAgentArtifacts(childPath, agentsUsed, artifacts);
6181
6734
  }
@@ -6203,13 +6756,13 @@ function collectAgencyArtifacts(repoPath) {
6203
6756
  if (SKIP_DIRS.has(entry.name) || AGENCY_AGENTS_EXCLUDED_DIRS.has(entry.name)) {
6204
6757
  continue;
6205
6758
  }
6206
- queue.push({ path: join15(state.path, entry.name), depth: state.depth + 1 });
6759
+ queue.push({ path: join16(state.path, entry.name), depth: state.depth + 1 });
6207
6760
  continue;
6208
6761
  }
6209
- if (!isMarkdownFile(entry.name, "readme.md") || !isReferenceFrontMatterFile(join15(state.path, entry.name))) {
6762
+ if (!isMarkdownFile(entry.name, "readme.md") || !isReferenceFrontMatterFile(join16(state.path, entry.name))) {
6210
6763
  continue;
6211
6764
  }
6212
- const itemPath = join15(state.path, entry.name);
6765
+ const itemPath = join16(state.path, entry.name);
6213
6766
  const targetName = uniqueSuffix(buildRelativeArtifactName(repoPath, itemPath), agentsUsed);
6214
6767
  artifacts.push({
6215
6768
  kind: "agent",
@@ -6223,13 +6776,13 @@ function collectAgencyArtifacts(repoPath) {
6223
6776
  function collectImpeccableArtifacts(repoPath) {
6224
6777
  const skillsUsed = /* @__PURE__ */ new Set();
6225
6778
  const artifacts = [];
6226
- const sourceSkills = join15(repoPath, "source", "skills");
6227
- if (existsSync12(sourceSkills)) {
6779
+ const sourceSkills = join16(repoPath, "source", "skills");
6780
+ if (existsSync13(sourceSkills)) {
6228
6781
  collectSkillArtifacts(sourceSkills, skillsUsed, artifacts);
6229
6782
  return artifacts;
6230
6783
  }
6231
- const claudeSkills = join15(repoPath, ".claude", "skills");
6232
- if (existsSync12(claudeSkills)) {
6784
+ const claudeSkills = join16(repoPath, ".claude", "skills");
6785
+ if (existsSync13(claudeSkills)) {
6233
6786
  collectSkillArtifacts(claudeSkills, skillsUsed, artifacts);
6234
6787
  }
6235
6788
  return artifacts;
@@ -6255,19 +6808,19 @@ function getReferenceRepositoriesRoot() {
6255
6808
  }
6256
6809
  function listReferenceRepositories() {
6257
6810
  return DEFAULT_REFERENCE_REPOSITORIES.map((repo) => {
6258
- const path = join15(REPOSITORY_ROOT, repo.id);
6811
+ const path = join16(REPOSITORY_ROOT, repo.id);
6259
6812
  const status = {
6260
6813
  id: repo.id,
6261
6814
  name: repo.name,
6262
6815
  url: repo.url,
6263
6816
  path,
6264
- present: existsSync12(path),
6817
+ present: existsSync13(path),
6265
6818
  synced: false
6266
6819
  };
6267
6820
  if (!status.present) {
6268
6821
  return status;
6269
6822
  }
6270
- if (!existsSync12(join15(path, ".git"))) {
6823
+ if (!existsSync13(join16(path, ".git"))) {
6271
6824
  status.error = "Path exists but is not a git repo";
6272
6825
  return status;
6273
6826
  }
@@ -6291,7 +6844,7 @@ function resolveReferenceRepository(query) {
6291
6844
  }
6292
6845
  function syncReferenceRepositories(repositoryId) {
6293
6846
  const root = REPOSITORY_ROOT;
6294
- mkdirSync6(root, { recursive: true });
6847
+ mkdirSync7(root, { recursive: true });
6295
6848
  const repos = repositoryId ? [resolveReferenceRepository(repositoryId)] : DEFAULT_REFERENCE_REPOSITORIES;
6296
6849
  const selected = repos.filter((repo) => Boolean(repo));
6297
6850
  if (repositoryId && selected.length === 0) {
@@ -6299,9 +6852,9 @@ function syncReferenceRepositories(repositoryId) {
6299
6852
  }
6300
6853
  const results = [];
6301
6854
  for (const repo of selected) {
6302
- const target = join15(root, repo.id);
6855
+ const target = join16(root, repo.id);
6303
6856
  const candidates = [repo.url, ...repo.fallbackUrls ?? []];
6304
- if (!existsSync12(target)) {
6857
+ if (!existsSync13(target)) {
6305
6858
  let cloneError;
6306
6859
  for (const candidate of candidates) {
6307
6860
  try {
@@ -6328,7 +6881,7 @@ function syncReferenceRepositories(repositoryId) {
6328
6881
  }
6329
6882
  continue;
6330
6883
  }
6331
- if (!existsSync12(join15(target, ".git"))) {
6884
+ if (!existsSync13(join16(target, ".git"))) {
6332
6885
  results.push({
6333
6886
  id: repo.id,
6334
6887
  path: target,
@@ -6363,14 +6916,14 @@ function importReferenceArtifacts(repositoryId, workspaceRoot, options) {
6363
6916
  if (!repository) {
6364
6917
  throw new Error(`Unknown reference repository: ${repositoryId}`);
6365
6918
  }
6366
- const localPath = join15(REPOSITORY_ROOT, repository.id);
6367
- if (!existsSync12(localPath)) {
6919
+ const localPath = join16(REPOSITORY_ROOT, repository.id);
6920
+ if (!existsSync13(localPath)) {
6368
6921
  throw new Error(`Repository not synced yet: ${repository.id}. Run 'fifony onboarding sync --repository ${repository.id}' first.`);
6369
6922
  }
6370
6923
  const basePath = resolve2(workspaceRoot);
6371
- const targetBase = options.importToGlobal ? join15(homedir3(), ".codex") : join15(basePath, ".codex");
6372
- const agentsDir = join15(targetBase, "agents");
6373
- const skillsDir = join15(targetBase, "skills");
6924
+ const targetBase = options.importToGlobal ? join16(homedir3(), ".codex") : join16(basePath, ".codex");
6925
+ const agentsDir = join16(targetBase, "agents");
6926
+ const skillsDir = join16(targetBase, "skills");
6374
6927
  const artifacts = collectArtifacts(localPath, repository.id);
6375
6928
  const filtered = options.kind === "all" ? artifacts : artifacts.filter((artifact) => artifact.kind === options.kind.slice(0, -1));
6376
6929
  const summary = {
@@ -6388,16 +6941,16 @@ function importReferenceArtifacts(repositoryId, workspaceRoot, options) {
6388
6941
  return summary;
6389
6942
  }
6390
6943
  if (!options.dryRun) {
6391
- mkdirSync6(targetBase, { recursive: true });
6392
- mkdirSync6(agentsDir, { recursive: true });
6393
- mkdirSync6(skillsDir, { recursive: true });
6944
+ mkdirSync7(targetBase, { recursive: true });
6945
+ mkdirSync7(agentsDir, { recursive: true });
6946
+ mkdirSync7(skillsDir, { recursive: true });
6394
6947
  }
6395
6948
  for (const artifact of filtered) {
6396
6949
  try {
6397
- const source = readFileSync9(artifact.sourcePath, "utf8");
6950
+ const source = readFileSync10(artifact.sourcePath, "utf8");
6398
6951
  if (artifact.kind === "agent") {
6399
- const target = join15(agentsDir, `${artifact.targetName}.md`);
6400
- if (!options.overwrite && existsSync12(target)) {
6952
+ const target = join16(agentsDir, `${artifact.targetName}.md`);
6953
+ if (!options.overwrite && existsSync13(target)) {
6401
6954
  summary.skippedAgents.push(artifact.targetName);
6402
6955
  continue;
6403
6956
  }
@@ -6405,12 +6958,12 @@ function importReferenceArtifacts(repositoryId, workspaceRoot, options) {
6405
6958
  summary.importedAgents.push(artifact.targetName);
6406
6959
  continue;
6407
6960
  }
6408
- writeFileSync10(target, source, "utf8");
6961
+ writeFileSync11(target, source, "utf8");
6409
6962
  summary.importedAgents.push(artifact.targetName);
6410
6963
  } else {
6411
- const targetDir = join15(skillsDir, artifact.targetName);
6412
- const target = join15(targetDir, "SKILL.md");
6413
- if (!options.overwrite && existsSync12(target)) {
6964
+ const targetDir = join16(skillsDir, artifact.targetName);
6965
+ const target = join16(targetDir, "SKILL.md");
6966
+ if (!options.overwrite && existsSync13(target)) {
6414
6967
  summary.skippedSkills.push(artifact.targetName);
6415
6968
  continue;
6416
6969
  }
@@ -6418,8 +6971,8 @@ function importReferenceArtifacts(repositoryId, workspaceRoot, options) {
6418
6971
  summary.importedSkills.push(artifact.targetName);
6419
6972
  continue;
6420
6973
  }
6421
- mkdirSync6(targetDir, { recursive: true });
6422
- writeFileSync10(target, source, "utf8");
6974
+ mkdirSync7(targetDir, { recursive: true });
6975
+ writeFileSync11(target, source, "utf8");
6423
6976
  summary.importedSkills.push(artifact.targetName);
6424
6977
  }
6425
6978
  } catch (error) {
@@ -6533,6 +7086,37 @@ function validateConfig(config) {
6533
7086
  }
6534
7087
 
6535
7088
  // src/domains/issues.ts
7089
+ function normalizeIssue(raw) {
7090
+ const id = toStringValue(raw.id, "");
7091
+ if (!id) return null;
7092
+ const createdAt = toStringValue(raw.createdAt, now());
7093
+ const updatedAt = toStringValue(raw.updatedAt, createdAt);
7094
+ const issue = {
7095
+ id,
7096
+ identifier: toStringValue(raw.identifier, id),
7097
+ title: toStringValue(raw.title, `Issue ${id}`),
7098
+ description: toStringValue(raw.description, ""),
7099
+ state: normalizeState(raw.state, raw.plan && typeof raw.plan === "object" ? "PendingApproval" : "Planning"),
7100
+ branchName: toStringValue(raw.branchName),
7101
+ url: toStringValue(raw.url),
7102
+ assigneeId: toStringValue(raw.assigneeId),
7103
+ labels: toStringArray(raw.labels),
7104
+ paths: toStringArray(raw.paths),
7105
+ blockedBy: toStringArray(raw.blockedBy),
7106
+ assignedToWorker: toBooleanValue(raw.assignedToWorker, true),
7107
+ createdAt,
7108
+ updatedAt,
7109
+ history: [],
7110
+ attempts: toNumberValue(raw.attempts, 0),
7111
+ maxAttempts: toNumberValue(raw.maxAttempts, 3),
7112
+ nextRetryAt: toStringValue(raw.nextRetryAt),
7113
+ planVersion: 0,
7114
+ executeAttempt: 0,
7115
+ reviewAttempt: 0,
7116
+ planHistory: []
7117
+ };
7118
+ return issue;
7119
+ }
6536
7120
  function nextLocalIssueId(issues) {
6537
7121
  const maxId = issues.reduce((current, issue) => {
6538
7122
  const match = issue.identifier.match(/^#(\d+)$/);
@@ -6550,13 +7134,12 @@ function createIssueFromPayload(payload, issues, defaultBranch) {
6550
7134
  const blockedBy = toStringArray(payload.blockedBy);
6551
7135
  const paths = toStringArray(payload.paths);
6552
7136
  const images = toStringArray(payload.images);
6553
- const initialState = parseIssueState(payload.state) ?? (payload.plan ? "Planned" : "Planning");
7137
+ const initialState = parseIssueState(payload.state) ?? (payload.plan ? "PendingApproval" : "Planning");
6554
7138
  const issue = {
6555
7139
  id,
6556
7140
  identifier,
6557
7141
  title: toStringValue(payload.title, `Issue ${identifier}`),
6558
7142
  description: toStringValue(payload.description, ""),
6559
- priority: clamp(toNumberValue(payload.priority, 1), 1, 10),
6560
7143
  state: initialState,
6561
7144
  branchName: toStringValue(payload.branchName),
6562
7145
  baseBranch: toStringValue(payload.baseBranch) || defaultBranch,
@@ -6564,10 +7147,6 @@ function createIssueFromPayload(payload, issues, defaultBranch) {
6564
7147
  assigneeId: toStringValue(payload.assigneeId),
6565
7148
  labels: toStringArray(payload.labels),
6566
7149
  paths,
6567
- inferredPaths: [],
6568
- capabilityCategory: "",
6569
- capabilityOverlays: [],
6570
- capabilityRationale: [],
6571
7150
  blockedBy,
6572
7151
  assignedToWorker: true,
6573
7152
  createdAt,
@@ -6589,21 +7168,10 @@ function createIssueFromPayload(payload, issues, defaultBranch) {
6589
7168
  if (issue.plan.suggestedPaths?.length && !issue.paths?.length) {
6590
7169
  issue.paths = issue.plan.suggestedPaths;
6591
7170
  }
6592
- if (issue.plan.suggestedLabels?.length && !issue.labels?.length) {
6593
- issue.labels = issue.plan.suggestedLabels;
6594
- }
6595
7171
  if (issue.plan.suggestedEffort && !issue.effort) {
6596
7172
  issue.effort = issue.plan.suggestedEffort;
6597
7173
  }
6598
7174
  }
6599
- applyCapabilityMetadata(issue, resolveTaskCapabilities({
6600
- id: issue.id,
6601
- identifier: issue.identifier,
6602
- title: issue.title,
6603
- description: issue.description,
6604
- labels: issue.labels,
6605
- paths: issue.paths
6606
- }, getCapabilityRoutingOptions()));
6607
7175
  return issue;
6608
7176
  }
6609
7177
  function dedupHistoryEntries(issues) {
@@ -6627,13 +7195,10 @@ function buildRuntimeState(previous, config, projectMetadata = resolveProjectMet
6627
7195
  identifier: toStringValue(existing.identifier, existing.id),
6628
7196
  title: toStringValue(existing.title, `Issue ${toStringValue(existing.identifier, existing.id)}`),
6629
7197
  description: toStringValue(existing.description, ""),
6630
- state: normalizeState(existing.state, existing.plan ? "Planned" : "Planning"),
7198
+ state: normalizeState(existing.state, existing.plan ? "PendingApproval" : "Planning"),
6631
7199
  paths: toStringArray(existing.paths),
6632
- inferredPaths: toStringArray(existing.inferredPaths),
6633
7200
  labels: toStringArray(existing.labels),
6634
- capabilityOverlays: toStringArray(existing.capabilityOverlays),
6635
- capabilityRationale: toStringArray(existing.capabilityRationale),
6636
- blockedBy: toStringArray(existing.blockedBy).length > 0 ? toStringArray(existing.blockedBy) : toStringArray(existing.blocked_by),
7201
+ blockedBy: toStringArray(existing.blockedBy),
6637
7202
  history: Array.isArray(existing.history) ? existing.history : [],
6638
7203
  attempts: clamp(toNumberValue(existing.attempts, 0), 0, config.maxAttemptsDefault),
6639
7204
  maxAttempts: clamp(toNumberValue(existing.maxAttempts, config.maxAttemptsDefault), 1, config.maxAttemptsDefault),
@@ -6706,6 +7271,14 @@ async function transitionIssue(issue, event, context2 = {}) {
6706
7271
  logger.debug({ issueId: issue.id, identifier: issue.identifier, from: issue.state, event, context: context2 }, "[State] Issue transition");
6707
7272
  await executeTransition(issue, event, { ...context2, issue });
6708
7273
  }
7274
+ function issueDependenciesResolved(issue, allIssues) {
7275
+ if (issue.blockedBy.length === 0) return true;
7276
+ const map = new Map(allIssues.map((entry) => [entry.id, entry]));
7277
+ return issue.blockedBy.every((dependencyId) => {
7278
+ const dep = map.get(dependencyId);
7279
+ return dep?.state === "Approved" || dep?.state === "Merged";
7280
+ });
7281
+ }
6709
7282
  function getNextRetryAt(issue, baseMs) {
6710
7283
  const nextAttempt = issue.attempts + 1;
6711
7284
  const nextDelay = withRetryBackoff(nextAttempt, baseMs);
@@ -6723,7 +7296,7 @@ async function handleStatePatch(state, issue, payload) {
6723
7296
  for (const event of path) {
6724
7297
  await transitionIssue(issue, event, { note: `Manual state update: ${nextState}`, reason: toStringValue(payload.reason) });
6725
7298
  }
6726
- if (nextState === "Planned") {
7299
+ if (nextState === "PendingApproval") {
6727
7300
  issue.nextRetryAt = void 0;
6728
7301
  issue.lastError = void 0;
6729
7302
  }
@@ -6733,6 +7306,34 @@ async function handleStatePatch(state, issue, payload) {
6733
7306
  addEvent(state, issue.id, "manual", `Manual state transition to ${nextState}`);
6734
7307
  }
6735
7308
 
7309
+ // src/commands/request-rework.command.ts
7310
+ async function requestReworkCommand(input, deps) {
7311
+ const { issue, reviewerFeedback, note } = input;
7312
+ if (issue.state !== "Reviewing" && issue.state !== "PendingDecision") {
7313
+ throw new Error(
7314
+ `requestReworkCommand requires Reviewing or PendingDecision state, got ${issue.state}.`
7315
+ );
7316
+ }
7317
+ issue.lastError = reviewerFeedback;
7318
+ issue.lastFailedPhase = "review";
7319
+ issue.attempts += 1;
7320
+ if (issue.state === "Reviewing") {
7321
+ await transitionIssueCommand(
7322
+ { issue, target: "PendingDecision", note: `Reviewer completed for ${issue.identifier}.` },
7323
+ deps
7324
+ );
7325
+ }
7326
+ await transitionIssueCommand(
7327
+ { issue, target: "Queued", note: note ?? `Reviewer requested rework for ${issue.identifier}.` },
7328
+ deps
7329
+ );
7330
+ deps.eventStore.addEvent(
7331
+ issue.id,
7332
+ "runner",
7333
+ `Issue ${issue.identifier} sent back for rework by reviewer.`
7334
+ );
7335
+ }
7336
+
6736
7337
  // src/agents/issue-runner.ts
6737
7338
  async function runPlanningJob(state, issue) {
6738
7339
  issue.planningStatus = "planning";
@@ -6741,8 +7342,8 @@ async function runPlanningJob(state, issue) {
6741
7342
  issue.updatedAt = now();
6742
7343
  markIssueDirty(issue.id);
6743
7344
  const safeId = idToSafePath(issue.id);
6744
- const workspaceDir = join16(WORKSPACE_ROOT, safeId);
6745
- mkdirSync7(workspaceDir, { recursive: true });
7345
+ const workspaceDir = join17(WORKSPACE_ROOT, safeId);
7346
+ mkdirSync8(workspaceDir, { recursive: true });
6746
7347
  addEvent(state, issue.id, "info", `Plan generation started for ${issue.identifier} (v${(issue.planVersion ?? 0) + 1}).`);
6747
7348
  try {
6748
7349
  const { plan, usage, prompt } = await generatePlan(
@@ -6756,7 +7357,6 @@ async function runPlanningJob(state, issue) {
6756
7357
  markIssuePlanDirty(issue.id);
6757
7358
  issue.planVersion = Math.max(issue.planVersion ?? 0, 1);
6758
7359
  if (plan.suggestedPaths?.length && !issue.paths?.length) issue.paths = plan.suggestedPaths;
6759
- if (plan.suggestedLabels?.length && !issue.labels?.length) issue.labels = plan.suggestedLabels;
6760
7360
  if (plan.suggestedEffort && !issue.effort) issue.effort = plan.suggestedEffort;
6761
7361
  if (usage.totalTokens > 0) {
6762
7362
  addTokenUsage(issue, {
@@ -6768,8 +7368,8 @@ async function runPlanningJob(state, issue) {
6768
7368
  }
6769
7369
  const pv = issue.planVersion;
6770
7370
  try {
6771
- writeFileSync11(join16(workspaceDir, `plan.v${pv}.json`), JSON.stringify(plan, null, 2), "utf8");
6772
- writeFileSync11(join16(workspaceDir, `plan.v${pv}.prompt.md`), prompt, "utf8");
7371
+ writeFileSync12(join17(workspaceDir, `plan.v${pv}.json`), JSON.stringify(plan, null, 2), "utf8");
7372
+ writeFileSync12(join17(workspaceDir, `plan.v${pv}.prompt.md`), prompt, "utf8");
6773
7373
  } catch (artifactErr) {
6774
7374
  logger.warn({ err: String(artifactErr) }, "[Agent] Failed to write versioned plan artifacts");
6775
7375
  }
@@ -6798,21 +7398,21 @@ async function handleReviewStage(state, issue, workspacePath, startTs, routedPro
6798
7398
  const reviewer = routedProviders.find((p) => p.role === "reviewer");
6799
7399
  if (!reviewer) {
6800
7400
  issue.mergedReason = "Auto-approved: no reviewer configured.";
6801
- await transitionIssueCommand({ issue, target: "Done", note: `No reviewer configured; auto-approved for ${issue.identifier}.` }, container);
7401
+ await transitionIssueCommand({ issue, target: "Approved", note: `No reviewer configured; auto-approved for ${issue.identifier}.` }, container);
6802
7402
  return;
6803
7403
  }
6804
7404
  addEvent(state, issue.id, "info", `Review provider: ${reviewer.role}:${reviewer.provider}${reviewer.model ? `/${reviewer.model}` : ""}${reviewer.profile ? `:${reviewer.profile}` : ""}.`);
6805
7405
  let diffSummary = "";
6806
7406
  try {
6807
7407
  if (issue.baseBranch && issue.branchName) {
6808
- const diffResult = execSync4(
7408
+ const diffResult = execSync5(
6809
7409
  `git diff --stat "${issue.baseBranch}"..."${issue.branchName}"`,
6810
7410
  { cwd: TARGET_ROOT, encoding: "utf8", maxBuffer: 512e3, timeout: 1e4 }
6811
7411
  );
6812
7412
  diffSummary = diffResult.trim();
6813
7413
  } else {
6814
7414
  const diffTarget = issue.worktreePath ?? workspacePath;
6815
- const diffResult = execSync4(
7415
+ const diffResult = execSync5(
6816
7416
  `git diff --no-index --stat -- "${SOURCE_ROOT}" "${diffTarget}" 2>/dev/null`,
6817
7417
  { encoding: "utf8", maxBuffer: 512e3, timeout: 1e4 }
6818
7418
  );
@@ -6821,10 +7421,10 @@ async function handleReviewStage(state, issue, workspacePath, startTs, routedPro
6821
7421
  } catch (err) {
6822
7422
  diffSummary = (err.stdout || "").trim();
6823
7423
  }
6824
- const compiled = await compileReview(issue, reviewer, workspacePath, diffSummary);
7424
+ const compiled = await compileReview(issue, reviewer, workspacePath, diffSummary, state.config);
6825
7425
  const effectiveReviewer = { ...reviewer, command: compiled.command || reviewer.command };
6826
- const reviewPromptFile = join16(workspacePath, "review-prompt.md");
6827
- writeFileSync11(reviewPromptFile, `${compiled.prompt}
7426
+ const reviewPromptFile = join17(workspacePath, "review-prompt.md");
7427
+ writeFileSync12(reviewPromptFile, `${compiled.prompt}
6828
7428
  `, "utf8");
6829
7429
  const reviewResult = await runAgentSession(state, issue, effectiveReviewer, 1, workspacePath, compiled.prompt, reviewPromptFile);
6830
7430
  issue.durationMs = (issue.durationMs ?? 0) + (Date.now() - startTs);
@@ -6835,27 +7435,40 @@ async function handleReviewStage(state, issue, workspacePath, startTs, routedPro
6835
7435
  try {
6836
7436
  const rpv = issue.planVersion ?? 1;
6837
7437
  const rra = issue.reviewAttempt ?? 1;
6838
- const vReviewPromptSrc = join16(workspacePath, "review-prompt.md");
6839
- const vReviewAuditSrc = join16(workspacePath, "execution-audit.json");
6840
- if (existsSync13(vReviewPromptSrc)) {
6841
- writeFileSync11(join16(workspacePath, `review.v${rpv}a${rra}.prompt.md`), readFileSync10(vReviewPromptSrc, "utf8"), "utf8");
7438
+ const vReviewPromptSrc = join17(workspacePath, "review-prompt.md");
7439
+ const vReviewAuditSrc = join17(workspacePath, "execution-audit.json");
7440
+ if (existsSync14(vReviewPromptSrc)) {
7441
+ writeFileSync12(join17(workspacePath, `review.v${rpv}a${rra}.prompt.md`), readFileSync11(vReviewPromptSrc, "utf8"), "utf8");
6842
7442
  }
6843
- if (existsSync13(vReviewAuditSrc)) {
6844
- writeFileSync11(join16(workspacePath, `review.v${rpv}a${rra}.audit.json`), readFileSync10(vReviewAuditSrc, "utf8"), "utf8");
7443
+ if (existsSync14(vReviewAuditSrc)) {
7444
+ writeFileSync12(join17(workspacePath, `review.v${rpv}a${rra}.audit.json`), readFileSync11(vReviewAuditSrc, "utf8"), "utf8");
6845
7445
  }
6846
7446
  } catch (vErr) {
6847
7447
  logger.warn({ err: String(vErr) }, "[Agent] Failed to write versioned review artifacts");
6848
7448
  }
6849
7449
  if (reviewResult.success) {
6850
7450
  issue.mergedReason = `Auto-approved by reviewer in ${reviewResult.turns} turn(s).`;
6851
- await transitionIssueCommand({ issue, target: "Reviewed", note: `Reviewer completed for ${issue.identifier}.` }, container);
6852
- await transitionIssueCommand({ issue, target: "Done", note: `Reviewer approved ${issue.identifier} in ${reviewResult.turns} turn(s).` }, container);
7451
+ await transitionIssueCommand({ issue, target: "PendingDecision", note: `Reviewer completed for ${issue.identifier}.` }, container);
7452
+ const validation = await runValidationGate(issue, state.config);
7453
+ if (validation) {
7454
+ issue.validationResult = validation;
7455
+ markIssueDirty(issue.id);
7456
+ if (!validation.passed) {
7457
+ addEvent(state, issue.id, "error", `Validation gate failed for ${issue.identifier}: ${validation.command}`);
7458
+ logger.warn({ issueId: issue.id, command: validation.command }, "[Agent] Validation gate failed \u2014 staying in Reviewed");
7459
+ return;
7460
+ }
7461
+ addEvent(state, issue.id, "info", `Validation gate passed for ${issue.identifier}.`);
7462
+ }
7463
+ await transitionIssueCommand({ issue, target: "Approved", note: `Reviewer approved ${issue.identifier} in ${reviewResult.turns} turn(s).` }, container);
6853
7464
  } else if (reviewResult.continueRequested) {
6854
- await transitionIssueCommand({ issue, target: "Reviewed", note: `Reviewer completed for ${issue.identifier}.` }, container);
6855
- await transitionIssueCommand({ issue, target: "Queued", note: `Reviewer requested rework for ${issue.identifier}.` }, container);
6856
- container.eventStore.addEvent(issue.id, "runner", `Issue ${issue.identifier} sent back for rework by reviewer.`);
7465
+ await requestReworkCommand(
7466
+ { issue, reviewerFeedback: reviewResult.output, note: `Reviewer requested rework for ${issue.identifier}.` },
7467
+ container
7468
+ );
6857
7469
  } else {
6858
7470
  issue.lastError = reviewResult.output;
7471
+ issue.lastFailedPhase = "review";
6859
7472
  issue.attempts += 1;
6860
7473
  if (issue.attempts >= issue.maxAttempts) {
6861
7474
  issue.cancelledReason = `Max attempts reached (${issue.attempts}/${issue.maxAttempts}): reviewer failed or blocked.`;
@@ -6873,12 +7486,8 @@ async function handleExecutionStage(state, issue, workspacePath, promptText, pro
6873
7486
  container.eventStore.addEvent(
6874
7487
  issue.id,
6875
7488
  "info",
6876
- `Capability routing selected ${routedProviders.map((p) => `${p.role}:${p.provider}${p.model ? `/${p.model}` : ""}${p.profile ? `:${p.profile}` : ""}${p.reasoningEffort ? ` [${p.reasoningEffort}]` : ""}`).join(", ")}.`
7489
+ `Agent providers: ${routedProviders.map((p) => `${p.role}:${p.provider}${p.model ? `/${p.model}` : ""}${p.reasoningEffort ? ` [${p.reasoningEffort}]` : ""}`).join(", ")}.`
6877
7490
  );
6878
- const routingSignals = describeRoutingSignals(issue, workspaceDerivedPaths);
6879
- if (routingSignals) {
6880
- container.eventStore.addEvent(issue.id, "info", `Capability routing signals: ${routingSignals}.`);
6881
- }
6882
7491
  const runResult = await runAgentPipeline(state, issue, workspacePath, promptText, promptFile, workflowConfig);
6883
7492
  issue.durationMs = Date.now() - startTs;
6884
7493
  issue.commandExitCode = runResult.code;
@@ -6898,13 +7507,13 @@ async function handleExecutionStage(state, issue, workspacePath, promptText, pro
6898
7507
  try {
6899
7508
  const epv = issue.planVersion ?? 1;
6900
7509
  const eea = issue.executeAttempt ?? 1;
6901
- const vExecPromptSrc = join16(workspacePath, "prompt.md");
6902
- const vExecAuditSrc = join16(workspacePath, "execution-audit.json");
6903
- if (existsSync13(vExecPromptSrc)) {
6904
- writeFileSync11(join16(workspacePath, `execute.v${epv}a${eea}.prompt.md`), readFileSync10(vExecPromptSrc, "utf8"), "utf8");
7510
+ const vExecPromptSrc = join17(workspacePath, "prompt.md");
7511
+ const vExecAuditSrc = join17(workspacePath, "execution-audit.json");
7512
+ if (existsSync14(vExecPromptSrc)) {
7513
+ writeFileSync12(join17(workspacePath, `execute.v${epv}a${eea}.prompt.md`), readFileSync11(vExecPromptSrc, "utf8"), "utf8");
6905
7514
  }
6906
- if (existsSync13(vExecAuditSrc)) {
6907
- writeFileSync11(join16(workspacePath, `execute.v${epv}a${eea}.audit.json`), readFileSync10(vExecAuditSrc, "utf8"), "utf8");
7515
+ if (existsSync14(vExecAuditSrc)) {
7516
+ writeFileSync12(join17(workspacePath, `execute.v${epv}a${eea}.audit.json`), readFileSync11(vExecAuditSrc, "utf8"), "utf8");
6908
7517
  }
6909
7518
  } catch (vErr) {
6910
7519
  logger.warn({ err: String(vErr) }, "[Agent] Failed to write versioned execute artifacts");
@@ -6921,6 +7530,7 @@ async function handleExecutionStage(state, issue, workspacePath, promptText, pro
6921
7530
  container.eventStore.addEvent(issue.id, "runner", `Issue ${issue.identifier} queued for next turn.`);
6922
7531
  } else {
6923
7532
  issue.lastError = runResult.output;
7533
+ issue.lastFailedPhase = "execute";
6924
7534
  issue.attempts += 1;
6925
7535
  if (issue.attempts >= issue.maxAttempts) {
6926
7536
  issue.commandExitCode = runResult.code;
@@ -6938,17 +7548,10 @@ async function runIssueOnce(state, issue, running) {
6938
7548
  const isResuming = issue.state === "Running";
6939
7549
  logger.info({ issueId: issue.id, identifier: issue.identifier, state: issue.state, isReviewing, isResuming, attempt: issue.attempts + 1, maxAttempts: issue.maxAttempts }, "[Agent] Starting issue execution");
6940
7550
  if (issue.state === "Planning") {
6941
- issue.startedAt = issue.startedAt ?? now();
6942
- runPlanningJob(state, issue).catch((err) => logger.error({ err, issueId: issue.id, identifier: issue.identifier }, "[Agent] Unexpected error in background planning job")).finally(() => {
6943
- state.metrics = computeMetrics(state.issues);
6944
- state.updatedAt = now();
6945
- persistState(state).catch(() => {
6946
- });
6947
- });
7551
+ logger.warn({ issueId: issue.id }, "[Agent] runIssueOnce called for Planning state \u2014 skipping (queue handles planning)");
6948
7552
  return;
6949
7553
  }
6950
7554
  running.add(issue.id);
6951
- state.metrics.activeWorkers += 1;
6952
7555
  issue.startedAt = issue.startedAt ?? now();
6953
7556
  let workflowConfig = null;
6954
7557
  try {
@@ -6973,20 +7576,10 @@ async function runIssueOnce(state, issue, running) {
6973
7576
  }
6974
7577
  try {
6975
7578
  const workspaceDerivedPaths = hydrateIssuePathsFromWorkspace(issue);
6976
- if ((issue.paths ?? []).length > 0) {
6977
- issue.inferredPaths = [.../* @__PURE__ */ new Set([...issue.inferredPaths ?? [], ...inferCapabilityPaths({
6978
- id: issue.id,
6979
- identifier: issue.identifier,
6980
- title: issue.title,
6981
- description: issue.description,
6982
- labels: issue.labels,
6983
- paths: issue.paths
6984
- })])];
6985
- }
6986
7579
  const { workspacePath, promptText, promptFile } = await prepareWorkspace(issue, state, state.config.defaultBranch);
6987
7580
  container.issueRepository.markDirty(issue.id);
6988
7581
  try {
6989
- const { getIssueStateResource: getIssueStateResource2 } = await import("./store-WN47MDT5.js");
7582
+ const { getIssueStateResource: getIssueStateResource2 } = await import("./store-AG6LLYJ7.js");
6990
7583
  const res = getIssueStateResource2();
6991
7584
  if (res) {
6992
7585
  await res.patch(issue.id, {
@@ -7008,6 +7601,7 @@ async function runIssueOnce(state, issue, running) {
7008
7601
  } catch (error) {
7009
7602
  issue.attempts += 1;
7010
7603
  issue.lastError = String(error);
7604
+ issue.lastFailedPhase = issue.lastFailedPhase ?? "execute";
7011
7605
  if (issue.attempts >= issue.maxAttempts) {
7012
7606
  issue.cancelledReason = `Max attempts reached (${issue.attempts}/${issue.maxAttempts}): unexpected failure \u2014 ${issue.lastError?.slice(0, 120) ?? "unknown error"}.`;
7013
7607
  await transitionIssueCommand({ issue, target: "Cancelled", note: `Issue failed unexpectedly: ${issue.lastError}` }, container);
@@ -7020,18 +7614,25 @@ async function runIssueOnce(state, issue, running) {
7020
7614
  logger.info({ issueId: issue.id, identifier: issue.identifier, finalState: issue.state, elapsedMs, attempts: issue.attempts }, "[Agent] Issue execution finished");
7021
7615
  issue.updatedAt = now();
7022
7616
  container.issueRepository.markDirty(issue.id);
7023
- state.metrics.activeWorkers = Math.max(state.metrics.activeWorkers - 1, 0);
7024
7617
  running.delete(issue.id);
7025
7618
  state.metrics = computeMetrics(state.issues);
7026
- state.metrics.activeWorkers = Math.max(state.metrics.activeWorkers, 0);
7027
7619
  state.updatedAt = now();
7028
7620
  await container.persistencePort.persistState(state);
7029
7621
  }
7030
7622
  }
7031
7623
 
7032
7624
  export {
7625
+ addTokenUsage,
7626
+ extractTokenUsage,
7627
+ tryParseJsonOutput,
7628
+ readAgentDirective,
7629
+ readAgentPid,
7630
+ isProcessAlive,
7033
7631
  isAgentStillRunning,
7034
7632
  cleanStalePidFile,
7633
+ loadAgentPipelineState,
7634
+ loadAgentPipelineSnapshotForIssue,
7635
+ loadAgentSessionSnapshotsForIssue,
7035
7636
  hydrate,
7036
7637
  recoverPlanningSession,
7037
7638
  loadRuntimeSettings,
@@ -7041,15 +7642,27 @@ export {
7041
7642
  createContainer,
7042
7643
  runPlanningJob,
7043
7644
  runIssueOnce,
7645
+ isShuttingDown,
7646
+ installGracefulShutdown,
7647
+ analyzeParallelizability,
7648
+ ensureNotStale,
7649
+ hasTerminalQueue,
7044
7650
  deriveConfig,
7045
7651
  applyWorkflowConfig,
7046
7652
  validateConfig,
7653
+ normalizeIssue,
7654
+ nextLocalIssueId,
7655
+ createIssueFromPayload,
7656
+ dedupHistoryEntries,
7047
7657
  buildRuntimeState,
7048
7658
  addEvent,
7049
- isShuttingDown,
7050
- installGracefulShutdown,
7051
- ensureNotStale,
7052
- hasTerminalQueue,
7659
+ transitionIssue,
7660
+ issueDependenciesResolved,
7661
+ getNextRetryAt,
7662
+ handleStatePatch,
7663
+ runAgentSession,
7664
+ runAgentPipeline,
7665
+ issueHasResumableSession,
7053
7666
  startApiServer,
7054
7667
  getStateDb,
7055
7668
  getIssueStateResource,
@@ -7078,4 +7691,4 @@ export {
7078
7691
  syncReferenceRepositories,
7079
7692
  importReferenceArtifacts
7080
7693
  };
7081
- //# sourceMappingURL=chunk-2F3Q2MAG.js.map
7694
+ //# sourceMappingURL=chunk-HSGUPFTV.js.map