@renseiai/agentfactory 0.8.18 → 0.8.19

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 (45) hide show
  1. package/dist/src/governor/decision-engine-adapter.d.ts +43 -0
  2. package/dist/src/governor/decision-engine-adapter.d.ts.map +1 -0
  3. package/dist/src/governor/decision-engine-adapter.js +422 -0
  4. package/dist/src/governor/decision-engine-adapter.test.d.ts +2 -0
  5. package/dist/src/governor/decision-engine-adapter.test.d.ts.map +1 -0
  6. package/dist/src/governor/decision-engine-adapter.test.js +363 -0
  7. package/dist/src/governor/index.d.ts +1 -0
  8. package/dist/src/governor/index.d.ts.map +1 -1
  9. package/dist/src/governor/index.js +1 -0
  10. package/dist/src/manifest/route-manifest.d.ts.map +1 -1
  11. package/dist/src/manifest/route-manifest.js +4 -0
  12. package/dist/src/orchestrator/orchestrator.d.ts +27 -0
  13. package/dist/src/orchestrator/orchestrator.d.ts.map +1 -1
  14. package/dist/src/orchestrator/orchestrator.js +289 -86
  15. package/dist/src/providers/claude-provider.d.ts.map +1 -1
  16. package/dist/src/providers/claude-provider.js +11 -0
  17. package/dist/src/providers/codex-app-server-provider.d.ts +201 -0
  18. package/dist/src/providers/codex-app-server-provider.d.ts.map +1 -0
  19. package/dist/src/providers/codex-app-server-provider.js +786 -0
  20. package/dist/src/providers/codex-app-server-provider.test.d.ts +2 -0
  21. package/dist/src/providers/codex-app-server-provider.test.d.ts.map +1 -0
  22. package/dist/src/providers/codex-app-server-provider.test.js +529 -0
  23. package/dist/src/providers/codex-provider.d.ts +24 -4
  24. package/dist/src/providers/codex-provider.d.ts.map +1 -1
  25. package/dist/src/providers/codex-provider.js +58 -6
  26. package/dist/src/providers/index.d.ts +1 -0
  27. package/dist/src/providers/index.d.ts.map +1 -1
  28. package/dist/src/providers/index.js +1 -0
  29. package/dist/src/routing/observation-recorder.test.js +1 -1
  30. package/dist/src/routing/observation-store.d.ts +15 -1
  31. package/dist/src/routing/observation-store.d.ts.map +1 -1
  32. package/dist/src/routing/observation-store.test.js +17 -11
  33. package/dist/src/templates/index.d.ts +2 -1
  34. package/dist/src/templates/index.d.ts.map +1 -1
  35. package/dist/src/templates/index.js +1 -0
  36. package/dist/src/templates/registry.d.ts +23 -0
  37. package/dist/src/templates/registry.d.ts.map +1 -1
  38. package/dist/src/templates/registry.js +80 -0
  39. package/dist/src/templates/schema.d.ts +31 -0
  40. package/dist/src/templates/schema.d.ts.map +1 -0
  41. package/dist/src/templates/schema.js +139 -0
  42. package/dist/src/templates/schema.test.d.ts +2 -0
  43. package/dist/src/templates/schema.test.d.ts.map +1 -0
  44. package/dist/src/templates/schema.test.js +215 -0
  45. package/package.json +2 -2
@@ -5,7 +5,7 @@
5
5
  */
6
6
  import { randomUUID } from 'crypto';
7
7
  import { execSync } from 'child_process';
8
- import { existsSync, lstatSync, mkdirSync, readFileSync, readdirSync, rmSync, statSync, symlinkSync, unlinkSync, writeFileSync } from 'fs';
8
+ import { copyFileSync, existsSync, lstatSync, mkdirSync, readFileSync, readdirSync, readlinkSync, rmSync, statSync, symlinkSync, unlinkSync, writeFileSync } from 'fs';
9
9
  import { resolve, dirname, basename } from 'path';
10
10
  import { parse as parseDotenv } from 'dotenv';
11
11
  import { createProvider, resolveProviderName, resolveProviderWithSource, } from '../providers/index.js';
@@ -27,6 +27,12 @@ import { ToolRegistry } from '../tools/index.js';
27
27
  import { createMergeQueueAdapter } from '../merge-queue/index.js';
28
28
  // Default inactivity timeout: 5 minutes
