fifony 0.1.27 → 0.1.29

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 (35) hide show
  1. package/README.md +51 -29
  2. package/app/dist/assets/{KeyboardShortcutsHelp-NmaeCZMn.js → KeyboardShortcutsHelp-BF5KYX3E.js} +1 -1
  3. package/app/dist/assets/OnboardingWizard-Cweg4Ch0.js +1 -0
  4. package/app/dist/assets/{analytics.lazy-BpH26eA2.js → analytics.lazy-BlFXDncv.js} +1 -1
  5. package/app/dist/assets/{createLucideIcon-BWC-guQt.js → createLucideIcon-DgMTp0yx.js} +1 -1
  6. package/app/dist/assets/index-CSquFPSf.js +45 -0
  7. package/app/dist/assets/{index-DntTEHv8.css → index-ZlyvZ7KI.css} +1 -1
  8. package/app/dist/assets/vendor-D-IqxHHu.js +9 -0
  9. package/app/dist/index.html +4 -4
  10. package/app/dist/service-worker.js +1 -1
  11. package/dist/agent/run-local.js +64 -144
  12. package/dist/agent-FPUYBJZD.js +74 -0
  13. package/dist/chunk-2G6SRDOC.js +847 -0
  14. package/dist/{chunk-G7W4NEOA.js → chunk-3FCJI2GK.js} +1232 -633
  15. package/dist/chunk-O5AEQXUV.js +311 -0
  16. package/dist/chunk-OONOOWNC.js +123 -0
  17. package/dist/chunk-VOQT7RVT.js +295 -0
  18. package/dist/{chunk-XN2QKKMY.js → chunk-XVF6GOVS.js} +456 -814
  19. package/dist/cli.js +6 -4
  20. package/dist/issue-runner-MRHO5ZAB.js +15 -0
  21. package/dist/{issue-state-machine-SKODQ6MG.js → issue-state-machine-V2KPUYPW.js} +5 -3
  22. package/dist/issues-3PUMY63N.js +40 -0
  23. package/dist/mcp/server.js +23 -121
  24. package/dist/queue-workers-EGHCDDLB.js +23 -0
  25. package/dist/scheduler-V4GMCBTE.js +21 -0
  26. package/dist/{store-366NGWR4.js → store-RVKQ6UEY.js} +7 -5
  27. package/dist/workspace-KEHFITYR.js +52 -0
  28. package/package.json +6 -6
  29. package/app/dist/assets/OnboardingWizard-CwW6b_X4.js +0 -1
  30. package/app/dist/assets/index-D6jtlB7h.js +0 -43
  31. package/app/dist/assets/vendor-BTlTWMUF.js +0 -9
  32. package/dist/chunk-AMOGDOM7.js +0 -796
  33. package/dist/chunk-MT3S55TM.js +0 -91
  34. package/dist/issue-runner-MTAIYNVN.js +0 -13
  35. package/dist/queue-workers-Q3IWRFLI.js +0 -20
@@ -1,36 +1,13 @@
1
1
  import {
2
- areQueueWorkersActive,
3
- enqueueForExecution,
4
- enqueueForPlanning,
5
- enqueueForReview
6
- } from "./chunk-MT3S55TM.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,69 @@ 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-2G6SRDOC.js";
25
+ import {
26
+ ADAPTERS,
27
+ assertIssueHasGitWorktree,
28
+ buildExecutionPayload,
29
+ buildFullPlanPrompt,
30
+ buildProviderBasePrompt,
31
+ buildRetryContext,
32
+ buildTurnPrompt,
33
+ cleanWorkspace,
34
+ computeDiffStats,
35
+ detectAvailableProviders,
36
+ discoverModels,
37
+ ensureGitRepoReadyForWorktrees,
38
+ ensureWorktreeCommitted,
39
+ getEffectiveAgentProviders,
40
+ getGitRepoStatus,
41
+ getProviderDefaultCommand,
42
+ hydrateIssuePathsFromWorkspace,
43
+ initializeGitRepoForWorktrees,
41
44
  mergeWorkspace,
42
45
  normalizeAgentProvider,
43
46
  parseDiffStats,
44
47
  prepareWorkspace,
45
- pushWorktreeBranch,
46
48
  readCodexConfig,
47
49
  resolveAgentCommand,
48
50
  runCommandWithTimeout,
49
- runHook,
50
- setFsmEventEmitter,
51
- setIssueResourceStateApi,
52
- setIssueStateMachinePlugin,
53
- snapshotAndClearDirtyEventIds,
54
- snapshotAndClearDirtyIssueIds,
55
- snapshotAndClearDirtyIssuePlanIds
56
- } from "./chunk-XN2QKKMY.js";
51
+ runHook
52
+ } from "./chunk-XVF6GOVS.js";
53
+ import {
54
+ appendFileTail,
55
+ clamp,
56
+ debugBoot,
57
+ extractJsonObjects,
58
+ fail,
59
+ idToSafePath,
60
+ isoWeek,
61
+ normalizeState,
62
+ now,
63
+ parseEnvNumber,
64
+ parseIntArg,
65
+ parseIssueState,
66
+ parsePositiveIntEnv,
67
+ renderPrompt,
68
+ repairTruncatedJson,
69
+ toBooleanValue,
70
+ toNumberValue,
71
+ toStringArray,
72
+ toStringValue,
73
+ withRetryBackoff
74
+ } from "./chunk-O5AEQXUV.js";
75
+ import {
76
+ enqueue
77
+ } from "./chunk-VOQT7RVT.js";
78
+ import {
79
+ logger
80
+ } from "./chunk-DVU3CXWA.js";
57
81
  import {
58
82
  ALLOWED_STATES,
59
83
  ATTACHMENTS_ROOT,
@@ -81,42 +105,18 @@ import {
81
105
  STATE_ROOT,
82
106
  TARGET_ROOT,
83
107
  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";
108
+ WORKSPACE_ROOT
109
+ } from "./chunk-OONOOWNC.js";
110
110
 
111
111
  // src/agents/issue-runner.ts
112
112
  import {
113
- existsSync as existsSync13,
114
- mkdirSync as mkdirSync7,
115
- readFileSync as readFileSync10,
116
- writeFileSync as writeFileSync11
113
+ existsSync as existsSync14,
114
+ mkdirSync as mkdirSync8,
115
+ readFileSync as readFileSync11,
116
+ writeFileSync as writeFileSync12
117
117
  } from "fs";
118
- import { join as join16 } from "path";
119
- import { execSync as execSync4 } from "child_process";
118
+ import { join as join17 } from "path";
119
+ import { execSync as execSync5 } from "child_process";
120
120
 
121
121
  // src/domains/tokens.ts
122
122
  var EMPTY = { inputTokens: 0, outputTokens: 0, totalTokens: 0 };
@@ -290,19 +290,19 @@ function getAnalytics(topN = 20) {
290
290
  }
291
291
 
292
292
  // src/domains/project.ts
293
- import { execFileSync, spawn as spawn3 } from "child_process";
293
+ import { execFileSync as execFileSync2, spawn as spawn3 } from "child_process";
294
294
  import { createHash } from "crypto";
295
295
  import {
296
- existsSync as existsSync12,
297
- mkdirSync as mkdirSync6,
296
+ existsSync as existsSync13,
297
+ mkdirSync as mkdirSync7,
298
298
  mkdtempSync as mkdtempSync4,
299
- readdirSync as readdirSync3,
300
- readFileSync as readFileSync9,
299
+ readdirSync as readdirSync4,
300
+ readFileSync as readFileSync10,
301
301
  rmSync as rmSync5,
302
- writeFileSync as writeFileSync10
302
+ writeFileSync as writeFileSync11
303
303
  } from "fs";
304
304
  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";
305
+ import { basename as basename4, dirname as dirname2, join as join16, relative as relativePath, resolve as resolve2 } from "path";
306
306
  import { env as env3 } from "process";
307
307
 
308
308
  // src/persistence/plugins/api-runtime-context.ts
@@ -322,8 +322,8 @@ function getApiRuntimeContextOrThrow() {
322
322
 
323
323
  // src/persistence/plugins/api-server.ts
324
324
  import {
325
- existsSync as existsSync11,
326
- readFileSync as readFileSync8
325
+ existsSync as existsSync12,
326
+ readFileSync as readFileSync9
327
327
  } from "fs";
328
328
 
329
329
  // src/persistence/resources/runtime-state.resource.ts
@@ -417,6 +417,31 @@ function extractTokenUsage(output, jsonObj) {
417
417
  };
418
418
  }
419
419
  }
420
+ const stats = jsonObj.stats;
421
+ const geminiModels = stats?.models ?? null;
422
+ if (geminiModels && typeof geminiModels === "object") {
423
+ let totalInput = 0, totalOutput = 0, primaryModel = "", maxTokens = 0;
424
+ for (const [model, data] of Object.entries(geminiModels)) {
425
+ const tokens = data?.tokens;
426
+ if (!tokens) continue;
427
+ const inp = Number(tokens.input || 0) + Number(tokens.cached || 0);
428
+ const out = Number(tokens.candidates || 0);
429
+ totalInput += inp;
430
+ totalOutput += out;
431
+ if (inp + out > maxTokens) {
432
+ maxTokens = inp + out;
433
+ primaryModel = model;
434
+ }
435
+ }
436
+ if (totalInput > 0 || totalOutput > 0) {
437
+ return {
438
+ inputTokens: totalInput,
439
+ outputTokens: totalOutput,
440
+ totalTokens: totalInput + totalOutput,
441
+ model: primaryModel || void 0
442
+ };
443
+ }
444
+ }
420
445
  const usage = jsonObj.usage;
421
446
  if (usage && typeof usage === "object") {
422
447
  const inp = Number(usage.input_tokens) || 0;
@@ -466,6 +491,15 @@ function tryParseJsonOutput(output) {
466
491
  }
467
492
  }
468
493
  if (obj.status) return obj;
494
+ if (typeof obj.response === "string") {
495
+ try {
496
+ const inner = JSON.parse(obj.response);
497
+ if (inner && typeof inner === "object" && !Array.isArray(inner)) {
498
+ return inner;
499
+ }
500
+ } catch {
501
+ }
502
+ }
469
503
  }
470
504
  } catch {
471
505
  }
