memorix 1.1.7 → 1.1.8
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/CHANGELOG.md +13 -0
- package/CLAUDE.md +6 -1
- package/README.md +21 -0
- package/README.zh-CN.md +21 -0
- package/TEAM.md +86 -86
- package/dist/cli/index.js +730 -150
- package/dist/cli/index.js.map +1 -1
- package/dist/dashboard/static/index.html +201 -201
- package/dist/dashboard/static/style.css +3584 -3584
- package/dist/index.js +15 -8
- package/dist/index.js.map +1 -1
- package/dist/memcode-runtime/CHANGELOG.md +13 -0
- package/dist/memcode-runtime/package.json +4 -4
- package/dist/sdk.js +15 -8
- package/dist/sdk.js.map +1 -1
- package/docs/AGENT_OPERATOR_PLAYBOOK.md +18 -0
- package/docs/DESIGN_DECISIONS.md +357 -357
- package/docs/SETUP.md +10 -0
- package/docs/dev-log/progress.txt +35 -26
- package/package.json +1 -1
- package/src/audit/index.ts +156 -156
- package/src/cli/commands/agent-integrations.ts +623 -0
- package/src/cli/commands/audit-list.ts +89 -89
- package/src/cli/commands/background.ts +659 -659
- package/src/cli/commands/cleanup.ts +255 -255
- package/src/cli/commands/doctor.ts +23 -0
- package/src/cli/commands/formation.ts +48 -48
- package/src/cli/commands/git-hook-install.ts +111 -111
- package/src/cli/commands/handoff.ts +66 -66
- package/src/cli/commands/hooks-status.ts +63 -63
- package/src/cli/commands/ingest-commit.ts +153 -153
- package/src/cli/commands/ingest-image.ts +73 -73
- package/src/cli/commands/ingest-log.ts +180 -180
- package/src/cli/commands/ingest.ts +44 -44
- package/src/cli/commands/integrate-shared.ts +15 -15
- package/src/cli/commands/lock.ts +96 -96
- package/src/cli/commands/message.ts +121 -121
- package/src/cli/commands/poll.ts +70 -70
- package/src/cli/commands/purge-all-memory.ts +85 -85
- package/src/cli/commands/purge-project-memory.ts +83 -83
- package/src/cli/commands/reasoning.ts +132 -132
- package/src/cli/commands/repair.ts +60 -0
- package/src/cli/commands/retention.ts +108 -108
- package/src/cli/commands/serve-shared.ts +118 -118
- package/src/cli/commands/setup.ts +3 -3
- package/src/cli/commands/skills.ts +123 -123
- package/src/cli/commands/task.ts +192 -192
- package/src/cli/commands/transfer.ts +73 -73
- package/src/cli/commands/uninstall-project-artifacts.ts +85 -85
- package/src/cli/index.ts +3 -1
- package/src/cli/tui/ChatView.tsx +234 -234
- package/src/cli/tui/CommandBar.tsx +312 -312
- package/src/cli/tui/ContextRail.tsx +118 -118
- package/src/cli/tui/HeaderBar.tsx +72 -72
- package/src/cli/tui/LogoBanner.tsx +51 -51
- package/src/cli/tui/Panels.tsx +632 -632
- package/src/cli/tui/Sidebar.tsx +179 -179
- package/src/cli/tui/chat-service.ts +742 -742
- package/src/cli/tui/data.ts +547 -547
- package/src/cli/tui/index.ts +41 -41
- package/src/cli/tui/markdown-render.tsx +371 -371
- package/src/cli/tui/theme.ts +178 -178
- package/src/cli/tui/use-mouse.ts +157 -157
- package/src/cli/tui/useNavigation.ts +56 -56
- package/src/cli/update-checker.ts +211 -211
- package/src/cli/version.ts +7 -7
- package/src/cli/workbench.ts +1 -1
- package/src/codegraph/context-pack.ts +5 -1
- package/src/codegraph/lite-provider.ts +5 -1
- package/src/codegraph/project-context.ts +5 -1
- package/src/compact/token-budget.ts +74 -74
- package/src/config/behavior.ts +59 -59
- package/src/dashboard/project-classification.ts +64 -64
- package/src/dashboard/static/index.html +201 -201
- package/src/dashboard/static/style.css +3584 -3584
- package/src/embedding/fastembed-provider.ts +142 -142
- package/src/embedding/transformers-provider.ts +111 -111
- package/src/git/extractor.ts +209 -209
- package/src/git/hooks-path.ts +85 -85
- package/src/git/noise-filter.ts +210 -210
- package/src/hooks/installers/index.ts +4 -4
- package/src/hooks/official-skills.ts +1 -1
- package/src/hooks/pattern-detector.ts +173 -173
- package/src/hooks/rules/memorix-agent-rules.md +2 -2
- package/src/hooks/significance-filter.ts +250 -250
- package/src/llm/memory-manager.ts +328 -328
- package/src/llm/provider.ts +885 -885
- package/src/llm/quality.ts +248 -248
- package/src/memory/attribution-guard.ts +249 -249
- package/src/memory/auto-relations.ts +107 -107
- package/src/memory/consolidation.ts +302 -302
- package/src/memory/disclosure-policy.ts +141 -141
- package/src/memory/entity-extractor.ts +197 -197
- package/src/memory/formation/evaluate.ts +217 -217
- package/src/memory/formation/extract.ts +361 -361
- package/src/memory/formation/index.ts +417 -417
- package/src/memory/formation/resolve.ts +344 -344
- package/src/memory/formation/types.ts +315 -315
- package/src/memory/freshness.ts +122 -122
- package/src/memory/graph.ts +197 -197
- package/src/memory/refs.ts +94 -94
- package/src/memory/retention.ts +433 -433
- package/src/memory/secret-filter.ts +79 -79
- package/src/memory/session.ts +523 -523
- package/src/multimodal/image-loader.ts +143 -143
- package/src/orchestrate/adapters/claude-stream.ts +192 -192
- package/src/orchestrate/adapters/claude.ts +111 -111
- package/src/orchestrate/adapters/codex-stream.ts +134 -134
- package/src/orchestrate/adapters/codex.ts +41 -41
- package/src/orchestrate/adapters/gemini-stream.ts +166 -166
- package/src/orchestrate/adapters/gemini.ts +42 -42
- package/src/orchestrate/adapters/index.ts +73 -73
- package/src/orchestrate/adapters/opencode-stream.ts +143 -143
- package/src/orchestrate/adapters/opencode.ts +47 -47
- package/src/orchestrate/adapters/spawn-helper.ts +286 -286
- package/src/orchestrate/adapters/types.ts +77 -77
- package/src/orchestrate/capability-router.ts +284 -284
- package/src/orchestrate/context-compact.ts +188 -188
- package/src/orchestrate/cost-tracker.ts +219 -219
- package/src/orchestrate/error-recovery.ts +191 -191
- package/src/orchestrate/evidence.ts +140 -140
- package/src/orchestrate/ledger.ts +110 -110
- package/src/orchestrate/memorix-bridge.ts +380 -380
- package/src/orchestrate/output-budget.ts +80 -80
- package/src/orchestrate/permission.ts +152 -152
- package/src/orchestrate/pipeline-trace.ts +131 -131
- package/src/orchestrate/prompt-builder.ts +155 -155
- package/src/orchestrate/ring-buffer.ts +37 -37
- package/src/orchestrate/task-graph.ts +389 -389
- package/src/orchestrate/verify-gate.ts +219 -219
- package/src/orchestrate/worktree.ts +232 -232
- package/src/project/aliases.ts +374 -374
- package/src/project/detector.ts +268 -268
- package/src/rules/adapters/claude-code.ts +99 -99
- package/src/rules/adapters/codex.ts +97 -97
- package/src/rules/adapters/copilot.ts +124 -124
- package/src/rules/adapters/cursor.ts +114 -114
- package/src/rules/adapters/kiro.ts +126 -126
- package/src/rules/adapters/trae.ts +56 -56
- package/src/rules/adapters/windsurf.ts +83 -83
- package/src/rules/syncer.ts +235 -235
- package/src/sdk.ts +327 -327
- package/src/search/intent-detector.ts +289 -289
- package/src/search/query-expansion.ts +52 -52
- package/src/server/formation-timeout.ts +27 -27
- package/src/skills/mini-skills.ts +386 -386
- package/src/store/chat-store.ts +119 -119
- package/src/store/file-lock.ts +100 -100
- package/src/store/graph-store.ts +249 -249
- package/src/store/mini-skill-store.ts +349 -349
- package/src/store/obs-store.ts +255 -255
- package/src/store/persistence-json.ts +212 -212
- package/src/store/persistence.ts +291 -291
- package/src/store/project-affinity.ts +195 -195
- package/src/store/session-store.ts +259 -259
- package/src/store/sqlite-store.ts +339 -339
- package/src/team/event-bus.ts +76 -76
- package/src/team/file-locks.ts +173 -173
- package/src/team/handoff.ts +167 -167
- package/src/team/messages.ts +203 -203
- package/src/team/poll.ts +132 -132
- package/src/team/tasks.ts +211 -211
- package/src/wiki/generator.ts +237 -237
- package/src/wiki/knowledge-graph.ts +334 -334
- package/src/wiki/types.ts +85 -85
- package/src/workspace/mcp-adapters/codex.ts +191 -191
- package/src/workspace/mcp-adapters/copilot.ts +105 -105
- package/src/workspace/mcp-adapters/cursor.ts +53 -53
- package/src/workspace/mcp-adapters/kiro.ts +64 -64
- package/src/workspace/mcp-adapters/opencode.ts +123 -123
- package/src/workspace/mcp-adapters/trae.ts +134 -134
- package/src/workspace/mcp-adapters/windsurf.ts +91 -91
- package/src/workspace/sanitizer.ts +60 -60
- package/src/workspace/workflow-sync.ts +131 -131
|
@@ -1,219 +1,219 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Verify Gates — Phase 7, Step 1: Compile + test verification after agent completion.
|
|
3
|
-
*
|
|
4
|
-
* Runs configurable shell commands (e.g. `tsc --noEmit`, `vitest run`) in the
|
|
5
|
-
* task's working directory. Returns a structured result used by the coordinator
|
|
6
|
-
* to decide whether to mark a task completed or enter the fix loop.
|
|
7
|
-
*
|
|
8
|
-
* Design principle: gates are OPTIONAL. If no command is configured, the gate
|
|
9
|
-
* is skipped and the task completes as before (backward compatible).
|
|
10
|
-
*/
|
|
11
|
-
|
|
12
|
-
import { spawn, execSync } from 'node:child_process';
|
|
13
|
-
|
|
14
|
-
// ── Types ──────────────────────────────────────────────────────────
|
|
15
|
-
|
|
16
|
-
export interface GateResult {
|
|
17
|
-
gate: 'compile' | 'test';
|
|
18
|
-
passed: boolean;
|
|
19
|
-
/** Combined stdout + stderr, trimmed to budget */
|
|
20
|
-
output: string;
|
|
21
|
-
/** Wall-clock duration in ms */
|
|
22
|
-
durationMs: number;
|
|
23
|
-
/** The command that was executed */
|
|
24
|
-
command: string;
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
export interface GateConfig {
|
|
28
|
-
/** Shell command to run for compile gate (e.g. 'npm run build', 'tsc --noEmit') */
|
|
29
|
-
compileCommand?: string;
|
|
30
|
-
/** Shell command to run for test gate (e.g. 'npm test', 'npx vitest run') */
|
|
31
|
-
testCommand?: string;
|
|
32
|
-
/** Compile gate timeout in ms (default: 120_000) */
|
|
33
|
-
compileTimeoutMs?: number;
|
|
34
|
-
/** Test gate timeout in ms (default: 300_000) */
|
|
35
|
-
testTimeoutMs?: number;
|
|
36
|
-
/** Max output bytes to keep in memory (default: 32_768 = 32KB) */
|
|
37
|
-
outputBudget?: number;
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
// ── Constants ──────────────────────────────────────────────────────
|
|
41
|
-
|
|
42
|
-
const DEFAULT_COMPILE_TIMEOUT = 120_000;
|
|
43
|
-
const DEFAULT_TEST_TIMEOUT = 300_000;
|
|
44
|
-
const DEFAULT_OUTPUT_BUDGET = 32_768; // 32KB
|
|
45
|
-
|
|
46
|
-
// ── Core ───────────────────────────────────────────────────────────
|
|
47
|
-
|
|
48
|
-
/**
|
|
49
|
-
* Run a single gate command. Resolves with the result — never throws.
|
|
50
|
-
*
|
|
51
|
-
* The command is run in a shell (`/bin/sh -c` on Unix, `cmd /c` on Windows)
|
|
52
|
-
* so pipes, redirects, and chained commands work naturally.
|
|
53
|
-
*/
|
|
54
|
-
export function runGate(
|
|
55
|
-
gate: 'compile' | 'test',
|
|
56
|
-
command: string,
|
|
57
|
-
cwd: string,
|
|
58
|
-
timeoutMs: number,
|
|
59
|
-
outputBudget: number = DEFAULT_OUTPUT_BUDGET,
|
|
60
|
-
): Promise<GateResult> {
|
|
61
|
-
const start = Date.now();
|
|
62
|
-
|
|
63
|
-
return new Promise<GateResult>((resolve) => {
|
|
64
|
-
const chunks: Buffer[] = [];
|
|
65
|
-
let totalBytes = 0;
|
|
66
|
-
|
|
67
|
-
const proc = spawn(command, {
|
|
68
|
-
cwd,
|
|
69
|
-
shell: true,
|
|
70
|
-
detached: process.platform !== 'win32',
|
|
71
|
-
stdio: ['ignore', 'pipe', 'pipe'],
|
|
72
|
-
env: { ...process.env, FORCE_COLOR: '0', NO_COLOR: '1' },
|
|
73
|
-
});
|
|
74
|
-
|
|
75
|
-
const collectOutput = (data: Buffer) => {
|
|
76
|
-
if (totalBytes < outputBudget) {
|
|
77
|
-
const remaining = outputBudget - totalBytes;
|
|
78
|
-
chunks.push(data.length > remaining ? data.subarray(0, remaining) : data);
|
|
79
|
-
}
|
|
80
|
-
totalBytes += data.length;
|
|
81
|
-
};
|
|
82
|
-
|
|
83
|
-
proc.stdout?.on('data', collectOutput);
|
|
84
|
-
proc.stderr?.on('data', collectOutput);
|
|
85
|
-
|
|
86
|
-
let killed = false;
|
|
87
|
-
const killTree = () => {
|
|
88
|
-
try {
|
|
89
|
-
if (process.platform === 'win32') {
|
|
90
|
-
// Windows: kill entire process tree via taskkill
|
|
91
|
-
execSync(`taskkill /F /T /PID ${proc.pid}`, { stdio: 'ignore' });
|
|
92
|
-
} else {
|
|
93
|
-
// Unix: kill process group
|
|
94
|
-
process.kill(-proc.pid!, 'SIGKILL');
|
|
95
|
-
}
|
|
96
|
-
} catch {
|
|
97
|
-
// Fallback: kill the process directly
|
|
98
|
-
try { proc.kill('SIGKILL'); } catch { /* already dead */ }
|
|
99
|
-
}
|
|
100
|
-
};
|
|
101
|
-
|
|
102
|
-
const timer = setTimeout(() => {
|
|
103
|
-
killed = true;
|
|
104
|
-
killTree();
|
|
105
|
-
}, timeoutMs);
|
|
106
|
-
|
|
107
|
-
proc.on('close', (code) => {
|
|
108
|
-
clearTimeout(timer);
|
|
109
|
-
const output = Buffer.concat(chunks).toString('utf-8');
|
|
110
|
-
const durationMs = Date.now() - start;
|
|
111
|
-
|
|
112
|
-
if (killed) {
|
|
113
|
-
resolve({
|
|
114
|
-
gate,
|
|
115
|
-
passed: false,
|
|
116
|
-
output: output + `\n[TIMEOUT] Gate "${gate}" killed after ${timeoutMs}ms`,
|
|
117
|
-
durationMs,
|
|
118
|
-
command,
|
|
119
|
-
});
|
|
120
|
-
return;
|
|
121
|
-
}
|
|
122
|
-
|
|
123
|
-
resolve({
|
|
124
|
-
gate,
|
|
125
|
-
passed: code === 0,
|
|
126
|
-
output,
|
|
127
|
-
durationMs,
|
|
128
|
-
command,
|
|
129
|
-
});
|
|
130
|
-
});
|
|
131
|
-
|
|
132
|
-
proc.on('error', (err) => {
|
|
133
|
-
clearTimeout(timer);
|
|
134
|
-
resolve({
|
|
135
|
-
gate,
|
|
136
|
-
passed: false,
|
|
137
|
-
output: `[ERROR] Failed to spawn gate command: ${err.message}`,
|
|
138
|
-
durationMs: Date.now() - start,
|
|
139
|
-
command,
|
|
140
|
-
});
|
|
141
|
-
});
|
|
142
|
-
});
|
|
143
|
-
}
|
|
144
|
-
|
|
145
|
-
/**
|
|
146
|
-
* Run compile and test gates sequentially.
|
|
147
|
-
*
|
|
148
|
-
* - If compileCommand is not set → skip compile gate
|
|
149
|
-
* - If testCommand is not set → skip test gate
|
|
150
|
-
* - If compile fails → do NOT run test gate (no point testing broken code)
|
|
151
|
-
* - Returns array of GateResults (0, 1, or 2 items)
|
|
152
|
-
*
|
|
153
|
-
* Never throws — all failures are captured in GateResult.
|
|
154
|
-
*/
|
|
155
|
-
export async function runVerifyGates(
|
|
156
|
-
cwd: string,
|
|
157
|
-
gateConfig: GateConfig,
|
|
158
|
-
): Promise<GateResult[]> {
|
|
159
|
-
const {
|
|
160
|
-
compileCommand,
|
|
161
|
-
testCommand,
|
|
162
|
-
compileTimeoutMs = DEFAULT_COMPILE_TIMEOUT,
|
|
163
|
-
testTimeoutMs = DEFAULT_TEST_TIMEOUT,
|
|
164
|
-
outputBudget = DEFAULT_OUTPUT_BUDGET,
|
|
165
|
-
} = gateConfig;
|
|
166
|
-
|
|
167
|
-
const results: GateResult[] = [];
|
|
168
|
-
|
|
169
|
-
// No gates configured → empty results (task completes as before)
|
|
170
|
-
if (!compileCommand && !testCommand) {
|
|
171
|
-
return results;
|
|
172
|
-
}
|
|
173
|
-
|
|
174
|
-
// Compile gate
|
|
175
|
-
if (compileCommand) {
|
|
176
|
-
const compileResult = await runGate('compile', compileCommand, cwd, compileTimeoutMs, outputBudget);
|
|
177
|
-
results.push(compileResult);
|
|
178
|
-
|
|
179
|
-
// If compile fails, skip test gate
|
|
180
|
-
if (!compileResult.passed) {
|
|
181
|
-
return results;
|
|
182
|
-
}
|
|
183
|
-
}
|
|
184
|
-
|
|
185
|
-
// Test gate
|
|
186
|
-
if (testCommand) {
|
|
187
|
-
const testResult = await runGate('test', testCommand, cwd, testTimeoutMs, outputBudget);
|
|
188
|
-
results.push(testResult);
|
|
189
|
-
}
|
|
190
|
-
|
|
191
|
-
return results;
|
|
192
|
-
}
|
|
193
|
-
|
|
194
|
-
/**
|
|
195
|
-
* Check if any gate failed.
|
|
196
|
-
*/
|
|
197
|
-
export function hasGateFailure(results: GateResult[]): boolean {
|
|
198
|
-
return results.some(r => !r.passed);
|
|
199
|
-
}
|
|
200
|
-
|
|
201
|
-
/**
|
|
202
|
-
* Get the first failed gate result, or null if all passed.
|
|
203
|
-
*/
|
|
204
|
-
export function getFirstFailure(results: GateResult[]): GateResult | null {
|
|
205
|
-
return results.find(r => !r.passed) ?? null;
|
|
206
|
-
}
|
|
207
|
-
|
|
208
|
-
/**
|
|
209
|
-
* Format gate results for human-readable output (logs, evidence).
|
|
210
|
-
*/
|
|
211
|
-
export function formatGateResults(results: GateResult[]): string {
|
|
212
|
-
if (results.length === 0) return 'No gates configured';
|
|
213
|
-
|
|
214
|
-
return results.map(r => {
|
|
215
|
-
const status = r.passed ? 'PASS' : 'FAIL';
|
|
216
|
-
const duration = `${(r.durationMs / 1000).toFixed(1)}s`;
|
|
217
|
-
return `[${status}] ${r.gate} (${duration}): ${r.command}\n${r.output}`;
|
|
218
|
-
}).join('\n\n');
|
|
219
|
-
}
|
|
1
|
+
/**
|
|
2
|
+
* Verify Gates — Phase 7, Step 1: Compile + test verification after agent completion.
|
|
3
|
+
*
|
|
4
|
+
* Runs configurable shell commands (e.g. `tsc --noEmit`, `vitest run`) in the
|
|
5
|
+
* task's working directory. Returns a structured result used by the coordinator
|
|
6
|
+
* to decide whether to mark a task completed or enter the fix loop.
|
|
7
|
+
*
|
|
8
|
+
* Design principle: gates are OPTIONAL. If no command is configured, the gate
|
|
9
|
+
* is skipped and the task completes as before (backward compatible).
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
import { spawn, execSync } from 'node:child_process';
|
|
13
|
+
|
|
14
|
+
// ── Types ──────────────────────────────────────────────────────────
|
|
15
|
+
|
|
16
|
+
export interface GateResult {
|
|
17
|
+
gate: 'compile' | 'test';
|
|
18
|
+
passed: boolean;
|
|
19
|
+
/** Combined stdout + stderr, trimmed to budget */
|
|
20
|
+
output: string;
|
|
21
|
+
/** Wall-clock duration in ms */
|
|
22
|
+
durationMs: number;
|
|
23
|
+
/** The command that was executed */
|
|
24
|
+
command: string;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export interface GateConfig {
|
|
28
|
+
/** Shell command to run for compile gate (e.g. 'npm run build', 'tsc --noEmit') */
|
|
29
|
+
compileCommand?: string;
|
|
30
|
+
/** Shell command to run for test gate (e.g. 'npm test', 'npx vitest run') */
|
|
31
|
+
testCommand?: string;
|
|
32
|
+
/** Compile gate timeout in ms (default: 120_000) */
|
|
33
|
+
compileTimeoutMs?: number;
|
|
34
|
+
/** Test gate timeout in ms (default: 300_000) */
|
|
35
|
+
testTimeoutMs?: number;
|
|
36
|
+
/** Max output bytes to keep in memory (default: 32_768 = 32KB) */
|
|
37
|
+
outputBudget?: number;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
// ── Constants ──────────────────────────────────────────────────────
|
|
41
|
+
|
|
42
|
+
const DEFAULT_COMPILE_TIMEOUT = 120_000;
|
|
43
|
+
const DEFAULT_TEST_TIMEOUT = 300_000;
|
|
44
|
+
const DEFAULT_OUTPUT_BUDGET = 32_768; // 32KB
|
|
45
|
+
|
|
46
|
+
// ── Core ───────────────────────────────────────────────────────────
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Run a single gate command. Resolves with the result — never throws.
|
|
50
|
+
*
|
|
51
|
+
* The command is run in a shell (`/bin/sh -c` on Unix, `cmd /c` on Windows)
|
|
52
|
+
* so pipes, redirects, and chained commands work naturally.
|
|
53
|
+
*/
|
|
54
|
+
export function runGate(
|
|
55
|
+
gate: 'compile' | 'test',
|
|
56
|
+
command: string,
|
|
57
|
+
cwd: string,
|
|
58
|
+
timeoutMs: number,
|
|
59
|
+
outputBudget: number = DEFAULT_OUTPUT_BUDGET,
|
|
60
|
+
): Promise<GateResult> {
|
|
61
|
+
const start = Date.now();
|
|
62
|
+
|
|
63
|
+
return new Promise<GateResult>((resolve) => {
|
|
64
|
+
const chunks: Buffer[] = [];
|
|
65
|
+
let totalBytes = 0;
|
|
66
|
+
|
|
67
|
+
const proc = spawn(command, {
|
|
68
|
+
cwd,
|
|
69
|
+
shell: true,
|
|
70
|
+
detached: process.platform !== 'win32',
|
|
71
|
+
stdio: ['ignore', 'pipe', 'pipe'],
|
|
72
|
+
env: { ...process.env, FORCE_COLOR: '0', NO_COLOR: '1' },
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
const collectOutput = (data: Buffer) => {
|
|
76
|
+
if (totalBytes < outputBudget) {
|
|
77
|
+
const remaining = outputBudget - totalBytes;
|
|
78
|
+
chunks.push(data.length > remaining ? data.subarray(0, remaining) : data);
|
|
79
|
+
}
|
|
80
|
+
totalBytes += data.length;
|
|
81
|
+
};
|
|
82
|
+
|
|
83
|
+
proc.stdout?.on('data', collectOutput);
|
|
84
|
+
proc.stderr?.on('data', collectOutput);
|
|
85
|
+
|
|
86
|
+
let killed = false;
|
|
87
|
+
const killTree = () => {
|
|
88
|
+
try {
|
|
89
|
+
if (process.platform === 'win32') {
|
|
90
|
+
// Windows: kill entire process tree via taskkill
|
|
91
|
+
execSync(`taskkill /F /T /PID ${proc.pid}`, { stdio: 'ignore' });
|
|
92
|
+
} else {
|
|
93
|
+
// Unix: kill process group
|
|
94
|
+
process.kill(-proc.pid!, 'SIGKILL');
|
|
95
|
+
}
|
|
96
|
+
} catch {
|
|
97
|
+
// Fallback: kill the process directly
|
|
98
|
+
try { proc.kill('SIGKILL'); } catch { /* already dead */ }
|
|
99
|
+
}
|
|
100
|
+
};
|
|
101
|
+
|
|
102
|
+
const timer = setTimeout(() => {
|
|
103
|
+
killed = true;
|
|
104
|
+
killTree();
|
|
105
|
+
}, timeoutMs);
|
|
106
|
+
|
|
107
|
+
proc.on('close', (code) => {
|
|
108
|
+
clearTimeout(timer);
|
|
109
|
+
const output = Buffer.concat(chunks).toString('utf-8');
|
|
110
|
+
const durationMs = Date.now() - start;
|
|
111
|
+
|
|
112
|
+
if (killed) {
|
|
113
|
+
resolve({
|
|
114
|
+
gate,
|
|
115
|
+
passed: false,
|
|
116
|
+
output: output + `\n[TIMEOUT] Gate "${gate}" killed after ${timeoutMs}ms`,
|
|
117
|
+
durationMs,
|
|
118
|
+
command,
|
|
119
|
+
});
|
|
120
|
+
return;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
resolve({
|
|
124
|
+
gate,
|
|
125
|
+
passed: code === 0,
|
|
126
|
+
output,
|
|
127
|
+
durationMs,
|
|
128
|
+
command,
|
|
129
|
+
});
|
|
130
|
+
});
|
|
131
|
+
|
|
132
|
+
proc.on('error', (err) => {
|
|
133
|
+
clearTimeout(timer);
|
|
134
|
+
resolve({
|
|
135
|
+
gate,
|
|
136
|
+
passed: false,
|
|
137
|
+
output: `[ERROR] Failed to spawn gate command: ${err.message}`,
|
|
138
|
+
durationMs: Date.now() - start,
|
|
139
|
+
command,
|
|
140
|
+
});
|
|
141
|
+
});
|
|
142
|
+
});
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
/**
|
|
146
|
+
* Run compile and test gates sequentially.
|
|
147
|
+
*
|
|
148
|
+
* - If compileCommand is not set → skip compile gate
|
|
149
|
+
* - If testCommand is not set → skip test gate
|
|
150
|
+
* - If compile fails → do NOT run test gate (no point testing broken code)
|
|
151
|
+
* - Returns array of GateResults (0, 1, or 2 items)
|
|
152
|
+
*
|
|
153
|
+
* Never throws — all failures are captured in GateResult.
|
|
154
|
+
*/
|
|
155
|
+
export async function runVerifyGates(
|
|
156
|
+
cwd: string,
|
|
157
|
+
gateConfig: GateConfig,
|
|
158
|
+
): Promise<GateResult[]> {
|
|
159
|
+
const {
|
|
160
|
+
compileCommand,
|
|
161
|
+
testCommand,
|
|
162
|
+
compileTimeoutMs = DEFAULT_COMPILE_TIMEOUT,
|
|
163
|
+
testTimeoutMs = DEFAULT_TEST_TIMEOUT,
|
|
164
|
+
outputBudget = DEFAULT_OUTPUT_BUDGET,
|
|
165
|
+
} = gateConfig;
|
|
166
|
+
|
|
167
|
+
const results: GateResult[] = [];
|
|
168
|
+
|
|
169
|
+
// No gates configured → empty results (task completes as before)
|
|
170
|
+
if (!compileCommand && !testCommand) {
|
|
171
|
+
return results;
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
// Compile gate
|
|
175
|
+
if (compileCommand) {
|
|
176
|
+
const compileResult = await runGate('compile', compileCommand, cwd, compileTimeoutMs, outputBudget);
|
|
177
|
+
results.push(compileResult);
|
|
178
|
+
|
|
179
|
+
// If compile fails, skip test gate
|
|
180
|
+
if (!compileResult.passed) {
|
|
181
|
+
return results;
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
// Test gate
|
|
186
|
+
if (testCommand) {
|
|
187
|
+
const testResult = await runGate('test', testCommand, cwd, testTimeoutMs, outputBudget);
|
|
188
|
+
results.push(testResult);
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
return results;
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
/**
|
|
195
|
+
* Check if any gate failed.
|
|
196
|
+
*/
|
|
197
|
+
export function hasGateFailure(results: GateResult[]): boolean {
|
|
198
|
+
return results.some(r => !r.passed);
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
/**
|
|
202
|
+
* Get the first failed gate result, or null if all passed.
|
|
203
|
+
*/
|
|
204
|
+
export function getFirstFailure(results: GateResult[]): GateResult | null {
|
|
205
|
+
return results.find(r => !r.passed) ?? null;
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
/**
|
|
209
|
+
* Format gate results for human-readable output (logs, evidence).
|
|
210
|
+
*/
|
|
211
|
+
export function formatGateResults(results: GateResult[]): string {
|
|
212
|
+
if (results.length === 0) return 'No gates configured';
|
|
213
|
+
|
|
214
|
+
return results.map(r => {
|
|
215
|
+
const status = r.passed ? 'PASS' : 'FAIL';
|
|
216
|
+
const duration = `${(r.durationMs / 1000).toFixed(1)}s`;
|
|
217
|
+
return `[${status}] ${r.gate} (${duration}): ${r.command}\n${r.output}`;
|
|
218
|
+
}).join('\n\n');
|
|
219
|
+
}
|