@renseiai/agentfactory 0.8.17 → 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 (82) 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/merge-queue/adapters/github-native.d.ts +8 -0
  13. package/dist/src/merge-queue/adapters/github-native.d.ts.map +1 -1
  14. package/dist/src/merge-queue/adapters/github-native.js +37 -7
  15. package/dist/src/merge-queue/adapters/github-native.test.js +71 -42
  16. package/dist/src/orchestrator/activity-emitter.d.ts +7 -0
  17. package/dist/src/orchestrator/activity-emitter.d.ts.map +1 -1
  18. package/dist/src/orchestrator/activity-emitter.js +19 -1
  19. package/dist/src/orchestrator/api-activity-emitter.d.ts +6 -0
  20. package/dist/src/orchestrator/api-activity-emitter.d.ts.map +1 -1
  21. package/dist/src/orchestrator/api-activity-emitter.js +35 -0
  22. package/dist/src/orchestrator/index.d.ts +2 -0
  23. package/dist/src/orchestrator/index.d.ts.map +1 -1
  24. package/dist/src/orchestrator/index.js +1 -0
  25. package/dist/src/orchestrator/issue-tracker-client.d.ts +2 -1
  26. package/dist/src/orchestrator/issue-tracker-client.d.ts.map +1 -1
  27. package/dist/src/orchestrator/orchestrator.d.ts +27 -0
  28. package/dist/src/orchestrator/orchestrator.d.ts.map +1 -1
  29. package/dist/src/orchestrator/orchestrator.js +331 -86
  30. package/dist/src/orchestrator/security-scan-event.d.ts +57 -0
  31. package/dist/src/orchestrator/security-scan-event.d.ts.map +1 -0
  32. package/dist/src/orchestrator/security-scan-event.js +192 -0
  33. package/dist/src/orchestrator/security-scan-event.test.d.ts +2 -0
  34. package/dist/src/orchestrator/security-scan-event.test.d.ts.map +1 -0
  35. package/dist/src/orchestrator/security-scan-event.test.js +219 -0
  36. package/dist/src/orchestrator/state-recovery.d.ts.map +1 -1
  37. package/dist/src/orchestrator/state-recovery.js +1 -0
  38. package/dist/src/orchestrator/work-types.d.ts +1 -1
  39. package/dist/src/orchestrator/work-types.d.ts.map +1 -1
  40. package/dist/src/providers/claude-provider.d.ts.map +1 -1
  41. package/dist/src/providers/claude-provider.js +11 -0
  42. package/dist/src/providers/codex-app-server-provider.d.ts +201 -0
  43. package/dist/src/providers/codex-app-server-provider.d.ts.map +1 -0
  44. package/dist/src/providers/codex-app-server-provider.js +786 -0
  45. package/dist/src/providers/codex-app-server-provider.test.d.ts +2 -0
  46. package/dist/src/providers/codex-app-server-provider.test.d.ts.map +1 -0
  47. package/dist/src/providers/codex-app-server-provider.test.js +529 -0
  48. package/dist/src/providers/codex-provider.d.ts +24 -4
  49. package/dist/src/providers/codex-provider.d.ts.map +1 -1
  50. package/dist/src/providers/codex-provider.js +58 -6
  51. package/dist/src/providers/index.d.ts +1 -0
  52. package/dist/src/providers/index.d.ts.map +1 -1
  53. package/dist/src/providers/index.js +1 -0
  54. package/dist/src/providers/types.d.ts +1 -0
  55. package/dist/src/providers/types.d.ts.map +1 -1
  56. package/dist/src/routing/observation-recorder.test.js +1 -1
  57. package/dist/src/routing/observation-store.d.ts +15 -1
  58. package/dist/src/routing/observation-store.d.ts.map +1 -1
  59. package/dist/src/routing/observation-store.test.js +17 -11
  60. package/dist/src/templates/index.d.ts +2 -1
  61. package/dist/src/templates/index.d.ts.map +1 -1
  62. package/dist/src/templates/index.js +1 -0
  63. package/dist/src/templates/registry.d.ts +23 -0
  64. package/dist/src/templates/registry.d.ts.map +1 -1
  65. package/dist/src/templates/registry.js +80 -0
  66. package/dist/src/templates/registry.test.js +3 -2
  67. package/dist/src/templates/schema.d.ts +31 -0
  68. package/dist/src/templates/schema.d.ts.map +1 -0
  69. package/dist/src/templates/schema.js +139 -0
  70. package/dist/src/templates/schema.test.d.ts +2 -0
  71. package/dist/src/templates/schema.test.d.ts.map +1 -0
  72. package/dist/src/templates/schema.test.js +215 -0
  73. package/dist/src/templates/types.d.ts +2 -0
  74. package/dist/src/templates/types.d.ts.map +1 -1
  75. package/dist/src/templates/types.js +1 -0
  76. package/dist/src/tools/index.d.ts +2 -0
  77. package/dist/src/tools/index.d.ts.map +1 -1
  78. package/dist/src/tools/index.js +1 -0
  79. package/dist/src/tools/tool-category.d.ts +16 -0
  80. package/dist/src/tools/tool-category.d.ts.map +1 -0
  81. package/dist/src/tools/tool-category.js +58 -0
  82. 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';