29
29
  const DEFAULT_INACTIVITY_TIMEOUT_MS = 300000;
30
+ // Coordination inactivity timeout: 30 minutes.
31
+ // Coordinators spawn foreground sub-agents via the Agent tool. During sub-agent
32
+ // execution the parent event stream is silent (no tool_progress events), so the
33
+ // standard 5-minute inactivity timeout kills coordinators prematurely. 30 minutes
34
+ // gives sub-agents ample time to complete complex work.
35
+ const COORDINATION_INACTIVITY_TIMEOUT_MS = 1800000;
30
36
  // Default max session timeout: unlimited (undefined)
31
37
  const DEFAULT_MAX_SESSION_TIMEOUT_MS = undefined;
32
38
  // Env vars that Claude Code interprets for authentication/routing. If these
@@ -1064,6 +1070,20 @@ export class AgentOrchestrator {
1064
1070
  maxSessionTimeoutMs: override?.maxSessionTimeoutMs ?? baseConfig.maxSessionTimeoutMs,
1065
1071
  };
1066
1072
  }
1073
+ // Coordination work types spawn foreground sub-agents via the Agent tool.
1074
+ // During sub-agent execution the parent event stream is silent (no
1075
+ // tool_progress events flow from Agent tool execution), so the standard
1076
+ // inactivity timeout would kill coordinators prematurely. Use a longer
1077
+ // default unless the user has configured a per-work-type override above.
1078
+ const isCoordination = workType === 'coordination' || workType === 'inflight-coordination'
1079
+ || workType === 'qa-coordination' || workType === 'acceptance-coordination'
1080
+ || workType === 'refinement-coordination';
1081
+ if (isCoordination) {
1082
+ return {
1083
+ inactivityTimeoutMs: Math.max(baseConfig.inactivityTimeoutMs, COORDINATION_INACTIVITY_TIMEOUT_MS),
1084
+ maxSessionTimeoutMs: baseConfig.maxSessionTimeoutMs,
1085
+ };
1086
+ }
1067
1087
  return baseConfig;
1068
1088
  }
