mustflow 2.17.0 → 2.18.2
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/README.md +3 -3
- package/dist/cli/commands/classify.js +13 -3
- package/dist/cli/commands/dashboard.js +2 -1
- package/dist/cli/commands/explain-verify.js +2 -2
- package/dist/cli/commands/impact.js +13 -3
- package/dist/cli/commands/run.js +156 -104
- package/dist/cli/commands/verify.js +157 -45
- package/dist/cli/i18n/en.js +10 -1
- package/dist/cli/i18n/es.js +10 -1
- package/dist/cli/i18n/fr.js +10 -1
- package/dist/cli/i18n/hi.js +10 -1
- package/dist/cli/i18n/ko.js +10 -1
- package/dist/cli/i18n/zh.js +10 -1
- package/dist/cli/lib/git-changes.js +25 -2
- package/dist/cli/lib/local-index/constants.js +4 -1
- package/dist/cli/lib/local-index/index.js +22 -5
- package/dist/cli/lib/repo-map.js +90 -30
- package/dist/cli/lib/run-plan.js +25 -2
- package/dist/cli/lib/validation/index.js +2 -1
- package/dist/core/atomic-state-write.js +31 -0
- package/dist/core/bounded-output.js +23 -1
- package/dist/core/check-issues.js +3 -0
- package/dist/core/command-contract-rules.js +104 -2
- package/dist/core/command-contract-validation.js +71 -9
- package/dist/core/command-intent-eligibility.js +9 -1
- package/dist/core/command-output-limits.js +5 -0
- package/dist/core/completion-verdict.js +2 -1
- package/dist/core/contract-lint.js +10 -1
- package/dist/core/public-json-contracts.js +1 -1
- package/dist/core/run-receipt.js +20 -13
- package/dist/core/source-anchors.js +96 -24
- package/dist/core/verification-evidence.js +4 -1
- package/package.json +1 -1
- package/schemas/README.md +4 -4
- package/schemas/change-verification-report.schema.json +2 -1
- package/schemas/contract-lint-report.schema.json +2 -1
- package/schemas/explain-report.schema.json +1 -0
- package/schemas/latest-run-pointer.schema.json +1 -0
- package/schemas/run-receipt.schema.json +26 -3
- package/schemas/verify-report.schema.json +2 -1
- package/schemas/verify-run-manifest.schema.json +2 -1
- package/templates/default/manifest.toml +1 -1
package/README.md
CHANGED
|
@@ -124,7 +124,7 @@ mustflow installs and validates an agent workflow for user projects.
|
|
|
124
124
|
- Classifies changed files, public surfaces, and validation reasons with `mf classify`.
|
|
125
125
|
- Prints execution-free verification plans with `mf verify --plan-only --json`, including a machine-readable verification decision graph and read-only local-index lock explanations when available.
|
|
126
126
|
- Runs only allowed one-shot commands within a timeout via `mf run <intent>` or `mf verify` when the selected intent is runnable.
|
|
127
|
-
- Writes command receipts
|
|
127
|
+
- Writes command receipts under `.mustflow/state/runs/run-*` and atomically updates `.mustflow/state/runs/latest.json`.
|
|
128
128
|
- Generates a concise repository navigation map, `REPO_MAP.md`, with `mf map`.
|
|
129
129
|
- Indexes and searches mustflow docs, skills, skill routes, command rules, command-effect locks, file fingerprints, and opt-in source anchor metadata with SQLite via `mf index` and `mf search`. The local SQLite file is a rebuildable lookup cache, not a memory store, audit log, command transcript store, command-authority source, or source-content database.
|
|
130
130
|
- Tracks agent-created or agent-modified documentation needing prose review with `mf docs review`.
|
|
@@ -354,11 +354,11 @@ Runnable work is declared in `.mustflow/config/commands.toml` so agents do not g
|
|
|
354
354
|
- `run_policy = "agent_allowed"`
|
|
355
355
|
- `stdin = "closed"`
|
|
356
356
|
|
|
357
|
-
Development servers, watch modes, browser UIs, interactive commands, and background processes do not run directly.
|
|
357
|
+
Development servers, watch modes, browser UIs, interactive commands, and background processes do not run directly. `mf run` also rejects obvious long-running `argv` shapes, such as shell-wrapper background payloads, interpreter loops, package-manager development scripts, watchers, and development servers declared as one-shot commands.
|
|
358
358
|
|
|
359
359
|
Use `mf verify --reason <event> --plan-only --json` to inspect matching verification intents, command eligibility, remaining gaps, and missing runnable coverage without executing commands. Use `mf run <intent> --dry-run --json` to inspect one resolved command intent without spawning a process or writing a run receipt. Plan-only verification includes a `decision_graph` that connects changed surfaces, classification reasons, command candidates, eligibility checks, effects, and gaps. When `.mustflow/cache/mustflow.sqlite` is fresh, scheduled entries also include read-only `effectGraph` metadata for write locks and lock conflicts. These graph rows are marked `explanation_only` and never grant command authority; `.mustflow/config/commands.toml` remains the only runnable command source.
|
|
360
360
|
|
|
361
|
-
Each executed command run writes
|
|
361
|
+
Each executed command run writes a run record under `.mustflow/state/runs/run-*` and atomically updates `.mustflow/state/runs/latest.json`. The record includes the intent name, working directory, timeout, exit code, timeout status, and the tail of stdout and stderr.
|
|
362
362
|
|
|
363
363
|
## Language and profiles
|
|
364
364
|
|
|
@@ -2,7 +2,7 @@ import { mkdirSync, writeFileSync } from 'node:fs';
|
|
|
2
2
|
import path from 'node:path';
|
|
3
3
|
import { createChangeClassificationReport, } from '../../core/change-classification.js';
|
|
4
4
|
import { printUsageError, renderHelp } from '../lib/cli-output.js';
|
|
5
|
-
import {
|
|
5
|
+
import { requireGitChangedFiles } from '../lib/git-changes.js';
|
|
6
6
|
import { t } from '../lib/i18n.js';
|
|
7
7
|
import { resolveMustflowRoot } from '../lib/project-root.js';
|
|
8
8
|
const CLASSIFY_SCHEMA_VERSION = '1';
|
|
@@ -67,7 +67,7 @@ function parseClassifyArgs(args) {
|
|
|
67
67
|
return { json, changed, writePath, paths };
|
|
68
68
|
}
|
|
69
69
|
export function createClassifyOutput(projectRoot, source, paths) {
|
|
70
|
-
const files = source === 'changed' ?
|
|
70
|
+
const files = source === 'changed' ? requireGitChangedFiles(projectRoot) : paths;
|
|
71
71
|
return {
|
|
72
72
|
schema_version: CLASSIFY_SCHEMA_VERSION,
|
|
73
73
|
command: 'classify',
|
|
@@ -136,7 +136,17 @@ export function runClassify(args, reporter, lang = 'en') {
|
|
|
136
136
|
return 1;
|
|
137
137
|
}
|
|
138
138
|
const projectRoot = resolveMustflowRoot();
|
|
139
|
-
|
|
139
|
+
let output;
|
|
140
|
+
try {
|
|
141
|
+
output = createClassifyOutput(projectRoot, parsed.changed ? 'changed' : 'paths', parsed.paths);
|
|
142
|
+
}
|
|
143
|
+
catch (error) {
|
|
144
|
+
const message = error instanceof Error && error.message === 'git_changed_files_unavailable'
|
|
145
|
+
? t(lang, 'classify.error.changed_files_unavailable')
|
|
146
|
+
: t(lang, 'cli.common.invalidInput');
|
|
147
|
+
printUsageError(reporter, message, 'mf classify --help', getClassifyHelp(lang), lang);
|
|
148
|
+
return 1;
|
|
149
|
+
}
|
|
140
150
|
if (parsed.writePath) {
|
|
141
151
|
try {
|
|
142
152
|
writeClassifyOutput(projectRoot, parsed.writePath, output);
|
|
@@ -682,7 +682,8 @@ async function renderStatusResponse(projectRoot) {
|
|
|
682
682
|
const activeDocuments = listDocReviewEntries(projectRoot);
|
|
683
683
|
const rawCommandContract = readDashboardCommandContract(projectRoot);
|
|
684
684
|
const commandContract = await renderCommandContractResponse(projectRoot, rawCommandContract);
|
|
685
|
-
const
|
|
685
|
+
const gitChangedFilesResult = readGitChangedFiles(projectRoot);
|
|
686
|
+
const gitChangedFiles = gitChangedFilesResult.ok ? gitChangedFilesResult.files : [];
|
|
686
687
|
const packageMetadata = readPackageMetadata();
|
|
687
688
|
const verification = createDashboardVerificationSnapshot(projectRoot, rawCommandContract, commandContract.intents, gitChangedFiles, manifest.changedFiles, manifest.missingFiles);
|
|
688
689
|
const readModel = await readLatestLocalVerificationReadModelQueries(projectRoot);
|
|
@@ -5,7 +5,7 @@ import { createVerificationPlan, } from '../../core/verification-plan.js';
|
|
|
5
5
|
import { createVerificationSchedule } from '../../core/verification-scheduler.js';
|
|
6
6
|
import { t } from '../lib/i18n.js';
|
|
7
7
|
import { readLatestLocalVerificationReadModelQueries, readLocalCommandEffectGraphs, } from '../lib/local-index.js';
|
|
8
|
-
import { planErrorMessageKey,
|
|
8
|
+
import { planErrorMessageKey, readInputFromClassificationReport } from './verify.js';
|
|
9
9
|
export function parseExplainVerifyArgs(args) {
|
|
10
10
|
let reason;
|
|
11
11
|
let fromPlan;
|
|
@@ -69,7 +69,7 @@ export function explainVerifyPlanErrorMessage(error, lang) {
|
|
|
69
69
|
return t(lang, planErrorMessageKey(code));
|
|
70
70
|
}
|
|
71
71
|
export function readExplainVerifyPlanReasons(projectRoot, planPath) {
|
|
72
|
-
return
|
|
72
|
+
return readInputFromClassificationReport(projectRoot, planPath).reasons;
|
|
73
73
|
}
|
|
74
74
|
export async function getVerifyExplainOutput(schemaVersion, projectRoot, reasons, inputReason, planSource) {
|
|
75
75
|
const contract = readCommandContract(projectRoot);
|
|
@@ -3,7 +3,7 @@ import { createChangeClassificationReport } from '../../core/change-classificati
|
|
|
3
3
|
import { summarizeVersionImpact } from '../../core/version-impact.js';
|
|
4
4
|
import { printUsageError, renderHelp } from '../lib/cli-output.js';
|
|
5
5
|
import { isRecord } from '../lib/command-contract.js';
|
|
6
|
-
import {
|
|
6
|
+
import { requireGitChangedFiles } from '../lib/git-changes.js';
|
|
7
7
|
import { t } from '../lib/i18n.js';
|
|
8
8
|
import { resolveMustflowRoot } from '../lib/project-root.js';
|
|
9
9
|
import { readTomlFile } from '../lib/toml.js';
|
|
@@ -56,7 +56,7 @@ function readPreferences(projectRoot) {
|
|
|
56
56
|
}
|
|
57
57
|
function createImpactOutput(projectRoot, parsed) {
|
|
58
58
|
const source = parsed.changed ? 'changed' : 'paths';
|
|
59
|
-
const files = parsed.changed ?
|
|
59
|
+
const files = parsed.changed ? requireGitChangedFiles(projectRoot) : parsed.paths;
|
|
60
60
|
const classificationReport = createChangeClassificationReport(source, files);
|
|
61
61
|
const versionSources = detectVersionSources(projectRoot);
|
|
62
62
|
return {
|
|
@@ -110,7 +110,17 @@ export function runImpact(args, reporter, lang = 'en') {
|
|
|
110
110
|
printUsageError(reporter, t(lang, 'impact.error.missingInput'), 'mf impact --help', getImpactHelp(lang), lang);
|
|
111
111
|
return 1;
|
|
112
112
|
}
|
|
113
|
-
|
|
113
|
+
let output;
|
|
114
|
+
try {
|
|
115
|
+
output = createImpactOutput(resolveMustflowRoot(), parsed);
|
|
116
|
+
}
|
|
117
|
+
catch (error) {
|
|
118
|
+
const message = error instanceof Error && error.message === 'git_changed_files_unavailable'
|
|
119
|
+
? t(lang, 'impact.error.changed_files_unavailable')
|
|
120
|
+
: t(lang, 'cli.common.invalidInput');
|
|
121
|
+
printUsageError(reporter, message, 'mf impact --help', getImpactHelp(lang), lang);
|
|
122
|
+
return 1;
|
|
123
|
+
}
|
|
114
124
|
if (parsed.json) {
|
|
115
125
|
reporter.stdout(JSON.stringify(output, null, 2));
|
|
116
126
|
return 0;
|
package/dist/cli/commands/run.js
CHANGED
|
@@ -10,10 +10,12 @@ import { t } from '../lib/i18n.js';
|
|
|
10
10
|
import { getPackageVersion } from '../lib/package-info.js';
|
|
11
11
|
import { resolveMustflowRoot } from '../lib/project-root.js';
|
|
12
12
|
import { createRunPlan, createRunPreview, isMustflowBuiltinIntent, renderRunPreviewText, } from '../lib/run-plan.js';
|
|
13
|
-
import { createRunReceipt, writeRunReceipt } from '../../core/run-receipt.js';
|
|
13
|
+
import { createRunReceipt, createRunReceiptRelativePath, writeRunReceipt, } from '../../core/run-receipt.js';
|
|
14
14
|
import { recordRunPerformanceHistory } from '../../core/run-performance-history.js';
|
|
15
15
|
import { RunProfiler } from '../../core/run-profile.js';
|
|
16
16
|
import { finishRunWriteTracking, startRunWriteTracking } from '../../core/run-write-drift.js';
|
|
17
|
+
const OUTPUT_LIMIT_ERROR_CODE = 'ENOBUFS';
|
|
18
|
+
const OUTPUT_LIMIT_ERROR_MESSAGE = /\bmaxBuffer\b.*\bexceeded\b/i;
|
|
17
19
|
function emitOutput(reporter, output, stream) {
|
|
18
20
|
if (!output) {
|
|
19
21
|
return;
|
|
@@ -24,7 +26,7 @@ function emitOutput(reporter, output, stream) {
|
|
|
24
26
|
}
|
|
25
27
|
reporter[stream](text);
|
|
26
28
|
}
|
|
27
|
-
function
|
|
29
|
+
function signalProcessTree(pid, signal) {
|
|
28
30
|
if (!pid || pid <= 0) {
|
|
29
31
|
return;
|
|
30
32
|
}
|
|
@@ -33,23 +35,87 @@ function terminateProcessTree(pid) {
|
|
|
33
35
|
stdio: 'ignore',
|
|
34
36
|
windowsHide: true,
|
|
35
37
|
});
|
|
38
|
+
if (signal === 'SIGKILL') {
|
|
39
|
+
try {
|
|
40
|
+
process.kill(pid, signal);
|
|
41
|
+
}
|
|
42
|
+
catch {
|
|
43
|
+
// taskkill may already have terminated the direct child.
|
|
44
|
+
}
|
|
45
|
+
}
|
|
36
46
|
return;
|
|
37
47
|
}
|
|
38
48
|
try {
|
|
39
|
-
process.kill(-pid,
|
|
49
|
+
process.kill(-pid, signal);
|
|
40
50
|
}
|
|
41
51
|
catch {
|
|
42
52
|
try {
|
|
43
|
-
process.kill(pid,
|
|
53
|
+
process.kill(pid, signal);
|
|
44
54
|
}
|
|
45
55
|
catch {
|
|
46
56
|
// The child may already be gone after Node's spawn timeout handling.
|
|
47
57
|
}
|
|
48
58
|
}
|
|
49
59
|
}
|
|
60
|
+
function signalProcessTreeNonBlocking(pid, signal) {
|
|
61
|
+
if (!pid || pid <= 0) {
|
|
62
|
+
return;
|
|
63
|
+
}
|
|
64
|
+
if (process.platform === 'win32') {
|
|
65
|
+
const killer = spawn('taskkill', ['/PID', String(pid), '/T', '/F'], {
|
|
66
|
+
stdio: 'ignore',
|
|
67
|
+
windowsHide: true,
|
|
68
|
+
detached: true,
|
|
69
|
+
});
|
|
70
|
+
killer.unref();
|
|
71
|
+
if (signal === 'SIGKILL') {
|
|
72
|
+
try {
|
|
73
|
+
process.kill(pid, signal);
|
|
74
|
+
}
|
|
75
|
+
catch {
|
|
76
|
+
// taskkill may already have terminated the direct child.
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
return;
|
|
80
|
+
}
|
|
81
|
+
try {
|
|
82
|
+
process.kill(-pid, signal);
|
|
83
|
+
}
|
|
84
|
+
catch {
|
|
85
|
+
try {
|
|
86
|
+
process.kill(pid, signal);
|
|
87
|
+
}
|
|
88
|
+
catch {
|
|
89
|
+
// The child may already be gone after the timeout fired.
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
function terminateProcessTree(pid) {
|
|
94
|
+
signalProcessTree(pid, 'SIGTERM');
|
|
95
|
+
}
|
|
96
|
+
function forceTerminateProcessTree(pid) {
|
|
97
|
+
signalProcessTree(pid, 'SIGKILL');
|
|
98
|
+
}
|
|
99
|
+
function terminateProcessTreeNonBlocking(pid) {
|
|
100
|
+
signalProcessTreeNonBlocking(pid, 'SIGTERM');
|
|
101
|
+
}
|
|
102
|
+
function forceTerminateProcessTreeNonBlocking(pid) {
|
|
103
|
+
signalProcessTreeNonBlocking(pid, 'SIGKILL');
|
|
104
|
+
}
|
|
50
105
|
function getKillMethod() {
|
|
51
106
|
return process.platform === 'win32' ? 'taskkill_process_tree' : 'process_group_sigterm';
|
|
52
107
|
}
|
|
108
|
+
function createPendingTimeoutTermination(method) {
|
|
109
|
+
return {
|
|
110
|
+
reason: 'timeout',
|
|
111
|
+
method,
|
|
112
|
+
graceful_signal: 'SIGTERM',
|
|
113
|
+
forced_signal: 'SIGKILL',
|
|
114
|
+
forced_kill_attempted: true,
|
|
115
|
+
confirmed: false,
|
|
116
|
+
cleanup_pending: true,
|
|
117
|
+
};
|
|
118
|
+
}
|
|
53
119
|
function createBufferedReporter() {
|
|
54
120
|
const stdout = [];
|
|
55
121
|
const stderr = [];
|
|
@@ -159,35 +225,27 @@ async function runBuiltinArgvInProcess(commandArgv, cwd, lang) {
|
|
|
159
225
|
};
|
|
160
226
|
}
|
|
161
227
|
}
|
|
162
|
-
function runArgvCommand(command, cwd, maxOutputBytes, env, timeoutSeconds) {
|
|
163
|
-
return spawnSync(command?.executable ?? '', command?.args ?? [], {
|
|
164
|
-
cwd,
|
|
165
|
-
encoding: 'utf8',
|
|
166
|
-
input: '',
|
|
167
|
-
maxBuffer: maxOutputBytes,
|
|
168
|
-
env,
|
|
169
|
-
shell: command?.shell ?? false,
|
|
170
|
-
stdio: ['ignore', 'pipe', 'pipe'],
|
|
171
|
-
timeout: timeoutSeconds * 1000,
|
|
172
|
-
windowsHide: true,
|
|
173
|
-
});
|
|
174
|
-
}
|
|
175
228
|
function writeStreamChunk(reporter, stream, chunk) {
|
|
176
229
|
if (stream === 'stdout') {
|
|
177
230
|
if (reporter.writeStdout) {
|
|
178
231
|
reporter.writeStdout(chunk);
|
|
179
232
|
return;
|
|
180
233
|
}
|
|
181
|
-
reporter.stdout(chunk.toString()
|
|
234
|
+
reporter.stdout(chunk.toString());
|
|
182
235
|
return;
|
|
183
236
|
}
|
|
184
237
|
if (reporter.writeStderr) {
|
|
185
238
|
reporter.writeStderr(chunk);
|
|
186
239
|
return;
|
|
187
240
|
}
|
|
188
|
-
reporter.stderr(chunk.toString()
|
|
241
|
+
reporter.stderr(chunk.toString());
|
|
242
|
+
}
|
|
243
|
+
function createOutputLimitError(stream, maxOutputBytes) {
|
|
244
|
+
return Object.assign(new Error(`${stream} exceeded max_output_bytes (${maxOutputBytes})`), {
|
|
245
|
+
code: OUTPUT_LIMIT_ERROR_CODE,
|
|
246
|
+
});
|
|
189
247
|
}
|
|
190
|
-
function
|
|
248
|
+
function runSpawnedCommandStreaming(command, cwd, env, timeoutSeconds, maxOutputBytes, stdoutTailBytes, stderrTailBytes, reporter, streamOutput, enforceOutputLimit) {
|
|
191
249
|
return new Promise((resolve) => {
|
|
192
250
|
const stdout = new BoundedOutputBuffer(stdoutTailBytes);
|
|
193
251
|
const stderr = new BoundedOutputBuffer(stderrTailBytes);
|
|
@@ -195,11 +253,14 @@ function runArgvCommandStreaming(command, cwd, env, timeoutSeconds, stdoutTailBy
|
|
|
195
253
|
let timedOut = false;
|
|
196
254
|
let childError;
|
|
197
255
|
let childPid;
|
|
256
|
+
let stdoutBytes = 0;
|
|
257
|
+
let stderrBytes = 0;
|
|
198
258
|
let timeout;
|
|
199
|
-
|
|
259
|
+
let termination = null;
|
|
260
|
+
const child = spawn(command.executable, command.args ?? [], {
|
|
200
261
|
cwd,
|
|
201
262
|
env,
|
|
202
|
-
shell: command
|
|
263
|
+
shell: command.shell,
|
|
203
264
|
stdio: ['ignore', 'pipe', 'pipe'],
|
|
204
265
|
windowsHide: true,
|
|
205
266
|
detached: process.platform !== 'win32',
|
|
@@ -214,89 +275,46 @@ function runArgvCommandStreaming(command, cwd, env, timeoutSeconds, stdoutTailBy
|
|
|
214
275
|
clearTimeout(timeout);
|
|
215
276
|
}
|
|
216
277
|
resolve({
|
|
217
|
-
status,
|
|
218
|
-
signal,
|
|
278
|
+
status: timedOut ? null : status,
|
|
279
|
+
signal: timedOut ? null : signal,
|
|
219
280
|
error: timedOut ? Object.assign(new Error('Command timed out'), { code: 'ETIMEDOUT' }) : childError,
|
|
220
281
|
stdout: stdout.toSnapshot(),
|
|
221
282
|
stderr: stderr.toSnapshot(),
|
|
222
283
|
pid: childPid,
|
|
284
|
+
termination,
|
|
223
285
|
});
|
|
224
286
|
};
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
writeStreamChunk(reporter, 'stdout', chunk);
|
|
228
|
-
});
|
|
229
|
-
child.stderr?.on('data', (chunk) => {
|
|
230
|
-
stderr.append(chunk);
|
|
231
|
-
writeStreamChunk(reporter, 'stderr', chunk);
|
|
232
|
-
});
|
|
233
|
-
child.once('error', (error) => {
|
|
234
|
-
childError = error;
|
|
235
|
-
});
|
|
236
|
-
child.once('close', (status, signal) => {
|
|
237
|
-
finish(status, signal);
|
|
238
|
-
});
|
|
239
|
-
timeout = setTimeout(() => {
|
|
240
|
-
timedOut = true;
|
|
241
|
-
terminateProcessTree(childPid);
|
|
242
|
-
}, timeoutSeconds * 1000);
|
|
243
|
-
});
|
|
244
|
-
}
|
|
245
|
-
function runShellCommand(command, cwd, maxOutputBytes, env, timeoutSeconds) {
|
|
246
|
-
return spawnSync(command ?? '', {
|
|
247
|
-
cwd,
|
|
248
|
-
encoding: 'utf8',
|
|
249
|
-
input: '',
|
|
250
|
-
maxBuffer: maxOutputBytes,
|
|
251
|
-
env,
|
|
252
|
-
shell: true,
|
|
253
|
-
stdio: ['ignore', 'pipe', 'pipe'],
|
|
254
|
-
timeout: timeoutSeconds * 1000,
|
|
255
|
-
windowsHide: true,
|
|
256
|
-
});
|
|
257
|
-
}
|
|
258
|
-
function runShellCommandStreaming(command, cwd, env, timeoutSeconds, stdoutTailBytes, stderrTailBytes, reporter) {
|
|
259
|
-
return new Promise((resolve) => {
|
|
260
|
-
const stdout = new BoundedOutputBuffer(stdoutTailBytes);
|
|
261
|
-
const stderr = new BoundedOutputBuffer(stderrTailBytes);
|
|
262
|
-
let settled = false;
|
|
263
|
-
let timedOut = false;
|
|
264
|
-
let childError;
|
|
265
|
-
let childPid;
|
|
266
|
-
let timeout;
|
|
267
|
-
const child = spawn(command ?? '', {
|
|
268
|
-
cwd,
|
|
269
|
-
env,
|
|
270
|
-
shell: true,
|
|
271
|
-
stdio: ['ignore', 'pipe', 'pipe'],
|
|
272
|
-
windowsHide: true,
|
|
273
|
-
detached: process.platform !== 'win32',
|
|
274
|
-
});
|
|
275
|
-
childPid = child.pid;
|
|
276
|
-
const finish = (status, signal) => {
|
|
277
|
-
if (settled) {
|
|
287
|
+
const stopForOutputLimit = (stream) => {
|
|
288
|
+
if (settled || childError) {
|
|
278
289
|
return;
|
|
279
290
|
}
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
error: timedOut ? Object.assign(new Error('Command timed out'), { code: 'ETIMEDOUT' }) : childError,
|
|
288
|
-
stdout: stdout.toSnapshot(),
|
|
289
|
-
stderr: stderr.toSnapshot(),
|
|
290
|
-
pid: childPid,
|
|
291
|
-
});
|
|
291
|
+
childError = createOutputLimitError(stream, maxOutputBytes);
|
|
292
|
+
child.stdout?.destroy();
|
|
293
|
+
child.stderr?.destroy();
|
|
294
|
+
child.unref();
|
|
295
|
+
terminateProcessTreeNonBlocking(childPid);
|
|
296
|
+
forceTerminateProcessTreeNonBlocking(childPid);
|
|
297
|
+
finish(null, null);
|
|
292
298
|
};
|
|
293
299
|
child.stdout?.on('data', (chunk) => {
|
|
294
300
|
stdout.append(chunk);
|
|
295
|
-
|
|
301
|
+
stdoutBytes += chunk.byteLength;
|
|
302
|
+
if (streamOutput) {
|
|
303
|
+
writeStreamChunk(reporter, 'stdout', chunk);
|
|
304
|
+
}
|
|
305
|
+
if (enforceOutputLimit && stdoutBytes > maxOutputBytes) {
|
|
306
|
+
stopForOutputLimit('stdout');
|
|
307
|
+
}
|
|
296
308
|
});
|
|
297
309
|
child.stderr?.on('data', (chunk) => {
|
|
298
310
|
stderr.append(chunk);
|
|
299
|
-
|
|
311
|
+
stderrBytes += chunk.byteLength;
|
|
312
|
+
if (streamOutput) {
|
|
313
|
+
writeStreamChunk(reporter, 'stderr', chunk);
|
|
314
|
+
}
|
|
315
|
+
if (enforceOutputLimit && stderrBytes > maxOutputBytes) {
|
|
316
|
+
stopForOutputLimit('stderr');
|
|
317
|
+
}
|
|
300
318
|
});
|
|
301
319
|
child.once('error', (error) => {
|
|
302
320
|
childError = error;
|
|
@@ -306,20 +324,42 @@ function runShellCommandStreaming(command, cwd, env, timeoutSeconds, stdoutTailB
|
|
|
306
324
|
});
|
|
307
325
|
timeout = setTimeout(() => {
|
|
308
326
|
timedOut = true;
|
|
309
|
-
|
|
327
|
+
child.stdout?.destroy();
|
|
328
|
+
child.stderr?.destroy();
|
|
329
|
+
child.unref();
|
|
330
|
+
termination = createPendingTimeoutTermination(getKillMethod());
|
|
331
|
+
terminateProcessTreeNonBlocking(childPid);
|
|
332
|
+
forceTerminateProcessTreeNonBlocking(childPid);
|
|
333
|
+
finish(null, null);
|
|
310
334
|
}, timeoutSeconds * 1000);
|
|
311
335
|
});
|
|
312
336
|
}
|
|
337
|
+
function runArgvCommandStreaming(command, cwd, env, timeoutSeconds, maxOutputBytes, stdoutTailBytes, stderrTailBytes, reporter, streamOutput, enforceOutputLimit) {
|
|
338
|
+
return runSpawnedCommandStreaming({ executable: command?.executable ?? '', args: command?.args ?? [], shell: command?.shell ?? false }, cwd, env, timeoutSeconds, maxOutputBytes, stdoutTailBytes, stderrTailBytes, reporter, streamOutput, enforceOutputLimit);
|
|
339
|
+
}
|
|
340
|
+
function runShellCommandStreaming(command, cwd, env, timeoutSeconds, maxOutputBytes, stdoutTailBytes, stderrTailBytes, reporter, streamOutput, enforceOutputLimit) {
|
|
341
|
+
return runSpawnedCommandStreaming({ executable: command ?? '', shell: true }, cwd, env, timeoutSeconds, maxOutputBytes, stdoutTailBytes, stderrTailBytes, reporter, streamOutput, enforceOutputLimit);
|
|
342
|
+
}
|
|
313
343
|
function getRunStatus(error, exitCode, successExitCodes) {
|
|
314
344
|
const errorWithCode = error;
|
|
315
345
|
if (errorWithCode?.code === 'ETIMEDOUT') {
|
|
316
346
|
return 'timed_out';
|
|
317
347
|
}
|
|
348
|
+
if (isOutputLimitExceededError(error)) {
|
|
349
|
+
return 'output_limit_exceeded';
|
|
350
|
+
}
|
|
318
351
|
if (error) {
|
|
319
352
|
return 'start_failed';
|
|
320
353
|
}
|
|
321
354
|
return exitCode !== null && successExitCodes.includes(exitCode) ? 'passed' : 'failed';
|
|
322
355
|
}
|
|
356
|
+
function isOutputLimitExceededError(error) {
|
|
357
|
+
if (!error) {
|
|
358
|
+
return false;
|
|
359
|
+
}
|
|
360
|
+
const errorWithCode = error;
|
|
361
|
+
return errorWithCode.code === OUTPUT_LIMIT_ERROR_CODE || OUTPUT_LIMIT_ERROR_MESSAGE.test(error.message);
|
|
362
|
+
}
|
|
323
363
|
function getRunPlanDetail(plan, lang, fallbackKey) {
|
|
324
364
|
return plan.detail ?? t(lang, fallbackKey);
|
|
325
365
|
}
|
|
@@ -356,12 +396,24 @@ function reportRunPlanFailure(plan, reporter, lang) {
|
|
|
356
396
|
detail: getRunPlanDetail(plan, lang, 'run.error.blockedShellBackgroundDetail'),
|
|
357
397
|
});
|
|
358
398
|
break;
|
|
399
|
+
case 'blocked_long_running_command_pattern':
|
|
400
|
+
message = t(lang, 'run.error.blockedLongRunningCommand', {
|
|
401
|
+
intent: plan.intentName,
|
|
402
|
+
detail: getRunPlanDetail(plan, lang, 'run.error.blockedLongRunningCommandDetail'),
|
|
403
|
+
});
|
|
404
|
+
break;
|
|
359
405
|
case 'cwd_outside_project':
|
|
360
406
|
message = t(lang, 'run.error.cwdOutsideProject', {
|
|
361
407
|
intent: plan.intentName,
|
|
362
408
|
detail: getRunPlanDetail(plan, lang, 'run.error.cwdOutsideProjectDetail'),
|
|
363
409
|
});
|
|
364
410
|
break;
|
|
411
|
+
case 'max_output_bytes_exceeds_limit':
|
|
412
|
+
message = t(lang, 'run.error.maxOutputBytes', {
|
|
413
|
+
intent: plan.intentName,
|
|
414
|
+
detail: getRunPlanDetail(plan, lang, 'run.error.maxOutputBytesDetail'),
|
|
415
|
+
});
|
|
416
|
+
break;
|
|
365
417
|
case 'intent_not_table':
|
|
366
418
|
default:
|
|
367
419
|
message = t(lang, 'run.error.unknownIntent', { intent: plan.intentName });
|
|
@@ -481,17 +533,11 @@ export async function runRun(args, reporter, lang = 'en', options = {}) {
|
|
|
481
533
|
}
|
|
482
534
|
}
|
|
483
535
|
if (plan.commandArgv) {
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
return runArgvCommandStreaming(plan.argvCommand, plan.cwd, env, plan.timeoutSeconds, stdoutTailBytes, stderrTailBytes, reporter);
|
|
487
|
-
}
|
|
488
|
-
return runArgvCommand(plan.argvCommand, plan.cwd, plan.maxOutputBytes, env, plan.timeoutSeconds);
|
|
489
|
-
}
|
|
490
|
-
if (!json) {
|
|
491
|
-
streamedOutput = true;
|
|
492
|
-
return runShellCommandStreaming(plan.shellCommand, plan.cwd, env, plan.timeoutSeconds, stdoutTailBytes, stderrTailBytes, reporter);
|
|
536
|
+
streamedOutput = !json;
|
|
537
|
+
return runArgvCommandStreaming(plan.argvCommand, plan.cwd, env, plan.timeoutSeconds, plan.maxOutputBytes, stdoutTailBytes, stderrTailBytes, reporter, !json, json);
|
|
493
538
|
}
|
|
494
|
-
|
|
539
|
+
streamedOutput = !json;
|
|
540
|
+
return runShellCommandStreaming(plan.shellCommand, plan.cwd, env, plan.timeoutSeconds, plan.maxOutputBytes, stdoutTailBytes, stderrTailBytes, reporter, !json, json);
|
|
495
541
|
});
|
|
496
542
|
const childDurationMs = performance.now() - childStartedAtMs;
|
|
497
543
|
const finishedAt = new Date();
|
|
@@ -499,9 +545,13 @@ export async function runRun(args, reporter, lang = 'en', options = {}) {
|
|
|
499
545
|
const exitCode = typeof result.status === 'number' ? result.status : null;
|
|
500
546
|
const runStatus = getRunStatus(result.error, exitCode, plan.successExitCodes);
|
|
501
547
|
let killMethod = null;
|
|
548
|
+
let termination = null;
|
|
502
549
|
if (runStatus === 'timed_out') {
|
|
503
|
-
|
|
504
|
-
|
|
550
|
+
termination = result.termination ?? createPendingTimeoutTermination(getKillMethod());
|
|
551
|
+
killMethod = termination.method;
|
|
552
|
+
if (!result.termination && result.pid) {
|
|
553
|
+
terminateProcessTree(result.pid);
|
|
554
|
+
}
|
|
505
555
|
}
|
|
506
556
|
const receipt = profiler.measure('receipt_create', () => createRunReceipt({
|
|
507
557
|
intent: intentName,
|
|
@@ -525,6 +575,7 @@ export async function runRun(args, reporter, lang = 'en', options = {}) {
|
|
|
525
575
|
signal: result.signal,
|
|
526
576
|
error: result.error?.message ?? null,
|
|
527
577
|
killMethod,
|
|
578
|
+
termination,
|
|
528
579
|
stdout: result.stdout,
|
|
529
580
|
stderr: result.stderr,
|
|
530
581
|
writeDrift,
|
|
@@ -539,6 +590,7 @@ export async function runRun(args, reporter, lang = 'en', options = {}) {
|
|
|
539
590
|
},
|
|
540
591
|
stdoutTailBytes: runReceiptPolicy.stdoutTailBytes,
|
|
541
592
|
stderrTailBytes: runReceiptPolicy.stderrTailBytes,
|
|
593
|
+
receiptPath: createRunReceiptRelativePath(),
|
|
542
594
|
}));
|
|
543
595
|
if (options.writeLatestReceipt !== false) {
|
|
544
596
|
profiler.measure('receipt_write', () => writeRunReceipt(projectRoot, receipt));
|