@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.
- package/dist/src/governor/decision-engine-adapter.d.ts +43 -0
- package/dist/src/governor/decision-engine-adapter.d.ts.map +1 -0
- package/dist/src/governor/decision-engine-adapter.js +422 -0
- package/dist/src/governor/decision-engine-adapter.test.d.ts +2 -0
- package/dist/src/governor/decision-engine-adapter.test.d.ts.map +1 -0
- package/dist/src/governor/decision-engine-adapter.test.js +363 -0
- package/dist/src/governor/index.d.ts +1 -0
- package/dist/src/governor/index.d.ts.map +1 -1
- package/dist/src/governor/index.js +1 -0
- package/dist/src/manifest/route-manifest.d.ts.map +1 -1
- package/dist/src/manifest/route-manifest.js +4 -0
- package/dist/src/merge-queue/adapters/github-native.d.ts +8 -0
- package/dist/src/merge-queue/adapters/github-native.d.ts.map +1 -1
- package/dist/src/merge-queue/adapters/github-native.js +37 -7
- package/dist/src/merge-queue/adapters/github-native.test.js +71 -42
- package/dist/src/orchestrator/activity-emitter.d.ts +7 -0
- package/dist/src/orchestrator/activity-emitter.d.ts.map +1 -1
- package/dist/src/orchestrator/activity-emitter.js +19 -1
- package/dist/src/orchestrator/api-activity-emitter.d.ts +6 -0
- package/dist/src/orchestrator/api-activity-emitter.d.ts.map +1 -1
- package/dist/src/orchestrator/api-activity-emitter.js +35 -0
- package/dist/src/orchestrator/index.d.ts +2 -0
- package/dist/src/orchestrator/index.d.ts.map +1 -1
- package/dist/src/orchestrator/index.js +1 -0
- package/dist/src/orchestrator/issue-tracker-client.d.ts +2 -1
- package/dist/src/orchestrator/issue-tracker-client.d.ts.map +1 -1
- package/dist/src/orchestrator/orchestrator.d.ts +27 -0
- package/dist/src/orchestrator/orchestrator.d.ts.map +1 -1
- package/dist/src/orchestrator/orchestrator.js +331 -86
- package/dist/src/orchestrator/security-scan-event.d.ts +57 -0
- package/dist/src/orchestrator/security-scan-event.d.ts.map +1 -0
- package/dist/src/orchestrator/security-scan-event.js +192 -0
- package/dist/src/orchestrator/security-scan-event.test.d.ts +2 -0
- package/dist/src/orchestrator/security-scan-event.test.d.ts.map +1 -0
- package/dist/src/orchestrator/security-scan-event.test.js +219 -0
- package/dist/src/orchestrator/state-recovery.d.ts.map +1 -1
- package/dist/src/orchestrator/state-recovery.js +1 -0
- package/dist/src/orchestrator/work-types.d.ts +1 -1
- package/dist/src/orchestrator/work-types.d.ts.map +1 -1
- package/dist/src/providers/claude-provider.d.ts.map +1 -1
- package/dist/src/providers/claude-provider.js +11 -0
- package/dist/src/providers/codex-app-server-provider.d.ts +201 -0
- package/dist/src/providers/codex-app-server-provider.d.ts.map +1 -0
- package/dist/src/providers/codex-app-server-provider.js +786 -0
- package/dist/src/providers/codex-app-server-provider.test.d.ts +2 -0
- package/dist/src/providers/codex-app-server-provider.test.d.ts.map +1 -0
- package/dist/src/providers/codex-app-server-provider.test.js +529 -0
- package/dist/src/providers/codex-provider.d.ts +24 -4
- package/dist/src/providers/codex-provider.d.ts.map +1 -1
- package/dist/src/providers/codex-provider.js +58 -6
- package/dist/src/providers/index.d.ts +1 -0
- package/dist/src/providers/index.d.ts.map +1 -1
- package/dist/src/providers/index.js +1 -0
- package/dist/src/providers/types.d.ts +1 -0
- package/dist/src/providers/types.d.ts.map +1 -1
- package/dist/src/routing/observation-recorder.test.js +1 -1
- package/dist/src/routing/observation-store.d.ts +15 -1
- package/dist/src/routing/observation-store.d.ts.map +1 -1
- package/dist/src/routing/observation-store.test.js +17 -11
- package/dist/src/templates/index.d.ts +2 -1
- package/dist/src/templates/index.d.ts.map +1 -1
- package/dist/src/templates/index.js +1 -0
- package/dist/src/templates/registry.d.ts +23 -0
- package/dist/src/templates/registry.d.ts.map +1 -1
- package/dist/src/templates/registry.js +80 -0
- package/dist/src/templates/registry.test.js +3 -2
- package/dist/src/templates/schema.d.ts +31 -0
- package/dist/src/templates/schema.d.ts.map +1 -0
- package/dist/src/templates/schema.js +139 -0
- package/dist/src/templates/schema.test.d.ts +2 -0
- package/dist/src/templates/schema.test.d.ts.map +1 -0
- package/dist/src/templates/schema.test.js +215 -0
- package/dist/src/templates/types.d.ts +2 -0
- package/dist/src/templates/types.d.ts.map +1 -1
- package/dist/src/templates/types.js +1 -0
- package/dist/src/tools/index.d.ts +2 -0
- package/dist/src/tools/index.d.ts.map +1 -1
- package/dist/src/tools/index.js +1 -0
- package/dist/src/tools/tool-category.d.ts +16 -0
- package/dist/src/tools/tool-category.d.ts.map +1 -0
- package/dist/src/tools/tool-category.js +58 -0
- 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
|
-
|
|
1816
|
-
symlinkSync(srcScoped, destScoped);
|
|
1817
|
-
}
|
|
1980
|
+
this.safeSymlink(srcScoped, destScoped);
|
|
1818
1981
|
}
|
|
1819
1982
|
continue;
|
|
1820
1983
|
}
|
|
1821
1984
|
}
|
|
1822
|
-
|
|
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
|
-
|
|
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
|
-
|
|
2307
|
-
|
|
2308
|
-
|
|
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('
|
|
2538
|
+
log?.info('Agent failed (crash/error), transitioning to fail status', { workType, targetStatus });
|
|
2325
2539
|
}
|
|
2326
2540
|
else {
|
|
2327
|
-
//
|
|
2328
|
-
|
|
2329
|
-
|
|
2330
|
-
|
|
2331
|
-
|
|
2332
|
-
|
|
2333
|
-
|
|
2334
|
-
|
|
2335
|
-
|
|
2336
|
-
|
|
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
|
-
|
|
2344
|
-
|
|
2345
|
-
|
|
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
|
|
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
|
-
//
|
|
3149
|
-
this.
|
|
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
|
-
//
|
|
3267
|
-
this.
|
|
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
|
-
//
|
|
3521
|
-
this.
|
|
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
|
-
//
|
|
3541
|
-
this.
|
|
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"}
|