1069
1089
  /**
@@ -1800,18 +1820,139 @@ ORCHESTRATOR_INSTALL=1 exec pnpm add "$@"
1800
1820
  this.linkNodeModulesContents(src, dest, identifier);
1801
1821
  }
1802
1822
  }
1823
+ // Fix 5: Also scan worktree for workspaces that exist on the branch
1824
+ // but not in the main repo's directory listing (e.g., newly added workspaces)
1825
+ for (const subdir of ['apps', 'packages']) {
1826
+ const wtSubdir = resolve(worktreePath, subdir);
1827
+ if (!existsSync(wtSubdir))
1828
+ continue;
1829
+ for (const entry of readdirSync(wtSubdir)) {
1830
+ const src = resolve(repoRoot, subdir, entry, 'node_modules');
1831
+ const dest = resolve(wtSubdir, entry, 'node_modules');
1832
+ if (!existsSync(src))
1833
+ continue; // No source deps to link
1834
+ if (existsSync(dest))
1835
+ continue; // Already linked above
1836
+ this.linkNodeModulesContents(src, dest, identifier);
1837
+ }
1838
+ }
1803
1839
  if (skipped > 0) {
1804
1840
  console.log(`[${identifier}] Dependencies linked successfully (${skipped} workspace(s) skipped — not on this branch)`);
1805
1841
  }
1806
1842
  else {
1807
1843
  console.log(`[${identifier}] Dependencies linked successfully`);
1808
1844
  }
1845
+ // Verify critical symlinks are intact; if not, remove and retry once
1846
+ if (!this.verifyDependencyLinks(worktreePath, identifier)) {
1847
+ console.warn(`[${identifier}] Dependency verification failed — removing and re-linking`);
1848
+ this.removeWorktreeNodeModules(worktreePath);
1849
+ const retryDest = resolve(worktreePath, 'node_modules');
1850
+ this.linkNodeModulesContents(mainNodeModules, retryDest, identifier);
1851
+ if (!this.verifyDependencyLinks(worktreePath, identifier)) {
1852
+ console.warn(`[${identifier}] Verification failed after retry — falling back to install`);
1853
+ this.installDependencies(worktreePath, identifier);
1854
+ }
1855
+ }
1809
1856
  }
1810
1857
  catch (error) {
1811
1858
  console.warn(`[${identifier}] Symlink failed, falling back to install:`, error instanceof Error ? error.message : String(error));
1812
1859
  this.installDependencies(worktreePath, identifier);
1813
1860
  }
1814
1861
  }
1862
+ /**
1863
+ * Verify that critical dependency symlinks are intact and resolvable.
1864
+ * Returns true if verification passes, false if re-linking is needed.
1865
+ */
1866
+ verifyDependencyLinks(worktreePath, identifier) {
1867
+ const destRoot = resolve(worktreePath, 'node_modules');
1868
+ if (!existsSync(destRoot))
1869
+ return false;
1870
+ // Sentinel packages that should always be present in a Node.js project
1871
+ const sentinels = ['typescript'];
1872
+ // Also check for .modules.yaml (pnpm store metadata) if it exists in main
1873
+ const repoRoot = findRepoRoot(worktreePath);
1874
+ if (repoRoot) {
1875
+ const pnpmMeta = resolve(repoRoot, 'node_modules', '.modules.yaml');
1876
+ if (existsSync(pnpmMeta)) {
1877
+ sentinels.push('.modules.yaml');
1878
+ }
1879
+ }
1880
+ for (const pkg of sentinels) {
1881
+ const pkgPath = resolve(destRoot, pkg);
1882
+ if (!existsSync(pkgPath)) {
1883
+ console.warn(`[${identifier}] Verification: missing ${pkg}`);
1884
+ return false;
1885
+ }
1886
+ // Follow the symlink — throws if target was deleted from main repo
1887
+ try {
1888
+ statSync(pkgPath);
1889
+ }
1890
+ catch {
1891
+ console.warn(`[${identifier}] Verification: broken symlink for ${pkg}`);
1892
+ return false;
1893
+ }
1894
+ }
1895
+ return true;
1896
+ }
1897
+ /**
1898
+ * Remove all node_modules directories from a worktree (root + per-workspace).
1899
+ */
1900
+ removeWorktreeNodeModules(worktreePath) {
1901
+ const destRoot = resolve(worktreePath, 'node_modules');
1902
+ try {
1903
+ if (existsSync(destRoot)) {
1904
+ rmSync(destRoot, { recursive: true, force: true });
1905
+ }
1906
+ }
1907
+ catch {
1908
+ // Ignore cleanup errors
1909
+ }
1910
+ for (const subdir of ['apps', 'packages']) {
1911
+ const subPath = resolve(worktreePath, subdir);
1912
+ if (!existsSync(subPath))
1913
+ continue;
1914
+ try {
1915
+ for (const entry of readdirSync(subPath)) {
1916
+ const nm = resolve(subPath, entry, 'node_modules');
1917
+ if (existsSync(nm)) {
1918
+ rmSync(nm, { recursive: true, force: true });
1919
+ }
1920
+ }
1921
+ }
1922
+ catch {
1923
+ // Ignore cleanup errors
1924
+ }
1925
+ }
1926
+ }
1927
+ /**
1928
+ * Create or update a symlink atomically, handling EEXIST races.
1929
+ *
1930
+ * If the destination already exists and points to the correct target, this is a no-op.
1931
+ * If it points elsewhere or isn't a symlink, it's replaced.
1932
+ */
1933
+ safeSymlink(src, dest) {
1934
+ try {
1935
+ symlinkSync(src, dest);
1936
+ }
1937
+ catch (error) {
1938
+ if (error.code === 'EEXIST') {
1939
+ // Verify existing symlink points to correct target
1940
+ try {
1941
+ const existing = readlinkSync(dest);
1942
+ if (resolve(existing) === resolve(src))
1943
+ return; // Already correct
1944
+ }
1945
+ catch {
1946
+ // Not a symlink or can't read — remove and retry
1947
+ }
1948
+ unlinkSync(dest);
1949
+ symlinkSync(src, dest);
1950
+ }
1951
+ else {
1952
+ throw error;
1953
+ }
1954
+ }
1955
+ }
1815
1956
  /**
1816
1957
  * Create a real node_modules directory and symlink each entry from the source.
1817
1958
  *
@@ -1819,10 +1960,11 @@ ORCHESTRATOR_INSTALL=1 exec pnpm add "$@"
1819
1960
  * resolve through the symlink and corrupt the original), we create a real
1820
1961
  * directory and symlink each entry individually. If pnpm "recreates" this
1821
1962
  * directory, it only destroys the worktree's symlinks — not the originals.
1963
+ *
1964
+ * Supports incremental sync: if the destination already exists, only missing
1965
+ * or stale entries are updated (safe for concurrent agents and phase reuse).
1822
1966
  */