@@ -16,6 +16,7 @@ import { createSessionLogger } from './session-logger.js';
16
16
  import { ContextManager } from './context-manager.js';
17
17
  import { isSessionLoggingEnabled, getLogAnalysisConfig } from './log-config.js';
18
18
  import { parseWorkResult } from './parse-work-result.js';
19
+ import { parseSecurityScanOutput } from './security-scan-event.js';
19
20
  import { runBackstop, formatBackstopComment } from './session-backstop.js';
20
21
  import { createActivityEmitter } from './activity-emitter.js';
21
22
  import { createApiActivityEmitter } from './api-activity-emitter.js';
@@ -26,6 +27,12 @@ import { ToolRegistry } from '../tools/index.js';
26
27
  import { createMergeQueueAdapter } from '../merge-queue/index.js';
27
28
  // Default inactivity timeout: 5 minutes
28
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;
29
36
  // Default max session timeout: unlimited (undefined)
30
37
  const DEFAULT_MAX_SESSION_TIMEOUT_MS = undefined;
31
38
  // Env vars that Claude Code interprets for authentication/routing. If these
@@ -746,6 +753,27 @@ Attempt rebase onto latest main.
746
753
  Resolve conflicts using mergiraf-enhanced git merge if available.
747
754
  Push updated branch and trigger merge via configured merge queue provider.${LINEAR_CLI_INSTRUCTION}`;
748
755
  break;
756
+ case 'security':
757
+ basePrompt = `Security scan ${identifier}.
758
+ Run security scanning tools (SAST, dependency audit) against the codebase and output structured results.
759
+
760
+ WORKFLOW:
761
+ 1. Identify the project type (Node.js, Python, etc.) by inspecting package.json, requirements.txt, etc.
762
+ 2. Run appropriate scanners (semgrep for SAST, npm-audit/pip-audit for dependencies)
763
+ 3. Parse scanner outputs and produce structured JSON summaries
764
+ 4. Output results in fenced code blocks tagged \`security-scan-result\`
765
+
766
+ IMPORTANT CONSTRAINTS:
767
+ - This is READ-ONLY scanning — do NOT make code changes, git commits, or fix vulnerabilities yourself.
768
+ - If critical or high severity issues found, emit <!-- WORK_RESULT:failed -->.
769
+ - If only medium/low or no issues found, emit <!-- WORK_RESULT:passed -->.
770
+ - If a scanner is not available, skip it and note this in your output.
771
+
772
+ STRUCTURED RESULT MARKER (REQUIRED):
773
+ You MUST include a structured result marker in your final output message.
774
+ - On pass: Include <!-- WORK_RESULT:passed --> in your final message
775
+ - On fail: Include <!-- WORK_RESULT:failed --> in your final message${LINEAR_CLI_INSTRUCTION}`;
776
+ break;
749
777
  }