@@ -764,24 +798,25 @@ async function loadAgentSessionSnapshotsForIssue(issue, providers, pipeline, _wo
764
798
 
765
799
  // src/agents/agent-pipeline.ts
766
800
  import {
767
- writeFileSync as writeFileSync2
801
+ mkdirSync as mkdirSync2,
802
+ writeFileSync as writeFileSync3
768
803
  } from "fs";
769
- import { join as join5 } from "path";
804
+ import { join as join6 } from "path";
770
805
 
771
806
  // src/agents/adapters/index.ts
772
807
  import { writeFileSync } from "fs";
773
808
  import { join as join3 } from "path";
774
- async function compileExecution(issue, provider, config, workspacePath, skillContext) {
809
+ async function compileExecution(issue, provider, config, workspacePath, skillContext, capabilitiesManifest) {
775
810
  const plan = issue.plan;
776
811
  if (!plan?.steps?.length) return null;
777
812
  const adapter = ADAPTERS[provider.provider];
778
813
  if (!adapter) return null;
779
814
  const payload = buildExecutionPayload(issue, provider, plan, workspacePath);
780
- const compiled = await adapter.compile(issue, provider, plan, config, workspacePath, skillContext);
815
+ const compiled = await adapter.compile(issue, provider, plan, config, workspacePath, skillContext, capabilitiesManifest);
781
816
  compiled.payload = payload;
782
817
  return compiled;
783
818
  }
784
- async function compileReview(issue, reviewer, workspacePath, diffSummary) {
819
+ async function compileReview(issue, reviewer, workspacePath, diffSummary, config) {
785
820
  const plan = issue.plan;
786
821
  const prompt = await renderPrompt("compile-review", {
787
822
  issueIdentifier: issue.identifier,
@@ -794,7 +829,7 @@ async function compileReview(issue, reviewer, workspacePath, diffSummary) {
794
829
  diffSummary
795
830
  });
796
831
  const adapter = ADAPTERS[reviewer.provider];
797
- const command = adapter ? adapter.buildReviewCommand(reviewer) : reviewer.command;
832
+ const command = adapter ? adapter.buildReviewCommand(reviewer, config) : reviewer.command;
798
833
  return { prompt, command };
799
834
  }
800
835
  function buildExecutionAudit(provider, compiled, issue, durationMs, result) {
@@ -863,32 +898,186 @@ function persistExecutionAudit(workspacePath, audit) {
863
898
  }
864
899
 
865
900
  // src/agents/skills.ts
866
- import { existsSync as existsSync3, readdirSync, readFileSync as readFileSync3 } from "fs";
901
+ import { existsSync as existsSync4, readdirSync, readFileSync as readFileSync4 } from "fs";
867
902
  import { homedir } from "os";
868
- import { join as join4, resolve } from "path";
903
+ import { join as join5, resolve } from "path";
904
+
905
+ // src/agents/catalog.ts
906
+ import { existsSync as existsSync3, mkdirSync, readFileSync as readFileSync3, writeFileSync as writeFileSync2 } from "fs";
907
+ import { join as join4 } from "path";
908
+ function parseFrontmatter(content) {
909
+ const match = content.match(/^---\r?\n([\s\S]*?)\r?\n---/);
910
+ if (!match) return {};
911
+ const result = {};
912
+ for (const line of match[1].split("\n")) {
913
+ const idx = line.indexOf(":");
914
+ if (idx === -1) continue;
915
+ const key = line.slice(0, idx).trim();
916
+ const value = line.slice(idx + 1).trim().replace(/^["']|["']$/g, "");
917
+ if (key) result[key] = value;
918
+ }
919
+ return result;
920
+ }
921
+ function loadAgentCatalog() {
922
+ const entries = [];
923
+ try {
924
+ const repos = listReferenceRepositories();
925
+ for (const repo of repos) {
926
+ if (!repo.present || !repo.synced) continue;
927
+ const artifacts = collectArtifacts(repo.path, repo.id).filter((a) => a.kind === "agent");
928
+ for (const artifact of artifacts) {
929
+ try {
930
+ const content = readFileSync3(artifact.sourcePath, "utf8");
931
+ const fm = parseFrontmatter(content);
932
+ entries.push({
933
+ name: artifact.targetName,
934
+ displayName: fm.name || artifact.targetName,
935
+ description: fm.description || "",
936
+ emoji: fm.emoji || "\u{1F916}",
937
+ domains: fm.domains ? fm.domains.split(",").map((d) => d.trim()).filter(Boolean) : [],
938
+ source: repo.id,
939
+ content
940
+ });
941
+ } catch (err) {
942
+ logger.warn({ err, path: artifact.sourcePath }, "Failed to read agent file");
943
+ }
944
+ }
945
+ }
946
+ } catch (error) {
947
+ logger.error({ err: error }, "Failed to load agent catalog from repositories");
948
+ }
949
+ return entries;
950
+ }
951
+ function loadSkillCatalog() {
952
+ return [];
953
+ }
954
+ function filterByDomains(catalog, domains) {
955
+ const domainSet = new Set(domains.map((d) => d.toLowerCase().trim()));
956
+ if (domainSet.size === 0) return catalog;
957
+ const scored = catalog.map((entry) => {
958
+ const matchCount = entry.domains.filter((d) => domainSet.has(d.toLowerCase())).length;
959
+ return { entry, matchCount };
960
+ });
961
+ return scored.filter((item) => item.matchCount > 0).sort((a, b) => b.matchCount - a.matchCount).map((item) => item.entry);
962
+ }
963
+ function installAgents(targetRoot, agentNames, catalog) {
964
+ const result = { installed: [], skipped: [], errors: [] };
965
+ const catalogMap = new Map(catalog.map((entry) => [entry.name, entry]));
966
+ const agentsDir = join4(targetRoot, ".claude", "agents");
967
+ try {
968
+ mkdirSync(agentsDir, { recursive: true });
969
+ } catch (error) {
970
+ logger.error({ err: error, path: agentsDir }, "Failed to create agents directory");
971
+ result.errors.push({ name: "_directory", error: `Failed to create ${agentsDir}` });
972
+ return result;
973
+ }
974
+ for (const name of agentNames) {
975
+ const entry = catalogMap.get(name);
976
+ if (!entry) {
977
+ result.errors.push({ name, error: "Agent not found in catalog" });
978
+ continue;
979
+ }
980
+ const filePath = join4(agentsDir, `${name}.md`);
981
+ if (existsSync3(filePath)) {
982
+ result.skipped.push(name);
983
+ continue;
984
+ }
985
+ try {
986
+ writeFileSync2(filePath, entry.content, "utf8");
987
+ result.installed.push(name);
988
+ logger.info({ agent: name, path: filePath }, "Agent installed");
989
+ } catch (error) {
990
+ result.errors.push({
991
+ name,
992
+ error: error instanceof Error ? error.message : String(error)
993
+ });
994
+ }
995
+ }
996
+ return result;
997
+ }
998
+ function installSkills(targetRoot, skillNames, catalog) {
999
+ const result = { installed: [], skipped: [], errors: [] };
1000
+ const catalogMap = new Map(catalog.map((entry) => [entry.name, entry]));
1001
+ const skillsDir = join4(targetRoot, ".claude", "skills");
1002
+ try {
1003
+ mkdirSync(skillsDir, { recursive: true });
1004
+ } catch (error) {
1005
+ logger.error({ err: error, path: skillsDir }, "Failed to create skills directory");
1006
+ result.errors.push({ name: "_directory", error: `Failed to create ${skillsDir}` });
1007
+ return result;
1008
+ }
1009
+ for (const name of skillNames) {
1010
+ const entry = catalogMap.get(name);
1011
+ if (!entry) {
1012
+ result.errors.push({ name, error: "Skill not found in catalog" });
1013
+ continue;
1014
+ }
1015
+ const skillDir = join4(skillsDir, name);
1016
+ const filePath = join4(skillDir, "SKILL.md");
1017
+ if (existsSync3(filePath)) {
1018
+ result.skipped.push(name);
1019
+ continue;
1020
+ }
1021
+ try {
1022
+ mkdirSync(skillDir, { recursive: true });
1023
+ if (entry.installType === "bundled" && entry.content) {
1024
+ writeFileSync2(filePath, entry.content, "utf8");
1025
+ } else {
1026
+ const referenceContent = [
1027
+ `# ${entry.displayName}`,
1028
+ "",
1029
+ entry.description,
1030
+ "",
1031
+ `**Source**: ${entry.source}`,
1032
+ entry.url ? `**URL**: ${entry.url}` : "",
1033
+ "",
1034
+ `> This skill references an external resource. Install it from the source above.`
1035
+ ].filter(Boolean).join("\n");
1036
+ writeFileSync2(filePath, referenceContent, "utf8");
1037
+ }
1038
+ result.installed.push(name);
1039
+ logger.info({ skill: name, path: filePath, type: entry.installType }, "Skill installed");
1040
+ } catch (error) {
1041
+ result.errors.push({
1042
+ name,
1043
+ error: error instanceof Error ? error.message : String(error)
1044
+ });
1045
+ }
1046
+ }
1047
+ return result;
1048
+ }
1049
+
1050
+ // src/agents/skills.ts
869
1051
  function discoverSkills(workspacePath) {
870
1052
  const home = homedir();
871
- const codePath = existsSync3(join4(workspacePath, "worktree")) ? join4(workspacePath, "worktree") : workspacePath;
1053
+ const codePath = existsSync4(join5(workspacePath, "worktree")) ? join5(workspacePath, "worktree") : workspacePath;
872
1054
  const searchPaths = [
873
1055
  resolve(codePath, ".codex", "skills"),
874
1056
  resolve(codePath, ".claude", "skills"),
875
- join4(home, ".codex", "skills"),
876
- join4(home, ".claude", "skills")
1057
+ join5(home, ".codex", "skills"),
1058
+ join5(home, ".claude", "skills")
877
1059
  ];
878
1060
  const seen = /* @__PURE__ */ new Set();
879
1061
  const skills = [];
880
1062
  for (const basePath of searchPaths) {
881
- if (!existsSync3(basePath)) continue;
1063
+ if (!existsSync4(basePath)) continue;
882
1064
  for (const entry of readdirSync(basePath, { withFileTypes: true })) {
883
1065
  if (!entry.isDirectory()) continue;
884
1066
  if (seen.has(entry.name)) continue;
885
- const skillFile = join4(basePath, entry.name, "SKILL.md");
886
- if (!existsSync3(skillFile)) continue;
1067
+ const skillFile = join5(basePath, entry.name, "SKILL.md");
1068
+ if (!existsSync4(skillFile)) continue;
887
1069
  try {
888
- const content = readFileSync3(skillFile, "utf8").trim();
1070
+ const content = readFileSync4(skillFile, "utf8").trim();
889
1071
  if (content) {
890
1072
  seen.add(entry.name);
891
- skills.push({ name: entry.name, content });
1073
+ const fm = parseFrontmatter(content);
1074
+ skills.push({
1075
+ name: entry.name,
1076
+ content,
1077
+ description: fm.description || void 0,
1078
+ whenToUse: fm.when_to_use || fm.whenToUse || void 0,
1079
+ avoidIf: fm.avoid_if || fm.avoidIf || void 0
1080
+ });
892
1081
  }
893
1082
  } catch {
894
1083
  }
@@ -906,8 +1095,141 @@ ${skill.content}`
906
1095
 
907
1096
  ${sections.join("\n\n")}`;
908
1097
  }
1098
+ function extractFirstLine(content) {
1099
+ for (const line of content.split("\n")) {
1100
+ const trimmed = line.replace(/^#+\s*/, "").trim();
1101
+ if (trimmed && !trimmed.startsWith("---")) return trimmed;
1102
+ }
1103
+ return "";
1104
+ }
1105
+ function discoverAgents(workspacePath) {
1106
+ const home = homedir();
1107
+ const codePath = existsSync4(join5(workspacePath, "worktree")) ? join5(workspacePath, "worktree") : workspacePath;
1108
+ const searchPaths = [
1109
+ resolve(codePath, ".claude", "agents"),
1110
+ resolve(codePath, ".codex", "agents"),
1111
+ join5(home, ".claude", "agents"),
1112
+ join5(home, ".codex", "agents")
1113
+ ];
1114
+ const seen = /* @__PURE__ */ new Set();
1115
+ const agents = [];
1116
+ for (const basePath of searchPaths) {
1117
+ if (!existsSync4(basePath)) continue;
1118
+ for (const entry of readdirSync(basePath, { withFileTypes: true })) {
1119
+ if (!entry.isFile() || !entry.name.endsWith(".md")) continue;
1120
+ const name = entry.name.replace(/\.md$/, "");
1121
+ if (seen.has(name)) continue;
1122
+ try {
1123
+ const content = readFileSync4(join5(basePath, entry.name), "utf8").trim();
1124
+ if (content) {
1125
+ seen.add(name);
1126
+ const fm = parseFrontmatter(content);
1127
+ agents.push({
1128
+ name,
1129
+ description: fm.description || extractFirstLine(content),
1130
+ whenToUse: fm.when_to_use || fm.whenToUse || void 0,
1131
+ avoidIf: fm.avoid_if || fm.avoidIf || void 0
1132
+ });
1133
+ }
1134
+ } catch {
1135
+ }
1136
+ }
1137
+ }
1138
+ return agents;
1139
+ }
1140
+ function discoverCommands(workspacePath) {
1141
+ const home = homedir();
1142
+ const codePath = existsSync4(join5(workspacePath, "worktree")) ? join5(workspacePath, "worktree") : workspacePath;
1143
+ const searchPaths = [
1144
+ resolve(codePath, ".claude", "commands"),
1145
+ resolve(codePath, ".codex", "commands"),
1146
+ join5(home, ".claude", "commands"),
1147
+ join5(home, ".codex", "commands")
1148
+ ];
1149
+ const seen = /* @__PURE__ */ new Set();
1150
+ const commands = [];
1151
+ for (const basePath of searchPaths) {
1152
+ if (!existsSync4(basePath)) continue;
1153
+ for (const entry of readdirSync(basePath, { withFileTypes: true })) {
1154
+ if (!entry.isFile() || !entry.name.endsWith(".md")) continue;
1155
+ const name = entry.name.replace(/\.md$/, "");
1156
+ if (seen.has(name)) continue;
1157
+ try {
1158
+ const content = readFileSync4(join5(basePath, entry.name), "utf8").trim();
1159
+ if (content) {
1160
+ seen.add(name);
1161
+ commands.push({ name, description: extractFirstLine(content) });
1162
+ }
1163
+ } catch {
1164
+ }
1165
+ }
1166
+ }
1167
+ return commands;
1168
+ }
1169
+ var MAX_CAPABILITIES_ITEMS = 40;
1170
+ function buildCapabilitiesManifest(skills, agents, commands) {
1171
+ if (skills.length === 0 && agents.length === 0 && commands.length === 0) return "";
1172
+ const sections = ["## Your Capabilities"];
1173
+ let itemCount = 0;
1174
+ if (commands.length > 0) {
1175
+ sections.push("");
1176
+ sections.push("### Slash Commands");
1177
+ sections.push("You have these commands available. Invoke with `/command-name`:");
1178
+ const show = commands.slice(0, MAX_CAPABILITIES_ITEMS);
1179
+ for (const cmd of show) {
1180
+ sections.push(`- \`/${cmd.name}\`${cmd.description ? ` \u2014 ${cmd.description}` : ""}`);
1181
+ itemCount++;
1182
+ }
1183
+ if (commands.length > show.length) {
1184
+ sections.push(`- ...and ${commands.length - show.length} more available`);
1185
+ }
1186
+ }
1187
+ if (skills.length > 0) {
1188
+ const remaining = Math.max(5, MAX_CAPABILITIES_ITEMS - itemCount);
1189
+ sections.push("");
1190
+ sections.push("### Skills");
1191
+ sections.push("Specialized procedures available for this workspace:");
1192
+ const show = skills.slice(0, remaining);
1193
+ for (const skill of show) {
1194
+ let line = `- **${skill.name}**`;
1195
+ if (skill.description) line += ` \u2014 ${skill.description}`;
1196
+ if (skill.whenToUse) line += ` (Use when: ${skill.whenToUse})`;
1197
+ sections.push(line);
1198
+ itemCount++;
1199
+ }
1200
+ if (skills.length > show.length) {
1201
+ sections.push(`- ...and ${skills.length - show.length} more available`);
1202
+ }
1203
+ }
1204
+ if (agents.length > 0) {
1205
+ const remaining = Math.max(5, MAX_CAPABILITIES_ITEMS - itemCount);
1206
+ sections.push("");
1207
+ sections.push("### Subagents");
1208
+ sections.push("You can delegate to these specialist agents via the Agent tool:");
1209
+ const show = agents.slice(0, remaining);
1210
+ for (const agent of show) {
1211
+ let line = `- **${agent.name}**`;
1212
+ if (agent.description) line += ` \u2014 ${agent.description}`;
1213
+ if (agent.whenToUse) line += ` (Use when: ${agent.whenToUse})`;
1214
+ if (agent.avoidIf) line += ` (Avoid if: ${agent.avoidIf})`;
1215
+ sections.push(line);
1216
+ }
1217
+ if (agents.length > show.length) {
1218
+ sections.push(`- ...and ${agents.length - show.length} more available`);
1219
+ }
1220
+ }
1221
+ sections.push("");
1222
+ sections.push("When a task matches a capability above, USE IT instead of doing everything manually.");
1223
+ return sections.join("\n");
1224
+ }
909
1225
 
910
1226
  // src/agents/agent-pipeline.ts
1227
+ function resolveOutputFileName(role, planVersion, attempt, turn) {
1228
+ if (role === "planner") {
1229
+ return `plan.v${planVersion}.t${turn}.stdout.log`;
1230
+ }
1231
+ return `${role === "reviewer" ? "review" : "execute"}.v${planVersion}a${attempt}.t${turn}.stdout.log`;
1232
+ }
911
1233
  async function runAgentSession(state, issue, provider, cycle, workspacePath, basePromptText, basePromptFile) {
912
1234
  const maxTurns = clamp(state.config.maxTurns, 1, 16);
913
1235
  const attempt = issue.attempts + 1;
@@ -919,7 +1241,7 @@ async function runAgentSession(state, issue, provider, cycle, workspacePath, bas
919
1241
  let nextPrompt = session.nextPrompt;
920
1242
  let lastCode = session.lastCode;
921
1243
  let lastOutput = session.lastOutput;
922
- const resultFile = join5(workspacePath, `result-${provider.role}-${provider.provider}.json`);
1244
+ const resultFile = join6(workspacePath, `result-${provider.role}-${provider.provider}.json`);
923
1245
  if (session.status === "done" && session.turns.length > 0) {
924
1246
  logger.debug({ issueId: issue.id, identifier: issue.identifier, provider: provider.provider, role: provider.role }, "[Agent] Session already completed, returning cached result");
925
1247
  return { success: true, blocked: false, continueRequested: false, code: session.lastCode, output: session.lastOutput, turns: session.turns.length };
@@ -936,14 +1258,23 @@ Agent requested additional turns beyond configured limit (${maxTurns}).`;
936
1258
  const compactedOutput = previousOutput.length > maxOutputChars ? `[...${previousOutput.length - maxOutputChars} chars truncated...]
937
1259
  ${previousOutput.slice(-maxOutputChars)}` : previousOutput;
938
1260
  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}
1261
+ const turnPromptFile = turnIndex === 1 ? basePromptFile : join6(workspacePath, `turn-${String(turnIndex).padStart(2, "0")}.md`);
1262
+ if (turnIndex > 1) writeFileSync3(turnPromptFile, `${turnPrompt}
941
1263
  `, "utf8");
942
1264
  session.status = "running";
943
1265
  session.lastPrompt = turnPrompt;
944
1266
  session.lastPromptFile = turnPromptFile;
945
1267
  session.maxTurns = maxTurns;
946
1268
  await persistAgentSessionState(sessionKey, issue, provider, cycle, session);
1269
+ const outputsDir = join6(workspacePath, "outputs");
1270
+ mkdirSync2(outputsDir, { recursive: true });
1271
+ const outputFileName = resolveOutputFileName(
1272
+ provider.role,
1273
+ issue.planVersion ?? 1,
1274
+ provider.role === "planner" ? 0 : issue.executeAttempt ?? 1,
1275
+ turnIndex
1276
+ );
1277
+ const outputFilePath = join6(outputsDir, outputFileName);
947
1278
  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
1279
  const turnStartedAt = now();
949
1280
  const turnEnv = {
@@ -967,7 +1298,7 @@ ${previousOutput.slice(-maxOutputChars)}` : previousOutput;
967
1298
  await runHook(state.config.beforeRunHook, workspacePath, issue, "before_run", turnEnv);
968
1299
  }
969
1300
  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);
1301
+ const turnResult = await runCommandWithTimeout(provider.command, workspacePath, issue, state.config, turnPrompt, turnPromptFile, turnEnv, outputFilePath);
971
1302
  if (state.config.afterRunHook) {
972
1303
  await runHook(state.config.afterRunHook, workspacePath, issue, "after_run", {
973
1304
  ...turnEnv,
@@ -1056,10 +1387,13 @@ async function runAgentPipeline(state, issue, workspacePath, basePromptText, bas
1056
1387
  const executorIndex = providers.findIndex((provider) => provider.role === "executor");
1057
1388
  const skills = discoverSkills(workspacePath);
1058
1389
  const skillContext = buildSkillContext(skills);
1390
+ const agents = discoverAgents(workspacePath);
1391
+ const commands = discoverCommands(workspacePath);
1392
+ const capabilitiesManifest = buildCapabilitiesManifest(skills, agents, commands);
1059
1393
  if (skillContext) {
1060
- writeFileSync2(join5(workspacePath, "skills.md"), skillContext, "utf8");
1394
+ writeFileSync3(join6(workspacePath, "skills.md"), skillContext, "utf8");
1061
1395
  }
1062
- const compiled = await compileExecution(issue, activeProvider, state.config, workspacePath, skillContext);
1396
+ const compiled = await compileExecution(issue, activeProvider, state.config, workspacePath, skillContext, capabilitiesManifest);
1063
1397
  let providerPrompt;
1064
1398
  let effectiveProvider = activeProvider;
1065
1399
  if (compiled) {
@@ -1073,16 +1407,24 @@ async function runAgentPipeline(state, issue, workspacePath, basePromptText, bas
1073
1407
  `Plan compiled for ${compiled.meta.adapter}: effort=${compiled.meta.reasoningEffort}, skills=[${compiled.meta.skillsActivated.join(",")}], subagents=[${compiled.meta.subagentsRequested.join(",")}].`
1074
1408
  );
1075
1409
  if (Object.keys(compiled.env).length > 0) {
1076
- const envFile = join5(workspacePath, ".compiled-env.sh");
1410
+ const envFile = join6(workspacePath, ".compiled-env.sh");
1077
1411
  const envLines = Object.entries(compiled.env).map(([k, v]) => `export ${k}=${JSON.stringify(v)}`).join("\n");
1078
- writeFileSync2(envFile, envLines, "utf8");
1412
+ writeFileSync3(envFile, envLines, "utf8");
1079
1413
  }
1080
1414
  } else {
1081
- providerPrompt = await buildProviderBasePrompt(activeProvider, issue, basePromptText, workspacePath, skillContext);
1415
+ providerPrompt = await buildProviderBasePrompt(activeProvider, issue, basePromptText, workspacePath, skillContext, capabilitiesManifest);
1082
1416
  }
1083
1417
  if (!effectiveProvider.command.trim()) {
1084
1418
  throw new Error(`No command configured for provider ${effectiveProvider.provider} (${effectiveProvider.role}).`);
1085
1419
  }
1420
+ if (issue.attempts > 0) {
1421
+ const retryCtx = buildRetryContext(issue);
1422
+ if (retryCtx) {
1423
+ providerPrompt = `${providerPrompt}
1424
+
1425
+ ${retryCtx}`;
1426
+ }
1427
+ }
1086
1428
  pipeline.history.push(`[${now()}] Running ${effectiveProvider.role}:${effectiveProvider.provider} in cycle ${pipeline.cycle}${compiled ? ` [${compiled.meta.adapter} adapter]` : ""}.`);
1087
1429
  await persistAgentPipelineState(pipelineFile, pipeline);
1088
1430
  const result = await runAgentSession(state, issue, effectiveProvider, pipeline.cycle, workspacePath, providerPrompt, basePromptFile);
@@ -1206,7 +1548,6 @@ function applyPlanUsage(issue, usage) {
1206
1548
  }
1207
1549
  function applyPlanSuggestions(issue, plan) {
1208
1550
  if (plan.suggestedPaths?.length && !issue.paths?.length) issue.paths = plan.suggestedPaths;
1209
- if (plan.suggestedLabels?.length && !issue.labels?.length) issue.labels = plan.suggestedLabels;
1210
1551
  if (plan.suggestedEffort && !issue.effort) issue.effort = plan.suggestedEffort;
1211
1552
  }
1212
1553
  async function mutateIssueState(state, c, updater) {
@@ -1240,30 +1581,11 @@ function createS3dbEventStore(state) {
1240
1581
  };
1241
1582
  }
1242
1583
 
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
1584
  // src/persistence/container.ts
1262
1585
  var _container = null;
1263
1586
  function createContainer(state) {
1264
1587
  const issueRepository = createS3dbIssueRepository(state);
1265
1588
  const eventStore = createS3dbEventStore(state);
1266
- const queuePort = createS3QueueAdapter();
1267
1589
  const persistencePort = {
1268
1590
  persistState: (s) => persistState(s),
1269
1591
  loadState: async () => null
@@ -1271,7 +1593,6 @@ function createContainer(state) {
1271
1593
  const container = {
1272
1594
  issueRepository,
1273
1595
  eventStore,
1274
- queuePort,
1275
1596
  persistencePort
1276
1597
  };
1277
1598
  _container = container;
@@ -1286,19 +1607,19 @@ function getContainer() {
1286
1607
  }
1287
1608
 
1288
1609
  // src/commands/create-issue.command.ts
1289
- import { existsSync as existsSync4, mkdirSync, renameSync } from "fs";
1290
- import { basename, join as join6 } from "path";
1610
+ import { existsSync as existsSync5, mkdirSync as mkdirSync3, renameSync } from "fs";
1611
+ import { basename, join as join7 } from "path";
1291
1612
  async function createIssueCommand(input, deps) {
1292
1613
  const { payload, state } = input;
1293
1614
  const issue = createIssueFromPayload(payload, state.issues, state.config.defaultBranch);
1294
1615
  const tempImages = Array.isArray(payload.images) ? payload.images : [];
1295
1616
  if (tempImages.length) {
1296
- const issueAttachDir = join6(ATTACHMENTS_ROOT, issue.id);
1297
- mkdirSync(issueAttachDir, { recursive: true });
1617
+ const issueAttachDir = join7(ATTACHMENTS_ROOT, issue.id);
1618
+ mkdirSync3(issueAttachDir, { recursive: true });
1298
1619
  const finalPaths = [];
1299
1620
  for (const tempPath of tempImages) {
1300
- if (typeof tempPath === "string" && existsSync4(tempPath)) {
1301
- const dest = join6(issueAttachDir, basename(tempPath));
1621
+ if (typeof tempPath === "string" && existsSync5(tempPath)) {
1622
+ const dest = join7(issueAttachDir, basename(tempPath));
1302
1623
  try {
1303
1624
  renameSync(tempPath, dest);
1304
1625
  finalPaths.push(dest);
@@ -1317,13 +1638,13 @@ async function createIssueCommand(input, deps) {
1317
1638
  }
1318
1639
  await deps.persistencePort.persistState(state);
1319
1640
  if (issue.state === "Planning") {
1320
- deps.queuePort.enqueueForPlanning(issue).catch(() => {
1641
+ enqueue(issue, "plan").catch(() => {
1321
1642
  });
1322
1643
  } else if (issue.state === "Queued" || issue.state === "Running") {
1323
- deps.queuePort.enqueueForExecution(issue).catch(() => {
1644
+ enqueue(issue, "execute").catch(() => {
1324
1645
  });
1325
1646
  } else if (issue.state === "Reviewing") {
1326
- deps.queuePort.enqueueForReview(issue).catch(() => {
1647
+ enqueue(issue, "review").catch(() => {
1327
1648
  });
1328
1649
  }
1329
1650
  return { issue };
@@ -1503,17 +1824,12 @@ var issues_resource_default = {
1503
1824
  identifier: "string|required",
1504
1825
  title: "string|required",
1505
1826
  description: "string|optional",
1506
- priority: "number|required",
1507
1827
  state: "string|required",
1508
1828
  branchName: "string|optional",
1509
1829
  url: "string|optional",
1510
1830
  assigneeId: "string|optional",
1511
1831
  labels: "json|required",
1512
1832
  paths: "json|optional",
1513
- inferredPaths: "json|optional",
1514
- capabilityCategory: "string|optional",
1515
- capabilityOverlays: "json|optional",
1516
- capabilityRationale: "json|optional",
1517
1833
  blockedBy: "json|required",
1518
1834
  assignedToWorker: "boolean|required",
1519
1835
  createdAt: "datetime|required",
@@ -1561,10 +1877,6 @@ var issues_resource_default = {
1561
1877
  },
1562
1878
  partitions: {
1563
1879
  byState: { fields: { state: "string" } },
1564
- byCapabilityCategory: { fields: { capabilityCategory: "string" } },
1565
- byStateAndCapability: {
1566
- fields: { state: "string", capabilityCategory: "string" }
1567
- },
1568
1880
  byTerminalWeek: { fields: { terminalWeek: "string" } }
1569
1881
  },
1570
1882
  asyncPartitions: true,
@@ -1801,7 +2113,6 @@ function broadcastToWebSocketClients(message) {
1801
2113
  type: "state:delta",
1802
2114
  seq: broadcastSeq,
1803
2115
  metrics: message.metrics,
1804
- capabilities: message.capabilities,
1805
2116
  updatedAt: message.updatedAt,
1806
2117
  issuesDelta: changedIssues,
1807
2118
  issuesRemoved: removedIds,
@@ -1835,7 +2146,6 @@ function makeWebSocketConfig(state) {
1835
2146
  seq: broadcastSeq,
1836
2147
  timestamp: now(),
1837
2148
  metrics: computeMetrics(state.issues),
1838
- capabilities: computeCapabilityCounts(state.issues),
1839
2149
  issues: state.issues,
1840
2150
  events: state.events.slice(0, 50)
1841
2151
  }));
@@ -1864,7 +2174,7 @@ var shuttingDown = false;
1864
2174
  function isShuttingDown() {
1865
2175
  return shuttingDown;
1866
2176
  }
1867
- function installGracefulShutdown(state, running) {
2177
+ function installGracefulShutdown(state) {
1868
2178
  const handler = async (signal) => {
1869
2179
  if (shuttingDown) {
1870
2180
  logger.warn(`Received ${signal} again, forcing exit.`);
@@ -1875,7 +2185,7 @@ function installGracefulShutdown(state, running) {
1875
2185
  const container = getContainer();
1876
2186
  container.eventStore.addEvent(void 0, "info", `Graceful shutdown initiated (${signal}).`);
1877
2187
  for (const issue of state.issues) {
1878
- if (running.has(issue.id) && (issue.state === "Running" || issue.state === "Reviewing")) {
2188
+ if (issue.state === "Running" || issue.state === "Reviewing") {
1879
2189
  try {
1880
2190
  await transitionIssueCommand({ issue, target: "Queued", note: `Interrupted by ${signal} \u2014 queued for resume on next start.`, fallbackToLocal: true }, container);
1881
2191
  } catch {
@@ -1907,7 +2217,7 @@ function installGracefulShutdown(state, running) {
1907
2217
  }
1908
2218
  function analyzeParallelizability(issues) {
1909
2219
  const todo = issues.filter(
1910
- (issue) => issue.state === "Planned" && issue.assignedToWorker && issue.blockedBy.length === 0
2220
+ (issue) => issue.state === "PendingApproval" && issue.assignedToWorker && issue.blockedBy.length === 0
1911
2221
  );
1912
2222
  if (todo.length === 0) {
1913
2223
  return {
@@ -1917,7 +2227,7 @@ function analyzeParallelizability(issues) {
1917
2227
  groups: []
1918
2228
  };
1919
2229
  }
1920
- const getIssuePaths = (issue) => /* @__PURE__ */ new Set([...issue.paths ?? [], ...issue.inferredPaths ?? []]);
2230
+ const getIssuePaths = (issue) => /* @__PURE__ */ new Set([...issue.paths ?? []]);
1921
2231
  const hasPathOverlap = (a, b) => {
1922
2232
  const pathsA = getIssuePaths(a);
1923
2233
  const pathsB = getIssuePaths(b);
@@ -1986,6 +2296,9 @@ async function ensureNotStale(state, staleTimeoutMs) {
1986
2296
  if (pidDead) {
1987
2297
  logger.info({ issueId: issue.id, identifier: issue.identifier, state: issue.state, pid: agentStatus.pid?.pid }, "[Scheduler] PID dead \u2014 silently recovering to Queued");
1988
2298
  issue.startedAt = void 0;
2299
+ issue.lastError = `Agent process died unexpectedly (PID ${agentStatus.pid.pid}).`;
2300
+ issue.lastFailedPhase = "crash";
2301
+ issue.attempts = (issue.attempts ?? 0) + 1;
1989
2302
  container.issueRepository.markDirty(issue.id);
1990
2303
  await transitionIssueCommand({ issue, target: "Queued", note: `Agent process died (PID ${agentStatus.pid.pid}) \u2014 auto-recovering.` }, container);
1991
2304
  container.eventStore.addEvent(issue.id, "info", `Issue ${issue.identifier} agent process died (PID ${agentStatus.pid.pid}), silently recovered to Queued.`);
@@ -2011,8 +2324,8 @@ function hasTerminalQueue(state) {
2011
2324
  // src/agents/providers-usage.ts
2012
2325
  import { execFile } from "child_process";
2013
2326
  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";
2327
+ import { existsSync as existsSync6, readFileSync as readFileSync5, readdirSync as readdirSync2, realpathSync } from "fs";
2328
+ import { join as join8, dirname } from "path";
2016
2329
  import { homedir as homedir2 } from "os";
2017
2330
  import { env } from "process";
2018
2331
  var execFileAsync = promisify(execFile);
@@ -2039,24 +2352,24 @@ function resolveCodexHomeCandidates() {
2039
2352
  ]);
2040
2353
  const candidates = [...homePaths, ...direct].filter(Boolean).flatMap((candidate) => {
2041
2354
  if (candidate.endsWith("/.codex") || candidate.endsWith("/codex")) return [candidate];
2042
- return [join7(candidate, ".codex"), join7(candidate, "codex")];
2355
+ return [join8(candidate, ".codex"), join8(candidate, "codex")];
2043
2356
  });
2044
2357
  return [...new Set(candidates)];
2045
2358
  }
2046
2359
  function resolveCodexDir() {
2047
2360
  for (const candidate of resolveCodexHomeCandidates()) {
2048
- if (existsSync5(candidate)) {
2361
+ if (existsSync6(candidate)) {
2049
2362
  return candidate;
2050
2363
  }
2051
2364
  }
2052
2365
  return null;
2053
2366
  }
2054
2367
  function findLatestCodexDb(codexDir) {
2055
- const explicit = join7(codexDir, "state_5.sqlite");
2056
- if (existsSync5(explicit)) return explicit;
2368
+ const explicit = join8(codexDir, "state_5.sqlite");
2369
+ if (existsSync6(explicit)) return explicit;
2057
2370
  const candidates = readdirSync2(codexDir).filter((name) => name.startsWith("state_") && name.endsWith(".sqlite")).sort().reverse();
2058
2371
  if (candidates.length === 0) return null;
2059
- return join7(codexDir, candidates[0]);
2372
+ return join8(codexDir, candidates[0]);
2060
2373
  }
2061
2374
  function computeNextMonday() {
2062
2375
  const now2 = /* @__PURE__ */ new Date();
@@ -2093,10 +2406,10 @@ var CLAUDE_PLAN_LIMITS = {
2093
2406
  };
2094
2407
  async function collectClaudeUsage() {
2095
2408
  const home = homedir2();
2096
- const claudeDir = join7(home, ".claude");
2097
- if (!existsSync5(claudeDir)) return null;
2409
+ const claudeDir = join8(home, ".claude");
2410
+ if (!existsSync6(claudeDir)) return null;
2098
2411
  const available = await whichExists("claude");
2099
- const projectsDir = join7(claudeDir, "projects");
2412
+ const projectsDir = join8(claudeDir, "projects");
2100
2413
  let totalInputTokens = 0;
2101
2414
  let totalOutputTokens = 0;
2102
2415
  let totalSessions = 0;
@@ -2110,12 +2423,12 @@ async function collectClaudeUsage() {
2110
2423
  const todayMs = todayStart.getTime();
2111
2424
  const weekStart = computeWeekStart();
2112
2425
  const weekMs = weekStart.getTime();
2113
- if (existsSync5(projectsDir)) {
2426
+ if (existsSync6(projectsDir)) {
2114
2427
  try {
2115
2428
  const projectDirs = readdirSync2(projectsDir, { withFileTypes: true });
2116
2429
  for (const dir of projectDirs) {
2117
2430
  if (!dir.isDirectory()) continue;
2118
- const projectPath = join7(projectsDir, dir.name);
2431
+ const projectPath = join8(projectsDir, dir.name);
2119
2432
  let sessionFiles;
2120
2433
  try {
2121
2434
  sessionFiles = readdirSync2(projectPath).filter((f) => f.endsWith(".jsonl"));
@@ -2123,10 +2436,10 @@ async function collectClaudeUsage() {
2123
2436
  continue;
2124
2437
  }
2125
2438
  for (const file of sessionFiles) {
2126
- const filePath = join7(projectPath, file);
2439
+ const filePath = join8(projectPath, file);
2127
2440
  let content;
2128
2441
  try {
2129
- content = readFileSync4(filePath, "utf8");
2442
+ content = readFileSync5(filePath, "utf8");
2130
2443
  } catch {
2131
2444
  continue;
2132
2445
  }
@@ -2181,10 +2494,10 @@ async function collectClaudeUsage() {
2181
2494
  let plan = "pro";
2182
2495
  let resetInfo = "Weekly reset (every Monday 00:00 UTC)";
2183
2496
  let currentModel = "";
2184
- const settingsPath = join7(claudeDir, "settings.json");
2185
- if (existsSync5(settingsPath)) {
2497
+ const settingsPath = join8(claudeDir, "settings.json");
2498
+ if (existsSync6(settingsPath)) {
2186
2499
  try {
2187
- const settings = JSON.parse(readFileSync4(settingsPath, "utf8"));
2500
+ const settings = JSON.parse(readFileSync5(settingsPath, "utf8"));
2188
2501
  if (settings.plan === "max" || settings.plan === "max5x") {
2189
2502
  plan = settings.plan;
2190
2503
  resetInfo = `Plan: ${settings.plan.toUpperCase()} \u2014 Weekly token limit resets every Monday 00:00 UTC`;
@@ -2220,11 +2533,11 @@ async function collectCodexUsage() {
2220
2533
  if (!codexDir) return null;
2221
2534
  const available = await whichExists("codex");
2222
2535
  const models = [];
2223
- const modelsCachePath = join7(codexDir, "models_cache.json");
2536
+ const modelsCachePath = join8(codexDir, "models_cache.json");
2224
2537
  let currentModel = "";
2225
- if (existsSync5(modelsCachePath)) {
2538
+ if (existsSync6(modelsCachePath)) {
2226
2539
  try {
2227
- const cache = JSON.parse(readFileSync4(modelsCachePath, "utf8"));
2540
+ const cache = JSON.parse(readFileSync5(modelsCachePath, "utf8"));
2228
2541
  for (const m of cache.models || []) {
2229
2542
  models.push({
2230
2543
  slug: m.slug,
@@ -2235,10 +2548,10 @@ async function collectCodexUsage() {
2235
2548
  } catch {
2236
2549
  }
2237
2550
  }
2238
- const configPath = join7(codexDir, "config.toml");
2239
- if (existsSync5(configPath)) {
2551
+ const configPath = join8(codexDir, "config.toml");
2552
+ if (existsSync6(configPath)) {
2240
2553
  try {
2241
- const configContent = readFileSync4(configPath, "utf8");
2554
+ const configContent = readFileSync5(configPath, "utf8");
2242
2555
  const modelMatch = configContent.match(/^model\s*=\s*"([^"]+)"/m);
2243
2556
  if (modelMatch) currentModel = modelMatch[1];
2244
2557
  } catch {
@@ -2327,9 +2640,9 @@ async function collectGeminiUsage() {
2327
2640
  try {
2328
2641
  const { stdout: binPath } = await execFileAsync("which", ["gemini"], { encoding: "utf8", timeout: 3e3 });
2329
2642
  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");
2643
+ const modelsPath = join8(dirname(dirname(realBin)), "node_modules", "@google", "gemini-cli-core", "dist", "src", "config", "models.js");
2644
+ if (existsSync6(modelsPath)) {
2645
+ const content = readFileSync5(modelsPath, "utf8");
2333
2646
  const regex = /export const ([A-Z0-9_]+)\s*=\s*'(gemini-[^']+)';/g;
2334
2647
  const seen = /* @__PURE__ */ new Set();
2335
2648
  let m;
@@ -2347,10 +2660,10 @@ async function collectGeminiUsage() {
2347
2660
  } catch {
2348
2661
  }
2349
2662
  let currentModel = "";
2350
- const settingsPath = join7(homedir2(), ".gemini", "settings.json");
2351
- if (existsSync5(settingsPath)) {
2663
+ const settingsPath = join8(homedir2(), ".gemini", "settings.json");
2664
+ if (existsSync6(settingsPath)) {
2352
2665
  try {
2353
- const settings = JSON.parse(readFileSync4(settingsPath, "utf8"));
2666
+ const settings = JSON.parse(readFileSync5(settingsPath, "utf8"));
2354
2667
  if (typeof settings.model === "string" && settings.model.trim()) {
2355
2668
  currentModel = settings.model.trim();
2356
2669
  }
@@ -2394,10 +2707,10 @@ async function collectProvidersUsage() {
2394
2707
  }
2395
2708
 
2396
2709
  // src/routes/state.ts
2397
- import { existsSync as existsSync7, mkdirSync as mkdirSync2, writeFileSync as writeFileSync3 } from "fs";
2710
+ import { existsSync as existsSync8, mkdirSync as mkdirSync4, writeFileSync as writeFileSync4 } from "fs";
2398
2711
  import { randomUUID } from "crypto";
2399
- import { execSync as execSync2 } from "child_process";
2400
- import { basename as basename2, extname, join as join8 } from "path";
2712
+ import { execSync as execSync3 } from "child_process";
2713
+ import { basename as basename2, extname, join as join9 } from "path";
2401
2714
 
2402
2715
  // src/commands/approve-plan.command.ts
2403
2716
  async function approvePlanCommand(input, deps) {
@@ -2405,8 +2718,9 @@ async function approvePlanCommand(input, deps) {
2405
2718
  if (issue.state !== "Planning") {
2406
2719
  throw new Error(`Cannot approve issue in state ${issue.state}. Must be in Planning.`);
2407
2720
  }
2721
+ ensureGitRepoReadyForWorktrees(TARGET_ROOT, "execute approved plans");
2408
2722
  await transitionIssueCommand(
2409
- { issue, target: "Planned", note: `Plan approved for ${issue.identifier}. Ready for execution.` },
2723
+ { issue, target: "PendingApproval", note: `Plan approved for ${issue.identifier}. Ready for execution.` },
2410
2724
  deps
2411
2725
  );
2412
2726
  await transitionIssueCommand(
@@ -2418,9 +2732,10 @@ async function approvePlanCommand(input, deps) {
2418
2732
  // src/commands/execute-issue.command.ts
2419
2733
  async function executeIssueCommand(input, deps) {
2420
2734
  const { issue } = input;
2421
- if (issue.state !== "Planned") {
2422
- throw new Error(`Cannot execute issue in state ${issue.state}. Must be in Planned.`);
2735
+ if (issue.state !== "PendingApproval") {
2736
+ throw new Error(`Cannot execute issue in state ${issue.state}. Must be in PendingApproval.`);
2423
2737
  }
2738
+ ensureGitRepoReadyForWorktrees(TARGET_ROOT, "execute issues");
2424
2739
  await transitionIssueCommand(
2425
2740
  { issue, target: "Queued", note: `Execution requested for ${issue.identifier}.` },
2426
2741
  deps
@@ -2460,22 +2775,65 @@ async function replanIssueCommand(input, deps) {
2460
2775
  }
2461
2776
 
2462
2777
  // src/commands/merge-workspace.command.ts
2463
- import { existsSync as existsSync6 } from "fs";
2778
+ import { existsSync as existsSync7 } from "fs";
2464
2779
  import { execSync } from "child_process";
2780
+
2781
+ // src/domains/validation.ts
2782
+ import { execFile as execFile2 } from "child_process";
2783
+ async function runValidationGate(issue, config) {
2784
+ if (!config.testCommand) return null;
2785
+ const cwd = issue.worktreePath ?? issue.workspacePath;
2786
+ if (!cwd) {
2787
+ logger.warn({ issueId: issue.id }, "[Validation] No workspace path \u2014 skipping gate");
2788
+ return null;
2789
+ }
2790
+ const command = config.testCommand;
2791
+ logger.info({ issueId: issue.id, command, cwd }, "[Validation] Running validation gate");
2792
+ return new Promise((resolve3) => {
2793
+ const child = execFile2("sh", ["-c", command], {
2794
+ cwd,
2795
+ encoding: "utf8",
2796
+ timeout: 3e5,
2797
+ maxBuffer: 2 * 1024 * 1024
2798
+ }, (err, stdout, stderr) => {
2799
+ const combined = (stdout || "") + (stderr || "");
2800
+ if (!err) {
2801
+ logger.info({ issueId: issue.id }, "[Validation] Gate passed");
2802
+ resolve3({
2803
+ passed: true,
2804
+ output: combined.slice(-2048),
2805
+ command,
2806
+ ranAt: (/* @__PURE__ */ new Date()).toISOString()
2807
+ });
2808
+ return;
2809
+ }
2810
+ logger.warn({ issueId: issue.id, exitCode: err.code }, "[Validation] Gate failed");
2811
+ resolve3({
2812
+ passed: false,
2813
+ output: combined.slice(-2048) || String(err).slice(0, 2048),
2814
+ command,
2815
+ ranAt: (/* @__PURE__ */ new Date()).toISOString()
2816
+ });
2817
+ });
2818
+ });
2819
+ }
2820
+
2821
+ // src/commands/merge-workspace.command.ts
2465
2822
  async function mergeWorkspaceCommand(input, deps) {
2466
2823
  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.`);
2824
+ if (!["Approved", "Reviewing", "PendingDecision"].includes(issue.state)) {
2825
+ throw new Error(`Issue ${issue.identifier} is in state ${issue.state}. Merge is only allowed in Reviewing, PendingDecision, or Approved state.`);
2469
2826
  }
2470
- if (issue.state === "Reviewing" || issue.state === "Reviewed") {
2827
+ ensureGitRepoReadyForWorktrees(TARGET_ROOT, "merge issues");
2828
+ if (issue.state === "Reviewing" || issue.state === "PendingDecision") {
2471
2829
  await transitionIssueCommand(
2472
- { issue, target: "Done", note: "Approved and merged by user." },
2830
+ { issue, target: "Approved", note: "Approved and merged by user." },
2473
2831
  deps
2474
2832
  );
2475
2833
  }
2476
2834
  const wp = issue.worktreePath ?? issue.workspacePath;
2477
- if (!wp || !existsSync6(wp)) {
2478
- throw new Error("No workspace found for this issue.");
2835
+ if (!wp || !existsSync7(wp)) {
2836
+ throw new Error(`No mergeable workspace found for ${issue.identifier}. This issue likely ran before git was initialized for the project. Re-run the issue after git setup.`);
2479
2837
  }
2480
2838
  if (issue.branchName && issue.baseBranch) {
2481
2839
  try {
@@ -2498,12 +2856,20 @@ async function mergeWorkspaceCommand(input, deps) {
2498
2856
  }
2499
2857
  } catch {
2500
2858
  }
2859
+ const validation = await runValidationGate(issue, state.config);
2860
+ if (validation) {
2861
+ issue.validationResult = validation;
2862
+ if (!validation.passed) {
2863
+ throw new Error(`Validation gate failed (${validation.command}): ${validation.output.slice(0, 500)}`);
2864
+ }
2865
+ }
2501
2866
  const result = mergeWorkspace(issue);
2502
2867
  issue.mergeResult = {
2503
2868
  copied: result.copied.length,
2504
2869
  deleted: result.deleted.length,
2505
2870
  skipped: result.skipped.length,
2506
- conflicts: result.conflicts.length
2871
+ conflicts: result.conflicts.length,
2872
+ conflictFiles: result.conflicts.length > 0 ? result.conflicts : void 0
2507
2873
  };
2508
2874
  if (result.conflicts.length > 0) {
2509
2875
  deps.eventStore.addEvent(issue.id, "error", `Merge conflicts: ${result.conflicts.join(", ")}`);
@@ -2527,6 +2893,138 @@ async function mergeWorkspaceCommand(input, deps) {
2527
2893
  return result;
2528
2894
  }
2529
2895
 
2896
+ // src/commands/push-workspace.command.ts
2897
+ import { execFileSync, execSync as execSync2 } from "child_process";
2898
+ function isGhAvailable() {
2899
+ try {
2900
+ execFileSync("gh", ["--version"], { stdio: "pipe", timeout: 5e3 });
2901
+ return true;
2902
+ } catch {
2903
+ return false;
2904
+ }
2905
+ }
2906
+ function getCompareUrl(branchName, baseBranch) {
2907
+ try {
2908
+ const remote = execSync2("git remote get-url origin", { cwd: TARGET_ROOT, encoding: "utf8", stdio: "pipe" }).trim();
2909
+ const cleanRemote = remote.replace(/\.git$/, "");
2910
+ return `${cleanRemote}/compare/${baseBranch}...${branchName}`;
2911
+ } catch {
2912
+ return `(branch pushed: ${branchName})`;
2913
+ }
2914
+ }
2915
+ function findExistingPr(branchName) {
2916
+ try {
2917
+ const result = execFileSync(
2918
+ "gh",
2919
+ ["pr", "view", branchName, "--json", "url,state", "--jq", 'select(.state == "OPEN") | .url'],
2920
+ { cwd: TARGET_ROOT, encoding: "utf8", stdio: "pipe", timeout: 15e3 }
2921
+ ).trim();
2922
+ return result || null;
2923
+ } catch {
2924
+ return null;
2925
+ }
2926
+ }
2927
+ function createPr(branchName, baseBranch, title, body) {
2928
+ return execFileSync(
2929
+ "gh",
2930
+ ["pr", "create", "--head", branchName, "--base", baseBranch, "--title", title, "--body", body],
2931
+ { cwd: TARGET_ROOT, encoding: "utf8", stdio: "pipe", timeout: 3e4 }
2932
+ ).trim();
2933
+ }
2934
+ async function pushWorkspaceCommand(input, deps) {
2935
+ const { issue, state } = input;
2936
+ if (!["Approved", "Reviewing", "PendingDecision"].includes(issue.state)) {
2937
+ throw new Error(`Issue ${issue.identifier} is in state ${issue.state}. Push is only allowed in Reviewing, PendingDecision, or Approved state.`);
2938
+ }
2939
+ ensureGitRepoReadyForWorktrees(TARGET_ROOT, "push issue branches");
2940
+ assertIssueHasGitWorktree(issue, "push");
2941
+ if (issue.state === "Reviewing" || issue.state === "PendingDecision") {
2942
+ await transitionIssueCommand(
2943
+ { issue, target: "Approved", note: "Approved and pushed by user." },
2944
+ deps
2945
+ );
2946
+ }
2947
+ ensureWorktreeCommitted(issue);
2948
+ const validation = await runValidationGate(issue, state.config);
2949
+ if (validation) {
2950
+ issue.validationResult = validation;
2951
+ if (!validation.passed) {
2952
+ throw new Error(`Validation gate failed (${validation.command}): ${validation.output.slice(0, 500)}`);
2953
+ }
2954
+ }
2955
+ computeDiffStats(issue);
2956
+ const planSummary = issue.plan?.summary ?? issue.title;
2957
+ let diffStat = "";
2958
+ try {
2959
+ diffStat = execSync2(
2960
+ `git diff --stat "${issue.baseBranch}"..."${issue.branchName}"`,
2961
+ { cwd: TARGET_ROOT, encoding: "utf8", maxBuffer: 512e3, timeout: 1e4, stdio: "pipe" }
2962
+ ).trim();
2963
+ } catch {
2964
+ }
2965
+ const body = `## Summary
2966
+ ${planSummary}
2967
+
2968
+ ## Diff Stats
2969
+ \`\`\`
2970
+ ${diffStat || "No diff stats available"}
2971
+ \`\`\`
2972
+
2973
+ *Automated by fifony*`;
2974
+ execSync2(`git push -u origin "${issue.branchName}"`, { cwd: TARGET_ROOT, stdio: "pipe" });
2975
+ const prBase = state.config.prBaseBranch || issue.baseBranch;
2976
+ const ghAvailable = isGhAvailable();
2977
+ let prUrl;
2978
+ if (!ghAvailable) {
2979
+ prUrl = getCompareUrl(issue.branchName, prBase);
2980
+ logger.info({ issueId: issue.id, prUrl }, "[Push] gh CLI not available \u2014 using compare URL");
2981
+ } else {
2982
+ const existingUrl = findExistingPr(issue.branchName);
2983
+ if (existingUrl) {
2984
+ prUrl = existingUrl;
2985
+ logger.info({ issueId: issue.id, prUrl }, "[Push] Existing open PR found");
2986
+ } else {
2987
+ try {
2988
+ prUrl = createPr(issue.branchName, prBase, issue.title, body);
2989
+ logger.info({ issueId: issue.id, prUrl }, "[Push] PR created");
2990
+ } catch (err) {
2991
+ const ghError = (err.stderr || err.stdout || String(err)).toString().slice(0, 500);
2992
+ logger.error({ issueId: issue.id, ghError }, "[Push] gh pr create failed");
2993
+ prUrl = getCompareUrl(issue.branchName, prBase);
2994
+ deps.eventStore.addEvent(issue.id, "error", `gh pr create failed: ${ghError}. Branch was pushed \u2014 use the compare URL to create the PR manually.`);
2995
+ }
2996
+ }
2997
+ }
2998
+ issue.prUrl = prUrl;
2999
+ if (!issue.mergedReason) issue.mergedReason = "Pushed to origin and PR created.";
3000
+ await transitionIssueCommand(
3001
+ { issue, target: "Merged", note: `Branch ${issue.branchName} pushed. PR: ${prUrl}` },
3002
+ deps
3003
+ );
3004
+ deps.eventStore.addEvent(issue.id, "merge", `PR created: ${prUrl}`);
3005
+ await deps.persistencePort.persistState(state);
3006
+ return { prUrl, ghAvailable };
3007
+ }
3008
+
3009
+ // src/commands/retry-execution.command.ts
3010
+ async function retryExecutionCommand(input, deps) {
3011
+ const { issue, note } = input;
3012
+ if (issue.state !== "Blocked") {
3013
+ throw new Error(
3014
+ `retryExecutionCommand requires Blocked state, got ${issue.state}. Use replanIssueCommand for re-planning or the generic /retry endpoint for other states.`
3015
+ );
3016
+ }
3017
+ await transitionIssueCommand(
3018
+ { issue, target: "Queued", note: note ?? `Retry execution for ${issue.identifier} (attempt ${issue.attempts + 1}).` },
3019
+ deps
3020
+ );
3021
+ deps.eventStore.addEvent(
3022
+ issue.id,
3023
+ "manual",
3024
+ `Execution retry requested for ${issue.identifier} \u2014 re-queued from Blocked.`
3025
+ );
3026
+ }
3027
+
2530
3028
  // src/routes/state.ts
2531
3029
  function getStateQuery(state, showAll = false) {
2532
3030
  let issues = state.issues;
@@ -2544,12 +3042,18 @@ function getStateQuery(state, showAll = false) {
2544
3042
  return {
2545
3043
  ...state,
2546
3044
  issues,
2547
- capabilities: computeCapabilityCounts(issues),
2548
3045
  metrics: computeMetrics(issues),
2549
3046
  _filter: showAll ? "all" : "recent",
2550
3047
  _totalIssues: state.issues.length
2551
3048
  };
2552
3049
  }
3050
+ function getWorkspaceActionErrorStatus(error) {
3051
+ const message = error instanceof Error ? error.message : String(error);
3052
+ if (message.includes("requires a git repository") || message.includes("requires at least one commit") || message.includes("has no git worktree") || message.includes("No mergeable workspace found") || message.includes("target repository has uncommitted changes") || message.includes("current branch is")) {
3053
+ return 409;
3054
+ }
3055
+ return 500;
3056
+ }
2553
3057
  function registerStateRoutes(app, state) {
2554
3058
  app.get("/api/state", async (c) => {
2555
3059
  const showAll = c.req.query("all") === "1";
@@ -2629,7 +3133,7 @@ function registerStateRoutes(app, state) {
2629
3133
  );
2630
3134
  if (issue.plan?.steps?.length) {
2631
3135
  await transitionIssueCommand(
2632
- { issue, target: "Planned", note: "Existing plan found." },
3136
+ { issue, target: "PendingApproval", note: "Existing plan found." },
2633
3137
  container
2634
3138
  );
2635
3139
  await transitionIssueCommand(
@@ -2638,11 +3142,26 @@ function registerStateRoutes(app, state) {
2638
3142
  );
2639
3143
  }
2640
3144
  } else if (issue.state === "Blocked") {
3145
+ await retryExecutionCommand(
3146
+ { issue, note: "Manual retry from Blocked." },
3147
+ container
3148
+ );
3149
+ } else if (issue.state === "Approved") {
2641
3150
  await transitionIssueCommand(
2642
- { issue, target: "Queued", note: "Manual retry from Blocked." },
3151
+ { issue, target: "Planning", note: "Requeued for rework after merge conflicts." },
2643
3152
  container
2644
3153
  );
2645
- } else if (issue.state === "Planned") {
3154
+ if (issue.plan?.steps?.length) {
3155
+ await transitionIssueCommand(
3156
+ { issue, target: "PendingApproval", note: "Existing plan found." },
3157
+ container
3158
+ );
3159
+ await transitionIssueCommand(
3160
+ { issue, target: "Queued", note: "Auto-queued for rework." },
3161
+ container
3162
+ );
3163
+ }
3164
+ } else if (issue.state === "PendingApproval") {
2646
3165
  await transitionIssueCommand(
2647
3166
  { issue, target: "Queued", note: "Manual retry \u2014 queued for execution." },
2648
3167
  container
@@ -2691,25 +3210,63 @@ function registerStateRoutes(app, state) {
2691
3210
  const issue = findIssue(state, issueId);
2692
3211
  if (!issue) return c.json({ ok: false, error: "Issue not found." }, 404);
2693
3212
  const container = getContainer();
3213
+ if (state.config.mergeMode === "push-pr") {
3214
+ const result2 = await pushWorkspaceCommand({ issue, state }, container);
3215
+ return c.json({ ok: true, prUrl: result2.prUrl, ghAvailable: result2.ghAvailable });
3216
+ }
2694
3217
  const result = await mergeWorkspaceCommand({ issue, state }, container);
2695
3218
  return c.json({ ok: true, ...result });
2696
3219
  } catch (error) {
2697
3220
  const issueId = parseIssue(c);
2698
3221
  logger.error(`Failed to merge workspace for ${issueId || "<unknown>"}: ${String(error)}`);
2699
- return c.json({ ok: false, error: String(error) }, 500);
3222
+ return c.json({ ok: false, error: String(error) }, getWorkspaceActionErrorStatus(error));
3223
+ }
3224
+ });
3225
+ app.get("/api/issues/:id/merge-preview", async (c) => {
3226
+ logger.info({ issueId: parseIssue(c) }, "[API] GET /api/issues/:id/merge-preview");
3227
+ try {
3228
+ const issueId = parseIssue(c);
3229
+ if (!issueId) return c.json({ ok: false, error: "Issue id is required." }, 400);
3230
+ const issue = findIssue(state, issueId);
3231
+ if (!issue) return c.json({ ok: false, error: "Issue not found." }, 404);
3232
+ const { dryMerge } = await import("./workspace-KEHFITYR.js");
3233
+ const result = dryMerge(issue);
3234
+ return c.json({ ok: true, ...result });
3235
+ } catch (error) {
3236
+ logger.error(`Failed to preview merge for ${parseIssue(c) || "<unknown>"}: ${String(error)}`);
3237
+ return c.json({ ok: false, error: String(error) }, getWorkspaceActionErrorStatus(error));
3238
+ }
3239
+ });
3240
+ app.post("/api/issues/:id/rebase", async (c) => {
3241
+ logger.info({ issueId: parseIssue(c) }, "[API] POST /api/issues/:id/rebase");
3242
+ try {
3243
+ const issueId = parseIssue(c);
3244
+ if (!issueId) return c.json({ ok: false, error: "Issue id is required." }, 400);
3245
+ const issue = findIssue(state, issueId);
3246
+ if (!issue) return c.json({ ok: false, error: "Issue not found." }, 404);
3247
+ const { rebaseWorktree } = await import("./workspace-KEHFITYR.js");
3248
+ const result = rebaseWorktree(issue);
3249
+ if (result.success) {
3250
+ addEvent(state, issue.id, "info", `Branch ${issue.branchName} rebased onto ${issue.baseBranch}.`);
3251
+ }
3252
+ await persistState(state);
3253
+ return c.json({ ok: true, ...result });
3254
+ } catch (error) {
3255
+ logger.error(`Failed to rebase for ${parseIssue(c) || "<unknown>"}: ${String(error)}`);
3256
+ return c.json({ ok: false, error: String(error) }, getWorkspaceActionErrorStatus(error));
2700
3257
  }
2701
3258
  });
2702
3259
  app.post("/api/issues/:id/try", async (c) => {
2703
3260
  logger.info({ issueId: parseIssue(c) }, "[API] POST /api/issues/:id/try");
2704
3261
  return mutateIssueState(state, c, async (issue) => {
2705
- if (!["Reviewing", "Reviewed"].includes(issue.state)) {
3262
+ if (!["Reviewing", "PendingDecision"].includes(issue.state)) {
2706
3263
  throw new Error(`Cannot apply test for issue in state ${issue.state}.`);
2707
3264
  }
2708
3265
  if (!issue.branchName) {
2709
3266
  throw new Error("No branch name found for this issue.");
2710
3267
  }
2711
3268
  try {
2712
- execSync2(
3269
+ execSync3(
2713
3270
  `git merge --squash "${issue.branchName}"`,
2714
3271
  { encoding: "utf8", cwd: TARGET_ROOT, stdio: "pipe", timeout: 3e4 }
2715
3272
  );
@@ -2724,8 +3281,8 @@ function registerStateRoutes(app, state) {
2724
3281
  logger.info({ issueId: parseIssue(c) }, "[API] POST /api/issues/:id/revert-try");
2725
3282
  return mutateIssueState(state, c, async (issue) => {
2726
3283
  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 });
3284
+ execSync3("git reset --hard HEAD", { cwd: TARGET_ROOT, stdio: "pipe", timeout: 15e3 });
3285
+ execSync3("git clean -fd", { cwd: TARGET_ROOT, stdio: "pipe", timeout: 15e3 });
2729
3286
  } catch (err) {
2730
3287
  const msg = err.stderr || err.stdout || String(err);
2731
3288
  throw new Error(`git reset/clean failed: ${msg}`);
@@ -2736,7 +3293,7 @@ function registerStateRoutes(app, state) {
2736
3293
  app.post("/api/issues/:id/rollback", async (c) => {
2737
3294
  logger.info({ issueId: parseIssue(c) }, "[API] POST /api/issues/:id/rollback");
2738
3295
  return mutateIssueState(state, c, async (issue) => {
2739
- if (!["Reviewing", "Reviewed", "Done"].includes(issue.state)) {
3296
+ if (!["Reviewing", "PendingDecision", "Approved"].includes(issue.state)) {
2740
3297
  throw new Error(`Cannot rollback issue in state ${issue.state}. Must be in Reviewing, Reviewed, or Done.`);
2741
3298
  }
2742
3299
  if (issue.workspacePath) {
@@ -2766,15 +3323,15 @@ function registerStateRoutes(app, state) {
2766
3323
  if (!Array.isArray(payload.files) || payload.files.length === 0) {
2767
3324
  return c.json({ ok: false, error: "No files provided." }, 400);
2768
3325
  }
2769
- const issueAttachDir = join8(ATTACHMENTS_ROOT, issue.id);
2770
- mkdirSync2(issueAttachDir, { recursive: true });
3326
+ const issueAttachDir = join9(ATTACHMENTS_ROOT, issue.id);
3327
+ mkdirSync4(issueAttachDir, { recursive: true });
2771
3328
  const newPaths = [];
2772
3329
  for (const file of payload.files) {
2773
3330
  if (typeof file.data !== "string" || !file.name) continue;
2774
3331
  const safeExt = extname(file.name).replace(/[^a-z0-9.]/gi, "").slice(0, 10) || ".bin";
2775
3332
  const safeName = `${randomUUID()}${safeExt}`;
2776
- const dest = join8(issueAttachDir, safeName);
2777
- writeFileSync3(dest, Buffer.from(file.data, "base64"));
3333
+ const dest = join9(issueAttachDir, safeName);
3334
+ writeFileSync4(dest, Buffer.from(file.data, "base64"));
2778
3335
  newPaths.push(dest);
2779
3336
  }
2780
3337
  issue.images = [...issue.images ?? [], ...newPaths];
@@ -2794,8 +3351,8 @@ function registerStateRoutes(app, state) {
2794
3351
  const filename = c.req.param?.("filename") ?? c.req.params?.filename ?? "";
2795
3352
  if (!filename) return c.json({ ok: false, error: "Filename is required." }, 400);
2796
3353
  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);
3354
+ const filePath = join9(ATTACHMENTS_ROOT, issueId, safeName);
3355
+ if (!existsSync8(filePath)) return c.json({ ok: false, error: "Image not found." }, 404);
2799
3356
  const ext = extname(safeName).toLowerCase();
2800
3357
  const mimeMap = {
2801
3358
  ".png": "image/png",
@@ -2806,8 +3363,8 @@ function registerStateRoutes(app, state) {
2806
3363
  ".svg": "image/svg+xml"
2807
3364
  };
2808
3365
  const mime = mimeMap[ext] ?? "application/octet-stream";
2809
- const { readFileSync: readFileSync11 } = await import("fs");
2810
- const data = readFileSync11(filePath);
3366
+ const { readFileSync: readFileSync12 } = await import("fs");
3367
+ const data = readFileSync12(filePath);
2811
3368
  return new Response(data, { headers: { "Content-Type": mime, "Cache-Control": "private, max-age=86400" } });
2812
3369
  } catch (error) {
2813
3370
  return c.json({ ok: false, error: error instanceof Error ? error.message : String(error) }, 500);
@@ -2819,7 +3376,7 @@ function registerStateRoutes(app, state) {
2819
3376
  const issue = findIssue(state, issueId);
2820
3377
  if (!issue) return c.json({ ok: false, error: "Issue not found." }, 404);
2821
3378
  try {
2822
- const { getIssueTransitionHistory } = await import("./issue-state-machine-SKODQ6MG.js");
3379
+ const { getIssueTransitionHistory } = await import("./issue-state-machine-V2KPUYPW.js");
2823
3380
  const limit = parseInt(c.req.query("limit") ?? "50", 10);
2824
3381
  const offset = parseInt(c.req.query("offset") ?? "0", 10);
2825
3382
  const transitions = await getIssueTransitionHistory(issue.id, { limit, offset });
@@ -2830,7 +3387,7 @@ function registerStateRoutes(app, state) {
2830
3387
  });
2831
3388
  app.get("/api/state-machine/transitions", async (c) => {
2832
3389
  try {
2833
- const { getStateMachineTransitions } = await import("./issue-state-machine-SKODQ6MG.js");
3390
+ const { getStateMachineTransitions } = await import("./issue-state-machine-V2KPUYPW.js");
2834
3391
  return c.json({ ok: true, transitions: getStateMachineTransitions() });
2835
3392
  } catch (error) {
2836
3393
  return c.json({ ok: false, error: error instanceof Error ? error.message : String(error) }, 500);
@@ -2838,7 +3395,7 @@ function registerStateRoutes(app, state) {
2838
3395
  });
2839
3396
  app.get("/api/state-machine/visualize", async (c) => {
2840
3397
  try {
2841
- const { visualizeStateMachine } = await import("./issue-state-machine-SKODQ6MG.js");
3398
+ const { visualizeStateMachine } = await import("./issue-state-machine-V2KPUYPW.js");
2842
3399
  const dot = visualizeStateMachine();
2843
3400
  if (!dot) return c.json({ ok: false, error: "Visualization not available." }, 404);
2844
3401
  return c.json({ ok: true, dot });
@@ -2923,8 +3480,8 @@ async function recoverPlanningSession() {
2923
3480
  }
2924
3481
 
2925
3482
  // src/agents/planning/plan-generator.ts
2926
- import { writeFileSync as writeFileSync5 } from "fs";
2927
- import { join as join10 } from "path";
3483
+ import { writeFileSync as writeFileSync6 } from "fs";
3484
+ import { join as join11 } from "path";
2928
3485
  import { mkdtempSync, rmSync as rmSync2 } from "fs";
2929
3486
  import { tmpdir } from "os";
2930
3487
 
@@ -2982,10 +3539,9 @@ function tryBuildPlan(parsed) {
2982
3539
  })) : void 0,
2983
3540
  validation: toStringArray(parsed.validation),
2984
3541
  deliverables: toStringArray(parsed.deliverables),
2985
- executionStrategy: parsed.executionStrategy || parsed.execution_strategy || void 0,
2986
- toolingDecision: parsed.toolingDecision || parsed.tooling_decision || void 0,
2987
3542
  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),
3543
+ suggestedSkills: toStringArray(parsed.suggestedSkills || parsed.suggested_skills),
3544
+ suggestedAgents: toStringArray(parsed.suggestedAgents || parsed.suggested_agents),
2989
3545
  suggestedEffort: parsed.suggestedEffort || parsed.suggested_effort || parsed.effortSuggestion || parsed.effort_suggestion || parsed.effort || { default: "medium" },
2990
3546
  provider: "",
2991
3547
  createdAt: now()
@@ -3096,28 +3652,27 @@ function extractPlanTokenUsage(raw) {
3096
3652
  }
3097
3653
 
3098
3654
  // src/agents/planning/planning-prompts.ts
3099
- import { mkdirSync as mkdirSync3, writeFileSync as writeFileSync4 } from "fs";
3655
+ import { mkdirSync as mkdirSync5, writeFileSync as writeFileSync5 } from "fs";
3100
3656
  import { spawn } from "child_process";
3101
- import { join as join9 } from "path";
3657
+ import { join as join10 } from "path";
3102
3658
 
3103
3659
  // src/agents/planning/planning-schema.ts
3104
3660
  var STEP_SCHEMA = {
3105
3661
  type: "object",
3106
3662
  additionalProperties: false,
3107
- required: ["step", "action", "files", "details", "ownerType", "doneWhen"],
3663
+ required: ["step", "action", "files", "details", "doneWhen"],
3108
3664
  properties: {
3109
3665
  step: { type: "number" },
3110
3666
  action: { type: "string" },
3111
3667
  files: { type: "array", items: { type: "string" } },
3112
3668
  details: { type: "string" },
3113
- ownerType: { type: "string", enum: ["human", "agent", "skill", "subagent", "tool"] },
3114
3669
  doneWhen: { type: "string" }
3115
3670
  }
3116
3671
  };
3117
3672
  var PLAN_JSON_SCHEMA = JSON.stringify({
3118
3673
  type: "object",
3119
3674
  additionalProperties: false,
3120
- required: ["summary", "steps", "phases", "estimatedComplexity", "suggestedPaths", "suggestedLabels", "assumptions", "constraints", "unknowns", "successCriteria", "executionStrategy", "toolingDecision", "risks", "validation", "deliverables", "suggestedEffort"],
3675
+ required: ["summary", "steps", "estimatedComplexity", "suggestedPaths", "suggestedEffort"],
3121
3676
  properties: {
3122
3677
  summary: { type: "string" },
3123
3678
  estimatedComplexity: { type: "string", enum: ["trivial", "low", "medium", "high"] },
@@ -3125,16 +3680,8 @@ var PLAN_JSON_SCHEMA = JSON.stringify({
3125
3680
  constraints: { type: "array", items: { type: "string" } },
3126
3681
  unknowns: { type: "array", items: { type: "object", additionalProperties: false, properties: { question: { type: "string" }, whyItMatters: { type: "string" }, howToResolve: { type: "string" } }, required: ["question", "whyItMatters", "howToResolve"] } },
3127
3682
  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
3683
  steps: { type: "array", items: STEP_SCHEMA },
3137
- phases: { type: "array", items: { type: "object", additionalProperties: false, required: ["phaseName", "goal", "tasks", "dependencies", "outputs"], properties: {
3684
+ phases: { type: "array", items: { type: "object", additionalProperties: false, required: ["phaseName", "goal", "tasks"], properties: {
3138
3685
  phaseName: { type: "string" },
3139
3686
  goal: { type: "string" },
3140
3687
  tasks: { type: "array", items: STEP_SCHEMA },
@@ -3145,8 +3692,9 @@ var PLAN_JSON_SCHEMA = JSON.stringify({
3145
3692
  validation: { type: "array", items: { type: "string" } },
3146
3693
  deliverables: { type: "array", items: { type: "string" } },
3147
3694
  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" } } }
3695
+ suggestedSkills: { type: "array", items: { type: "string" } },
3696
+ suggestedAgents: { type: "array", items: { type: "string" } },
3697
+ suggestedEffort: { type: "object", additionalProperties: false, required: ["default"], properties: { default: { type: "string" }, planner: { type: "string" }, executor: { type: "string" }, reviewer: { type: "string" } } }
3150
3698
  }
3151
3699
  });
3152
3700
  var PLAN_SCHEMA_OBJECT = JSON.parse(PLAN_JSON_SCHEMA);
@@ -3166,6 +3714,9 @@ var SETTING_ID_AGENT_COMMAND = "runtime.agentCommand";
3166
3714
  var SETTING_ID_DEFAULT_EFFORT = "runtime.defaultEffort";
3167
3715
  var SETTING_ID_DETECTED_PROVIDERS = "providers.detected";
3168
3716
  var SETTING_ID_WORKFLOW_CONFIG = "runtime.workflowConfig";
3717
+ var SETTING_ID_TEST_COMMAND = "runtime.testCommand";
3718
+ var SETTING_ID_MERGE_MODE = "runtime.mergeMode";
3719
+ var SETTING_ID_PR_BASE_BRANCH = "runtime.prBaseBranch";
3169
3720
  async function loadRuntimeSettings() {
3170
3721
  return loadPersistedSettings();
3171
3722
  }
@@ -3181,7 +3732,10 @@ var RUNTIME_CONFIG_SETTING_IDS = /* @__PURE__ */ new Set([
3181
3732
  SETTING_ID_MAX_CONCURRENT_BY_STATE,
3182
3733
  SETTING_ID_AGENT_PROVIDER,
3183
3734
  SETTING_ID_AGENT_COMMAND,
3184
- SETTING_ID_DEFAULT_EFFORT
3735
+ SETTING_ID_DEFAULT_EFFORT,
3736
+ SETTING_ID_TEST_COMMAND,
3737
+ SETTING_ID_MERGE_MODE,
3738
+ SETTING_ID_PR_BASE_BRANCH
3185
3739
  ]);
3186
3740
  var VALID_REASONING_EFFORTS = /* @__PURE__ */ new Set(["low", "medium", "high", "extra-high"]);
3187
3741
  function parseIntegerSetting(value) {
@@ -3240,7 +3794,10 @@ function buildRuntimeConfigSettings(config, source) {
3240
3794
  { id: SETTING_ID_MAX_CONCURRENT_BY_STATE, scope: "runtime", value: config.maxConcurrentByState, source, updatedAt },
3241
3795
  { id: SETTING_ID_AGENT_PROVIDER, scope: "runtime", value: config.agentProvider, source, updatedAt },
3242
3796
  { 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 }
3797
+ { id: SETTING_ID_DEFAULT_EFFORT, scope: "runtime", value: config.defaultEffort, source, updatedAt },
3798
+ { id: SETTING_ID_TEST_COMMAND, scope: "runtime", value: config.testCommand ?? "", source, updatedAt },
3799
+ { id: SETTING_ID_MERGE_MODE, scope: "runtime", value: config.mergeMode ?? "local", source, updatedAt },
3800
+ { id: SETTING_ID_PR_BASE_BRANCH, scope: "runtime", value: config.prBaseBranch ?? "", source, updatedAt }
3244
3801
  ];
3245
3802
  }
3246
3803
  function applyPersistedSettings(config, settings) {
@@ -3331,6 +3888,24 @@ function applyPersistedSettings(config, settings) {
3331
3888
  }
3332
3889
  break;
3333
3890
  }
3891
+ case SETTING_ID_TEST_COMMAND: {
3892
+ if (typeof setting.value === "string") {
3893
+ nextConfig.testCommand = setting.value.trim() || void 0;
3894
+ }
3895
+ break;
3896
+ }
3897
+ case SETTING_ID_MERGE_MODE: {
3898
+ if (setting.value === "local" || setting.value === "push-pr") {
3899
+ nextConfig.mergeMode = setting.value;
3900
+ }
3901
+ break;
3902
+ }
3903
+ case SETTING_ID_PR_BASE_BRANCH: {
3904
+ if (typeof setting.value === "string" && setting.value.trim()) {
3905
+ nextConfig.prBaseBranch = setting.value.trim();
3906
+ }
3907
+ break;
3908
+ }
3334
3909
  default:
3335
3910
  break;
3336
3911
  }
@@ -3435,11 +4010,19 @@ async function persistWorkflowConfig(config) {
3435
4010
 
3436
4011
  // src/agents/planning/planning-prompts.ts
3437
4012
  async function buildPlanPrompt(title, description, fast = false, images) {
4013
+ const skills = discoverSkills(TARGET_ROOT);
4014
+ const agents = discoverAgents(TARGET_ROOT);
4015
+ const commands = discoverCommands(TARGET_ROOT);
4016
+ const hasCapabilities = skills.length > 0 || agents.length > 0 || commands.length > 0;
3438
4017
  return renderPrompt("issue-planner", {
3439
4018
  title,
3440
4019
  description: description || "(none provided)",
3441
4020
  fast,
3442
- images: images?.length ? images : void 0
4021
+ images: images?.length ? images : void 0,
4022
+ availableCapabilities: hasCapabilities,
4023
+ availableSkills: skills.map((s) => ({ name: s.name, description: s.description || "", whenToUse: s.whenToUse || "" })),
4024
+ availableAgents: agents.map((a) => ({ name: a.name, description: a.description || "", whenToUse: a.whenToUse || "", avoidIf: a.avoidIf || "" })),
4025
+ availableCommands: commands.map((c) => ({ name: c.name }))
3443
4026
  });
3444
4027
  }
3445
4028
  async function buildRefinePrompt(title, description, currentPlan, feedback) {
@@ -3458,11 +4041,11 @@ function getPlanCommand(provider, model, imagePaths) {
3458
4041
  }
3459
4042
  function savePlanDebugFiles(slug, prompt, output) {
3460
4043
  try {
3461
- const debugDir = join9(STATE_ROOT, "debug");
3462
- mkdirSync3(debugDir, { recursive: true });
4044
+ const debugDir = join10(STATE_ROOT, "debug");
4045
+ mkdirSync5(debugDir, { recursive: true });
3463
4046
  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");
4047
+ writeFileSync5(join10(debugDir, `plan-${slug}-${ts}-prompt.md`), prompt, "utf8");
4048
+ if (output) writeFileSync5(join10(debugDir, `plan-${slug}-${ts}-output.txt`), output, "utf8");
3466
4049
  } catch {
3467
4050
  }
3468
4051
  }
@@ -3620,9 +4203,9 @@ async function generatePlan(title, description, config, _workflowDefinition, opt
3620
4203
  const command = getPlanCommand(preferred, planStageModel, images);
3621
4204
  if (!command) throw new Error(`No command configured for provider ${preferred}.`);
3622
4205
  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}
4206
+ const tempDir = mkdtempSync(join11(tmpdir(), "fifony-plan-"));
4207
+ const promptFile = join11(tempDir, "fifony-plan-prompt.md");
4208
+ writeFileSync6(promptFile, `${prompt}
3626
4209
  `, "utf8");
3627
4210
  let lastProgressPersist = 0;
3628
4211
  const PROGRESS_INTERVAL_MS = 2e3;
@@ -3699,8 +4282,8 @@ async function generatePlan(title, description, config, _workflowDefinition, opt
3699
4282
  }
3700
4283
 
3701
4284
  // src/agents/planning/plan-refiner.ts
3702
- import { writeFileSync as writeFileSync6 } from "fs";
3703
- import { join as join11 } from "path";
4285
+ import { writeFileSync as writeFileSync7 } from "fs";
4286
+ import { join as join12 } from "path";
3704
4287
  import { mkdtempSync as mkdtempSync2, rmSync as rmSync3 } from "fs";
3705
4288
  import { tmpdir as tmpdir2 } from "os";
3706
4289
  async function refinePlan(issue, feedback, config, _workflowDefinition) {
@@ -3713,9 +4296,9 @@ async function refinePlan(issue, feedback, config, _workflowDefinition) {
3713
4296
  {
3714
4297
  const command = getPlanCommand(preferred, planStageModel);
3715
4298
  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}
4299
+ const tempDir = mkdtempSync2(join12(tmpdir2(), "fifony-refine-"));
4300
+ const promptFile = join12(tempDir, "fifony-refine-prompt.md");
4301
+ writeFileSync7(promptFile, `${prompt}
3719
4302
  `, "utf8");
3720
4303
  const output = await runPlanningProcess({
3721
4304
  command,
@@ -3836,10 +4419,10 @@ function refinePlanInBackground(issue, feedback, config, _workflowDefinition, ca
3836
4419
 
3837
4420
  // src/agents/planning/issue-enhancer.ts
3838
4421
  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";
4422
+ import { existsSync as existsSync9, mkdtempSync as mkdtempSync3, readFileSync as readFileSync6, rmSync as rmSync4, writeFileSync as writeFileSync8 } from "fs";
3840
4423
  import { spawn as spawn2 } from "child_process";
3841
4424
  import { tmpdir as tmpdir3 } from "os";
3842
- import { join as join12 } from "path";
4425
+ import { join as join13 } from "path";
3843
4426
  function getProviderCommand(provider, config) {
3844
4427
  return resolveAgentCommand(provider, config.agentCommand || "", "", "");
3845
4428
  }
@@ -3908,22 +4491,22 @@ function parseCandidate(raw, expectedField) {
3908
4491
  return "";
3909
4492
  }
3910
4493
  function readProviderOutput(resultFile, fallback) {
3911
- if (existsSync8(resultFile)) {
4494
+ if (existsSync9(resultFile)) {
3912
4495
  try {
3913
- return readFileSync5(resultFile, "utf8").trim();
4496
+ return readFileSync6(resultFile, "utf8").trim();
3914
4497
  } catch {
3915
4498
  }
3916
4499
  }
3917
4500
  return fallback;
3918
4501
  }
3919
4502
  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}
4503
+ const tempDir = mkdtempSync3(join13(tmpdir3(), "fifony-enhance-"));
4504
+ const promptFile = join13(tempDir, "fifony-enhance-prompt.md");
4505
+ const issuePayloadFile = join13(tempDir, "fifony-issue.json");
4506
+ const resultFile = join13(tempDir, "fifony-result.txt");
4507
+ writeFileSync8(promptFile, `${prompt}
3925
4508
  `, "utf8");
3926
- writeFileSync7(issuePayloadFile, JSON.stringify({ title, description, field }, null, 2), "utf8");
4509
+ writeFileSync8(issuePayloadFile, JSON.stringify({ title, description, field }, null, 2), "utf8");
3927
4510
  let effectiveCommand = command;
3928
4511
  if (provider === "codex" && images?.length) {
3929
4512
  const imageFlags = images.map((p) => `--image "${p}"`).join(" ");
@@ -4140,7 +4723,6 @@ function registerPlanRoutes(app, state) {
4140
4723
  applyUsage: (iss, usage) => applyPlanUsage(iss, usage),
4141
4724
  applySuggestions: (iss, plan) => {
4142
4725
  if (plan.suggestedPaths?.length) iss.paths = plan.suggestedPaths;
4143
- if (plan.suggestedLabels?.length) iss.labels = plan.suggestedLabels;
4144
4726
  if (plan.suggestedEffort) iss.effort = plan.suggestedEffort;
4145
4727
  }
4146
4728
  });
@@ -4322,7 +4904,7 @@ function registerAnalyticsRoutes(app) {
4322
4904
  try {
4323
4905
  const context2 = getApiRuntimeContextOrThrow();
4324
4906
  const doneIssues = context2.state.issues.filter(
4325
- (i) => (i.state === "Done" || i.state === "Merged") && i.completedAt
4907
+ (i) => (i.state === "Approved" || i.state === "Merged") && i.completedAt
4326
4908
  );
4327
4909
  const msToDay = (ms) => ms / (1e3 * 60 * 60 * 24);
4328
4910
  const avg = (arr) => arr.length ? arr.reduce((a, b) => a + b, 0) / arr.length : null;
@@ -4379,149 +4961,59 @@ function registerScanningRoutes(app, state) {
4379
4961
  });
4380
4962
  }
4381
4963
 
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
- }
4964
+ // src/agents/claude-md-manager.ts
4965
+ import { existsSync as existsSync10, readFileSync as readFileSync7, writeFileSync as writeFileSync9 } from "fs";
4966
+ import { join as join14 } from "path";
4967
+ var BLOCK_START = "<!-- FIFONY:START \u2014 managed by fifony, do not edit manually -->";
4968
+ var BLOCK_END = "<!-- FIFONY:END -->";
4969
+ var BLOCK_PATTERN = /<!-- FIFONY:START[^>]*-->[\s\S]*?<!-- FIFONY:END -->/;
4970
+ function buildManagedBlock(skills, agents, commands) {
4971
+ const lines = [
4972
+ BLOCK_START,
4973
+ "## Fifony \u2014 Installed Capabilities",
4974
+ "",
4975
+ "This workspace has fifony-managed agents and skills installed.",
4976
+ ""
4977
+ ];
4978
+ if (commands.length > 0) {
4979
+ lines.push(`**Commands**: ${commands.map((c) => `/${c.name}`).join(", ")}`);
4980
+ }
4981
+ if (skills.length > 0) {
4982
+ lines.push(`**Skills**: ${skills.map((s) => s.name).join(", ")}`);
4983
+ }
4984
+ if (agents.length > 0) {
4985
+ lines.push(`**Agents**: ${agents.map((a) => a.name).join(", ")}`);
4986
+ }
4987
+ lines.push("");
4988
+ lines.push("Use these capabilities when working on tasks. For details:");
4989
+ lines.push("- Skills: `.claude/skills/*/SKILL.md`");
4990
+ lines.push("- Agents: `.claude/agents/*.md`");
4991
+ lines.push("- Commands: `.claude/commands/*.md`");
4992
+ lines.push(BLOCK_END);
4993
+ return lines.join("\n");
4994
+ }
4995
+ function updateClaudeMdManagedBlock(targetRoot, skills, agents, commands) {
4996
+ if (skills.length === 0 && agents.length === 0 && commands.length === 0) return;
4997
+ const claudeMdPath = join14(targetRoot, "CLAUDE.md");
4998
+ const newBlock = buildManagedBlock(skills, agents, commands);
4999
+ let existing = "";
5000
+ if (existsSync10(claudeMdPath)) {
5001
+ existing = readFileSync7(claudeMdPath, "utf8");
5002
+ }
5003
+ let updated;
5004
+ if (BLOCK_PATTERN.test(existing)) {
5005
+ updated = existing.replace(BLOCK_PATTERN, newBlock);
5006
+ } else if (existing) {
5007
+ updated = `${existing.trimEnd()}
5008
+
5009
+ ${newBlock}
5010
+ `;
5011
+ } else {
5012
+ updated = `${newBlock}
5013
+ `;
4523
5014
  }
4524
- return result;
5015
+ if (updated === existing) return;
5016
+ writeFileSync9(claudeMdPath, updated, "utf8");
4525
5017
  }
4526
5018
 
4527
5019
  // src/routes/catalog.ts
@@ -4545,6 +5037,10 @@ function registerCatalogRoutes(app) {
4545
5037
  }
4546
5038
  const catalog = loadAgentCatalog();
4547
5039
  const result = installAgents(TARGET_ROOT, agentNames, catalog);
5040
+ try {
5041
+ updateClaudeMdManagedBlock(TARGET_ROOT, discoverSkills(TARGET_ROOT), discoverAgents(TARGET_ROOT), discoverCommands(TARGET_ROOT));
5042
+ } catch {
5043
+ }
4548
5044
  return c.json({ ok: true, ...result });
4549
5045
  } catch (error) {
4550
5046
  logger.error({ err: error }, "Failed to install agents");
@@ -4560,6 +5056,10 @@ function registerCatalogRoutes(app) {
4560
5056
  }
4561
5057
  const catalog = loadSkillCatalog();
4562
5058
  const result = installSkills(TARGET_ROOT, skillNames, catalog);
5059
+ try {
5060
+ updateClaudeMdManagedBlock(TARGET_ROOT, discoverSkills(TARGET_ROOT), discoverAgents(TARGET_ROOT), discoverCommands(TARGET_ROOT));
5061
+ } catch {
5062
+ }
4563
5063
  return c.json({ ok: true, ...result });
4564
5064
  } catch (error) {
4565
5065
  logger.error({ err: error }, "Failed to install skills");
@@ -4622,6 +5122,12 @@ function registerReferenceRepositoryRoutes(app) {
4622
5122
  dryRun: payload?.dryRun === true,
4623
5123
  importToGlobal: payload?.global === true
4624
5124
  });
5125
+ if (!payload?.dryRun) {
5126
+ try {
5127
+ updateClaudeMdManagedBlock(TARGET_ROOT, discoverSkills(TARGET_ROOT), discoverAgents(TARGET_ROOT), discoverCommands(TARGET_ROOT));
5128
+ } catch {
5129
+ }
5130
+ }
4625
5131
  return c.json({
4626
5132
  ok: true,
4627
5133
  ...summary
@@ -4636,35 +5142,34 @@ function registerReferenceRepositoryRoutes(app) {
4636
5142
  }
4637
5143
 
4638
5144
  // src/routes/misc.ts
4639
- import { execSync as execSync3 } from "child_process";
5145
+ import { execSync as execSync4 } from "child_process";
4640
5146
  import {
4641
5147
  appendFileSync,
4642
5148
  closeSync,
4643
- existsSync as existsSync10,
4644
- mkdirSync as mkdirSync5,
5149
+ existsSync as existsSync11,
5150
+ mkdirSync as mkdirSync6,
4645
5151
  openSync,
4646
- readFileSync as readFileSync7,
5152
+ readdirSync as readdirSync3,
5153
+ readFileSync as readFileSync8,
4647
5154
  readSync,
4648
5155
  statSync,
4649
- writeFileSync as writeFileSync9
5156
+ writeFileSync as writeFileSync10
4650
5157
  } from "fs";
4651
5158
  import { randomUUID as randomUUID2 } from "crypto";
4652
- import { extname as extname2, join as join14 } from "path";
5159
+ import { basename as basename3, extname as extname2, join as join15 } from "path";
4653
5160
  function registerMiscRoutes(app, state) {
4654
5161
  app.post("/api/issues/:id/push", async (c) => {
4655
5162
  const issueId = parseIssue(c);
4656
5163
  if (!issueId) return c.json({ ok: false, error: "Issue id is required." }, 400);
4657
5164
  const issue = findIssue(state, issueId);
4658
5165
  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);
5166
+ if (!["Approved", "Reviewing", "PendingDecision"].includes(issue.state)) {
5167
+ 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
5168
  }
4662
5169
  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 });
5170
+ const container = getContainer();
5171
+ const result = await pushWorkspaceCommand({ issue, state }, container);
5172
+ return c.json({ ok: true, prUrl: result.prUrl, ghAvailable: result.ghAvailable });
4668
5173
  } catch (error) {
4669
5174
  logger.error({ err: error }, `[API] Failed to push branch for ${issueId}`);
4670
5175
  return c.json({ ok: false, error: error instanceof Error ? error.message : String(error) }, 500);
@@ -4687,7 +5192,7 @@ function registerMiscRoutes(app, state) {
4687
5192
  const wp = issue.workspacePath;
4688
5193
  const liveLog = wp ? `${wp}/live-output.log` : null;
4689
5194
  let lastSize = 0;
4690
- if (liveLog && existsSync10(liveLog)) {
5195
+ if (liveLog && existsSync11(liveLog)) {
4691
5196
  try {
4692
5197
  const stat = statSync(liveLog);
4693
5198
  lastSize = stat.size;
@@ -4715,7 +5220,7 @@ function registerMiscRoutes(app, state) {
4715
5220
  return;
4716
5221
  }
4717
5222
  const logPath = currentIssue.workspacePath ? `${currentIssue.workspacePath}/live-output.log` : null;
4718
- if (logPath && existsSync10(logPath)) {
5223
+ if (logPath && existsSync11(logPath)) {
4719
5224
  try {
4720
5225
  const stat = statSync(logPath);
4721
5226
  if (stat.size > lastSize) {
@@ -4770,7 +5275,7 @@ function registerMiscRoutes(app, state) {
4770
5275
  const liveLog = wp ? `${wp}/live-output.log` : null;
4771
5276
  let logTail = "";
4772
5277
  let logSize = 0;
4773
- if (liveLog && existsSync10(liveLog)) {
5278
+ if (liveLog && existsSync11(liveLog)) {
4774
5279
  try {
4775
5280
  const stat = statSync(liveLog);
4776
5281
  logSize = stat.size;
@@ -4810,13 +5315,13 @@ function registerMiscRoutes(app, state) {
4810
5315
  const issue = findIssue(state, issueId);
4811
5316
  if (!issue) return c.json({ ok: false, error: "Issue not found." }, 404);
4812
5317
  const wp = issue.workspacePath;
4813
- if (!wp || !existsSync10(wp)) {
5318
+ if (!wp || !existsSync11(wp)) {
4814
5319
  return c.json({ ok: true, files: [], diff: "", message: "No workspace found." });
4815
5320
  }
4816
5321
  let raw = "";
4817
5322
  if (issue.branchName && issue.baseBranch) {
4818
5323
  try {
4819
- raw = execSync3(
5324
+ raw = execSync4(
4820
5325
  `git diff --no-color "${issue.baseBranch}"..."${issue.branchName}"`,
4821
5326
  { encoding: "utf8", maxBuffer: 4 * 1024 * 1024, timeout: 15e3, cwd: TARGET_ROOT, stdio: "pipe" }
4822
5327
  );
@@ -4824,11 +5329,11 @@ function registerMiscRoutes(app, state) {
4824
5329
  raw = err.stdout || "";
4825
5330
  }
4826
5331
  } else {
4827
- if (!existsSync10(SOURCE_ROOT)) {
5332
+ if (!existsSync11(SOURCE_ROOT)) {
4828
5333
  return c.json({ ok: true, files: [], diff: "", message: "Source root not found." });
4829
5334
  }
4830
5335
  try {
4831
- raw = execSync3(
5336
+ raw = execSync4(
4832
5337
  `git diff --no-index --no-color -- "${SOURCE_ROOT}" "${wp}"`,
4833
5338
  { encoding: "utf8", maxBuffer: 4 * 1024 * 1024, timeout: 15e3 }
4834
5339
  );
@@ -4876,43 +5381,17 @@ function registerMiscRoutes(app, state) {
4876
5381
  });
4877
5382
  app.get("/api/git/status", async (c) => {
4878
5383
  try {
4879
- const isGit = (() => {
4880
- try {
4881
- execSync3("git rev-parse --git-dir", { cwd: TARGET_ROOT, stdio: "pipe" });
4882
- return true;
4883
- } catch {
4884
- return false;
4885
- }
4886
- })();
4887
- if (!isGit) return c.json({ isGit: false, branch: null, hasCommits: false });
4888
- const branch = (() => {
4889
- try {
4890
- return execSync3("git rev-parse --abbrev-ref HEAD", { cwd: TARGET_ROOT, encoding: "utf8", stdio: "pipe" }).trim();
4891
- } catch {
4892
- return null;
4893
- }
4894
- })();
4895
- const hasCommits = (() => {
4896
- try {
4897
- execSync3("git rev-parse HEAD", { cwd: TARGET_ROOT, stdio: "pipe" });
4898
- return true;
4899
- } catch {
4900
- return false;
4901
- }
4902
- })();
4903
- return c.json({ isGit: true, branch, hasCommits });
5384
+ return c.json(getGitRepoStatus(TARGET_ROOT));
4904
5385
  } catch (error) {
4905
5386
  return c.json({ ok: false, error: String(error) }, 500);
4906
5387
  }
4907
5388
  });
4908
5389
  app.post("/api/git/init", async (c) => {
4909
5390
  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();
4913
- state.config.defaultBranch = branch;
5391
+ const status = initializeGitRepoForWorktrees(TARGET_ROOT);
5392
+ state.config.defaultBranch = status.branch || state.config.defaultBranch || "main";
4914
5393
  await persistState(state);
4915
- return c.json({ ok: true, branch });
5394
+ return c.json({ ok: true, ...status });
4916
5395
  } catch (error) {
4917
5396
  return c.json({ ok: false, error: error instanceof Error ? error.message : String(error) }, 500);
4918
5397
  }
@@ -4923,7 +5402,7 @@ function registerMiscRoutes(app, state) {
4923
5402
  if (!branchName || !/^[a-zA-Z0-9/_.-]+$/.test(branchName)) {
4924
5403
  return c.json({ ok: false, error: "Invalid branch name." }, 400);
4925
5404
  }
4926
- execSync3(`git checkout -b "${branchName}"`, { cwd: TARGET_ROOT, stdio: "pipe" });
5405
+ execSync4(`git checkout -b "${branchName}"`, { cwd: TARGET_ROOT, stdio: "pipe" });
4927
5406
  state.config.defaultBranch = branchName;
4928
5407
  await persistState(state);
4929
5408
  return c.json({ ok: true, defaultBranch: branchName });
@@ -4931,6 +5410,26 @@ function registerMiscRoutes(app, state) {
4931
5410
  return c.json({ ok: false, error: error instanceof Error ? error.message : String(error) }, 500);
4932
5411
  }
4933
5412
  });
5413
+ app.post("/api/git/switch", async (c) => {
5414
+ try {
5415
+ const { branchName } = await c.req.json();
5416
+ if (!branchName || !/^[a-zA-Z0-9/_.-]+$/.test(branchName)) {
5417
+ return c.json({ ok: false, error: "Invalid branch name." }, 400);
5418
+ }
5419
+ let created = false;
5420
+ try {
5421
+ execSync4(`git checkout "${branchName}"`, { cwd: TARGET_ROOT, stdio: "pipe" });
5422
+ } catch {
5423
+ execSync4(`git checkout -b "${branchName}"`, { cwd: TARGET_ROOT, stdio: "pipe" });
5424
+ created = true;
5425
+ }
5426
+ state.config.defaultBranch = branchName;
5427
+ await persistState(state);
5428
+ return c.json({ ok: true, defaultBranch: branchName, created });
5429
+ } catch (error) {
5430
+ return c.json({ ok: false, error: error instanceof Error ? error.message : String(error) }, 500);
5431
+ }
5432
+ });
4934
5433
  app.get("/api/events/feed", async (c) => {
4935
5434
  const since = c.req.query("since");
4936
5435
  const issueId = c.req.query("issueId");
@@ -4944,11 +5443,11 @@ function registerMiscRoutes(app, state) {
4944
5443
  });
4945
5444
  app.get("/api/gitignore/status", async (c) => {
4946
5445
  try {
4947
- const gitignorePath = join14(TARGET_ROOT, ".gitignore");
4948
- if (!existsSync10(gitignorePath)) {
5446
+ const gitignorePath = join15(TARGET_ROOT, ".gitignore");
5447
+ if (!existsSync11(gitignorePath)) {
4949
5448
  return c.json({ exists: false, hasFifony: false });
4950
5449
  }
4951
- const content = readFileSync7(gitignorePath, "utf-8");
5450
+ const content = readFileSync8(gitignorePath, "utf-8");
4952
5451
  const lines = content.split("\n").map((l) => l.trim());
4953
5452
  const hasFifony = lines.some((l) => l === ".fifony" || l === ".fifony/" || l === "/.fifony" || l === "/.fifony/");
4954
5453
  return c.json({ exists: true, hasFifony });
@@ -4959,12 +5458,12 @@ function registerMiscRoutes(app, state) {
4959
5458
  });
4960
5459
  app.post("/api/gitignore/add", async (c) => {
4961
5460
  try {
4962
- const gitignorePath = join14(TARGET_ROOT, ".gitignore");
4963
- if (!existsSync10(gitignorePath)) {
4964
- writeFileSync9(gitignorePath, "# Fifony state directory\n.fifony/\n", "utf-8");
5461
+ const gitignorePath = join15(TARGET_ROOT, ".gitignore");
5462
+ if (!existsSync11(gitignorePath)) {
5463
+ writeFileSync10(gitignorePath, "# Fifony state directory\n.fifony/\n", "utf-8");
4965
5464
  return c.json({ ok: true, created: true });
4966
5465
  }
4967
- const content = readFileSync7(gitignorePath, "utf-8");
5466
+ const content = readFileSync8(gitignorePath, "utf-8");
4968
5467
  const lines = content.split("\n").map((l) => l.trim());
4969
5468
  const hasFifony = lines.some((l) => l === ".fifony" || l === ".fifony/" || l === "/.fifony" || l === "/.fifony/");
4970
5469
  if (hasFifony) {
@@ -4981,6 +5480,51 @@ function registerMiscRoutes(app, state) {
4981
5480
  return c.json({ ok: false, error: "Failed to update .gitignore" }, 500);
4982
5481
  }
4983
5482
  });
5483
+ app.get("/api/issues/:id/outputs", async (c) => {
5484
+ try {
5485
+ const issueId = parseIssue(c);
5486
+ if (!issueId) return c.json({ ok: false, error: "Issue id is required." }, 400);
5487
+ const issue = findIssue(state, issueId);
5488
+ if (!issue) return c.json({ ok: false, error: "Issue not found." }, 404);
5489
+ const wp = issue.workspacePath;
5490
+ if (!wp) return c.json({ ok: true, files: [] });
5491
+ const outputsDir = join15(wp, "outputs");
5492
+ if (!existsSync11(outputsDir)) return c.json({ ok: true, files: [] });
5493
+ const entries = readdirSync3(outputsDir).filter((f) => f.endsWith(".stdout.log")).map((f) => {
5494
+ try {
5495
+ const s = statSync(join15(outputsDir, f));
5496
+ return { name: f, size: s.size };
5497
+ } catch {
5498
+ return { name: f, size: 0 };
5499
+ }
5500
+ });
5501
+ return c.json({ ok: true, files: entries });
5502
+ } catch (error) {
5503
+ return c.json({ ok: false, error: error instanceof Error ? error.message : String(error) }, 500);
5504
+ }
5505
+ });
5506
+ app.get("/api/issues/:id/outputs/:filename", async (c) => {
5507
+ try {
5508
+ const issueId = parseIssue(c);
5509
+ if (!issueId) return c.json({ ok: false, error: "Issue id is required." }, 400);
5510
+ const issue = findIssue(state, issueId);
5511
+ if (!issue) return c.json({ ok: false, error: "Issue not found." }, 404);
5512
+ const filename = c.req.param?.("filename") ?? c.req.params?.filename ?? "";
5513
+ if (!filename) return c.json({ ok: false, error: "Filename is required." }, 400);
5514
+ const safeName = basename3(filename);
5515
+ if (safeName !== filename || !safeName.endsWith(".stdout.log")) {
5516
+ return c.json({ ok: false, error: "Invalid filename." }, 400);
5517
+ }
5518
+ const wp = issue.workspacePath;
5519
+ if (!wp) return c.json({ ok: false, error: "No workspace found." }, 404);
5520
+ const filePath = join15(wp, "outputs", safeName);
5521
+ if (!existsSync11(filePath)) return c.json({ ok: false, error: "Output file not found." }, 404);
5522
+ const content = readFileSync8(filePath, "utf8");
5523
+ return new Response(content, { headers: { "Content-Type": "text/plain; charset=utf-8" } });
5524
+ } catch (error) {
5525
+ return c.json({ ok: false, error: error instanceof Error ? error.message : String(error) }, 500);
5526
+ }
5527
+ });
4984
5528
  app.post("/api/attachments/upload", async (c) => {
4985
5529
  try {
4986
5530
  const payload = await c.req.json();
@@ -4988,15 +5532,15 @@ function registerMiscRoutes(app, state) {
4988
5532
  return c.json({ ok: false, error: "No files provided." }, 400);
4989
5533
  }
4990
5534
  const uploadId = randomUUID2();
4991
- const uploadDir = join14(ATTACHMENTS_ROOT, "temp", uploadId);
4992
- mkdirSync5(uploadDir, { recursive: true });
5535
+ const uploadDir = join15(ATTACHMENTS_ROOT, "temp", uploadId);
5536
+ mkdirSync6(uploadDir, { recursive: true });
4993
5537
  const paths = [];
4994
5538
  for (const file of payload.files) {
4995
5539
  if (typeof file.data !== "string" || !file.name) continue;
4996
5540
  const safeExt = extname2(file.name).replace(/[^a-z0-9.]/gi, "").slice(0, 10) || ".bin";
4997
5541
  const safeName = `${randomUUID2()}${safeExt}`;
4998
- const dest = join14(uploadDir, safeName);
4999
- writeFileSync9(dest, Buffer.from(file.data, "base64"));
5542
+ const dest = join15(uploadDir, safeName);
5543
+ writeFileSync10(dest, Buffer.from(file.data, "base64"));
5000
5544
  paths.push(dest);
5001
5545
  }
5002
5546
  return c.json({ ok: true, paths, uploadId });
@@ -5051,10 +5595,10 @@ async function startApiServer(state, port) {
5051
5595
  }
5052
5596
  setApiRuntimeContext(state);
5053
5597
  const serveTextFile = (filePath, contentType, cacheControl = "no-cache") => {
5054
- if (!existsSync11(filePath)) {
5598
+ if (!existsSync12(filePath)) {
5055
5599
  return new Response("Not found", { status: 404 });
5056
5600
  }
5057
- return new Response(readFileSync8(filePath), {
5601
+ return new Response(readFileSync9(filePath), {
5058
5602
  headers: {
5059
5603
  "content-type": contentType,
5060
5604
  "cache-control": cacheControl
@@ -5062,10 +5606,10 @@ async function startApiServer(state, port) {
5062
5606
  });
5063
5607
  };
5064
5608
  const serveAppShell = () => {
5065
- if (!existsSync11(FRONTEND_INDEX)) {
5609
+ if (!existsSync12(FRONTEND_INDEX)) {
5066
5610
  return new Response("Not found", { status: 404 });
5067
5611
  }
5068
- const html = readFileSync8(FRONTEND_INDEX, "utf8").replace('href="/assets/manifest.webmanifest"', 'href="/manifest.webmanifest"').replaceAll('href="/assets/icon.svg"', 'href="/icon.svg"');
5612
+ const html = readFileSync9(FRONTEND_INDEX, "utf8").replace('href="/assets/manifest.webmanifest"', 'href="/manifest.webmanifest"').replaceAll('href="/assets/icon.svg"', 'href="/icon.svg"');
5069
5613
  return new Response(html, {
5070
5614
  headers: {
5071
5615
  "content-type": "text/html; charset=utf-8",
@@ -5086,6 +5630,9 @@ async function startApiServer(state, port) {
5086
5630
  port,
5087
5631
  host: "0.0.0.0",
5088
5632
  versionPrefix: false,
5633
+ metrics: {
5634
+ logLevel: "info"
5635
+ },
5089
5636
  // HTTP + WebSocket on the same port via listeners
5090
5637
  listeners: [{
5091
5638
  bind: { host: "0.0.0.0", port },
@@ -5434,7 +5981,6 @@ async function persistState(state) {
5434
5981
  broadcastToWebSocketClients({
5435
5982
  type: "state:update",
5436
5983
  metrics: state.metrics,
5437
- capabilities: computeCapabilityCounts(state.issues),
5438
5984
  issues: state.issues,
5439
5985
  events: state.events.slice(0, 50),
5440
5986
  updatedAt: state.updatedAt
@@ -5578,12 +6124,6 @@ async function closeStateStore() {
5578
6124
 
5579
6125
  // src/domains/project.ts
5580
6126
  var SETTING_ID_PROJECT_NAME = "system.projectName";
5581
- var LEGACY_PROJECT_SETTING_IDS = [
5582
- "runtime.projectName",
5583
- "ui.projectName",
5584
- "projectName",
5585
- "project.name"
5586
- ];
5587
6127
  function normalizeProjectName(value) {
5588
6128
  return typeof value === "string" ? value.trim().replace(/\s+/g, " ") : "";
5589
6129
  }
@@ -5593,14 +6133,7 @@ function detectProjectName(targetRoot) {
5593
6133
  return normalizeProjectName(basename4(normalizedPath));
5594
6134
  }
5595
6135
  function readSavedProjectName(settings) {
5596
- const settingIds = [SETTING_ID_PROJECT_NAME, ...LEGACY_PROJECT_SETTING_IDS];
5597
- for (const id of settingIds) {
5598
- const value = normalizeProjectName(settings.find((setting) => setting.id === id)?.value);
5599
- if (value) {
5600
- return value;
5601
- }
5602
- }
5603
- return "";
6136
+ return normalizeProjectName(settings.find((s) => s.id === SETTING_ID_PROJECT_NAME)?.value);
5604
6137
  }
5605
6138
  function buildQueueTitle(projectName) {
5606
6139
  const normalizedProjectName = normalizeProjectName(projectName);
@@ -5618,7 +6151,7 @@ function resolveProjectMetadata(settings, targetRoot) {
5618
6151
  };
5619
6152
  }
5620
6153
  function scanProjectFiles(targetRoot) {
5621
- const check = (rel) => existsSync12(join15(targetRoot, rel));
6154
+ const check = (rel) => existsSync13(join16(targetRoot, rel));
5622
6155
  const files = {
5623
6156
  claudeMd: check("CLAUDE.md"),
5624
6157
  claudeDir: check(".claude"),
@@ -5639,10 +6172,10 @@ function scanProjectFiles(targetRoot) {
5639
6172
  };
5640
6173
  const existingAgents = [];
5641
6174
  for (const agentDir of [".claude/agents", ".codex/agents"]) {
5642
- const fullPath = join15(targetRoot, agentDir);
5643
- if (!existsSync12(fullPath)) continue;
6175
+ const fullPath = join16(targetRoot, agentDir);
6176
+ if (!existsSync13(fullPath)) continue;
5644
6177
  try {
5645
- const entries = readdirSync3(fullPath);
6178
+ const entries = readdirSync4(fullPath);
5646
6179
  for (const entry of entries) {
5647
6180
  if (entry.endsWith(".md")) {
5648
6181
  existingAgents.push(basename4(entry, ".md"));
@@ -5653,13 +6186,13 @@ function scanProjectFiles(targetRoot) {
5653
6186
  }
5654
6187
  const existingSkills = [];
5655
6188
  for (const skillDir of [".claude/skills", ".codex/skills"]) {
5656
- const fullPath = join15(targetRoot, skillDir);
5657
- if (!existsSync12(fullPath)) continue;
6189
+ const fullPath = join16(targetRoot, skillDir);
6190
+ if (!existsSync13(fullPath)) continue;
5658
6191
  try {
5659
- const entries = readdirSync3(fullPath);
6192
+ const entries = readdirSync4(fullPath);
5660
6193
  for (const entry of entries) {
5661
- const skillFile = join15(fullPath, entry, "SKILL.md");
5662
- if (existsSync12(skillFile)) {
6194
+ const skillFile = join16(fullPath, entry, "SKILL.md");
6195
+ if (existsSync13(skillFile)) {
5663
6196
  existingSkills.push(entry);
5664
6197
  }
5665
6198
  }
@@ -5667,20 +6200,20 @@ function scanProjectFiles(targetRoot) {
5667
6200
  }
5668
6201
  }
5669
6202
  let readmeExcerpt = "";
5670
- const readmePath = join15(targetRoot, "README.md");
5671
- if (existsSync12(readmePath)) {
6203
+ const readmePath = join16(targetRoot, "README.md");
6204
+ if (existsSync13(readmePath)) {
5672
6205
  try {
5673
- const content = readFileSync9(readmePath, "utf8");
6206
+ const content = readFileSync10(readmePath, "utf8");
5674
6207
  readmeExcerpt = content.slice(0, 200).trim();
5675
6208
  } catch {
5676
6209
  }
5677
6210
  }
5678
6211
  let packageName = "";
5679
6212
  let packageDescription = "";
5680
- const pkgPath = join15(targetRoot, "package.json");
5681
- if (existsSync12(pkgPath)) {
6213
+ const pkgPath = join16(targetRoot, "package.json");
6214
+ if (existsSync13(pkgPath)) {
5682
6215
  try {
5683
- const pkg = JSON.parse(readFileSync9(pkgPath, "utf8"));
6216
+ const pkg = JSON.parse(readFileSync10(pkgPath, "utf8"));
5684
6217
  packageName = typeof pkg.name === "string" ? pkg.name : "";
5685
6218
  packageDescription = typeof pkg.description === "string" ? pkg.description : "";
5686
6219
  } catch {
@@ -5722,39 +6255,39 @@ function buildFallbackAnalysis(targetRoot) {
5722
6255
  let description = "";
5723
6256
  let readmeExcerpt = "";
5724
6257
  for (const readmeFile of ["README.md", "README.rst", "README.txt", "README"]) {
5725
- const p = join15(targetRoot, readmeFile);
5726
- if (existsSync12(p)) {
6258
+ const p = join16(targetRoot, readmeFile);
6259
+ if (existsSync13(p)) {
5727
6260
  try {
5728
- readmeExcerpt = readFileSync9(p, "utf8").slice(0, 300).trim();
6261
+ readmeExcerpt = readFileSync10(p, "utf8").slice(0, 300).trim();
5729
6262
  break;
5730
6263
  } catch {
5731
6264
  }
5732
6265
  }
5733
6266
  }
5734
- const pkgPath = join15(targetRoot, "package.json");
5735
- if (existsSync12(pkgPath)) {
6267
+ const pkgPath = join16(targetRoot, "package.json");
6268
+ if (existsSync13(pkgPath)) {
5736
6269
  try {
5737
- const pkg = JSON.parse(readFileSync9(pkgPath, "utf8"));
6270
+ const pkg = JSON.parse(readFileSync10(pkgPath, "utf8"));
5738
6271
  const name = typeof pkg.name === "string" ? pkg.name : "";
5739
6272
  const desc = typeof pkg.description === "string" ? pkg.description : "";
5740
6273
  if (desc) description = name ? `${name}: ${desc}` : desc;
5741
6274
  } catch {
5742
6275
  }
5743
6276
  }
5744
- const cargoPath = join15(targetRoot, "Cargo.toml");
5745
- if (!description && existsSync12(cargoPath)) {
6277
+ const cargoPath = join16(targetRoot, "Cargo.toml");
6278
+ if (!description && existsSync13(cargoPath)) {
5746
6279
  try {
5747
- const content = readFileSync9(cargoPath, "utf8");
6280
+ const content = readFileSync10(cargoPath, "utf8");
5748
6281
  const descMatch = content.match(/^description\s*=\s*"([^"]+)"/m);
5749
6282
  const nameMatch = content.match(/^name\s*=\s*"([^"]+)"/m);
5750
6283
  if (descMatch) description = nameMatch ? `${nameMatch[1]}: ${descMatch[1]}` : descMatch[1];
5751
6284
  } catch {
5752
6285
  }
5753
6286
  }
5754
- const pyprojectPath = join15(targetRoot, "pyproject.toml");
5755
- if (!description && existsSync12(pyprojectPath)) {
6287
+ const pyprojectPath = join16(targetRoot, "pyproject.toml");
6288
+ if (!description && existsSync13(pyprojectPath)) {
5756
6289
  try {
5757
- const content = readFileSync9(pyprojectPath, "utf8");
6290
+ const content = readFileSync10(pyprojectPath, "utf8");
5758
6291
  const descMatch = content.match(/^description\s*=\s*"([^"]+)"/m);
5759
6292
  const nameMatch = content.match(/^name\s*=\s*"([^"]+)"/m);
5760
6293
  if (descMatch) description = nameMatch ? `${nameMatch[1]}: ${descMatch[1]}` : descMatch[1];
@@ -5767,7 +6300,7 @@ function buildFallbackAnalysis(targetRoot) {
5767
6300
  let language = "unknown";
5768
6301
  const stack = [];
5769
6302
  for (const [file, signal] of Object.entries(BUILD_FILE_SIGNALS)) {
5770
- if (existsSync12(join15(targetRoot, file))) {
6303
+ if (existsSync13(join16(targetRoot, file))) {
5771
6304
  if (language === "unknown" && signal.language !== "unknown") {
5772
6305
  language = signal.language;
5773
6306
  }
@@ -5850,7 +6383,7 @@ function isBlockedProjectAnalysisResponse(analysis) {
5850
6383
  var ANALYSIS_CACHE_TTL_MS = 24 * 60 * 60 * 1e3;
5851
6384
  function computeProjectHash(targetRoot) {
5852
6385
  const buildFiles = Object.keys(BUILD_FILE_SIGNALS);
5853
- const found = buildFiles.filter((f) => existsSync12(join15(targetRoot, f))).sort();
6386
+ const found = buildFiles.filter((f) => existsSync13(join16(targetRoot, f))).sort();
5854
6387
  return createHash("sha256").update(found.join(",")).digest("hex").slice(0, 16);
5855
6388
  }
5856
6389
  async function loadCachedAnalysis(targetRoot) {
@@ -5902,10 +6435,10 @@ async function analyzeProjectWithCli(provider, targetRoot, options) {
5902
6435
  );
5903
6436
  return buildFallbackAnalysis(targetRoot);
5904
6437
  }
5905
- const tempDir = mkdtempSync4(join15(tmpdir4(), "fifony-scan-"));
5906
- const promptFile = join15(tempDir, "fifony-scan-prompt.txt");
6438
+ const tempDir = mkdtempSync4(join16(tmpdir4(), "fifony-scan-"));
6439
+ const promptFile = join16(tempDir, "fifony-scan-prompt.txt");
5907
6440
  const analysisPrompt = await renderPrompt("project-analysis");
5908
- writeFileSync10(promptFile, analysisPrompt, "utf8");
6441
+ writeFileSync11(promptFile, analysisPrompt, "utf8");
5909
6442
  const processEnv = {};
5910
6443
  for (const [key, value] of Object.entries(env3)) {
5911
6444
  if (typeof value === "string") processEnv[key] = value;
@@ -6023,6 +6556,12 @@ var DEFAULT_REFERENCE_REPOSITORIES = [
6023
6556
  name: "pbakaus/impeccable",
6024
6557
  url: "https://github.com/pbakaus/impeccable.git",
6025
6558
  description: "Frontend polish and impeccable-style quality workflows."
6559
+ },
6560
+ {
6561
+ id: "everything-claude-code",
6562
+ name: "affaan-m/everything-claude-code",
6563
+ url: "https://github.com/affaan-m/everything-claude-code.git",
6564
+ description: "28 specialized agents, 116 skills, and 59 commands \u2014 agent harness performance system."
6026
6565
  }
6027
6566
  ];
6028
6567
  var REPOSITORY_ROOT = resolve2(homedir3(), ".fifony", "repositories");
@@ -6047,7 +6586,7 @@ var REFERENCE_REPOSITORY_PARSERS = {
6047
6586
  impeccable: collectImpeccableArtifacts
6048
6587
  };
6049
6588
  function runGit(args, cwd) {
6050
- return execFileSync("git", args, {
6589
+ return execFileSync2("git", args, {
6051
6590
  cwd,
6052
6591
  encoding: "utf8",
6053
6592
  stdio: "pipe",
@@ -6074,7 +6613,7 @@ function uniqueSuffix(base, used) {
6074
6613
  }
6075
6614
  function collectDirectoryEntries(path) {
6076
6615
  try {
6077
- return readdirSync3(path, { withFileTypes: true });
6616
+ return readdirSync4(path, { withFileTypes: true });
6078
6617
  } catch {
6079
6618
  return [];
6080
6619
  }
@@ -6100,7 +6639,7 @@ function isMarkdownFile(value, expectedName) {
6100
6639
  function isReferenceFrontMatterFile(filePath) {
6101
6640
  let source;
6102
6641
  try {
6103
- source = readFileSync9(filePath, "utf8");
6642
+ source = readFileSync10(filePath, "utf8");
6104
6643
  } catch {
6105
6644
  return false;
6106
6645
  }
@@ -6122,10 +6661,10 @@ function collectAgentArtifacts(agentsDir, usedNames, out) {
6122
6661
  const parent = slugify(basename4(dirname2(agentsDir)));
6123
6662
  const entries = collectDirectoryEntries(agentsDir);
6124
6663
  for (const entry of entries) {
6125
- const itemPath = join15(agentsDir, entry.name);
6664
+ const itemPath = join16(agentsDir, entry.name);
6126
6665
  if (entry.isDirectory()) {
6127
- const nestedAgentSpec = join15(itemPath, "AGENT.md");
6128
- if (existsSync12(nestedAgentSpec)) {
6666
+ const nestedAgentSpec = join16(itemPath, "AGENT.md");
6667
+ if (existsSync13(nestedAgentSpec)) {
6129
6668
  const name2 = uniqueSuffix(`${parent}__${slugify(entry.name)}`, usedNames);
6130
6669
  out.push({ kind: "agent", sourcePath: nestedAgentSpec, targetName: name2 });
6131
6670
  }
@@ -6146,10 +6685,10 @@ function collectSkillArtifacts(skillsDir, usedNames, out) {
6146
6685
  const parent = slugify(basename4(dirname2(skillsDir)));
6147
6686
  const entries = collectDirectoryEntries(skillsDir);
6148
6687
  for (const entry of entries) {
6149
- const itemPath = join15(skillsDir, entry.name);
6688
+ const itemPath = join16(skillsDir, entry.name);
6150
6689
  if (entry.isDirectory()) {
6151
- const skillFile = join15(itemPath, "SKILL.md");
6152
- if (existsSync12(skillFile)) {
6690
+ const skillFile = join16(itemPath, "SKILL.md");
6691
+ if (existsSync13(skillFile)) {
6153
6692
  const name = uniqueSuffix(`${parent}__${slugify(entry.name)}`, usedNames);
6154
6693
  out.push({ kind: "skill", sourcePath: skillFile, targetName: name });
6155
6694
  }
@@ -6176,7 +6715,7 @@ function collectStandardArtifacts(repoPath) {
6176
6715
  for (const entry of entries) {
6177
6716
  if (!entry.isDirectory()) continue;
6178
6717
  if (SKIP_DIRS.has(entry.name)) continue;
6179
- const childPath = join15(state.path, entry.name);
6718
+ const childPath = join16(state.path, entry.name);
6180
6719
  if (entry.name === "agents") {
6181
6720
  collectAgentArtifacts(childPath, agentsUsed, artifacts);
6182
6721
  }
@@ -6204,13 +6743,13 @@ function collectAgencyArtifacts(repoPath) {
6204
6743
  if (SKIP_DIRS.has(entry.name) || AGENCY_AGENTS_EXCLUDED_DIRS.has(entry.name)) {
6205
6744
  continue;
6206
6745
  }
6207
- queue.push({ path: join15(state.path, entry.name), depth: state.depth + 1 });
6746
+ queue.push({ path: join16(state.path, entry.name), depth: state.depth + 1 });
6208
6747
  continue;
6209
6748
  }
6210
- if (!isMarkdownFile(entry.name, "readme.md") || !isReferenceFrontMatterFile(join15(state.path, entry.name))) {
6749
+ if (!isMarkdownFile(entry.name, "readme.md") || !isReferenceFrontMatterFile(join16(state.path, entry.name))) {
6211
6750
  continue;
6212
6751
  }
6213
- const itemPath = join15(state.path, entry.name);
6752
+ const itemPath = join16(state.path, entry.name);
6214
6753
  const targetName = uniqueSuffix(buildRelativeArtifactName(repoPath, itemPath), agentsUsed);
6215
6754
  artifacts.push({
6216
6755
  kind: "agent",
@@ -6224,13 +6763,13 @@ function collectAgencyArtifacts(repoPath) {
6224
6763
  function collectImpeccableArtifacts(repoPath) {
6225
6764
  const skillsUsed = /* @__PURE__ */ new Set();
6226
6765
  const artifacts = [];
6227
- const sourceSkills = join15(repoPath, "source", "skills");
6228
- if (existsSync12(sourceSkills)) {
6766
+ const sourceSkills = join16(repoPath, "source", "skills");
6767
+ if (existsSync13(sourceSkills)) {
6229
6768
  collectSkillArtifacts(sourceSkills, skillsUsed, artifacts);
6230
6769
  return artifacts;
6231
6770
  }
6232
- const claudeSkills = join15(repoPath, ".claude", "skills");
6233
- if (existsSync12(claudeSkills)) {
6771
+ const claudeSkills = join16(repoPath, ".claude", "skills");
6772
+ if (existsSync13(claudeSkills)) {
6234
6773
  collectSkillArtifacts(claudeSkills, skillsUsed, artifacts);
6235
6774
  }
6236
6775
  return artifacts;
@@ -6256,19 +6795,19 @@ function getReferenceRepositoriesRoot() {
6256
6795
  }
6257
6796
  function listReferenceRepositories() {
6258
6797
  return DEFAULT_REFERENCE_REPOSITORIES.map((repo) => {
6259
- const path = join15(REPOSITORY_ROOT, repo.id);
6798
+ const path = join16(REPOSITORY_ROOT, repo.id);
6260
6799
  const status = {
6261
6800
  id: repo.id,
6262
6801
  name: repo.name,
6263
6802
  url: repo.url,
6264
6803
  path,
6265
- present: existsSync12(path),
6804
+ present: existsSync13(path),
6266
6805
  synced: false
6267
6806
  };
6268
6807
  if (!status.present) {
6269
6808
  return status;
6270
6809
  }
6271
- if (!existsSync12(join15(path, ".git"))) {
6810
+ if (!existsSync13(join16(path, ".git"))) {
6272
6811
  status.error = "Path exists but is not a git repo";
6273
6812
  return status;
6274
6813
  }
@@ -6292,7 +6831,7 @@ function resolveReferenceRepository(query) {
6292
6831
  }
6293
6832
  function syncReferenceRepositories(repositoryId) {
6294
6833
  const root = REPOSITORY_ROOT;
6295
- mkdirSync6(root, { recursive: true });
6834
+ mkdirSync7(root, { recursive: true });
6296
6835
  const repos = repositoryId ? [resolveReferenceRepository(repositoryId)] : DEFAULT_REFERENCE_REPOSITORIES;
6297
6836
  const selected = repos.filter((repo) => Boolean(repo));
6298
6837
  if (repositoryId && selected.length === 0) {
@@ -6300,9 +6839,9 @@ function syncReferenceRepositories(repositoryId) {
6300
6839
  }
6301
6840
  const results = [];
6302
6841
  for (const repo of selected) {
6303
- const target = join15(root, repo.id);
6842
+ const target = join16(root, repo.id);
6304
6843
  const candidates = [repo.url, ...repo.fallbackUrls ?? []];
6305
- if (!existsSync12(target)) {
6844
+ if (!existsSync13(target)) {
6306
6845
  let cloneError;
6307
6846
  for (const candidate of candidates) {
6308
6847
  try {
@@ -6329,7 +6868,7 @@ function syncReferenceRepositories(repositoryId) {
6329
6868
  }
6330
6869
  continue;
6331
6870
  }
6332
- if (!existsSync12(join15(target, ".git"))) {
6871
+ if (!existsSync13(join16(target, ".git"))) {
6333
6872
  results.push({
6334
6873
  id: repo.id,
6335
6874
  path: target,
@@ -6364,14 +6903,14 @@ function importReferenceArtifacts(repositoryId, workspaceRoot, options) {
6364
6903
  if (!repository) {
6365
6904
  throw new Error(`Unknown reference repository: ${repositoryId}`);
6366
6905
  }
6367
- const localPath = join15(REPOSITORY_ROOT, repository.id);
6368
- if (!existsSync12(localPath)) {
6906
+ const localPath = join16(REPOSITORY_ROOT, repository.id);
6907
+ if (!existsSync13(localPath)) {
6369
6908
  throw new Error(`Repository not synced yet: ${repository.id}. Run 'fifony onboarding sync --repository ${repository.id}' first.`);
6370
6909
  }
6371
6910
  const basePath = resolve2(workspaceRoot);
6372
- const targetBase = options.importToGlobal ? join15(homedir3(), ".codex") : join15(basePath, ".codex");
6373
- const agentsDir = join15(targetBase, "agents");
6374
- const skillsDir = join15(targetBase, "skills");
6911
+ const targetBase = options.importToGlobal ? join16(homedir3(), ".codex") : join16(basePath, ".codex");
6912
+ const agentsDir = join16(targetBase, "agents");
6913
+ const skillsDir = join16(targetBase, "skills");
6375
6914
  const artifacts = collectArtifacts(localPath, repository.id);
6376
6915
  const filtered = options.kind === "all" ? artifacts : artifacts.filter((artifact) => artifact.kind === options.kind.slice(0, -1));
6377
6916
  const summary = {
@@ -6389,16 +6928,16 @@ function importReferenceArtifacts(repositoryId, workspaceRoot, options) {
6389
6928
  return summary;
6390
6929
  }
6391
6930
  if (!options.dryRun) {
6392
- mkdirSync6(targetBase, { recursive: true });
6393
- mkdirSync6(agentsDir, { recursive: true });
6394
- mkdirSync6(skillsDir, { recursive: true });
6931
+ mkdirSync7(targetBase, { recursive: true });
6932
+ mkdirSync7(agentsDir, { recursive: true });
6933
+ mkdirSync7(skillsDir, { recursive: true });
6395
6934
  }
6396
6935
  for (const artifact of filtered) {
6397
6936
  try {
6398
- const source = readFileSync9(artifact.sourcePath, "utf8");
6937
+ const source = readFileSync10(artifact.sourcePath, "utf8");
6399
6938
  if (artifact.kind === "agent") {
6400
- const target = join15(agentsDir, `${artifact.targetName}.md`);
6401
- if (!options.overwrite && existsSync12(target)) {
6939
+ const target = join16(agentsDir, `${artifact.targetName}.md`);
6940
+ if (!options.overwrite && existsSync13(target)) {
6402
6941
  summary.skippedAgents.push(artifact.targetName);
6403
6942
  continue;
6404
6943
  }
@@ -6406,12 +6945,12 @@ function importReferenceArtifacts(repositoryId, workspaceRoot, options) {
6406
6945
  summary.importedAgents.push(artifact.targetName);
6407
6946
  continue;
6408
6947
  }
6409
- writeFileSync10(target, source, "utf8");
6948
+ writeFileSync11(target, source, "utf8");
6410
6949
  summary.importedAgents.push(artifact.targetName);
6411
6950
  } else {
6412
- const targetDir = join15(skillsDir, artifact.targetName);
6413
- const target = join15(targetDir, "SKILL.md");
6414
- if (!options.overwrite && existsSync12(target)) {
6951
+ const targetDir = join16(skillsDir, artifact.targetName);
6952
+ const target = join16(targetDir, "SKILL.md");
6953
+ if (!options.overwrite && existsSync13(target)) {
6415
6954
  summary.skippedSkills.push(artifact.targetName);
6416
6955
  continue;
6417
6956
  }
@@ -6419,8 +6958,8 @@ function importReferenceArtifacts(repositoryId, workspaceRoot, options) {
6419
6958
  summary.importedSkills.push(artifact.targetName);
6420
6959
  continue;
6421
6960
  }
6422
- mkdirSync6(targetDir, { recursive: true });
6423
- writeFileSync10(target, source, "utf8");
6961
+ mkdirSync7(targetDir, { recursive: true });
6962
+ writeFileSync11(target, source, "utf8");
6424
6963
  summary.importedSkills.push(artifact.targetName);
6425
6964
  }
6426
6965
  } catch (error) {
@@ -6534,6 +7073,37 @@ function validateConfig(config) {
6534
7073
  }
6535
7074
 
6536
7075
  // src/domains/issues.ts
7076
+ function normalizeIssue(raw) {
7077
+ const id = toStringValue(raw.id, "");
7078
+ if (!id) return null;
7079
+ const createdAt = toStringValue(raw.createdAt, now());
7080
+ const updatedAt = toStringValue(raw.updatedAt, createdAt);
7081
+ const issue = {
7082
+ id,
7083
+ identifier: toStringValue(raw.identifier, id),
7084
+ title: toStringValue(raw.title, `Issue ${id}`),
7085
+ description: toStringValue(raw.description, ""),
7086
+ state: normalizeState(raw.state, raw.plan && typeof raw.plan === "object" ? "PendingApproval" : "Planning"),
7087
+ branchName: toStringValue(raw.branchName),
7088
+ url: toStringValue(raw.url),
7089
+ assigneeId: toStringValue(raw.assigneeId),
7090
+ labels: toStringArray(raw.labels),
7091
+ paths: toStringArray(raw.paths),
7092
+ blockedBy: toStringArray(raw.blockedBy),
7093
+ assignedToWorker: toBooleanValue(raw.assignedToWorker, true),
7094
+ createdAt,
7095
+ updatedAt,
7096
+ history: [],
7097
+ attempts: toNumberValue(raw.attempts, 0),
7098
+ maxAttempts: toNumberValue(raw.maxAttempts, 3),
7099
+ nextRetryAt: toStringValue(raw.nextRetryAt),
7100
+ planVersion: 0,
7101
+ executeAttempt: 0,
7102
+ reviewAttempt: 0,
7103
+ planHistory: []
7104
+ };
7105
+ return issue;
7106
+ }
6537
7107
  function nextLocalIssueId(issues) {
6538
7108
  const maxId = issues.reduce((current, issue) => {
6539
7109
  const match = issue.identifier.match(/^#(\d+)$/);
@@ -6551,13 +7121,12 @@ function createIssueFromPayload(payload, issues, defaultBranch) {
6551
7121
  const blockedBy = toStringArray(payload.blockedBy);
6552
7122
  const paths = toStringArray(payload.paths);
6553
7123
  const images = toStringArray(payload.images);
6554
- const initialState = parseIssueState(payload.state) ?? (payload.plan ? "Planned" : "Planning");
7124
+ const initialState = parseIssueState(payload.state) ?? (payload.plan ? "PendingApproval" : "Planning");
6555
7125
  const issue = {
6556
7126
  id,
6557
7127
  identifier,
6558
7128
  title: toStringValue(payload.title, `Issue ${identifier}`),
6559
7129
  description: toStringValue(payload.description, ""),
6560
- priority: clamp(toNumberValue(payload.priority, 1), 1, 10),
6561
7130
  state: initialState,
6562
7131
  branchName: toStringValue(payload.branchName),
6563
7132
  baseBranch: toStringValue(payload.baseBranch) || defaultBranch,
@@ -6565,10 +7134,6 @@ function createIssueFromPayload(payload, issues, defaultBranch) {
6565
7134
  assigneeId: toStringValue(payload.assigneeId),
6566
7135
  labels: toStringArray(payload.labels),
6567
7136
  paths,
6568
- inferredPaths: [],
6569
- capabilityCategory: "",
6570
- capabilityOverlays: [],
6571
- capabilityRationale: [],
6572
7137
  blockedBy,
6573
7138
  assignedToWorker: true,
6574
7139
  createdAt,
@@ -6590,21 +7155,10 @@ function createIssueFromPayload(payload, issues, defaultBranch) {
6590
7155
  if (issue.plan.suggestedPaths?.length && !issue.paths?.length) {
6591
7156
  issue.paths = issue.plan.suggestedPaths;
6592
7157
  }
6593
- if (issue.plan.suggestedLabels?.length && !issue.labels?.length) {
6594
- issue.labels = issue.plan.suggestedLabels;
6595
- }
6596
7158
  if (issue.plan.suggestedEffort && !issue.effort) {
6597
7159
  issue.effort = issue.plan.suggestedEffort;
6598
7160
  }
6599
7161
  }
6600
- applyCapabilityMetadata(issue, resolveTaskCapabilities({
6601
- id: issue.id,
6602
- identifier: issue.identifier,
6603
- title: issue.title,
6604
- description: issue.description,
6605
- labels: issue.labels,
6606
- paths: issue.paths
6607
- }, getCapabilityRoutingOptions()));
6608
7162
  return issue;
6609
7163
  }
6610
7164
  function dedupHistoryEntries(issues) {
@@ -6628,13 +7182,10 @@ function buildRuntimeState(previous, config, projectMetadata = resolveProjectMet
6628
7182
  identifier: toStringValue(existing.identifier, existing.id),
6629
7183
  title: toStringValue(existing.title, `Issue ${toStringValue(existing.identifier, existing.id)}`),
6630
7184
  description: toStringValue(existing.description, ""),
6631
- state: normalizeState(existing.state, existing.plan ? "Planned" : "Planning"),
7185
+ state: normalizeState(existing.state, existing.plan ? "PendingApproval" : "Planning"),
6632
7186
  paths: toStringArray(existing.paths),
6633
- inferredPaths: toStringArray(existing.inferredPaths),
6634
7187
  labels: toStringArray(existing.labels),
6635
- capabilityOverlays: toStringArray(existing.capabilityOverlays),
6636
- capabilityRationale: toStringArray(existing.capabilityRationale),
6637
- blockedBy: toStringArray(existing.blockedBy).length > 0 ? toStringArray(existing.blockedBy) : toStringArray(existing.blocked_by),
7188
+ blockedBy: toStringArray(existing.blockedBy),
6638
7189
  history: Array.isArray(existing.history) ? existing.history : [],
6639
7190
  attempts: clamp(toNumberValue(existing.attempts, 0), 0, config.maxAttemptsDefault),
6640
7191
  maxAttempts: clamp(toNumberValue(existing.maxAttempts, config.maxAttemptsDefault), 1, config.maxAttemptsDefault),
@@ -6707,6 +7258,14 @@ async function transitionIssue(issue, event, context2 = {}) {
6707
7258
  logger.debug({ issueId: issue.id, identifier: issue.identifier, from: issue.state, event, context: context2 }, "[State] Issue transition");
6708
7259
  await executeTransition(issue, event, { ...context2, issue });
6709
7260
  }
7261
+ function issueDependenciesResolved(issue, allIssues) {
7262
+ if (issue.blockedBy.length === 0) return true;
7263
+ const map = new Map(allIssues.map((entry) => [entry.id, entry]));
7264
+ return issue.blockedBy.every((dependencyId) => {
7265
+ const dep = map.get(dependencyId);
7266
+ return dep?.state === "Approved" || dep?.state === "Merged";
7267
+ });
7268
+ }
6710
7269
  function getNextRetryAt(issue, baseMs) {
6711
7270
  const nextAttempt = issue.attempts + 1;
6712
7271
  const nextDelay = withRetryBackoff(nextAttempt, baseMs);
@@ -6724,7 +7283,7 @@ async function handleStatePatch(state, issue, payload) {
6724
7283
  for (const event of path) {
6725
7284
  await transitionIssue(issue, event, { note: `Manual state update: ${nextState}`, reason: toStringValue(payload.reason) });
6726
7285
  }
6727
- if (nextState === "Planned") {
7286
+ if (nextState === "PendingApproval") {
6728
7287
  issue.nextRetryAt = void 0;
6729
7288
  issue.lastError = void 0;
6730
7289
  }
@@ -6734,6 +7293,34 @@ async function handleStatePatch(state, issue, payload) {
6734
7293
  addEvent(state, issue.id, "manual", `Manual state transition to ${nextState}`);
6735
7294
  }
6736
7295
 
7296
+ // src/commands/request-rework.command.ts
7297
+ async function requestReworkCommand(input, deps) {
7298
+ const { issue, reviewerFeedback, note } = input;
7299
+ if (issue.state !== "Reviewing" && issue.state !== "PendingDecision") {
7300
+ throw new Error(
7301
+ `requestReworkCommand requires Reviewing or PendingDecision state, got ${issue.state}.`
7302
+ );
7303
+ }
7304
+ issue.lastError = reviewerFeedback;
7305
+ issue.lastFailedPhase = "review";
7306
+ issue.attempts += 1;
7307
+ if (issue.state === "Reviewing") {
7308
+ await transitionIssueCommand(
7309
+ { issue, target: "PendingDecision", note: `Reviewer completed for ${issue.identifier}.` },
7310
+ deps
7311
+ );
7312
+ }
7313
+ await transitionIssueCommand(
7314
+ { issue, target: "Queued", note: note ?? `Reviewer requested rework for ${issue.identifier}.` },
7315
+ deps
7316
+ );
7317
+ deps.eventStore.addEvent(
7318
+ issue.id,
7319
+ "runner",
7320
+ `Issue ${issue.identifier} sent back for rework by reviewer.`
7321
+ );
7322
+ }
7323
+
6737
7324
  // src/agents/issue-runner.ts
6738
7325
  async function runPlanningJob(state, issue) {
6739
7326
  issue.planningStatus = "planning";
@@ -6742,8 +7329,8 @@ async function runPlanningJob(state, issue) {
6742
7329
  issue.updatedAt = now();
6743
7330
  markIssueDirty(issue.id);
6744
7331
  const safeId = idToSafePath(issue.id);
6745
- const workspaceDir = join16(WORKSPACE_ROOT, safeId);
6746
- mkdirSync7(workspaceDir, { recursive: true });
7332
+ const workspaceDir = join17(WORKSPACE_ROOT, safeId);
7333
+ mkdirSync8(workspaceDir, { recursive: true });
6747
7334
  addEvent(state, issue.id, "info", `Plan generation started for ${issue.identifier} (v${(issue.planVersion ?? 0) + 1}).`);
6748
7335
  try {
6749
7336
  const { plan, usage, prompt } = await generatePlan(
@@ -6757,7 +7344,6 @@ async function runPlanningJob(state, issue) {
6757
7344
  markIssuePlanDirty(issue.id);
6758
7345
  issue.planVersion = Math.max(issue.planVersion ?? 0, 1);
6759
7346
  if (plan.suggestedPaths?.length && !issue.paths?.length) issue.paths = plan.suggestedPaths;
6760
- if (plan.suggestedLabels?.length && !issue.labels?.length) issue.labels = plan.suggestedLabels;
6761
7347
  if (plan.suggestedEffort && !issue.effort) issue.effort = plan.suggestedEffort;
6762
7348
  if (usage.totalTokens > 0) {
6763
7349
  addTokenUsage(issue, {
@@ -6769,8 +7355,8 @@ async function runPlanningJob(state, issue) {
6769
7355
  }
6770
7356
  const pv = issue.planVersion;
6771
7357
  try {
6772
- writeFileSync11(join16(workspaceDir, `plan.v${pv}.json`), JSON.stringify(plan, null, 2), "utf8");
6773
- writeFileSync11(join16(workspaceDir, `plan.v${pv}.prompt.md`), prompt, "utf8");
7358
+ writeFileSync12(join17(workspaceDir, `plan.v${pv}.json`), JSON.stringify(plan, null, 2), "utf8");
7359
+ writeFileSync12(join17(workspaceDir, `plan.v${pv}.prompt.md`), prompt, "utf8");
6774
7360
  } catch (artifactErr) {
6775
7361
  logger.warn({ err: String(artifactErr) }, "[Agent] Failed to write versioned plan artifacts");
6776
7362
  }
@@ -6799,21 +7385,21 @@ async function handleReviewStage(state, issue, workspacePath, startTs, routedPro
6799
7385
  const reviewer = routedProviders.find((p) => p.role === "reviewer");
6800
7386
  if (!reviewer) {
6801
7387
  issue.mergedReason = "Auto-approved: no reviewer configured.";
6802
- await transitionIssueCommand({ issue, target: "Done", note: `No reviewer configured; auto-approved for ${issue.identifier}.` }, container);
7388
+ await transitionIssueCommand({ issue, target: "Approved", note: `No reviewer configured; auto-approved for ${issue.identifier}.` }, container);
6803
7389
  return;
6804
7390
  }
6805
7391
  addEvent(state, issue.id, "info", `Review provider: ${reviewer.role}:${reviewer.provider}${reviewer.model ? `/${reviewer.model}` : ""}${reviewer.profile ? `:${reviewer.profile}` : ""}.`);
6806
7392
  let diffSummary = "";
6807
7393
  try {
6808
7394
  if (issue.baseBranch && issue.branchName) {
6809
- const diffResult = execSync4(
7395
+ const diffResult = execSync5(
6810
7396
  `git diff --stat "${issue.baseBranch}"..."${issue.branchName}"`,
6811
7397
  { cwd: TARGET_ROOT, encoding: "utf8", maxBuffer: 512e3, timeout: 1e4 }
6812
7398
  );
6813
7399
  diffSummary = diffResult.trim();
6814
7400
  } else {
6815
7401
  const diffTarget = issue.worktreePath ?? workspacePath;
6816
- const diffResult = execSync4(
7402
+ const diffResult = execSync5(
6817
7403
  `git diff --no-index --stat -- "${SOURCE_ROOT}" "${diffTarget}" 2>/dev/null`,
6818
7404
  { encoding: "utf8", maxBuffer: 512e3, timeout: 1e4 }
6819
7405
  );
@@ -6822,10 +7408,10 @@ async function handleReviewStage(state, issue, workspacePath, startTs, routedPro
6822
7408
  } catch (err) {
6823
7409
  diffSummary = (err.stdout || "").trim();
6824
7410
  }
6825
- const compiled = await compileReview(issue, reviewer, workspacePath, diffSummary);
7411
+ const compiled = await compileReview(issue, reviewer, workspacePath, diffSummary, state.config);
6826
7412
  const effectiveReviewer = { ...reviewer, command: compiled.command || reviewer.command };
6827
- const reviewPromptFile = join16(workspacePath, "review-prompt.md");
6828
- writeFileSync11(reviewPromptFile, `${compiled.prompt}
7413
+ const reviewPromptFile = join17(workspacePath, "review-prompt.md");
7414
+ writeFileSync12(reviewPromptFile, `${compiled.prompt}
6829
7415
  `, "utf8");
6830
7416
  const reviewResult = await runAgentSession(state, issue, effectiveReviewer, 1, workspacePath, compiled.prompt, reviewPromptFile);
6831
7417
  issue.durationMs = (issue.durationMs ?? 0) + (Date.now() - startTs);
@@ -6836,27 +7422,40 @@ async function handleReviewStage(state, issue, workspacePath, startTs, routedPro
6836
7422
  try {
6837
7423
  const rpv = issue.planVersion ?? 1;
6838
7424
  const rra = issue.reviewAttempt ?? 1;
6839
- const vReviewPromptSrc = join16(workspacePath, "review-prompt.md");
6840
- const vReviewAuditSrc = join16(workspacePath, "execution-audit.json");
6841
- if (existsSync13(vReviewPromptSrc)) {
6842
- writeFileSync11(join16(workspacePath, `review.v${rpv}a${rra}.prompt.md`), readFileSync10(vReviewPromptSrc, "utf8"), "utf8");
7425
+ const vReviewPromptSrc = join17(workspacePath, "review-prompt.md");
7426
+ const vReviewAuditSrc = join17(workspacePath, "execution-audit.json");
7427
+ if (existsSync14(vReviewPromptSrc)) {
7428
+ writeFileSync12(join17(workspacePath, `review.v${rpv}a${rra}.prompt.md`), readFileSync11(vReviewPromptSrc, "utf8"), "utf8");
6843
7429
  }
6844
- if (existsSync13(vReviewAuditSrc)) {
6845
- writeFileSync11(join16(workspacePath, `review.v${rpv}a${rra}.audit.json`), readFileSync10(vReviewAuditSrc, "utf8"), "utf8");
7430
+ if (existsSync14(vReviewAuditSrc)) {
7431
+ writeFileSync12(join17(workspacePath, `review.v${rpv}a${rra}.audit.json`), readFileSync11(vReviewAuditSrc, "utf8"), "utf8");
6846
7432
  }
6847
7433
  } catch (vErr) {
6848
7434
  logger.warn({ err: String(vErr) }, "[Agent] Failed to write versioned review artifacts");
6849
7435
  }
6850
7436
  if (reviewResult.success) {
6851
7437
  issue.mergedReason = `Auto-approved by reviewer in ${reviewResult.turns} turn(s).`;
6852
- await transitionIssueCommand({ issue, target: "Reviewed", note: `Reviewer completed for ${issue.identifier}.` }, container);
6853
- await transitionIssueCommand({ issue, target: "Done", note: `Reviewer approved ${issue.identifier} in ${reviewResult.turns} turn(s).` }, container);
7438
+ await transitionIssueCommand({ issue, target: "PendingDecision", note: `Reviewer completed for ${issue.identifier}.` }, container);
7439
+ const validation = await runValidationGate(issue, state.config);
7440
+ if (validation) {
7441
+ issue.validationResult = validation;
7442
+ markIssueDirty(issue.id);
7443
+ if (!validation.passed) {
7444
+ addEvent(state, issue.id, "error", `Validation gate failed for ${issue.identifier}: ${validation.command}`);
7445
+ logger.warn({ issueId: issue.id, command: validation.command }, "[Agent] Validation gate failed \u2014 staying in Reviewed");
7446
+ return;
7447
+ }
7448
+ addEvent(state, issue.id, "info", `Validation gate passed for ${issue.identifier}.`);
7449
+ }
7450
+ await transitionIssueCommand({ issue, target: "Approved", note: `Reviewer approved ${issue.identifier} in ${reviewResult.turns} turn(s).` }, container);
6854
7451
  } else if (reviewResult.continueRequested) {
6855
- await transitionIssueCommand({ issue, target: "Reviewed", note: `Reviewer completed for ${issue.identifier}.` }, container);
6856
- await transitionIssueCommand({ issue, target: "Queued", note: `Reviewer requested rework for ${issue.identifier}.` }, container);
6857
- container.eventStore.addEvent(issue.id, "runner", `Issue ${issue.identifier} sent back for rework by reviewer.`);
7452
+ await requestReworkCommand(
7453
+ { issue, reviewerFeedback: reviewResult.output, note: `Reviewer requested rework for ${issue.identifier}.` },
7454
+ container
7455
+ );
6858
7456
  } else {
6859
7457
  issue.lastError = reviewResult.output;
7458
+ issue.lastFailedPhase = "review";
6860
7459
  issue.attempts += 1;
6861
7460
  if (issue.attempts >= issue.maxAttempts) {
6862
7461
  issue.cancelledReason = `Max attempts reached (${issue.attempts}/${issue.maxAttempts}): reviewer failed or blocked.`;
@@ -6874,12 +7473,8 @@ async function handleExecutionStage(state, issue, workspacePath, promptText, pro
6874
7473
  container.eventStore.addEvent(
6875
7474
  issue.id,
6876
7475
  "info",
6877
- `Capability routing selected ${routedProviders.map((p) => `${p.role}:${p.provider}${p.model ? `/${p.model}` : ""}${p.profile ? `:${p.profile}` : ""}${p.reasoningEffort ? ` [${p.reasoningEffort}]` : ""}`).join(", ")}.`
7476
+ `Agent providers: ${routedProviders.map((p) => `${p.role}:${p.provider}${p.model ? `/${p.model}` : ""}${p.reasoningEffort ? ` [${p.reasoningEffort}]` : ""}`).join(", ")}.`
6878
7477
  );
6879
- const routingSignals = describeRoutingSignals(issue, workspaceDerivedPaths);
6880
- if (routingSignals) {
6881
- container.eventStore.addEvent(issue.id, "info", `Capability routing signals: ${routingSignals}.`);
6882
- }
6883
7478
  const runResult = await runAgentPipeline(state, issue, workspacePath, promptText, promptFile, workflowConfig);
6884
7479
  issue.durationMs = Date.now() - startTs;
6885
7480
  issue.commandExitCode = runResult.code;
@@ -6899,13 +7494,13 @@ async function handleExecutionStage(state, issue, workspacePath, promptText, pro
6899
7494
  try {
6900
7495
  const epv = issue.planVersion ?? 1;
6901
7496
  const eea = issue.executeAttempt ?? 1;
6902
- const vExecPromptSrc = join16(workspacePath, "prompt.md");
6903
- const vExecAuditSrc = join16(workspacePath, "execution-audit.json");
6904
- if (existsSync13(vExecPromptSrc)) {
6905
- writeFileSync11(join16(workspacePath, `execute.v${epv}a${eea}.prompt.md`), readFileSync10(vExecPromptSrc, "utf8"), "utf8");
7497
+ const vExecPromptSrc = join17(workspacePath, "prompt.md");
7498
+ const vExecAuditSrc = join17(workspacePath, "execution-audit.json");
7499
+ if (existsSync14(vExecPromptSrc)) {
7500
+ writeFileSync12(join17(workspacePath, `execute.v${epv}a${eea}.prompt.md`), readFileSync11(vExecPromptSrc, "utf8"), "utf8");
6906
7501
  }
6907
- if (existsSync13(vExecAuditSrc)) {
6908
- writeFileSync11(join16(workspacePath, `execute.v${epv}a${eea}.audit.json`), readFileSync10(vExecAuditSrc, "utf8"), "utf8");
7502
+ if (existsSync14(vExecAuditSrc)) {
7503
+ writeFileSync12(join17(workspacePath, `execute.v${epv}a${eea}.audit.json`), readFileSync11(vExecAuditSrc, "utf8"), "utf8");
6909
7504
  }
6910
7505
  } catch (vErr) {
6911
7506
  logger.warn({ err: String(vErr) }, "[Agent] Failed to write versioned execute artifacts");
@@ -6922,6 +7517,7 @@ async function handleExecutionStage(state, issue, workspacePath, promptText, pro
6922
7517
  container.eventStore.addEvent(issue.id, "runner", `Issue ${issue.identifier} queued for next turn.`);
6923
7518
  } else {
6924
7519
  issue.lastError = runResult.output;
7520
+ issue.lastFailedPhase = "execute";
6925
7521
  issue.attempts += 1;
6926
7522
  if (issue.attempts >= issue.maxAttempts) {
6927
7523
  issue.commandExitCode = runResult.code;
@@ -6939,17 +7535,10 @@ async function runIssueOnce(state, issue, running) {
6939
7535
  const isResuming = issue.state === "Running";
6940
7536
  logger.info({ issueId: issue.id, identifier: issue.identifier, state: issue.state, isReviewing, isResuming, attempt: issue.attempts + 1, maxAttempts: issue.maxAttempts }, "[Agent] Starting issue execution");
6941
7537
  if (issue.state === "Planning") {
6942
- issue.startedAt = issue.startedAt ?? now();
6943
- runPlanningJob(state, issue).catch((err) => logger.error({ err, issueId: issue.id, identifier: issue.identifier }, "[Agent] Unexpected error in background planning job")).finally(() => {
6944
- state.metrics = computeMetrics(state.issues);
6945
- state.updatedAt = now();
6946
- persistState(state).catch(() => {
6947
- });
6948
- });
7538
+ logger.warn({ issueId: issue.id }, "[Agent] runIssueOnce called for Planning state \u2014 skipping (queue handles planning)");
6949
7539
  return;
6950
7540
  }
6951
7541
  running.add(issue.id);
6952
- state.metrics.activeWorkers += 1;
6953
7542
  issue.startedAt = issue.startedAt ?? now();
6954
7543
  let workflowConfig = null;
6955
7544
  try {
@@ -6974,20 +7563,10 @@ async function runIssueOnce(state, issue, running) {
6974
7563
  }
6975
7564
  try {
6976
7565
  const workspaceDerivedPaths = hydrateIssuePathsFromWorkspace(issue);
6977
- if ((issue.paths ?? []).length > 0) {
6978
- issue.inferredPaths = [.../* @__PURE__ */ new Set([...issue.inferredPaths ?? [], ...inferCapabilityPaths({
6979
- id: issue.id,
6980
- identifier: issue.identifier,
6981
- title: issue.title,
6982
- description: issue.description,
6983
- labels: issue.labels,
6984
- paths: issue.paths
6985
- })])];
6986
- }
6987
7566
  const { workspacePath, promptText, promptFile } = await prepareWorkspace(issue, state, state.config.defaultBranch);
6988
7567
  container.issueRepository.markDirty(issue.id);
6989
7568
  try {
6990
- const { getIssueStateResource: getIssueStateResource2 } = await import("./store-366NGWR4.js");
7569
+ const { getIssueStateResource: getIssueStateResource2 } = await import("./store-RVKQ6UEY.js");
6991
7570
  const res = getIssueStateResource2();
6992
7571
  if (res) {
6993
7572
  await res.patch(issue.id, {
@@ -7009,6 +7588,7 @@ async function runIssueOnce(state, issue, running) {
7009
7588
  } catch (error) {
7010
7589
  issue.attempts += 1;
7011
7590
  issue.lastError = String(error);
7591
+ issue.lastFailedPhase = issue.lastFailedPhase ?? "execute";
7012
7592
  if (issue.attempts >= issue.maxAttempts) {
7013
7593
  issue.cancelledReason = `Max attempts reached (${issue.attempts}/${issue.maxAttempts}): unexpected failure \u2014 ${issue.lastError?.slice(0, 120) ?? "unknown error"}.`;
7014
7594
  await transitionIssueCommand({ issue, target: "Cancelled", note: `Issue failed unexpectedly: ${issue.lastError}` }, container);
@@ -7021,18 +7601,25 @@ async function runIssueOnce(state, issue, running) {
7021
7601
  logger.info({ issueId: issue.id, identifier: issue.identifier, finalState: issue.state, elapsedMs, attempts: issue.attempts }, "[Agent] Issue execution finished");
7022
7602
  issue.updatedAt = now();
7023
7603
  container.issueRepository.markDirty(issue.id);
7024
- state.metrics.activeWorkers = Math.max(state.metrics.activeWorkers - 1, 0);
7025
7604
  running.delete(issue.id);
7026
7605
  state.metrics = computeMetrics(state.issues);
7027
- state.metrics.activeWorkers = Math.max(state.metrics.activeWorkers, 0);
7028
7606
  state.updatedAt = now();
7029
7607
  await container.persistencePort.persistState(state);
7030
7608
  }
7031
7609
  }
7032
7610
 
7033
7611
  export {
7612
+ addTokenUsage,
7613
+ extractTokenUsage,
7614
+ tryParseJsonOutput,
7615
+ readAgentDirective,
7616
+ readAgentPid,
7617
+ isProcessAlive,
7034
7618
  isAgentStillRunning,
7035
7619
  cleanStalePidFile,
7620
+ loadAgentPipelineState,
7621
+ loadAgentPipelineSnapshotForIssue,
7622
+ loadAgentSessionSnapshotsForIssue,
7036
7623
  hydrate,
7037
7624
  recoverPlanningSession,
7038
7625
  loadRuntimeSettings,
@@ -7042,15 +7629,27 @@ export {
7042
7629
  createContainer,
7043
7630
  runPlanningJob,
7044
7631
  runIssueOnce,
7632
+ isShuttingDown,
7633
+ installGracefulShutdown,
7634
+ analyzeParallelizability,
7635
+ ensureNotStale,
7636
+ hasTerminalQueue,
7045
7637
  deriveConfig,
7046
7638
  applyWorkflowConfig,
7047
7639
  validateConfig,
7640
+ normalizeIssue,
7641
+ nextLocalIssueId,
7642
+ createIssueFromPayload,
7643
+ dedupHistoryEntries,
7048
7644
  buildRuntimeState,
7049
7645
  addEvent,
7050
- isShuttingDown,
7051
- installGracefulShutdown,
7052
- ensureNotStale,
7053
- hasTerminalQueue,
7646
+ transitionIssue,
7647
+ issueDependenciesResolved,
7648
+ getNextRetryAt,
7649
+ handleStatePatch,
7650
+ runAgentSession,
7651
+ runAgentPipeline,
7652
+ issueHasResumableSession,
7054
7653
  startApiServer,
7055
7654
  getStateDb,
7056
7655
  getIssueStateResource,
@@ -7079,4 +7678,4 @@ export {
7079
7678
  syncReferenceRepositories,
7080
7679
  importReferenceArtifacts
7081
7680
  };
7082
- //# sourceMappingURL=chunk-G7W4NEOA.js.map
7681
+ //# sourceMappingURL=chunk-3FCJI2GK.js.map