1823
1967
  linkNodeModulesContents(srcNodeModules, destNodeModules, identifier) {
1824
- if (existsSync(destNodeModules))
1825
- return;
1826
1968
  mkdirSync(destNodeModules, { recursive: true });
1827
1969
  for (const entry of readdirSync(srcNodeModules)) {
1828
1970
  const srcEntry = resolve(srcNodeModules, entry);
@@ -1835,16 +1977,12 @@ ORCHESTRATOR_INSTALL=1 exec pnpm add "$@"
1835
1977
  for (const scopedEntry of readdirSync(srcEntry)) {
1836
1978
  const srcScoped = resolve(srcEntry, scopedEntry);
1837
1979
  const destScoped = resolve(destEntry, scopedEntry);
1838
- if (!existsSync(destScoped)) {
1839
- symlinkSync(srcScoped, destScoped);
1840
- }
1980
+ this.safeSymlink(srcScoped, destScoped);
1841
1981
  }
1842
1982
  continue;
1843
1983
  }
1844
1984
  }
1845
- if (!existsSync(destEntry)) {
1846
- symlinkSync(srcEntry, destEntry);
1847
- }
1985
+ this.safeSymlink(srcEntry, destEntry);
1848
1986
  }
1849
1987
  }
1850
1988
  /**
@@ -1853,36 +1991,8 @@ ORCHESTRATOR_INSTALL=1 exec pnpm add "$@"
1853
1991
  */
1854
1992
  installDependencies(worktreePath, identifier) {
1855
1993
  console.log(`[${identifier}] Installing dependencies via pnpm...`);
1856
- // Remove any node_modules from a partial linkDependencies attempt.
1857
- // Handles both old format (directory-level symlink) and new format
1858
- // (real directory with symlinked contents).
1859
- const destRoot = resolve(worktreePath, 'node_modules');
1860
- try {
1861
- if (existsSync(destRoot)) {
1862
- rmSync(destRoot, { recursive: true, force: true });
1863
- console.log(`[${identifier}] Removed partial node_modules before install`);
1864
- }
1865
- }
1866
- catch {
1867
- // Ignore cleanup errors — pnpm install may still work
1868
- }
1869
- // Also remove any per-workspace node_modules that were partially created
1870
- for (const subdir of ['apps', 'packages']) {
1871
- const subPath = resolve(worktreePath, subdir);
1872
- if (!existsSync(subPath))
1873
- continue;
1874
- try {
1875
- for (const entry of readdirSync(subPath)) {
1876
- const nm = resolve(subPath, entry, 'node_modules');
1877
- if (existsSync(nm)) {
1878
- rmSync(nm, { recursive: true, force: true });
1879
- }
1880
- }
1881
- }
1882
- catch {
1883
- // Ignore cleanup errors
1884
- }
1885
- }
1994
+ // Remove any node_modules from a partial linkDependencies attempt
1995
+ this.removeWorktreeNodeModules(worktreePath);
1886
1996
  // Set ORCHESTRATOR_INSTALL=1 to bypass the preinstall guard script
1887
1997
  // that blocks pnpm install in worktrees (to prevent symlink corruption).
1888
1998
  const installEnv = { ...process.env, ORCHESTRATOR_INSTALL: '1' };
@@ -1912,6 +2022,82 @@ ORCHESTRATOR_INSTALL=1 exec pnpm add "$@"
1912
2022
  }
1913
2023
  }
1914
2024
  }