750
778
  // Inject workflow failure context for retries
751
779
  if (options?.failureContext) {
@@ -774,6 +802,7 @@ const WORK_TYPE_SUFFIX = {
774
802
  'qa-coordination': 'QA-COORD',
775
803
  'acceptance-coordination': 'AC-COORD',
776
804
  merge: 'MRG',
805
+ security: 'SEC',
777
806
  };
778
807
  /**
779
808
  * Generate a worktree identifier that includes the work type suffix
@@ -1041,6 +1070,20 @@ export class AgentOrchestrator {
1041
1070
  maxSessionTimeoutMs: override?.maxSessionTimeoutMs ?? baseConfig.maxSessionTimeoutMs,
1042
1071
  };
1043
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
+ }
1044
1087
  return baseConfig;
1045
1088
  }
1046
1089
  /**
@@ -1777,18 +1820,139 @@ ORCHESTRATOR_INSTALL=1 exec pnpm add "$@"
1777
1820
  this.linkNodeModulesContents(src, dest, identifier);
1778
1821
  }
1779
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
+ }
1780
1839
  if (skipped > 0) {
1781
1840
  console.log(`[${identifier}] Dependencies linked successfully (${skipped} workspace(s) skipped — not on this branch)`);
1782
1841
  }
1783
1842
  else {
1784
1843
  console.log(`[${identifier}] Dependencies linked successfully`);
1785
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
+ }
1786
1856
  }
1787
1857
  catch (error) {
1788
1858
  console.warn(`[${identifier}] Symlink failed, falling back to install:`, error instanceof Error ? error.message : String(error));
1789
1859
  this.installDependencies(worktreePath, identifier);
1790
1860
  }
1791
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
+ }
1792
1956
  /**
1793
1957
  * Create a real node_modules directory and symlink each entry from the source.
1794
1958
  *
@@ -1796,10 +1960,11 @@ ORCHESTRATOR_INSTALL=1 exec pnpm add "$@"
1796
1960
  * resolve through the symlink and corrupt the original), we create a real
1797
1961
  * directory and symlink each entry individually. If pnpm "recreates" this
1798
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).
1799
1966
  */
1800
1967
  linkNodeModulesContents(srcNodeModules, destNodeModules, identifier) {
1801
- if (existsSync(destNodeModules))
1802
- return;
1803
1968
  mkdirSync(destNodeModules, { recursive: true });
1804
1969
  for (const entry of readdirSync(srcNodeModules)) {
1805
1970
  const srcEntry = resolve(srcNodeModules, entry);
@@ -1812,16 +1977,12 @@ ORCHESTRATOR_INSTALL=1 exec pnpm add "$@"
1812
1977
  for (const scopedEntry of readdirSync(srcEntry)) {
1813
1978
  const srcScoped = resolve(srcEntry, scopedEntry);
1814
1979
  const destScoped = resolve(destEntry, scopedEntry);
1815
- if (!existsSync(destScoped)) {
1816
- symlinkSync(srcScoped, destScoped);
1817
- }
1980
+ this.safeSymlink(srcScoped, destScoped);
1818
1981
  }
1819
1982
  continue;
1820
1983
  }
1821
1984
  }
1822
- if (!existsSync(destEntry)) {
1823
- symlinkSync(srcEntry, destEntry);
1824
- }
1985
+ this.safeSymlink(srcEntry, destEntry);
1825
1986
  }
1826
1987
  }
1827
1988
  /**
@@ -1830,36 +1991,8 @@ ORCHESTRATOR_INSTALL=1 exec pnpm add "$@"
1830
1991
  */
