mustflow 2.18.3 → 2.18.7
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 +2 -0
- package/dist/cli/commands/run/executor.js +57 -20
- package/dist/cli/commands/run/process-tree.js +2 -2
- package/dist/cli/commands/run.js +7 -3
- package/dist/cli/i18n/en.js +1 -0
- package/dist/cli/i18n/es.js +1 -0
- package/dist/cli/i18n/fr.js +1 -0
- package/dist/cli/i18n/hi.js +1 -0
- package/dist/cli/i18n/ko.js +1 -0
- package/dist/cli/i18n/zh.js +1 -0
- package/dist/cli/lib/dashboard-export.js +2 -1
- package/dist/cli/lib/dashboard-html/locale-bootstrap.js +3 -2
- package/dist/cli/lib/dashboard-html/template.js +5 -4
- package/dist/cli/lib/html-json.js +11 -0
- package/dist/cli/lib/local-index/index.js +166 -14
- package/dist/cli/lib/run-plan.js +6 -0
- package/dist/core/check-issues.js +1 -0
- package/dist/core/command-contract-rules.js +0 -3
- package/dist/core/command-contract-validation.js +42 -4
- package/dist/core/command-intent-eligibility.js +4 -4
- package/dist/core/contract-lint.js +3 -3
- package/package.json +1 -1
- package/templates/default/manifest.toml +1 -1
package/README.md
CHANGED
|
@@ -356,6 +356,8 @@ Runnable work is declared in `.mustflow/config/commands.toml` so agents do not g
|
|
|
356
356
|
|
|
357
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
|
+
Command environments remove the project-local `node_modules/.bin` path from `PATH` by default. If an intent needs a project dependency binary such as `eslint`, `tsc`, or `vitest`, declare it through the package manager, for example `npm exec eslint -- ...`, `pnpm exec tsc -- --noEmit`, `bun x eslint ...`, or `yarn exec eslint ...`. `mf check --strict` warns when an agent-runnable intent uses a bare executable name that appears under the project-local `.bin` directory.
|
|
360
|
+
|
|
359
361
|
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
362
|
|
|
361
363
|
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.
|
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
import { spawn } from 'node:child_process';
|
|
2
2
|
import { BoundedOutputBuffer } from '../../../core/bounded-output.js';
|
|
3
|
-
import { createPendingTimeoutTermination,
|
|
3
|
+
import { createPendingTimeoutTermination, forceTerminateProcessTree, getKillMethod, terminateProcessTree, } from './process-tree.js';
|
|
4
4
|
import { createOutputLimitError, isOutputLimitExceededError, writeStreamChunk } from './output.js';
|
|
5
|
-
|
|
5
|
+
const TERMINATION_CONFIRMATION_FALLBACK_MS = 1000;
|
|
6
|
+
function runSpawnedCommandStreaming(command, cwd, env, timeoutSeconds, killAfterSeconds, maxOutputBytes, stdoutTailBytes, stderrTailBytes, reporter, streamOutput, enforceOutputLimit) {
|
|
6
7
|
return new Promise((resolve) => {
|
|
7
8
|
const stdout = new BoundedOutputBuffer(stdoutTailBytes);
|
|
8
9
|
const stderr = new BoundedOutputBuffer(stderrTailBytes);
|
|
@@ -13,6 +14,9 @@ function runSpawnedCommandStreaming(command, cwd, env, timeoutSeconds, maxOutput
|
|
|
13
14
|
let stdoutBytes = 0;
|
|
14
15
|
let stderrBytes = 0;
|
|
15
16
|
let timeout;
|
|
17
|
+
let forceKillTimeout;
|
|
18
|
+
let terminationFallbackTimeout;
|
|
19
|
+
let terminationStarted = false;
|
|
16
20
|
let termination = null;
|
|
17
21
|
const child = spawn(command.executable, command.args ?? [], {
|
|
18
22
|
cwd,
|
|
@@ -23,7 +27,7 @@ function runSpawnedCommandStreaming(command, cwd, env, timeoutSeconds, maxOutput
|
|
|
23
27
|
detached: process.platform !== 'win32',
|
|
24
28
|
});
|
|
25
29
|
childPid = child.pid;
|
|
26
|
-
const finish = (status, signal) => {
|
|
30
|
+
const finish = (status, signal, terminationConfirmed = true) => {
|
|
27
31
|
if (settled) {
|
|
28
32
|
return;
|
|
29
33
|
}
|
|
@@ -31,6 +35,19 @@ function runSpawnedCommandStreaming(command, cwd, env, timeoutSeconds, maxOutput
|
|
|
31
35
|
if (timeout) {
|
|
32
36
|
clearTimeout(timeout);
|
|
33
37
|
}
|
|
38
|
+
if (forceKillTimeout) {
|
|
39
|
+
clearTimeout(forceKillTimeout);
|
|
40
|
+
}
|
|
41
|
+
if (terminationFallbackTimeout) {
|
|
42
|
+
clearTimeout(terminationFallbackTimeout);
|
|
43
|
+
}
|
|
44
|
+
const confirmedTermination = termination ?
|
|
45
|
+
{
|
|
46
|
+
...termination,
|
|
47
|
+
confirmed: terminationConfirmed,
|
|
48
|
+
cleanup_pending: !terminationConfirmed,
|
|
49
|
+
} :
|
|
50
|
+
null;
|
|
34
51
|
resolve({
|
|
35
52
|
status: timedOut ? null : status,
|
|
36
53
|
signal: timedOut ? null : signal,
|
|
@@ -38,20 +55,42 @@ function runSpawnedCommandStreaming(command, cwd, env, timeoutSeconds, maxOutput
|
|
|
38
55
|
stdout: stdout.toSnapshot(),
|
|
39
56
|
stderr: stderr.toSnapshot(),
|
|
40
57
|
pid: childPid,
|
|
41
|
-
termination,
|
|
58
|
+
termination: confirmedTermination,
|
|
42
59
|
});
|
|
43
60
|
};
|
|
61
|
+
const beginTermination = () => {
|
|
62
|
+
if (terminationStarted) {
|
|
63
|
+
return;
|
|
64
|
+
}
|
|
65
|
+
terminationStarted = true;
|
|
66
|
+
child.stdout?.destroy();
|
|
67
|
+
child.stderr?.destroy();
|
|
68
|
+
terminateProcessTree(childPid);
|
|
69
|
+
const forceAfterMs = killAfterSeconds * 1000;
|
|
70
|
+
forceKillTimeout = setTimeout(() => {
|
|
71
|
+
if (termination) {
|
|
72
|
+
termination = {
|
|
73
|
+
...termination,
|
|
74
|
+
forced_kill_attempted: true,
|
|
75
|
+
};
|
|
76
|
+
}
|
|
77
|
+
forceTerminateProcessTree(childPid);
|
|
78
|
+
}, forceAfterMs);
|
|
79
|
+
terminationFallbackTimeout = setTimeout(() => {
|
|
80
|
+
child.unref();
|
|
81
|
+
finish(null, null, false);
|
|
82
|
+
}, forceAfterMs + TERMINATION_CONFIRMATION_FALLBACK_MS);
|
|
83
|
+
};
|
|
44
84
|
const stopForOutputLimit = (stream) => {
|
|
45
85
|
if (settled || childError) {
|
|
46
86
|
return;
|
|
47
87
|
}
|
|
48
88
|
childError = createOutputLimitError(stream, maxOutputBytes);
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
finish(null, null);
|
|
89
|
+
if (timeout) {
|
|
90
|
+
clearTimeout(timeout);
|
|
91
|
+
timeout = undefined;
|
|
92
|
+
}
|
|
93
|
+
beginTermination();
|
|
55
94
|
};
|
|
56
95
|
child.stdout?.on('data', (chunk) => {
|
|
57
96
|
stdout.append(chunk);
|
|
@@ -80,22 +119,20 @@ function runSpawnedCommandStreaming(command, cwd, env, timeoutSeconds, maxOutput
|
|
|
80
119
|
finish(status, signal);
|
|
81
120
|
});
|
|
82
121
|
timeout = setTimeout(() => {
|
|
122
|
+
if (settled || childError) {
|
|
123
|
+
return;
|
|
124
|
+
}
|
|
83
125
|
timedOut = true;
|
|
84
|
-
child.stdout?.destroy();
|
|
85
|
-
child.stderr?.destroy();
|
|
86
|
-
child.unref();
|
|
87
126
|
termination = createPendingTimeoutTermination(getKillMethod());
|
|
88
|
-
|
|
89
|
-
forceTerminateProcessTreeNonBlocking(childPid);
|
|
90
|
-
finish(null, null);
|
|
127
|
+
beginTermination();
|
|
91
128
|
}, timeoutSeconds * 1000);
|
|
92
129
|
});
|
|
93
130
|
}
|
|
94
|
-
export function runArgvCommandStreaming(command, cwd, env, timeoutSeconds, maxOutputBytes, stdoutTailBytes, stderrTailBytes, reporter, streamOutput, enforceOutputLimit) {
|
|
95
|
-
return runSpawnedCommandStreaming({ executable: command?.executable ?? '', args: command?.args ?? [], shell: command?.shell ?? false }, cwd, env, timeoutSeconds, maxOutputBytes, stdoutTailBytes, stderrTailBytes, reporter, streamOutput, enforceOutputLimit);
|
|
131
|
+
export function runArgvCommandStreaming(command, cwd, env, timeoutSeconds, killAfterSeconds, maxOutputBytes, stdoutTailBytes, stderrTailBytes, reporter, streamOutput, enforceOutputLimit) {
|
|
132
|
+
return runSpawnedCommandStreaming({ executable: command?.executable ?? '', args: command?.args ?? [], shell: command?.shell ?? false }, cwd, env, timeoutSeconds, killAfterSeconds, maxOutputBytes, stdoutTailBytes, stderrTailBytes, reporter, streamOutput, enforceOutputLimit);
|
|
96
133
|
}
|
|
97
|
-
export function runShellCommandStreaming(command, cwd, env, timeoutSeconds, maxOutputBytes, stdoutTailBytes, stderrTailBytes, reporter, streamOutput, enforceOutputLimit) {
|
|
98
|
-
return runSpawnedCommandStreaming({ executable: command ?? '', shell: true }, cwd, env, timeoutSeconds, maxOutputBytes, stdoutTailBytes, stderrTailBytes, reporter, streamOutput, enforceOutputLimit);
|
|
134
|
+
export function runShellCommandStreaming(command, cwd, env, timeoutSeconds, killAfterSeconds, maxOutputBytes, stdoutTailBytes, stderrTailBytes, reporter, streamOutput, enforceOutputLimit) {
|
|
135
|
+
return runSpawnedCommandStreaming({ executable: command ?? '', shell: true }, cwd, env, timeoutSeconds, killAfterSeconds, maxOutputBytes, stdoutTailBytes, stderrTailBytes, reporter, streamOutput, enforceOutputLimit);
|
|
99
136
|
}
|
|
100
137
|
export function getRunStatus(error, exitCode, successExitCodes) {
|
|
101
138
|
const errorWithCode = error;
|
|
@@ -78,13 +78,13 @@ export function forceTerminateProcessTreeNonBlocking(pid) {
|
|
|
78
78
|
export function getKillMethod() {
|
|
79
79
|
return process.platform === 'win32' ? 'taskkill_process_tree' : 'process_group_sigterm';
|
|
80
80
|
}
|
|
81
|
-
export function createPendingTimeoutTermination(method) {
|
|
81
|
+
export function createPendingTimeoutTermination(method, forcedKillAttempted = false) {
|
|
82
82
|
return {
|
|
83
83
|
reason: 'timeout',
|
|
84
84
|
method,
|
|
85
85
|
graceful_signal: 'SIGTERM',
|
|
86
86
|
forced_signal: 'SIGKILL',
|
|
87
|
-
forced_kill_attempted:
|
|
87
|
+
forced_kill_attempted: forcedKillAttempted,
|
|
88
88
|
confirmed: false,
|
|
89
89
|
cleanup_pending: true,
|
|
90
90
|
};
|
package/dist/cli/commands/run.js
CHANGED
|
@@ -12,7 +12,7 @@ import { RunProfiler } from '../../core/run-profile.js';
|
|
|
12
12
|
import { finishRunWriteTracking, startRunWriteTracking } from '../../core/run-write-drift.js';
|
|
13
13
|
import { runBuiltinArgvInProcess } from './run/builtin-dispatch.js';
|
|
14
14
|
import { getRunStatus, runArgvCommandStreaming, runShellCommandStreaming } from './run/executor.js';
|
|
15
|
-
import { emitOutput } from './run/output.js';
|
|
15
|
+
import { emitOutput, isOutputLimitExceededError } from './run/output.js';
|
|
16
16
|
import { createPendingTimeoutTermination, getKillMethod, terminateProcessTree } from './run/process-tree.js';
|
|
17
17
|
import { assembleRunReceipt } from './run/receipt.js';
|
|
18
18
|
function getRunPlanDetail(plan, lang, fallbackKey) {
|
|
@@ -187,10 +187,10 @@ export async function runRun(args, reporter, lang = 'en', options = {}) {
|
|
|
187
187
|
}
|
|
188
188
|
if (plan.commandArgv) {
|
|
189
189
|
streamedOutput = !json;
|
|
190
|
-
return runArgvCommandStreaming(plan.argvCommand, plan.cwd, env, plan.timeoutSeconds, plan.maxOutputBytes, stdoutTailBytes, stderrTailBytes, reporter, !json,
|
|
190
|
+
return runArgvCommandStreaming(plan.argvCommand, plan.cwd, env, plan.timeoutSeconds, plan.killAfterSeconds, plan.maxOutputBytes, stdoutTailBytes, stderrTailBytes, reporter, !json, true);
|
|
191
191
|
}
|
|
192
192
|
streamedOutput = !json;
|
|
193
|
-
return runShellCommandStreaming(plan.shellCommand, plan.cwd, env, plan.timeoutSeconds, plan.maxOutputBytes, stdoutTailBytes, stderrTailBytes, reporter, !json,
|
|
193
|
+
return runShellCommandStreaming(plan.shellCommand, plan.cwd, env, plan.timeoutSeconds, plan.killAfterSeconds, plan.maxOutputBytes, stdoutTailBytes, stderrTailBytes, reporter, !json, true);
|
|
194
194
|
});
|
|
195
195
|
const childDurationMs = performance.now() - childStartedAtMs;
|
|
196
196
|
const finishedAt = new Date();
|
|
@@ -247,6 +247,10 @@ export async function runRun(args, reporter, lang = 'en', options = {}) {
|
|
|
247
247
|
reporter.stderr(t(lang, 'run.error.timedOut', { intent: intentName, seconds: plan.timeoutSeconds }));
|
|
248
248
|
return 1;
|
|
249
249
|
}
|
|
250
|
+
if (isOutputLimitExceededError(result.error)) {
|
|
251
|
+
reporter.stderr(t(lang, 'run.error.outputLimitExceeded', { intent: intentName, message: result.error.message }));
|
|
252
|
+
return 1;
|
|
253
|
+
}
|
|
250
254
|
reporter.stderr(t(lang, 'run.error.startFailed', { intent: intentName, message: result.error.message }));
|
|
251
255
|
return 1;
|
|
252
256
|
}
|
package/dist/cli/i18n/en.js
CHANGED
|
@@ -672,6 +672,7 @@ Read these files before working:
|
|
|
672
672
|
"run.error.maxOutputBytesDetail": "The output limit must stay within the allowed maximum.",
|
|
673
673
|
"run.error.conflictingPreviewModes": "Use either --dry-run or --plan-only, not both",
|
|
674
674
|
"run.error.timedOut": 'Command "{intent}" timed out after {seconds} seconds',
|
|
675
|
+
"run.error.outputLimitExceeded": 'Command "{intent}" exceeded max_output_bytes: {message}',
|
|
675
676
|
"run.error.startFailed": 'Command "{intent}" failed to start: {message}',
|
|
676
677
|
"search.help.summary": "Search the local SQLite index for the mustflow workflow.",
|
|
677
678
|
"search.help.option.limit": "Set the number of results to print. Default: 10, max: 50",
|
package/dist/cli/i18n/es.js
CHANGED
|
@@ -672,6 +672,7 @@ Lee estos archivos antes de trabajar:
|
|
|
672
672
|
"run.error.maxOutputBytesDetail": "El límite de salida debe permanecer dentro del máximo permitido.",
|
|
673
673
|
"run.error.conflictingPreviewModes": "Usa --dry-run o --plan-only, no ambos",
|
|
674
674
|
"run.error.timedOut": 'El comando "{intent}" agotó el tiempo después de {seconds} segundos',
|
|
675
|
+
"run.error.outputLimitExceeded": 'El comando "{intent}" superó max_output_bytes: {message}',
|
|
675
676
|
"run.error.startFailed": 'No se pudo iniciar el comando "{intent}": {message}',
|
|
676
677
|
"search.help.summary": "Busca en el índice SQLite local del flujo de trabajo mustflow.",
|
|
677
678
|
"search.help.option.limit": "Define la cantidad de resultados que se imprimen. Predeterminado: 10, máximo: 50",
|
package/dist/cli/i18n/fr.js
CHANGED
|
@@ -672,6 +672,7 @@ Lisez ces fichiers avant de travailler :
|
|
|
672
672
|
"run.error.maxOutputBytesDetail": "La limite de sortie doit rester dans le maximum autorisé.",
|
|
673
673
|
"run.error.conflictingPreviewModes": "Utilisez --dry-run ou --plan-only, pas les deux",
|
|
674
674
|
"run.error.timedOut": 'La commande "{intent}" a expiré après {seconds} secondes',
|
|
675
|
+
"run.error.outputLimitExceeded": 'La commande "{intent}" a dépassé max_output_bytes : {message}',
|
|
675
676
|
"run.error.startFailed": 'Impossible de démarrer la commande "{intent}" : {message}',
|
|
676
677
|
"search.help.summary": "Recherche dans l'index SQLite local du flux de travail mustflow.",
|
|
677
678
|
"search.help.option.limit": "Définit le nombre de résultats à imprimer. Par défaut : 10, max : 50",
|
package/dist/cli/i18n/hi.js
CHANGED
|
@@ -672,6 +672,7 @@ export const hiMessages = {
|
|
|
672
672
|
"run.error.maxOutputBytesDetail": "Output limit अनुमत maximum के अंदर रहनी चाहिए।",
|
|
673
673
|
"run.error.conflictingPreviewModes": "--dry-run या --plan-only में से एक इस्तेमाल करें, दोनों नहीं",
|
|
674
674
|
"run.error.timedOut": 'कमांड "{intent}" {seconds} सेकंड बाद time out हुई',
|
|
675
|
+
"run.error.outputLimitExceeded": 'कमांड "{intent}" ने max_output_bytes सीमा पार की: {message}',
|
|
675
676
|
"run.error.startFailed": 'कमांड "{intent}" शुरू नहीं हो सकी: {message}',
|
|
676
677
|
"search.help.summary": "mustflow वर्कफ़्लो के लिए स्थानीय SQLite इंडेक्स में खोजें।",
|
|
677
678
|
"search.help.option.limit": "प्रिंट किए जाने वाले परिणामों की संख्या सेट करें। डिफ़ॉल्ट: 10, अधिकतम: 50",
|
package/dist/cli/i18n/ko.js
CHANGED
|
@@ -672,6 +672,7 @@ export const koMessages = {
|
|
|
672
672
|
"run.error.maxOutputBytesDetail": "출력 상한은 허용된 최댓값 안에 있어야 합니다.",
|
|
673
673
|
"run.error.conflictingPreviewModes": "--dry-run과 --plan-only 중 하나만 사용하세요",
|
|
674
674
|
"run.error.timedOut": '명령 "{intent}"가 {seconds}초 뒤 시간 초과되었습니다',
|
|
675
|
+
"run.error.outputLimitExceeded": '명령 "{intent}"가 max_output_bytes 제한을 넘었습니다: {message}',
|
|
675
676
|
"run.error.startFailed": '명령 "{intent}"를 시작하지 못했습니다: {message}',
|
|
676
677
|
"search.help.summary": "로컬 SQLite 색인에서 mustflow 워크플로우를 검색합니다.",
|
|
677
678
|
"search.help.option.limit": "출력할 검색 결과 수를 설정합니다. 기본값: 10, 최대: 50",
|
package/dist/cli/i18n/zh.js
CHANGED
|
@@ -672,6 +672,7 @@ export const zhMessages = {
|
|
|
672
672
|
"run.error.maxOutputBytesDetail": "输出限制必须保持在允许的最大值内。",
|
|
673
673
|
"run.error.conflictingPreviewModes": "只能使用 --dry-run 或 --plan-only,不能同时使用",
|
|
674
674
|
"run.error.timedOut": '命令 "{intent}" 在 {seconds} 秒后超时',
|
|
675
|
+
"run.error.outputLimitExceeded": '命令 "{intent}" 超过 max_output_bytes:{message}',
|
|
675
676
|
"run.error.startFailed": '命令 "{intent}" 启动失败:{message}',
|
|
676
677
|
"search.help.summary": "搜索本地 SQLite 索引中的 mustflow 工作流。",
|
|
677
678
|
"search.help.option.limit": "设置要输出的结果数量。默认值:10,最大:50",
|
|
@@ -4,6 +4,7 @@ import { createDashboardCompletionVerdict } from '../../core/completion-verdict.
|
|
|
4
4
|
import { createDashboardEvidenceModel } from '../../core/verification-evidence.js';
|
|
5
5
|
import { redactSecretLikeText } from '../../core/secret-redaction.js';
|
|
6
6
|
import { ensureFileTargetInsideWithoutSymlinks, ensureInside, toPosixPath, writeUtf8FileInsideWithoutSymlinks, } from './filesystem.js';
|
|
7
|
+
import { safeJsonForInlineScript } from './html-json.js';
|
|
7
8
|
export class DashboardExportPathError extends Error {
|
|
8
9
|
targetPath;
|
|
9
10
|
constructor(targetPath) {
|
|
@@ -634,7 +635,7 @@ export function renderDashboardExportHtml(snapshot) {
|
|
|
634
635
|
const graphSummary = asRecord(harnessVerification.decision_graph_summary);
|
|
635
636
|
const harnessRunHistory = asRecord(harnessReport.run_history);
|
|
636
637
|
const harnessDocsReview = asRecord(harnessReport.docs_review);
|
|
637
|
-
const embeddedJson =
|
|
638
|
+
const embeddedJson = safeJsonForInlineScript(snapshot);
|
|
638
639
|
return `<!doctype html>
|
|
639
640
|
<html lang="en">
|
|
640
641
|
<head>
|
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
import { getDashboardLocaleBundle } from '../dashboard-locale.js';
|
|
2
|
+
import { safeJsonForInlineScript } from '../html-json.js';
|
|
2
3
|
export function createDashboardLocaleBootstrap() {
|
|
3
4
|
const localeBundle = getDashboardLocaleBundle();
|
|
4
5
|
return {
|
|
5
|
-
serializedLocaleBundle:
|
|
6
|
-
serializedAvailableLocales:
|
|
6
|
+
serializedLocaleBundle: safeJsonForInlineScript(localeBundle),
|
|
7
|
+
serializedAvailableLocales: safeJsonForInlineScript(localeBundle.locales),
|
|
7
8
|
};
|
|
8
9
|
}
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { safeJsonForInlineScript } from '../html-json.js';
|
|
1
2
|
import { renderDashboardClientScript } from './client-script.js';
|
|
2
3
|
import { createDashboardLocaleBootstrap } from './locale-bootstrap.js';
|
|
3
4
|
import { renderDashboardStyles } from './styles.js';
|
|
@@ -12,10 +13,10 @@ function escapeHtml(value) {
|
|
|
12
13
|
export function renderDashboardHtml(snapshot, token, statusSnapshot, docReviewSnapshot) {
|
|
13
14
|
const root = escapeHtml(snapshot.projectRoot);
|
|
14
15
|
const preferencesPath = escapeHtml(snapshot.preferencesPath);
|
|
15
|
-
const serializedSnapshot =
|
|
16
|
-
const serializedStatusSnapshot =
|
|
17
|
-
const serializedDocReviewSnapshot =
|
|
18
|
-
const serializedToken =
|
|
16
|
+
const serializedSnapshot = safeJsonForInlineScript(snapshot);
|
|
17
|
+
const serializedStatusSnapshot = safeJsonForInlineScript(statusSnapshot);
|
|
18
|
+
const serializedDocReviewSnapshot = safeJsonForInlineScript(docReviewSnapshot);
|
|
19
|
+
const serializedToken = safeJsonForInlineScript(token);
|
|
19
20
|
const { serializedLocaleBundle, serializedAvailableLocales } = createDashboardLocaleBootstrap();
|
|
20
21
|
return `<!doctype html>
|
|
21
22
|
<html lang="en">
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
const INLINE_SCRIPT_JSON_ESCAPES = {
|
|
2
|
+
'<': '\\u003C',
|
|
3
|
+
'>': '\\u003E',
|
|
4
|
+
'&': '\\u0026',
|
|
5
|
+
'\u2028': '\\u2028',
|
|
6
|
+
'\u2029': '\\u2029',
|
|
7
|
+
};
|
|
8
|
+
export function safeJsonForInlineScript(value) {
|
|
9
|
+
const json = JSON.stringify(value);
|
|
10
|
+
return (json ?? 'null').replace(/[<>&\u2028\u2029]/gu, (character) => INLINE_SCRIPT_JSON_ESCAPES[character]);
|
|
11
|
+
}
|
|
@@ -284,7 +284,21 @@ function collectCommandIntents(projectRoot) {
|
|
|
284
284
|
}
|
|
285
285
|
return intents;
|
|
286
286
|
}
|
|
287
|
+
function normalizeIndexedFileSourceScope(value) {
|
|
288
|
+
const sourceScope = toSearchString(value);
|
|
289
|
+
if (sourceScope === 'source_anchor' || sourceScope === 'state') {
|
|
290
|
+
return sourceScope;
|
|
291
|
+
}
|
|
292
|
+
return 'workflow';
|
|
293
|
+
}
|
|
287
294
|
function readIndexedFileRecord(projectRoot, relativePath, sourceScope, contentHash = null) {
|
|
295
|
+
const metadata = readIndexedFileMetadataRecord(projectRoot, relativePath, sourceScope);
|
|
296
|
+
return {
|
|
297
|
+
...metadata,
|
|
298
|
+
contentHash: contentHash ?? sha256Bytes(readFileSync(path.join(projectRoot, ...relativePath.split('/')))),
|
|
299
|
+
};
|
|
300
|
+
}
|
|
301
|
+
function readIndexedFileMetadataRecord(projectRoot, relativePath, sourceScope) {
|
|
288
302
|
const fullPath = path.join(projectRoot, ...relativePath.split('/'));
|
|
289
303
|
const stats = statSync(fullPath);
|
|
290
304
|
return {
|
|
@@ -292,7 +306,6 @@ function readIndexedFileRecord(projectRoot, relativePath, sourceScope, contentHa
|
|
|
292
306
|
sourceScope,
|
|
293
307
|
sizeBytes: stats.size,
|
|
294
308
|
mtimeMs: Math.round(stats.mtimeMs),
|
|
295
|
-
contentHash: contentHash ?? sha256Bytes(readFileSync(fullPath)),
|
|
296
309
|
};
|
|
297
310
|
}
|
|
298
311
|
function collectIndexedFileRecords(projectRoot, documents, sourceAnchors) {
|
|
@@ -305,8 +318,21 @@ function collectIndexedFileRecords(projectRoot, documents, sourceAnchors) {
|
|
|
305
318
|
records.set(anchorPath, readIndexedFileRecord(projectRoot, anchorPath, 'source_anchor'));
|
|
306
319
|
}
|
|
307
320
|
}
|
|
321
|
+
if (existsSync(path.join(projectRoot, ...LATEST_RUN_STATE_RELATIVE_PATH.split('/')))) {
|
|
322
|
+
records.set(LATEST_RUN_STATE_RELATIVE_PATH, readIndexedFileRecord(projectRoot, LATEST_RUN_STATE_RELATIVE_PATH, 'state'));
|
|
323
|
+
}
|
|
308
324
|
return [...records.values()].sort((left, right) => left.path.localeCompare(right.path));
|
|
309
325
|
}
|
|
326
|
+
function collectFastPreflightIndexedFileMetadataRecords(projectRoot, includeSource) {
|
|
327
|
+
if (includeSource) {
|
|
328
|
+
return null;
|
|
329
|
+
}
|
|
330
|
+
const records = getExistingIndexablePaths(projectRoot).map((relativePath) => readIndexedFileMetadataRecord(projectRoot, relativePath, 'workflow'));
|
|
331
|
+
if (existsSync(path.join(projectRoot, ...LATEST_RUN_STATE_RELATIVE_PATH.split('/')))) {
|
|
332
|
+
records.push(readIndexedFileMetadataRecord(projectRoot, LATEST_RUN_STATE_RELATIVE_PATH, 'state'));
|
|
333
|
+
}
|
|
334
|
+
return records.sort((left, right) => left.path.localeCompare(right.path));
|
|
335
|
+
}
|
|
310
336
|
function normalizeSearchText(value) {
|
|
311
337
|
return value.trim().replace(/\s+/g, ' ');
|
|
312
338
|
}
|
|
@@ -1948,6 +1974,86 @@ function populateDatabase(database, capabilities, documents, skills, skillRoutes
|
|
|
1948
1974
|
populatePathSurfaceReadModel(database);
|
|
1949
1975
|
populateSearchTables(database, capabilities, documents, skills, skillRoutes, commandIntents, sourceAnchors);
|
|
1950
1976
|
}
|
|
1977
|
+
function readCount(database, tableName) {
|
|
1978
|
+
if (!hasTable(database, tableName)) {
|
|
1979
|
+
return 0;
|
|
1980
|
+
}
|
|
1981
|
+
const [row] = queryRows(database, `SELECT COUNT(*) AS count FROM ${tableName}`);
|
|
1982
|
+
const count = row?.count;
|
|
1983
|
+
return typeof count === 'number' && Number.isFinite(count) ? count : 0;
|
|
1984
|
+
}
|
|
1985
|
+
function readStoredIndexedPaths(database) {
|
|
1986
|
+
if (!hasTable(database, 'documents')) {
|
|
1987
|
+
return [];
|
|
1988
|
+
}
|
|
1989
|
+
return queryRows(database, 'SELECT path FROM documents ORDER BY path')
|
|
1990
|
+
.map((row) => toSearchString(row.path))
|
|
1991
|
+
.filter(Boolean);
|
|
1992
|
+
}
|
|
1993
|
+
function createStoredLocalIndexResult(projectRoot, databasePath, dryRun, indexMode, database, capabilities) {
|
|
1994
|
+
return {
|
|
1995
|
+
schema_version: LOCAL_INDEX_SCHEMA_VERSION,
|
|
1996
|
+
command: 'index',
|
|
1997
|
+
ok: true,
|
|
1998
|
+
mustflow_root: path.resolve(projectRoot),
|
|
1999
|
+
database_path: databasePath,
|
|
2000
|
+
dry_run: dryRun,
|
|
2001
|
+
wrote_files: false,
|
|
2002
|
+
index_mode: indexMode,
|
|
2003
|
+
reused_existing: true,
|
|
2004
|
+
rebuild_reason: null,
|
|
2005
|
+
document_count: readCount(database, 'documents'),
|
|
2006
|
+
skill_count: readCount(database, 'skills'),
|
|
2007
|
+
skill_route_count: readCount(database, 'skill_routes'),
|
|
2008
|
+
command_intent_count: readCount(database, 'command_intents'),
|
|
2009
|
+
command_effect_count: readCount(database, 'command_effects'),
|
|
2010
|
+
verification_evidence_summary_count: readCount(database, 'verification_evidence_summaries'),
|
|
2011
|
+
verification_plan_count: readCount(database, 'verification_plans'),
|
|
2012
|
+
acceptance_criteria_count: readCount(database, 'acceptance_criteria'),
|
|
2013
|
+
criterion_coverage_count: readCount(database, 'criterion_coverage'),
|
|
2014
|
+
verification_receipt_summary_count: readCount(database, 'verification_receipt_summaries'),
|
|
2015
|
+
command_receipt_summary_count: readCount(database, 'command_receipt_summaries'),
|
|
2016
|
+
verification_coverage_state_count: readCount(database, 'verification_coverage_states'),
|
|
2017
|
+
verification_risk_signal_count: readCount(database, 'verification_risk_signals'),
|
|
2018
|
+
validation_ratchet_signal_count: readCount(database, 'validation_ratchet_signals'),
|
|
2019
|
+
completion_verdict_summary_count: readCount(database, 'completion_verdict_summaries'),
|
|
2020
|
+
repro_route_count: readCount(database, 'repro_routes'),
|
|
2021
|
+
repro_observation_count: readCount(database, 'repro_observations'),
|
|
2022
|
+
failure_fingerprint_count: readCount(database, 'verification_failure_fingerprints'),
|
|
2023
|
+
source_index_enabled: readMetadataValue(database, 'source_index_enabled') === 'true',
|
|
2024
|
+
source_anchor_count: readCount(database, 'source_anchors'),
|
|
2025
|
+
source_anchor_risk_signal_count: readCount(database, 'source_anchor_risk_signals'),
|
|
2026
|
+
search_backend: capabilities.backend,
|
|
2027
|
+
search_fts5_available: capabilities.fts5Available,
|
|
2028
|
+
content_mode: LOCAL_INDEX_CONTENT_MODE,
|
|
2029
|
+
store_full_content: LOCAL_INDEX_STORE_FULL_CONTENT,
|
|
2030
|
+
max_snippet_bytes_per_document: MAX_SNIPPET_BYTES_PER_DOCUMENT,
|
|
2031
|
+
excluded_raw_data_kinds: [...LOCAL_INDEX_EXCLUDED_RAW_DATA_KINDS],
|
|
2032
|
+
indexed_file_count: readCount(database, 'indexed_files'),
|
|
2033
|
+
indexed_paths: readStoredIndexedPaths(database),
|
|
2034
|
+
};
|
|
2035
|
+
}
|
|
2036
|
+
function indexedFileMetadataMatch(database, currentFiles) {
|
|
2037
|
+
const rows = queryRows(database, 'SELECT path, source_scope, size_bytes, mtime_ms, parser_version FROM indexed_files ORDER BY path');
|
|
2038
|
+
if (rows.length !== currentFiles.length) {
|
|
2039
|
+
return false;
|
|
2040
|
+
}
|
|
2041
|
+
const currentByPath = new Map(currentFiles.map((file) => [file.path, file]));
|
|
2042
|
+
for (const row of rows) {
|
|
2043
|
+
const storedPath = toSearchString(row.path);
|
|
2044
|
+
const current = currentByPath.get(storedPath);
|
|
2045
|
+
if (!current) {
|
|
2046
|
+
return false;
|
|
2047
|
+
}
|
|
2048
|
+
if (normalizeIndexedFileSourceScope(row.source_scope) !== current.sourceScope ||
|
|
2049
|
+
row.size_bytes !== current.sizeBytes ||
|
|
2050
|
+
row.mtime_ms !== current.mtimeMs ||
|
|
2051
|
+
toSearchString(row.parser_version) !== LOCAL_INDEX_PARSER_VERSION) {
|
|
2052
|
+
return false;
|
|
2053
|
+
}
|
|
2054
|
+
}
|
|
2055
|
+
return true;
|
|
2056
|
+
}
|
|
1951
2057
|
function indexedFilesMatch(database, currentFiles) {
|
|
1952
2058
|
const rows = queryRows(database, 'SELECT path, source_scope, content_hash, parser_version FROM indexed_files ORDER BY path');
|
|
1953
2059
|
if (rows.length !== currentFiles.length) {
|
|
@@ -1960,7 +2066,7 @@ function indexedFilesMatch(database, currentFiles) {
|
|
|
1960
2066
|
if (!current) {
|
|
1961
2067
|
return false;
|
|
1962
2068
|
}
|
|
1963
|
-
if (
|
|
2069
|
+
if (normalizeIndexedFileSourceScope(row.source_scope) !== current.sourceScope ||
|
|
1964
2070
|
toSearchString(row.content_hash) !== current.contentHash ||
|
|
1965
2071
|
toSearchString(row.parser_version) !== LOCAL_INDEX_PARSER_VERSION) {
|
|
1966
2072
|
return false;
|
|
@@ -1968,6 +2074,44 @@ function indexedFilesMatch(database, currentFiles) {
|
|
|
1968
2074
|
}
|
|
1969
2075
|
return true;
|
|
1970
2076
|
}
|
|
2077
|
+
async function readIncrementalPreflightReuse(SQL, databasePath, projectRoot, currentFiles, sourceScopeHash, dryRun, indexMode) {
|
|
2078
|
+
if (!currentFiles) {
|
|
2079
|
+
return { result: null, rebuildReason: null };
|
|
2080
|
+
}
|
|
2081
|
+
if (!existsSync(databasePath)) {
|
|
2082
|
+
return { result: null, rebuildReason: 'missing_index' };
|
|
2083
|
+
}
|
|
2084
|
+
let database;
|
|
2085
|
+
try {
|
|
2086
|
+
database = new SQL.Database(readFileSync(databasePath));
|
|
2087
|
+
if (readStoredSchemaVersion(database) !== LOCAL_INDEX_SCHEMA_VERSION) {
|
|
2088
|
+
return { result: null, rebuildReason: 'schema_version_mismatch' };
|
|
2089
|
+
}
|
|
2090
|
+
if (readMetadataValue(database, 'parser_version') !== LOCAL_INDEX_PARSER_VERSION) {
|
|
2091
|
+
return { result: null, rebuildReason: 'parser_version_mismatch' };
|
|
2092
|
+
}
|
|
2093
|
+
if (readMetadataValue(database, 'source_scope_hash') !== sourceScopeHash) {
|
|
2094
|
+
return { result: null, rebuildReason: 'source_scope_mismatch' };
|
|
2095
|
+
}
|
|
2096
|
+
if (!hasTable(database, 'indexed_files')) {
|
|
2097
|
+
return { result: null, rebuildReason: 'indexed_files_missing' };
|
|
2098
|
+
}
|
|
2099
|
+
if (!indexedFileMetadataMatch(database, currentFiles)) {
|
|
2100
|
+
return { result: null, rebuildReason: 'file_fingerprint_mismatch' };
|
|
2101
|
+
}
|
|
2102
|
+
const capabilities = readStoredSearchCapabilities(database);
|
|
2103
|
+
return {
|
|
2104
|
+
result: createStoredLocalIndexResult(projectRoot, databasePath, dryRun, indexMode, database, capabilities),
|
|
2105
|
+
rebuildReason: null,
|
|
2106
|
+
};
|
|
2107
|
+
}
|
|
2108
|
+
catch {
|
|
2109
|
+
return { result: null, rebuildReason: 'unreadable_index' };
|
|
2110
|
+
}
|
|
2111
|
+
finally {
|
|
2112
|
+
database?.close();
|
|
2113
|
+
}
|
|
2114
|
+
}
|
|
1971
2115
|
async function readIncrementalReuseDecision(SQL, databasePath, currentFiles, sourceScopeHash) {
|
|
1972
2116
|
if (!existsSync(databasePath)) {
|
|
1973
2117
|
return { reusable: false, rebuildReason: 'missing_index', capabilities: null };
|
|
@@ -2015,13 +2159,28 @@ export async function createLocalIndex(projectRoot, options = {}) {
|
|
|
2015
2159
|
const dryRun = options.dryRun === true;
|
|
2016
2160
|
const incremental = options.incremental === true;
|
|
2017
2161
|
const indexMode = incremental ? 'incremental' : 'full';
|
|
2162
|
+
const sourceConfig = readLocalIndexSourceConfig(projectRoot);
|
|
2163
|
+
const includeSource = options.includeSource === true || sourceConfig.enabledByDefault;
|
|
2164
|
+
const sourceScopeHash = getSourceScopeHash(includeSource, sourceConfig);
|
|
2165
|
+
let capabilities = searchCapabilities(false);
|
|
2166
|
+
let reusedExisting = false;
|
|
2167
|
+
let rebuildReason = null;
|
|
2168
|
+
const SQL = await loadSqlJs();
|
|
2169
|
+
const capabilityDatabase = new SQL.Database();
|
|
2170
|
+
capabilities = detectLocalSearchCapabilities(capabilityDatabase);
|
|
2171
|
+
capabilityDatabase.close();
|
|
2172
|
+
if (incremental) {
|
|
2173
|
+
const preflightFiles = collectFastPreflightIndexedFileMetadataRecords(projectRoot, includeSource);
|
|
2174
|
+
const preflightReuse = await readIncrementalPreflightReuse(SQL, databasePath, projectRoot, preflightFiles, sourceScopeHash, dryRun, indexMode);
|
|
2175
|
+
if (preflightReuse.result) {
|
|
2176
|
+
return preflightReuse.result;
|
|
2177
|
+
}
|
|
2178
|
+
rebuildReason = preflightReuse.rebuildReason;
|
|
2179
|
+
}
|
|
2018
2180
|
const documents = collectDocuments(projectRoot);
|
|
2019
2181
|
const skills = collectSkills(documents);
|
|
2020
2182
|
const skillRoutes = collectSkillRoutes(projectRoot);
|
|
2021
2183
|
const commandIntents = collectCommandIntents(projectRoot);
|
|
2022
|
-
const sourceConfig = readLocalIndexSourceConfig(projectRoot);
|
|
2023
|
-
const includeSource = options.includeSource === true || sourceConfig.enabledByDefault;
|
|
2024
|
-
const sourceScopeHash = getSourceScopeHash(includeSource, sourceConfig);
|
|
2025
2184
|
const previousSourceAnchors = includeSource
|
|
2026
2185
|
? await readPreviousSourceAnchorSnapshots(databasePath).catch(() => [])
|
|
2027
2186
|
: [];
|
|
@@ -2033,17 +2192,10 @@ export async function createLocalIndex(projectRoot, options = {}) {
|
|
|
2033
2192
|
: [];
|
|
2034
2193
|
const verificationEvidence = createVerificationEvidenceIndex(projectRoot);
|
|
2035
2194
|
const indexedFiles = collectIndexedFileRecords(projectRoot, documents, sourceAnchors);
|
|
2036
|
-
let capabilities = searchCapabilities(false);
|
|
2037
|
-
let reusedExisting = false;
|
|
2038
|
-
let rebuildReason = null;
|
|
2039
|
-
const SQL = await loadSqlJs();
|
|
2040
|
-
const capabilityDatabase = new SQL.Database();
|
|
2041
|
-
capabilities = detectLocalSearchCapabilities(capabilityDatabase);
|
|
2042
|
-
capabilityDatabase.close();
|
|
2043
2195
|
if (incremental) {
|
|
2044
2196
|
const reuseDecision = await readIncrementalReuseDecision(SQL, databasePath, indexedFiles, sourceScopeHash);
|
|
2045
2197
|
reusedExisting = reuseDecision.reusable;
|
|
2046
|
-
rebuildReason = reuseDecision.rebuildReason;
|
|
2198
|
+
rebuildReason = reuseDecision.rebuildReason ?? rebuildReason;
|
|
2047
2199
|
capabilities = reuseDecision.capabilities ?? capabilities;
|
|
2048
2200
|
}
|
|
2049
2201
|
if (!dryRun && !reusedExisting) {
|
|
@@ -2110,7 +2262,7 @@ function getStalePaths(projectRoot, database) {
|
|
|
2110
2262
|
const indexedPaths = new Set(indexedRows.map((row) => toSearchString(row.path)));
|
|
2111
2263
|
for (const row of indexedRows) {
|
|
2112
2264
|
const indexedPath = toSearchString(row.path);
|
|
2113
|
-
const sourceScope =
|
|
2265
|
+
const sourceScope = normalizeIndexedFileSourceScope(row.source_scope);
|
|
2114
2266
|
try {
|
|
2115
2267
|
const current = readIndexedFileRecord(projectRoot, indexedPath, sourceScope);
|
|
2116
2268
|
if (current.contentHash !== toSearchString(row.content_hash)) {
|
package/dist/cli/lib/run-plan.js
CHANGED
|
@@ -78,6 +78,9 @@ function readEffectiveMaxOutputBytes(contract, intent) {
|
|
|
78
78
|
readPositiveInteger(contract.defaults, 'max_output_bytes') ??
|
|
79
79
|
DEFAULT_COMMAND_MAX_OUTPUT_BYTES;
|
|
80
80
|
}
|
|
81
|
+
function readEffectiveKillAfterSeconds(contract) {
|
|
82
|
+
return readPositiveInteger(contract.defaults, 'kill_after_seconds') ?? 5;
|
|
83
|
+
}
|
|
81
84
|
function getMaxOutputBytesLimitDetail(contract, intent) {
|
|
82
85
|
const intentValue = readPositiveInteger(intent, 'max_output_bytes');
|
|
83
86
|
if (intentValue !== undefined) {
|
|
@@ -103,6 +106,7 @@ function readRunIntentMetadata(contract, intent) {
|
|
|
103
106
|
kind: readString(intent, 'kind') ?? null,
|
|
104
107
|
configuredCwd,
|
|
105
108
|
timeoutSeconds: readPositiveInteger(intent, 'timeout_seconds') ?? null,
|
|
109
|
+
killAfterSeconds: readEffectiveKillAfterSeconds(contract),
|
|
106
110
|
maxOutputBytes: readEffectiveMaxOutputBytes(contract, intent),
|
|
107
111
|
successExitCodes: getSuccessExitCodes(intent),
|
|
108
112
|
commandArgv,
|
|
@@ -138,6 +142,7 @@ function createBlockedRunPlan(contract, intentName, intent, eligibility, reasonC
|
|
|
138
142
|
cwd: null,
|
|
139
143
|
relativeCwd: null,
|
|
140
144
|
timeoutSeconds: metadata?.timeoutSeconds ?? null,
|
|
145
|
+
killAfterSeconds: metadata?.killAfterSeconds ?? null,
|
|
141
146
|
maxOutputBytes: metadata?.maxOutputBytes ?? null,
|
|
142
147
|
successExitCodes: metadata?.successExitCodes ?? null,
|
|
143
148
|
commandArgv: metadata?.commandArgv,
|
|
@@ -199,6 +204,7 @@ export function createRunPlan(projectRoot, contract, intentName, options = {}) {
|
|
|
199
204
|
cwd,
|
|
200
205
|
relativeCwd: getRelativeProjectPath(projectRoot, cwd),
|
|
201
206
|
timeoutSeconds: metadata.timeoutSeconds,
|
|
207
|
+
killAfterSeconds: metadata.killAfterSeconds,
|
|
202
208
|
maxOutputBytes: metadata.maxOutputBytes,
|
|
203
209
|
successExitCodes: metadata.successExitCodes,
|
|
204
210
|
commandArgv,
|
|
@@ -15,6 +15,7 @@ const CHECK_ISSUE_ID_RULES = [
|
|
|
15
15
|
['mustflow.command_contract.effect_path_escape', /^Strict: Command effect path must stay inside the current root:/u],
|
|
16
16
|
['mustflow.command_contract.shared_writes_without_effects', /^Strict warning: configured agent-runnable intents .+ share path:.+ through writes without explicit effects or resource locks$/u],
|
|
17
17
|
['mustflow.command_contract.broad_env_inheritance', /^Strict warning: configured agent-runnable intent [^\s]+ (?:implicitly inherits the host environment|uses env_policy = "inherit")/u],
|
|
18
|
+
['mustflow.command_contract.project_local_bin_bare_executable', /^Strict warning: configured agent-runnable intent [^\s]+ uses bare executable "[^"]+" that matches project-local node_modules\/\.bin/u],
|
|
18
19
|
['mustflow.prompt_cache.required', /^Strict: \[prompt_cache\] table is required$/u],
|
|
19
20
|
['mustflow.prompt_cache.volatile_in_stable', /^Strict: \[prompt_cache\.layers\.stable\]\.read must not include volatile path /u],
|
|
20
21
|
['mustflow.refresh.hash_method_required', /^Strict: \[refresh\]\.default_method should be "hash_check" for cache-friendly refresh$/u],
|
|
@@ -45,9 +45,6 @@ export function commandIntentHasCommandSource(intent) {
|
|
|
45
45
|
export function shellCommandHasBlockedBackgroundPattern(command) {
|
|
46
46
|
return BACKGROUND_SHELL_PATTERNS.some((pattern) => pattern.test(command));
|
|
47
47
|
}
|
|
48
|
-
export function commandIntentHasBlockedShellBackgroundPattern(intent) {
|
|
49
|
-
return intent.mode === 'shell' && typeof intent.cmd === 'string' && shellCommandHasBlockedBackgroundPattern(intent.cmd);
|
|
50
|
-
}
|
|
51
48
|
function normalizeExecutableName(value) {
|
|
52
49
|
return path.basename(value).replace(/\.(?:cmd|exe|ps1)$/iu, '').toLowerCase();
|
|
53
50
|
}
|
|
@@ -1,7 +1,9 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { existsSync } from 'node:fs';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import { COMMAND_LIFECYCLES, COMMAND_RUN_POLICIES, LONG_RUNNING_LIFECYCLES, isRecord, readStringArray, } from './config-loading.js';
|
|
2
4
|
import { COMMAND_ENV_POLICIES, DEFAULT_COMMAND_ENV_POLICY } from './command-env.js';
|
|
3
5
|
import { COMMAND_EFFECT_CONCURRENCY, COMMAND_EFFECT_MODES, COMMAND_EFFECT_TYPES, validateCommandEffectLockWarnings, validateCommandEffects, } from './command-effects.js';
|
|
4
|
-
import { commandIntentBlockedCommandPattern,
|
|
6
|
+
import { commandIntentBlockedCommandPattern, commandIntentHasCommandSource, commandIntentNameIsSafe, } from './command-contract-rules.js';
|
|
5
7
|
import { MAX_COMMAND_OUTPUT_BYTES, commandMaxOutputBytesLimitMessage } from './command-output-limits.js';
|
|
6
8
|
function commandContractIssue(message) {
|
|
7
9
|
return { message };
|
|
@@ -186,10 +188,10 @@ function validateCommandIntent(intentName, intent, issues) {
|
|
|
186
188
|
if (!commandIntentHasCommandSource(intent)) {
|
|
187
189
|
issues.push(commandContractIssue(`Configured intent ${intentName} must define argv or mode = "shell" with cmd`));
|
|
188
190
|
}
|
|
189
|
-
|
|
191
|
+
const blockedCommandPattern = commandIntentBlockedCommandPattern(intent);
|
|
192
|
+
if (blockedCommandPattern?.code === 'shell_background_pattern') {
|
|
190
193
|
issues.push(commandContractIssue(`Shell intent ${intentName} contains a blocked long-running or background pattern`));
|
|
191
194
|
}
|
|
192
|
-
const blockedCommandPattern = commandIntentBlockedCommandPattern(intent);
|
|
193
195
|
if (blockedCommandPattern?.code === 'long_running_command_pattern') {
|
|
194
196
|
issues.push(commandContractIssue(`Intent ${intentName} contains a blocked long-running or background command pattern`));
|
|
195
197
|
}
|
|
@@ -245,6 +247,41 @@ function validateCommandEnvInheritanceWarnings(commandsToml) {
|
|
|
245
247
|
}
|
|
246
248
|
return issues;
|
|
247
249
|
}
|
|
250
|
+
function projectLocalBinExecutableExists(projectRoot, executable) {
|
|
251
|
+
const localBinPath = path.join(projectRoot, 'node_modules', '.bin');
|
|
252
|
+
const executableName = path.basename(executable).replace(/\.(?:cmd|exe|ps1)$/iu, '');
|
|
253
|
+
const candidates = [
|
|
254
|
+
executableName,
|
|
255
|
+
`${executableName}.cmd`,
|
|
256
|
+
`${executableName}.exe`,
|
|
257
|
+
`${executableName}.ps1`,
|
|
258
|
+
];
|
|
259
|
+
return candidates.some((candidate) => existsSync(path.join(localBinPath, candidate)));
|
|
260
|
+
}
|
|
261
|
+
function validateProjectLocalBinWarnings(projectRoot, commandsToml) {
|
|
262
|
+
const issues = [];
|
|
263
|
+
if (!isRecord(commandsToml?.intents)) {
|
|
264
|
+
return issues;
|
|
265
|
+
}
|
|
266
|
+
for (const [intentName, intent] of Object.entries(commandsToml.intents)) {
|
|
267
|
+
if (!isRecord(intent)) {
|
|
268
|
+
continue;
|
|
269
|
+
}
|
|
270
|
+
if (intent.status !== 'configured' || intent.lifecycle !== 'oneshot' || intent.run_policy !== 'agent_allowed') {
|
|
271
|
+
continue;
|
|
272
|
+
}
|
|
273
|
+
const argv = readStringArray(intent, 'argv');
|
|
274
|
+
const executable = argv?.[0];
|
|
275
|
+
if (!executable || executable.includes('/') || executable.includes('\\')) {
|
|
276
|
+
continue;
|
|
277
|
+
}
|
|
278
|
+
if (!projectLocalBinExecutableExists(projectRoot, executable)) {
|
|
279
|
+
continue;
|
|
280
|
+
}
|
|
281
|
+
issues.push(commandContractWarning(`configured agent-runnable intent ${intentName} uses bare executable "${executable}" that matches project-local node_modules/.bin; use a package-manager mediated command such as npm exec, pnpm exec, bun x, or yarn exec`));
|
|
282
|
+
}
|
|
283
|
+
return issues;
|
|
284
|
+
}
|
|
248
285
|
/**
|
|
249
286
|
* mf:anchor core.command-contract-validation
|
|
250
287
|
* purpose: Validate command intent declarations that gate agent-executable repository commands.
|
|
@@ -287,6 +324,7 @@ export function validateCommandContractStrictDefaults(projectRoot, commandsToml)
|
|
|
287
324
|
}
|
|
288
325
|
}
|
|
289
326
|
issues.push(...validateCommandEnvInheritanceWarnings(commandsToml));
|
|
327
|
+
issues.push(...validateProjectLocalBinWarnings(projectRoot, commandsToml));
|
|
290
328
|
issues.push(...validateCommandEffects(projectRoot, commandsToml));
|
|
291
329
|
issues.push(...validateCommandEffectLockWarnings(commandsToml));
|
|
292
330
|
return issues;
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { isRecord, readString } from './config-loading.js';
|
|
2
|
-
import { commandIntentBlockedCommandPattern,
|
|
2
|
+
import { commandIntentBlockedCommandPattern, commandIntentHasCommandSource, commandIntentNameIsSafe, } from './command-contract-rules.js';
|
|
3
3
|
export function evaluateCommandIntentEligibility(intentName, rawIntent) {
|
|
4
4
|
if (!commandIntentNameIsSafe(intentName)) {
|
|
5
5
|
return {
|
|
@@ -61,14 +61,14 @@ export function evaluateCommandIntentEligibility(intentName, rawIntent) {
|
|
|
61
61
|
detail: 'Intent does not define argv or shell cmd.',
|
|
62
62
|
};
|
|
63
63
|
}
|
|
64
|
-
|
|
64
|
+
const blockedPattern = commandIntentBlockedCommandPattern(rawIntent);
|
|
65
|
+
if (blockedPattern?.code === 'shell_background_pattern') {
|
|
65
66
|
return {
|
|
66
67
|
ok: false,
|
|
67
68
|
code: 'blocked_shell_background_pattern',
|
|
68
|
-
detail:
|
|
69
|
+
detail: blockedPattern.detail,
|
|
69
70
|
};
|
|
70
71
|
}
|
|
71
|
-
const blockedPattern = commandIntentBlockedCommandPattern(rawIntent);
|
|
72
72
|
if (blockedPattern?.code === 'long_running_command_pattern') {
|
|
73
73
|
return {
|
|
74
74
|
ok: false,
|
|
@@ -2,7 +2,7 @@ import { existsSync, readFileSync } from 'node:fs';
|
|
|
2
2
|
import path from 'node:path';
|
|
3
3
|
import { COMMAND_LIFECYCLES, COMMAND_RUN_POLICIES, LONG_RUNNING_LIFECYCLES, isRecord, readPositiveInteger, readString, readStringArray, } from './config-loading.js';
|
|
4
4
|
import { evaluateCommandIntentEligibility, } from './command-intent-eligibility.js';
|
|
5
|
-
import { commandIntentBlockedCommandPattern,
|
|
5
|
+
import { commandIntentBlockedCommandPattern, commandIntentHasCommandSource, commandIntentNameIsSafe, } from './command-contract-rules.js';
|
|
6
6
|
import { MAX_COMMAND_OUTPUT_BYTES } from './command-output-limits.js';
|
|
7
7
|
import { commandEffectsConflict, normalizeCommandEffects } from './command-effects.js';
|
|
8
8
|
import { listChangeClassificationValidationReasons } from './change-classification.js';
|
|
@@ -323,10 +323,10 @@ function lintIntent(name, value, issues) {
|
|
|
323
323
|
if (!commandIntentHasCommandSource(value)) {
|
|
324
324
|
pushIssue(issues, 'error', 'executable_source_missing', name, `Configured intent ${name} must define argv or shell cmd.`);
|
|
325
325
|
}
|
|
326
|
-
|
|
326
|
+
const blockedCommandPattern = commandIntentBlockedCommandPattern(value);
|
|
327
|
+
if (blockedCommandPattern?.code === 'shell_background_pattern') {
|
|
327
328
|
pushIssue(issues, 'error', 'shell_background_pattern', name, `Shell intent ${name} contains a blocked long-running or background pattern.`);
|
|
328
329
|
}
|
|
329
|
-
const blockedCommandPattern = commandIntentBlockedCommandPattern(value);
|
|
330
330
|
if (blockedCommandPattern?.code === 'long_running_command_pattern') {
|
|
331
331
|
pushIssue(issues, 'error', 'long_running_command_pattern', name, `Intent ${name} contains a blocked long-running or background command pattern.`);
|
|
332
332
|
}
|
package/package.json
CHANGED