2025
+ /**
2026
+ * Sync dependencies between worktree and main repo before linking.
2027
+ *
2028
+ * When a development agent adds new packages on a branch, the lockfile in the
2029
+ * worktree diverges from the main repo. This method detects lockfile drift,
2030
+ * updates the main repo's node_modules, then re-links into the worktree.
2031
+ */
2032
+ syncDependencies(worktreePath, identifier) {
2033
+ const repoRoot = findRepoRoot(worktreePath);
2034
+ if (!repoRoot) {
2035
+ this.linkDependencies(worktreePath, identifier);
2036
+ return;
2037
+ }
2038
+ const worktreeLock = resolve(worktreePath, 'pnpm-lock.yaml');
2039
+ const mainLock = resolve(repoRoot, 'pnpm-lock.yaml');
2040
+ // Detect lockfile drift: if the worktree has a lockfile that differs from main,
2041
+ // a dev agent added/changed dependencies on the branch
2042
+ let lockfileDrifted = false;
2043
+ if (existsSync(worktreeLock) && existsSync(mainLock)) {
2044
+ try {
2045
+ const wtContent = readFileSync(worktreeLock, 'utf-8');
2046
+ const mainContent = readFileSync(mainLock, 'utf-8');
2047
+ lockfileDrifted = wtContent !== mainContent;
2048
+ }
2049
+ catch {
2050
+ // If we can't read either file, proceed without sync
2051
+ }
2052
+ }
2053
+ if (lockfileDrifted) {
2054
+ console.log(`[${identifier}] Lockfile drift detected — syncing main repo dependencies`);
2055
+ try {
2056
+ // Copy the worktree's lockfile to the main repo so install picks up new deps
2057
+ copyFileSync(worktreeLock, mainLock);
2058
+ // Also copy any changed package.json files from worktree workspaces to main
2059
+ for (const subdir of ['', 'apps', 'packages']) {
2060
+ const wtDir = subdir ? resolve(worktreePath, subdir) : worktreePath;
2061
+ const mainDir = subdir ? resolve(repoRoot, subdir) : repoRoot;
2062
+ if (subdir && !existsSync(wtDir))
2063
+ continue;
2064
+ const entries = subdir ? readdirSync(wtDir) : [''];
2065
+ for (const entry of entries) {
2066
+ const wtPkg = resolve(wtDir, entry, 'package.json');
2067
+ const mainPkg = resolve(mainDir, entry, 'package.json');
2068
+ if (!existsSync(wtPkg))
2069
+ continue;
2070
+ try {
2071
+ const wtPkgContent = readFileSync(wtPkg, 'utf-8');
2072
+ const mainPkgContent = existsSync(mainPkg) ? readFileSync(mainPkg, 'utf-8') : '';
2073
+ if (wtPkgContent !== mainPkgContent) {
2074
+ copyFileSync(wtPkg, mainPkg);
2075
+ }
2076
+ }
2077
+ catch {
2078
+ // Skip files we can't read
2079
+ }
2080
+ }
2081
+ }
2082
+ // Install in the main repo (not the worktree) to update node_modules
2083
+ const installEnv = { ...process.env, ORCHESTRATOR_INSTALL: '1' };
2084
+ execSync('pnpm install --frozen-lockfile 2>&1', {
2085
+ cwd: repoRoot,
2086
+ stdio: 'pipe',
2087
+ encoding: 'utf-8',
2088
+ timeout: 120_000,
2089
+ env: installEnv,
2090
+ });
2091
+ console.log(`[${identifier}] Main repo dependencies synced`);
2092
+ // Remove stale worktree node_modules so linkDependencies creates fresh symlinks
2093
+ this.removeWorktreeNodeModules(worktreePath);
2094
+ }
2095
+ catch (error) {
2096
+ console.warn(`[${identifier}] Dependency sync failed, proceeding with existing state:`, error instanceof Error ? error.message : String(error));
2097
+ }
2098
+ }
2099
+ this.linkDependencies(worktreePath, identifier);
2100
+ }
1915
2101
  /**
1916
2102
  * @deprecated Use linkDependencies() instead. This now delegates to linkDependencies.
1917
2103
  */
@@ -2340,56 +2526,64 @@ ORCHESTRATOR_INSTALL=1 exec pnpm add "$@"
2340
2526
  }
2341
2527
  }
2342
2528
  // Update Linear status based on work type if auto-transition is enabled