1831
1992
  installDependencies(worktreePath, identifier) {
1832
1993
  console.log(`[${identifier}] Installing dependencies via pnpm...`);
1833
- // Remove any node_modules from a partial linkDependencies attempt.
1834
- // Handles both old format (directory-level symlink) and new format
1835
- // (real directory with symlinked contents).
1836
- const destRoot = resolve(worktreePath, 'node_modules');
1837
- try {
1838
- if (existsSync(destRoot)) {
1839
- rmSync(destRoot, { recursive: true, force: true });
1840
- console.log(`[${identifier}] Removed partial node_modules before install`);
1841
- }
1842
- }
1843
- catch {
1844
- // Ignore cleanup errors — pnpm install may still work
1845
- }
1846
- // Also remove any per-workspace node_modules that were partially created
1847
- for (const subdir of ['apps', 'packages']) {
1848
- const subPath = resolve(worktreePath, subdir);
1849
- if (!existsSync(subPath))
1850
- continue;
1851
- try {
1852
- for (const entry of readdirSync(subPath)) {
1853
- const nm = resolve(subPath, entry, 'node_modules');
1854
- if (existsSync(nm)) {
1855
- rmSync(nm, { recursive: true, force: true });
1856
- }
1857
- }
1858
- }
1859
- catch {
1860
- // Ignore cleanup errors
1861
- }
1862
- }
1994
+ // Remove any node_modules from a partial linkDependencies attempt
1995
+ this.removeWorktreeNodeModules(worktreePath);
1863
1996
  // Set ORCHESTRATOR_INSTALL=1 to bypass the preinstall guard script
1864
1997
  // that blocks pnpm install in worktrees (to prevent symlink corruption).
1865
1998
  const installEnv = { ...process.env, ORCHESTRATOR_INSTALL: '1' };
@@ -1889,6 +2022,82 @@ ORCHESTRATOR_INSTALL=1 exec pnpm add "$@"
1889
2022
  }
1890
2023
  }
1891
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
+ }
1892
2101
  /**
1893
2102
  * @deprecated Use linkDependencies() instead. This now delegates to linkDependencies.
1894
2103
  */
@@ -2201,6 +2410,25 @@ ORCHESTRATOR_INSTALL=1 exec pnpm add "$@"
2201
2410
  // Ignore state update errors
2202
2411
  }
2203
2412
  }
2413
+ // Emit structured security scan events for security work type agents
2414
+ if (emitter && agent.status === 'completed' && agent.workType === 'security') {
2415
+ const fullOutput = assistantTextChunks.join('\n');
2416
+ const scanEvents = parseSecurityScanOutput(fullOutput);
2417
+ for (const scanEvent of scanEvents) {
2418
+ try {
2419
+ await emitter.emitSecurityScan(scanEvent);
2420
+ log?.info('Security scan event emitted', {
2421
+ scanner: scanEvent.scanner,
2422
+ findings: scanEvent.totalFindings,
2423
+ });
2424
+ }
2425
+ catch (scanError) {
2426
+ log?.warn('Failed to emit security scan event', {
2427
+ error: scanError instanceof Error ? scanError.message : String(scanError),
2428
+ });
2429
+ }
2430
+ }
2431
+ }
2204
2432
  // Emit a final response activity to close the Linear agent session.
2205
2433
  // Linear auto-transitions sessions to "complete" when a response activity is emitted.
2206
2434
  if (emitter && (agent.status === 'completed' || agent.status === 'failed')) {
@@ -2298,56 +2526,64 @@ ORCHESTRATOR_INSTALL=1 exec pnpm add "$@"
2298
2526
  }
2299
2527
  }
2300
2528
  // Update Linear status based on work type if auto-transition is enabled