2343
- if (agent.status === 'completed' && this.config.autoTransition) {
2529
+ if ((agent.status === 'completed' || agent.status === 'failed') && this.config.autoTransition) {
2344
2530
  const workType = agent.workType ?? 'development';
2345
2531
  const isResultSensitive = workType === 'qa' || workType === 'acceptance' || workType === 'coordination' || workType === 'qa-coordination' || workType === 'acceptance-coordination';
2346
2532
  let targetStatus = null;
2347
2533
  if (isResultSensitive) {
2348
- // For QA/acceptance: parse result to decide promote vs reject.
2349
- // Try the final result message first, then fall back to scanning
2350
- // all accumulated assistant text (the marker may be in an earlier turn).
2351
- let workResult = parseWorkResult(agent.resultMessage, workType);
2352
- if (workResult === 'unknown' && assistantTextChunks.length > 0) {
2353
- const fullText = assistantTextChunks.join('\n');
2354
- workResult = parseWorkResult(fullText, workType);
2355
- if (workResult !== 'unknown') {
2356
- log?.info('Work result found in accumulated text (not in final message)', { workResult });
2357
- }
2358
- }
2359
- agent.workResult = workResult;
2360
- if (workResult === 'passed') {
2361
- targetStatus = this.statusMappings.workTypeCompleteStatus[workType];
2362
- log?.info('Work result: passed, promoting', { workType, targetStatus });
2363
- }
2364
- else if (workResult === 'failed') {
2534
+ if (agent.status === 'failed') {
2535
+ // Agent crashed/errored treat as QA/acceptance failure
2536
+ agent.workResult = 'failed';
2365
2537
  targetStatus = this.statusMappings.workTypeFailStatus[workType];
2366
- log?.info('Work result: failed, transitioning to fail status', { workType, targetStatus });
2538
+ log?.info('Agent failed (crash/error), transitioning to fail status', { workType, targetStatus });
2367
2539
  }
2368
2540
  else {
2369
- // unknown safe default: don't transition
2370
- log?.warn('Work result: unknown, skipping auto-transition', {
2371
- workType,
2372
- hasResultMessage: !!agent.resultMessage,
2373
- });
2374
- // Post a diagnostic comment so the issue doesn't silently stall
2375
- try {
2376
- await this.client.createComment(issueId, `⚠️ Agent completed but no structured result marker was detected in the output.\n\n` +
2377
- `**Issue status was NOT updated automatically.**\n\n` +
2378
- `The orchestrator expected one of:\n` +
2379
- `- \`<!-- WORK_RESULT:passed -->\` to promote the issue\n` +
2380
- `- \`<!-- WORK_RESULT:failed -->\` to record a failure\n\n` +
2381
- `This usually means the agent exited early (timeout, error, or missing logic). ` +
2382
- `Check the agent logs for details, then manually update the issue status or re-trigger the agent.`);
2383
- log?.info('Posted diagnostic comment for unknown work result');
2541
+ // For QA/acceptance: parse result to decide promote vs reject.
2542
+ // Try the final result message first, then fall back to scanning
2543
+ // all accumulated assistant text (the marker may be in an earlier turn).
2544
+ let workResult = parseWorkResult(agent.resultMessage, workType);
2545
+ if (workResult === 'unknown' && assistantTextChunks.length > 0) {
2546
+ const fullText = assistantTextChunks.join('\n');
2547
+ workResult = parseWorkResult(fullText, workType);
2548
+ if (workResult !== 'unknown') {
2549
+ log?.info('Work result found in accumulated text (not in final message)', { workResult });
2550
+ }
2384
2551
  }
2385
- catch (error) {
2386
- log?.warn('Failed to post diagnostic comment for unknown work result', {
2387
- error: error instanceof Error ? error.message : String(error),
2552
+ agent.workResult = workResult;
2553
+ if (workResult === 'passed') {
2554
+ targetStatus = this.statusMappings.workTypeCompleteStatus[workType];
2555
+ log?.info('Work result: passed, promoting', { workType, targetStatus });
2556
+ }
2557
+ else if (workResult === 'failed') {
2558
+ targetStatus = this.statusMappings.workTypeFailStatus[workType];
2559
+ log?.info('Work result: failed, transitioning to fail status', { workType, targetStatus });
2560
+ }
2561
+ else {
2562
+ // unknown — safe default: don't transition
2563
+ log?.warn('Work result: unknown, skipping auto-transition', {
2564
+ workType,
2565
+ hasResultMessage: !!agent.resultMessage,
2388
2566
  });
2567
+ // Post a diagnostic comment so the issue doesn't silently stall
2568
+ try {
2569
+ await this.client.createComment(issueId, `⚠️ Agent completed but no structured result marker was detected in the output.\n\n` +
2570
+ `**Issue status was NOT updated automatically.**\n\n` +
2571
+ `The orchestrator expected one of:\n` +
2572
+ `- \`<!-- WORK_RESULT:passed -->\` to promote the issue\n` +
2573
+ `- \`<!-- WORK_RESULT:failed -->\` to record a failure\n\n` +
2574
+ `This usually means the agent exited early (timeout, error, or missing logic). ` +
2575
+ `Check the agent logs for details, then manually update the issue status or re-trigger the agent.`);
2576
+ log?.info('Posted diagnostic comment for unknown work result');
2577
+ }
2578
+ catch (error) {
2579
+ log?.warn('Failed to post diagnostic comment for unknown work result', {
2580
+ error: error instanceof Error ? error.message : String(error),
2581
+ });
2582
+ }
2389
2583
  }
2390
2584
  }
2391
2585
  }
2392
- else {
2586
+ else if (agent.status === 'completed') {
2393
2587
  // Non-QA/acceptance: promote on completion, but validate code-producing work types first
2394
2588
  const isCodeProducing = workType === 'development' || workType === 'inflight';
2395
2589
  if (isCodeProducing && agent.worktreePath && !agent.pullRequestUrl) {
@@ -2704,7 +2898,9 @@ ORCHESTRATOR_INSTALL=1 exec pnpm add "$@"
2704
2898
  agent.providerSessionId = event.sessionId;
2705
2899
  this.updateLastActivity(issueId, 'init');
2706
2900
  // Update state with provider session ID (only for worktree-based agents)
2707
- if (agent.worktreePath) {
2901
+ // Skip if agent already failed — a late init event after an error would
2902
+ // re-persist a stale session ID, preventing fresh recovery on next attempt
2903
+ if (agent.worktreePath && agent.status !== 'failed') {
2708
2904
  try {
2709
2905
  updateState(agent.worktreePath, {
2710
2906
  providerSessionId: event.sessionId,
@@ -2882,10 +3078,17 @@ ORCHESTRATOR_INSTALL=1 exec pnpm add "$@"
2882
3078
  : `Agent error: ${event.errorSubtype}`;
2883
3079
  if (agent.worktreePath) {
2884
3080
  try {
3081
+ // If the error is a stale session (resume failed), clear providerSessionId
3082
+ // so the next recovery attempt starts fresh instead of hitting the same error
3083
+ const isStaleSession = errorMessage.includes('No conversation found with session ID');
2885
3084
  updateState(agent.worktreePath, {
2886
3085
  status: 'failed',
2887
3086
  errorMessage,
3087
+ ...(isStaleSession && { providerSessionId: null }),
2888
3088
  });
3089
+ if (isStaleSession) {
3090
+ log?.info('Cleared stale providerSessionId from state — next recovery will start fresh');
3091
+ }
2889
3092
  }
2890
3093
  catch {
2891
3094
  // Ignore state update errors
@@ -3187,8 +3390,8 @@ ORCHESTRATOR_INSTALL=1 exec pnpm add "$@"
3187
3390
  const workType = await this.detectWorkType(issue.id, 'Backlog');
3188
3391
  // Create worktree with work type suffix
3189
3392
  const { worktreePath, worktreeIdentifier } = this.createWorktree(issue.identifier, workType);
3190
- // Link dependencies from main repo into worktree
3191
- this.linkDependencies(worktreePath, issue.identifier);
3393
+ // Sync and link dependencies from main repo into worktree
3394
+ this.syncDependencies(worktreePath, issue.identifier);
3192
3395
  const startStatus = this.statusMappings.workTypeStartStatus[workType];
3193
3396
  // Update issue status based on work type if auto-transition is enabled
3194
3397
  if (this.config.autoTransition && startStatus) {
@@ -3305,8 +3508,8 @@ ORCHESTRATOR_INSTALL=1 exec pnpm add "$@"
3305
3508
  const wt = this.createWorktree(identifier, effectiveWorkType);
3306
3509
  worktreePath = wt.worktreePath;
3307
3510
  worktreeIdentifier = wt.worktreeIdentifier;
3308
- // Link dependencies from main repo into worktree
3309
- this.linkDependencies(worktreePath, identifier);
3511
+ // Sync and link dependencies from main repo into worktree
3512
+ this.syncDependencies(worktreePath, identifier);
3310
3513
  // Check for existing state and potential recovery
3311
3514
  const recoveryCheck = checkRecovery(worktreePath, {
3312
3515
  heartbeatTimeoutMs: getHeartbeatTimeoutFromEnv(),
@@ -3559,8 +3762,8 @@ ORCHESTRATOR_INSTALL=1 exec pnpm add "$@"
3559
3762
  const result = this.createWorktree(identifier, workType);
3560
3763
  worktreePath = result.worktreePath;
3561
3764
  worktreeIdentifier = result.worktreeIdentifier;
3562
- // Link dependencies from main repo into worktree
3563
- this.linkDependencies(worktreePath, identifier);
3765
+ // Sync and link dependencies from main repo into worktree
3766
+ this.syncDependencies(worktreePath, identifier);
3564
3767
  }
3565
3768
  }
3566
3769
  catch (error) {
@@ -3579,8 +3782,8 @@ ORCHESTRATOR_INSTALL=1 exec pnpm add "$@"
3579
3782
  const result = this.createWorktree(identifier, effectiveWorkType);
3580
3783
  worktreePath = result.worktreePath;
3581
3784
  worktreeIdentifier = result.worktreeIdentifier;
3582
- // Link dependencies from main repo into worktree
3583
- this.linkDependencies(worktreePath, identifier);
3785
+ // Sync and link dependencies from main repo into worktree
3786
+ this.syncDependencies(worktreePath, identifier);
3584
3787
  }
3585
3788
  catch (error) {
3586
3789
  return {
@@ -1 +1 @@
1
- {"version":3,"file":"claude-provider.d.ts","sourceRoot":"","sources":["../../../src/providers/claude-provider.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAUH,OAAO,KAAK,EACV,aAAa,EACb,gBAAgB,EAChB,WAAW,EAEZ,MAAM,YAAY,CAAA;AAiFnB,qBAAa,cAAe,YAAW,aAAa;IAClD,QAAQ,CAAC,IAAI,EAAG,QAAQ,CAAS;IACjC,QAAQ,CAAC,YAAY;;;MAGX;IAEV,KAAK,CAAC,MAAM,EAAE,gBAAgB,GAAG,WAAW;IAI5C,MAAM,CAAC,SAAS,EAAE,MAAM,EAAE,MAAM,EAAE,gBAAgB,GAAG,WAAW;IAIhE,OAAO,CAAC,YAAY;CA8KrB;AAsND;;GAEG;AACH,wBAAgB,oBAAoB,IAAI,cAAc,CAErD"}
1
+ {"version":3,"file":"claude-provider.d.ts","sourceRoot":"","sources":["../../../src/providers/claude-provider.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAUH,OAAO,KAAK,EACV,aAAa,EACb,gBAAgB,EAChB,WAAW,EAEZ,MAAM,YAAY,CAAA;AA6FnB,qBAAa,cAAe,YAAW,aAAa;IAClD,QAAQ,CAAC,IAAI,EAAG,QAAQ,CAAS;IACjC,QAAQ,CAAC,YAAY;;;MAGX;IAEV,KAAK,CAAC,MAAM,EAAE,gBAAgB,GAAG,WAAW;IAI5C,MAAM,CAAC,SAAS,EAAE,MAAM,EAAE,MAAM,EAAE,gBAAgB,GAAG,WAAW;IAIhE,OAAO,CAAC,YAAY;CA8KrB;AAsND;;GAEG;AACH,wBAAgB,oBAAoB,IAAI,cAAc,CAErD"}
@@ -26,6 +26,17 @@ const autonomousCanUseTool = async (toolName, input) => {
26
26
  if (['Write', 'Edit', 'NotebookEdit'].includes(toolName)) {
27
27
  return { behavior: 'allow', updatedInput: input };
28
28
  }
29
+ // Agent tool: always force foreground execution.
30
+ // Coordinators that spawn sub-agents with run_in_background=true exit
31
+ // before sub-agents finish, orphaning work. Strip the flag so the Agent
32
+ // tool blocks until the sub-agent completes.
33
+ if (toolName === 'Agent') {
34
+ if (input.run_in_background) {
35
+ const { run_in_background: _, ...rest } = input;
36
+ return { behavior: 'allow', updatedInput: rest };
37
+ }
38
+ return { behavior: 'allow', updatedInput: input };
39
+ }
29
40
  // Task management and planning
30
41
  if (['Task', 'TaskCreate', 'TaskUpdate', 'TaskGet', 'TaskList',
31
42
  'EnterPlanMode', 'ExitPlanMode', 'Skill'].includes(toolName)) {