2301
- if (agent.status === 'completed' && this.config.autoTransition) {
2529
+ if ((agent.status === 'completed' || agent.status === 'failed') && this.config.autoTransition) {
2302
2530
  const workType = agent.workType ?? 'development';
2303
2531
  const isResultSensitive = workType === 'qa' || workType === 'acceptance' || workType === 'coordination' || workType === 'qa-coordination' || workType === 'acceptance-coordination';
2304
2532
  let targetStatus = null;
2305
2533
  if (isResultSensitive) {
2306
- // For QA/acceptance: parse result to decide promote vs reject.
2307
- // Try the final result message first, then fall back to scanning
2308
- // all accumulated assistant text (the marker may be in an earlier turn).
2309
- let workResult = parseWorkResult(agent.resultMessage, workType);
2310
- if (workResult === 'unknown' && assistantTextChunks.length > 0) {
2311
- const fullText = assistantTextChunks.join('\n');
2312
- workResult = parseWorkResult(fullText, workType);
2313
- if (workResult !== 'unknown') {
2314
- log?.info('Work result found in accumulated text (not in final message)', { workResult });
2315
- }
2316
- }
2317
- agent.workResult = workResult;
2318
- if (workResult === 'passed') {
2319
- targetStatus = this.statusMappings.workTypeCompleteStatus[workType];
2320
- log?.info('Work result: passed, promoting', { workType, targetStatus });
2321
- }
2322
- else if (workResult === 'failed') {
2534
+ if (agent.status === 'failed') {
2535
+ // Agent crashed/errored treat as QA/acceptance failure
2536
+ agent.workResult = 'failed';
2323
2537
  targetStatus = this.statusMappings.workTypeFailStatus[workType];
2324
- log?.info('Work result: failed, transitioning to fail status', { workType, targetStatus });
2538
+ log?.info('Agent failed (crash/error), transitioning to fail status', { workType, targetStatus });
2325
2539
  }
2326
2540
  else {
2327
- // unknown safe default: don't transition
2328
- log?.warn('Work result: unknown, skipping auto-transition', {
2329
- workType,
2330
- hasResultMessage: !!agent.resultMessage,
2331
- });
2332
- // Post a diagnostic comment so the issue doesn't silently stall
2333
- try {
2334
- await this.client.createComment(issueId, `⚠️ Agent completed but no structured result marker was detected in the output.\n\n` +
2335
- `**Issue status was NOT updated automatically.**\n\n` +
2336
- `The orchestrator expected one of:\n` +
2337
- `- \`<!-- WORK_RESULT:passed -->\` to promote the issue\n` +
2338
- `- \`<!-- WORK_RESULT:failed -->\` to record a failure\n\n` +
2339
- `This usually means the agent exited early (timeout, error, or missing logic). ` +
2340
- `Check the agent logs for details, then manually update the issue status or re-trigger the agent.`);
2341
- 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
+ }
2342
2551
  }
2343
- catch (error) {
2344
- log?.warn('Failed to post diagnostic comment for unknown work result', {
2345
- 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,
2346
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
+ }
2347
2583
  }
2348
2584
  }
2349
2585
  }
2350
- else {
2586
+ else if (agent.status === 'completed') {
2351
2587
  // Non-QA/acceptance: promote on completion, but validate code-producing work types first
2352
2588
  const isCodeProducing = workType === 'development' || workType === 'inflight';
2353
2589
  if (isCodeProducing && agent.worktreePath && !agent.pullRequestUrl) {
@@ -2662,7 +2898,9 @@ ORCHESTRATOR_INSTALL=1 exec pnpm add "$@"
2662
2898
  agent.providerSessionId = event.sessionId;
2663
2899
  this.updateLastActivity(issueId, 'init');
2664
2900
  // Update state with provider session ID (only for worktree-based agents)
2665
- 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') {
2666
2904
  try {
2667
2905
  updateState(agent.worktreePath, {
2668
2906
  providerSessionId: event.sessionId,
@@ -2840,10 +3078,17 @@ ORCHESTRATOR_INSTALL=1 exec pnpm add "$@"
2840
3078
  : `Agent error: ${event.errorSubtype}`;
2841
3079
  if (agent.worktreePath) {
2842
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');
2843
3084
  updateState(agent.worktreePath, {
2844
3085
  status: 'failed',
2845
3086
  errorMessage,
3087
+ ...(isStaleSession && { providerSessionId: null }),
2846
3088
  });
3089
+ if (isStaleSession) {
3090
+ log?.info('Cleared stale providerSessionId from state — next recovery will start fresh');
3091
+ }
2847
3092
  }
2848
3093
  catch {
2849
3094
  // Ignore state update errors
@@ -3145,8 +3390,8 @@ ORCHESTRATOR_INSTALL=1 exec pnpm add "$@"
3145
3390
  const workType = await this.detectWorkType(issue.id, 'Backlog');
3146
3391
  // Create worktree with work type suffix
3147
3392
  const { worktreePath, worktreeIdentifier } = this.createWorktree(issue.identifier, workType);
3148
- // Link dependencies from main repo into worktree
3149
- this.linkDependencies(worktreePath, issue.identifier);
3393
+ // Sync and link dependencies from main repo into worktree
3394
+ this.syncDependencies(worktreePath, issue.identifier);
3150
3395
  const startStatus = this.statusMappings.workTypeStartStatus[workType];
3151
3396
  // Update issue status based on work type if auto-transition is enabled
3152
3397
  if (this.config.autoTransition && startStatus) {
@@ -3263,8 +3508,8 @@ ORCHESTRATOR_INSTALL=1 exec pnpm add "$@"
3263
3508
  const wt = this.createWorktree(identifier, effectiveWorkType);
3264
3509
  worktreePath = wt.worktreePath;
3265
3510
  worktreeIdentifier = wt.worktreeIdentifier;
3266
- // Link dependencies from main repo into worktree
3267
- this.linkDependencies(worktreePath, identifier);
3511
+ // Sync and link dependencies from main repo into worktree
3512
+ this.syncDependencies(worktreePath, identifier);
3268
3513
  // Check for existing state and potential recovery
3269
3514
  const recoveryCheck = checkRecovery(worktreePath, {
3270
3515
  heartbeatTimeoutMs: getHeartbeatTimeoutFromEnv(),
@@ -3517,8 +3762,8 @@ ORCHESTRATOR_INSTALL=1 exec pnpm add "$@"
3517
3762
  const result = this.createWorktree(identifier, workType);
3518
3763
  worktreePath = result.worktreePath;
3519
3764
  worktreeIdentifier = result.worktreeIdentifier;
3520
- // Link dependencies from main repo into worktree
3521
- this.linkDependencies(worktreePath, identifier);
3765
+ // Sync and link dependencies from main repo into worktree
3766
+ this.syncDependencies(worktreePath, identifier);
3522
3767
  }
3523
3768
  }
3524
3769
  catch (error) {
@@ -3537,8 +3782,8 @@ ORCHESTRATOR_INSTALL=1 exec pnpm add "$@"
3537
3782
  const result = this.createWorktree(identifier, effectiveWorkType);
3538
3783
  worktreePath = result.worktreePath;
3539
3784
  worktreeIdentifier = result.worktreeIdentifier;
3540
- // Link dependencies from main repo into worktree
3541
- this.linkDependencies(worktreePath, identifier);
3785
+ // Sync and link dependencies from main repo into worktree
3786
+ this.syncDependencies(worktreePath, identifier);
3542
3787
  }
3543
3788
  catch (error) {
3544
3789
  return {
@@ -0,0 +1,57 @@
1
+ /**
2
+ * Security Scan Event
3
+ *
4
+ * Defines the `SecurityScanEvent` type and parsers for extracting structured
5
+ * vulnerability data from security scanner output (semgrep JSON, npm-audit JSON, etc.).
6
+ *
7
+ * The security station template instructs agents to output structured JSON in
8
+ * fenced code blocks tagged `security-scan-result`. This module parses that output.
9
+ */
10
+ import { z } from 'zod';
11
+ export interface SecurityScanEvent {
12
+ type: 'agent.security-scan';
13
+ scanner: string;
14
+ severityCounts: {
15
+ critical: number;
16
+ high: number;
17
+ medium: number;
18
+ low: number;
19
+ };
20
+ totalFindings: number;
21
+ target: string;
22
+ scanDurationMs: number;
23
+ timestamp: string;
24
+ }
25
+ export declare const SecurityScanEventSchema: z.ZodObject<{
26
+ type: z.ZodLiteral<"agent.security-scan">;
27
+ scanner: z.ZodString;
28
+ severityCounts: z.ZodObject<{
29
+ critical: z.ZodNumber;
30
+ high: z.ZodNumber;
31
+ medium: z.ZodNumber;
32
+ low: z.ZodNumber;
33
+ }, z.core.$strip>;
34
+ totalFindings: z.ZodNumber;
35
+ target: z.ZodString;
36
+ scanDurationMs: z.ZodNumber;
37
+ timestamp: z.ZodString;
38
+ }, z.core.$strip>;
39
+ /**
40
+ * Parse structured security scan results from agent output.
41
+ *
42
+ * Looks for fenced code blocks tagged `security-scan-result` and parses
43
+ * the JSON inside each block. Returns one `SecurityScanEvent` per block.
44
+ *
45
+ * Gracefully handles malformed output — returns partial data with zero counts
46
+ * rather than throwing.
47
+ */
48
+ export declare function parseSecurityScanOutput(rawOutput: string): SecurityScanEvent[];
49
+ /**
50
+ * Parse semgrep --json output into a SecurityScanEvent.
51
+ */
52
+ export declare function parseSemgrepOutput(rawJson: string, target: string, durationMs: number): SecurityScanEvent;
53
+ /**
54
+ * Parse npm audit --json output into a SecurityScanEvent.
55
+ */
56
+ export declare function parseNpmAuditOutput(rawJson: string, target: string, durationMs: number): SecurityScanEvent;
57
+ //# sourceMappingURL=security-scan-event.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"security-scan-event.d.ts","sourceRoot":"","sources":["../../../src/orchestrator/security-scan-event.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAEH,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAA;AAMvB,MAAM,WAAW,iBAAiB;IAChC,IAAI,EAAE,qBAAqB,CAAA;IAC3B,OAAO,EAAE,MAAM,CAAA;IACf,cAAc,EAAE;QAAE,QAAQ,EAAE,MAAM,CAAC;QAAC,IAAI,EAAE,MAAM,CAAC;QAAC,MAAM,EAAE,MAAM,CAAC;QAAC,GAAG,EAAE,MAAM,CAAA;KAAE,CAAA;IAC/E,aAAa,EAAE,MAAM,CAAA;IACrB,MAAM,EAAE,MAAM,CAAA;IACd,cAAc,EAAE,MAAM,CAAA;IACtB,SAAS,EAAE,MAAM,CAAA;CAClB;AAMD,eAAO,MAAM,uBAAuB;;;;;;;;;;;;;iBAalC,CAAA;AAYF;;;;;;;;GAQG;AACH,wBAAgB,uBAAuB,CAAC,SAAS,EAAE,MAAM,GAAG,iBAAiB,EAAE,CAkB9E;AAED;;GAEG;AACH,wBAAgB,kBAAkB,CAAC,OAAO,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,UAAU,EAAE,MAAM,GAAG,iBAAiB,CA2BzG;AAED;;GAEG;AACH,wBAAgB,mBAAmB,CAAC,OAAO,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,UAAU,EAAE,MAAM,GAAG,iBAAiB,CAoD1G"}