mustflow 2.39.1 → 2.58.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cli/commands/run/executor.js +16 -0
- package/dist/cli/commands/run/process-tree.js +6 -3
- package/dist/cli/commands/verify.js +3 -1
- package/dist/cli/lib/git-changes.js +11 -1
- package/dist/cli/lib/i18n.js +1 -1
- package/dist/cli/lib/local-index/index.js +8 -4
- package/dist/cli/lib/local-index/populate.js +17 -3
- package/dist/cli/lib/local-index/search-read-model.js +9 -7
- package/dist/cli/lib/local-index/search-text.js +2 -2
- package/dist/cli/lib/local-index/workflow-documents.js +17 -2
- package/dist/cli/lib/mustflow-read.js +14 -2
- package/dist/cli/lib/repo-map.js +16 -3
- package/dist/cli/lib/templates.js +8 -7
- package/dist/cli/lib/validation/constants.js +1 -1
- package/dist/core/active-run-locks.js +78 -20
- package/dist/core/change-classification.js +4 -0
- package/dist/core/command-contract-rules.js +1 -1
- package/dist/core/command-contract-validation.js +1 -1
- package/dist/core/command-cwd.js +13 -2
- package/dist/core/command-effects.js +22 -4
- package/dist/core/command-env.js +8 -6
- package/dist/core/command-preconditions.js +28 -2
- package/dist/core/completion-verdict.js +1 -1
- package/dist/core/line-endings.js +8 -4
- package/dist/core/safe-filesystem.js +9 -1
- package/dist/core/source-anchor-validation.js +7 -1
- package/dist/core/source-anchors.js +8 -2
- package/dist/core/verification-scheduler.js +8 -2
- package/package.json +1 -1
- package/templates/default/i18n.toml +330 -1
- package/templates/default/locales/en/.mustflow/skills/INDEX.md +302 -5
- package/templates/default/locales/en/.mustflow/skills/agent-eval-integrity-review/SKILL.md +160 -0
- package/templates/default/locales/en/.mustflow/skills/agent-execution-control-review/SKILL.md +163 -0
- package/templates/default/locales/en/.mustflow/skills/ai-generated-code-hardening/SKILL.md +49 -13
- package/templates/default/locales/en/.mustflow/skills/api-access-control-review/SKILL.md +298 -0
- package/templates/default/locales/en/.mustflow/skills/api-misuse-resistance-review/SKILL.md +297 -0
- package/templates/default/locales/en/.mustflow/skills/api-request-performance-review/SKILL.md +189 -0
- package/templates/default/locales/en/.mustflow/skills/app-startup-performance-review/SKILL.md +309 -0
- package/templates/default/locales/en/.mustflow/skills/backend-log-evidence-review/SKILL.md +213 -0
- package/templates/default/locales/en/.mustflow/skills/business-rule-leakage-review/SKILL.md +295 -0
- package/templates/default/locales/en/.mustflow/skills/cache-integrity-review/SKILL.md +291 -0
- package/templates/default/locales/en/.mustflow/skills/change-blast-radius-review/SKILL.md +297 -0
- package/templates/default/locales/en/.mustflow/skills/client-bundle-pruning-review/SKILL.md +160 -0
- package/templates/default/locales/en/.mustflow/skills/cloud-cost-guardrail-review/SKILL.md +321 -0
- package/templates/default/locales/en/.mustflow/skills/concurrency-invariant-review/SKILL.md +193 -0
- package/templates/default/locales/en/.mustflow/skills/core-web-vitals-field-review/SKILL.md +161 -0
- package/templates/default/locales/en/.mustflow/skills/credit-ledger-integrity-review/SKILL.md +156 -0
- package/templates/default/locales/en/.mustflow/skills/database-json-modeling-review/SKILL.md +171 -0
- package/templates/default/locales/en/.mustflow/skills/database-lock-contention-review/SKILL.md +192 -0
- package/templates/default/locales/en/.mustflow/skills/database-migration-change/SKILL.md +76 -34
- package/templates/default/locales/en/.mustflow/skills/database-query-bottleneck-review/SKILL.md +194 -0
- package/templates/default/locales/en/.mustflow/skills/deletion-lifecycle-review/SKILL.md +171 -0
- package/templates/default/locales/en/.mustflow/skills/deployment-rollout-safety-review/SKILL.md +321 -0
- package/templates/default/locales/en/.mustflow/skills/desktop-auto-update-safety-review/SKILL.md +265 -0
- package/templates/default/locales/en/.mustflow/skills/desktop-background-process-stability-review/SKILL.md +318 -0
- package/templates/default/locales/en/.mustflow/skills/desktop-memory-footprint-review/SKILL.md +318 -0
- package/templates/default/locales/en/.mustflow/skills/error-message-integrity-review/SKILL.md +283 -0
- package/templates/default/locales/en/.mustflow/skills/failure-integrity-review/SKILL.md +193 -0
- package/templates/default/locales/en/.mustflow/skills/file-upload-security-review/SKILL.md +305 -0
- package/templates/default/locales/en/.mustflow/skills/frame-render-performance-review/SKILL.md +159 -0
- package/templates/default/locales/en/.mustflow/skills/frontend-accessibility-tree-review/SKILL.md +202 -0
- package/templates/default/locales/en/.mustflow/skills/frontend-localization-review/SKILL.md +202 -0
- package/templates/default/locales/en/.mustflow/skills/frontend-state-ownership-review/SKILL.md +183 -0
- package/templates/default/locales/en/.mustflow/skills/frontend-stress-layout-review/SKILL.md +193 -0
- package/templates/default/locales/en/.mustflow/skills/hot-path-performance-review/SKILL.md +159 -0
- package/templates/default/locales/en/.mustflow/skills/idempotency-integrity-review/SKILL.md +195 -0
- package/templates/default/locales/en/.mustflow/skills/image-delivery-performance-review/SKILL.md +161 -0
- package/templates/default/locales/en/.mustflow/skills/incident-triage-review/SKILL.md +185 -0
- package/templates/default/locales/en/.mustflow/skills/llm-hallucination-control-review/SKILL.md +155 -0
- package/templates/default/locales/en/.mustflow/skills/llm-response-latency-review/SKILL.md +155 -0
- package/templates/default/locales/en/.mustflow/skills/llm-service-ux-review/SKILL.md +2 -0
- package/templates/default/locales/en/.mustflow/skills/llm-token-cost-control-review/SKILL.md +155 -0
- package/templates/default/locales/en/.mustflow/skills/low-end-device-support-review/SKILL.md +340 -0
- package/templates/default/locales/en/.mustflow/skills/memory-lifetime-review/SKILL.md +169 -0
- package/templates/default/locales/en/.mustflow/skills/mobile-energy-efficiency-review/SKILL.md +329 -0
- package/templates/default/locales/en/.mustflow/skills/module-boundary-review/SKILL.md +278 -0
- package/templates/default/locales/en/.mustflow/skills/multi-agent-work-coordination/SKILL.md +1 -0
- package/templates/default/locales/en/.mustflow/skills/observability-debuggability-review/SKILL.md +208 -0
- package/templates/default/locales/en/.mustflow/skills/payment-integrity-review/SKILL.md +155 -0
- package/templates/default/locales/en/.mustflow/skills/prompt-contract-quality-review/SKILL.md +158 -0
- package/templates/default/locales/en/.mustflow/skills/quadratic-scan-review/SKILL.md +155 -0
- package/templates/default/locales/en/.mustflow/skills/queue-processing-integrity-review/SKILL.md +193 -0
- package/templates/default/locales/en/.mustflow/skills/race-condition-review/SKILL.md +188 -0
- package/templates/default/locales/en/.mustflow/skills/rate-limit-integrity-review/SKILL.md +344 -0
- package/templates/default/locales/en/.mustflow/skills/retry-policy-integrity-review/SKILL.md +195 -0
- package/templates/default/locales/en/.mustflow/skills/routes.toml +330 -0
- package/templates/default/locales/en/.mustflow/skills/security-flow-review/SKILL.md +279 -0
- package/templates/default/locales/en/.mustflow/skills/testability-boundary-review/SKILL.md +295 -0
- package/templates/default/locales/en/.mustflow/skills/transaction-boundary-integrity-review/SKILL.md +196 -0
- package/templates/default/locales/en/.mustflow/skills/type-state-modeling-review/SKILL.md +179 -0
- package/templates/default/locales/en/.mustflow/skills/web-render-performance-review/SKILL.md +164 -0
- package/templates/default/manifest.toml +386 -1
|
@@ -3,7 +3,23 @@ import { BoundedOutputBuffer } from '../../../core/bounded-output.js';
|
|
|
3
3
|
import { createPendingTimeoutTermination, forceTerminateProcessTree, getKillMethod, terminateProcessTree, } from './process-tree.js';
|
|
4
4
|
import { createOutputLimitError, isOutputLimitExceededError, writeOutputLimitTerminationMarker, writeStreamChunk, writeStreamChunkPrefix, } from './output.js';
|
|
5
5
|
const TERMINATION_CONFIRMATION_FALLBACK_MS = 1000;
|
|
6
|
+
function createEmptyOutputSnapshot(maxBytes) {
|
|
7
|
+
return new BoundedOutputBuffer(maxBytes).toSnapshot();
|
|
8
|
+
}
|
|
9
|
+
function createInvalidExecutableError() {
|
|
10
|
+
return Object.assign(new Error('Command executable must not be empty'), { code: 'EINVAL' });
|
|
11
|
+
}
|
|
6
12
|
function runSpawnedCommandStreaming(command, cwd, env, timeoutSeconds, killAfterSeconds, maxOutputBytes, stdoutTailBytes, stderrTailBytes, reporter, streamOutput, enforceOutputLimit) {
|
|
13
|
+
if (command.executable.trim().length === 0) {
|
|
14
|
+
return Promise.resolve({
|
|
15
|
+
status: null,
|
|
16
|
+
signal: null,
|
|
17
|
+
error: createInvalidExecutableError(),
|
|
18
|
+
stdout: createEmptyOutputSnapshot(stdoutTailBytes),
|
|
19
|
+
stderr: createEmptyOutputSnapshot(stderrTailBytes),
|
|
20
|
+
termination: null,
|
|
21
|
+
});
|
|
22
|
+
}
|
|
7
23
|
return new Promise((resolve) => {
|
|
8
24
|
const stdout = new BoundedOutputBuffer(stdoutTailBytes);
|
|
9
25
|
const stderr = new BoundedOutputBuffer(stderrTailBytes);
|
|
@@ -1,10 +1,13 @@
|
|
|
1
1
|
import { spawn, spawnSync } from 'node:child_process';
|
|
2
|
+
function windowsTaskkillArgs(pid, signal) {
|
|
3
|
+
return signal === 'SIGKILL' ? ['/PID', String(pid), '/T', '/F'] : ['/PID', String(pid), '/T'];
|
|
4
|
+
}
|
|
2
5
|
function signalProcessTree(pid, signal) {
|
|
3
6
|
if (!pid || pid <= 0) {
|
|
4
7
|
return;
|
|
5
8
|
}
|
|
6
9
|
if (process.platform === 'win32') {
|
|
7
|
-
spawnSync('taskkill',
|
|
10
|
+
spawnSync('taskkill', windowsTaskkillArgs(pid, signal), {
|
|
8
11
|
stdio: 'ignore',
|
|
9
12
|
windowsHide: true,
|
|
10
13
|
});
|
|
@@ -35,7 +38,7 @@ function signalProcessTreeNonBlocking(pid, signal) {
|
|
|
35
38
|
return;
|
|
36
39
|
}
|
|
37
40
|
if (process.platform === 'win32') {
|
|
38
|
-
const killer = spawn('taskkill',
|
|
41
|
+
const killer = spawn('taskkill', windowsTaskkillArgs(pid, signal), {
|
|
39
42
|
stdio: 'ignore',
|
|
40
43
|
windowsHide: true,
|
|
41
44
|
detached: true,
|
|
@@ -76,7 +79,7 @@ export function forceTerminateProcessTreeNonBlocking(pid) {
|
|
|
76
79
|
signalProcessTreeNonBlocking(pid, 'SIGKILL');
|
|
77
80
|
}
|
|
78
81
|
export function getKillMethod() {
|
|
79
|
-
return process.platform === 'win32' ? '
|
|
82
|
+
return process.platform === 'win32' ? 'taskkill_process_tree' : 'process_group_sigterm';
|
|
80
83
|
}
|
|
81
84
|
export function createPendingTimeoutTermination(method, forcedKillAttempted = false) {
|
|
82
85
|
return {
|
|
@@ -206,10 +206,11 @@ async function runVerificationEntriesInParallelChunks(projectRoot, entries, para
|
|
|
206
206
|
const chunk = entries.slice(index, index + parallelism);
|
|
207
207
|
const batchTracker = startRunWriteBatchTracking(projectRoot, createCommandEnv(projectRoot, { policy: 'minimal', allowlist: [] }));
|
|
208
208
|
const chunkResults = await Promise.all(chunk.map((entry) => runVerificationIntent(entry.intent, lang, verificationPlanId, correlationId, scheduledTestTargets.get(entry.intent) ?? [])));
|
|
209
|
+
const chunkResultsByIntent = new Map(chunkResults.map((result) => [result.intent, result]));
|
|
209
210
|
const writeDriftByIntent = finishRunWriteBatchTracking(batchTracker, chunk.map((entry) => ({
|
|
210
211
|
intentName: entry.intent,
|
|
211
212
|
declaredPaths: declaredWritePathsForScheduleEntry(entry),
|
|
212
|
-
observedPaths: observedWriteDriftPaths(
|
|
213
|
+
observedPaths: observedWriteDriftPaths(chunkResultsByIntent.get(entry.intent)),
|
|
213
214
|
})));
|
|
214
215
|
results.push(...chunkResults.map((result) => applyParallelChunkWriteDrift(result, writeDriftByIntent)));
|
|
215
216
|
}
|
|
@@ -493,6 +494,7 @@ function createCompletionVerdictForResults(input) {
|
|
|
493
494
|
failedIntents: input.summary.failed,
|
|
494
495
|
skippedIntents: input.summary.skipped,
|
|
495
496
|
receiptCount: input.results.filter((result) => result.receipt !== null).length,
|
|
497
|
+
gapCount: input.report.gaps.length,
|
|
496
498
|
sourceAnchorRiskCount: input.sourceAnchorRiskCount,
|
|
497
499
|
scopeDiffRiskCount: input.scopeDiffRiskCount,
|
|
498
500
|
repeatedFailureCount: input.repeatedFailureRisks.length,
|
|
@@ -33,7 +33,17 @@ export function readGitChangedFiles(projectRoot) {
|
|
|
33
33
|
stderr,
|
|
34
34
|
};
|
|
35
35
|
}
|
|
36
|
-
|
|
36
|
+
try {
|
|
37
|
+
return { ok: true, files: parseGitStatusOutput(result.stdout) };
|
|
38
|
+
}
|
|
39
|
+
catch (error) {
|
|
40
|
+
return {
|
|
41
|
+
ok: false,
|
|
42
|
+
message: error instanceof Error ? error.message : String(error),
|
|
43
|
+
status: result.status,
|
|
44
|
+
stderr: '',
|
|
45
|
+
};
|
|
46
|
+
}
|
|
37
47
|
}
|
|
38
48
|
export function requireGitChangedFiles(projectRoot) {
|
|
39
49
|
const result = readGitChangedFiles(projectRoot);
|
package/dist/cli/lib/i18n.js
CHANGED
|
@@ -24,7 +24,7 @@ function interpolate(template, params) {
|
|
|
24
24
|
});
|
|
25
25
|
}
|
|
26
26
|
export function t(lang, key, params = {}) {
|
|
27
|
-
return interpolate(MESSAGE_CATALOGS[lang][key] ?? enMessages[key], params);
|
|
27
|
+
return interpolate(MESSAGE_CATALOGS[lang][key] ?? enMessages[key] ?? key, params);
|
|
28
28
|
}
|
|
29
29
|
export function localeMessage(locale, key, params = {}) {
|
|
30
30
|
return t(isCliLang(locale) ? locale : DEFAULT_CLI_LANG, key, params);
|
|
@@ -310,10 +310,14 @@ export async function createLocalIndex(projectRoot, options = {}) {
|
|
|
310
310
|
}
|
|
311
311
|
if (!dryRun && !reusedExisting) {
|
|
312
312
|
const database = new SQL.Database();
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
313
|
+
try {
|
|
314
|
+
createSchema(database, capabilities);
|
|
315
|
+
populateDatabase(database, capabilities, documents, skills, skillRoutes, commandIntents, sourceAnchors, indexedFiles, verificationEvidence, indexMode, sourceScopeHash, includeSource, new Date().toISOString());
|
|
316
|
+
writeFileInsideWithoutSymlinks(projectRoot, databasePath, database.export());
|
|
317
|
+
}
|
|
318
|
+
finally {
|
|
319
|
+
database.close();
|
|
320
|
+
}
|
|
317
321
|
}
|
|
318
322
|
return {
|
|
319
323
|
schema_version: LOCAL_INDEX_SCHEMA_VERSION,
|
|
@@ -46,11 +46,25 @@ function populatePathSurfaceReadModel(database) {
|
|
|
46
46
|
insertPathSurfaceReasons(database, rule.id, 'drift_check', rule.surface.driftChecks);
|
|
47
47
|
}
|
|
48
48
|
}
|
|
49
|
+
function readDocumentTermsByPath(database) {
|
|
50
|
+
const termsByPath = new Map();
|
|
51
|
+
for (const row of queryRows(database, 'SELECT document_path, term FROM document_terms ORDER BY document_path, term')) {
|
|
52
|
+
const documentPath = toSearchString(row.document_path);
|
|
53
|
+
const term = toSearchString(row.term);
|
|
54
|
+
const terms = termsByPath.get(documentPath);
|
|
55
|
+
if (terms) {
|
|
56
|
+
terms.push(term);
|
|
57
|
+
}
|
|
58
|
+
else {
|
|
59
|
+
termsByPath.set(documentPath, [term]);
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
return termsByPath;
|
|
63
|
+
}
|
|
49
64
|
function populateSearchTables(database, capabilities, documents, skills, skillRoutes, commandIntents, sourceAnchors) {
|
|
65
|
+
const documentTermsByPath = readDocumentTermsByPath(database);
|
|
50
66
|
for (const document of documents) {
|
|
51
|
-
const documentTerms =
|
|
52
|
-
document.path,
|
|
53
|
-
]).map((row) => toSearchString(row.term));
|
|
67
|
+
const documentTerms = documentTermsByPath.get(document.path) ?? [];
|
|
54
68
|
insertSearchNgrams(database, 'document', document.path, [
|
|
55
69
|
document.path,
|
|
56
70
|
document.type,
|
|
@@ -299,6 +299,7 @@ function collectBoundedDirectSearchDocuments(projectRoot) {
|
|
|
299
299
|
}
|
|
300
300
|
function searchLocalWorkflowFilesDirectly(projectRoot, databasePath, normalizedQuery, limit, scope) {
|
|
301
301
|
const cacheLayers = readCacheLayerSets(projectRoot);
|
|
302
|
+
const querySnippetNgrams = buildSearchNgrams([normalizedQuery]);
|
|
302
303
|
const results = [];
|
|
303
304
|
if (scope === 'workflow' || scope === 'all') {
|
|
304
305
|
const documents = collectBoundedDirectSearchDocuments(projectRoot);
|
|
@@ -322,7 +323,7 @@ function searchLocalWorkflowFilesDirectly(projectRoot, databasePath, normalizedQ
|
|
|
322
323
|
title: document.title,
|
|
323
324
|
document_type: document.type,
|
|
324
325
|
...workflowAuthorityForDocument(document.type),
|
|
325
|
-
match: getMatchSnippet(fields, normalizedQuery),
|
|
326
|
+
match: getMatchSnippet(fields, normalizedQuery, querySnippetNgrams),
|
|
326
327
|
score: scoreMatch(primaryFields, secondaryFields, normalizedQuery),
|
|
327
328
|
}, cacheLayers));
|
|
328
329
|
}
|
|
@@ -337,7 +338,7 @@ function searchLocalWorkflowFilesDirectly(projectRoot, databasePath, normalizedQ
|
|
|
337
338
|
path: skill.path,
|
|
338
339
|
title: skill.title,
|
|
339
340
|
...skillAuthority(),
|
|
340
|
-
match: getMatchSnippet(fields, normalizedQuery),
|
|
341
|
+
match: getMatchSnippet(fields, normalizedQuery, querySnippetNgrams),
|
|
341
342
|
score: scoreMatch(fields, [], normalizedQuery),
|
|
342
343
|
}, cacheLayers));
|
|
343
344
|
}
|
|
@@ -393,6 +394,7 @@ export async function searchLocalIndex(projectRoot, query, options = {}) {
|
|
|
393
394
|
const stalePaths = getStalePaths(projectRoot, database);
|
|
394
395
|
capabilities = readStoredSearchCapabilities(database);
|
|
395
396
|
const indexedMatches = getIndexedSearchMatches(database, normalizedQuery);
|
|
397
|
+
const querySnippetNgrams = buildSearchNgrams([normalizedQuery]);
|
|
396
398
|
if (stalePaths.length > 0) {
|
|
397
399
|
throw new Error(`Local mustflow index is stale: ${stalePaths.join(', ')}. Run \`mf index\` before searching. Refresh command: mf index`);
|
|
398
400
|
}
|
|
@@ -419,7 +421,7 @@ export async function searchLocalIndex(projectRoot, query, options = {}) {
|
|
|
419
421
|
title,
|
|
420
422
|
document_type: typeValue,
|
|
421
423
|
...workflowAuthorityForDocument(typeValue),
|
|
422
|
-
match: getMatchSnippet(fields, normalizedQuery),
|
|
424
|
+
match: getMatchSnippet(fields, normalizedQuery, querySnippetNgrams),
|
|
423
425
|
score: scoreIndexedOrTableScan(primaryFields, secondaryFields, normalizedQuery, indexedMatches, indexedMatches.documents, pathValue),
|
|
424
426
|
}, cacheLayers));
|
|
425
427
|
}
|
|
@@ -437,7 +439,7 @@ export async function searchLocalIndex(projectRoot, query, options = {}) {
|
|
|
437
439
|
path: pathValue,
|
|
438
440
|
title,
|
|
439
441
|
...skillAuthority(),
|
|
440
|
-
match: getMatchSnippet(fields, normalizedQuery),
|
|
442
|
+
match: getMatchSnippet(fields, normalizedQuery, querySnippetNgrams),
|
|
441
443
|
score: scoreIndexedOrTableScan([name, pathValue, title], [], normalizedQuery, indexedMatches, indexedMatches.skills, name),
|
|
442
444
|
}, cacheLayers));
|
|
443
445
|
}
|
|
@@ -468,7 +470,7 @@ export async function searchLocalIndex(projectRoot, query, options = {}) {
|
|
|
468
470
|
route_risk: risk,
|
|
469
471
|
verification_intents: verificationIntents,
|
|
470
472
|
...skillAuthority(),
|
|
471
|
-
match: getMatchSnippet(fields, normalizedQuery),
|
|
473
|
+
match: getMatchSnippet(fields, normalizedQuery, querySnippetNgrams),
|
|
472
474
|
score: indexedRouteMatch
|
|
473
475
|
? Math.max(scoreMatch(primaryFields, secondaryFields, normalizedQuery), 20)
|
|
474
476
|
: scoreMatch(primaryFields, secondaryFields, normalizedQuery),
|
|
@@ -502,7 +504,7 @@ export async function searchLocalIndex(projectRoot, query, options = {}) {
|
|
|
502
504
|
effect_paths: effectPaths,
|
|
503
505
|
effect_modes: effectModes,
|
|
504
506
|
...commandIntentAuthority(),
|
|
505
|
-
match: getMatchSnippet(fields, normalizedQuery),
|
|
507
|
+
match: getMatchSnippet(fields, normalizedQuery, querySnippetNgrams),
|
|
506
508
|
score: scoreIndexedOrTableScan(primaryFields, secondaryFields, normalizedQuery, indexedMatches, indexedMatches.commandIntents, name),
|
|
507
509
|
}, cacheLayers));
|
|
508
510
|
}
|
|
@@ -532,7 +534,7 @@ export async function searchLocalIndex(projectRoot, query, options = {}) {
|
|
|
532
534
|
...sourceAnchorAuthority(),
|
|
533
535
|
stale_status: toSearchString(row.status),
|
|
534
536
|
stale_confidence: Number(row.confidence),
|
|
535
|
-
match: getMatchSnippet(fields, normalizedQuery),
|
|
537
|
+
match: getMatchSnippet(fields, normalizedQuery, querySnippetNgrams),
|
|
536
538
|
score: scoreIndexedOrTableScan(primaryFields, secondaryFields, normalizedQuery, indexedMatches, indexedMatches.sourceAnchors, id),
|
|
537
539
|
}, cacheLayers));
|
|
538
540
|
}
|
|
@@ -28,13 +28,13 @@ export function buildSearchNgrams(values) {
|
|
|
28
28
|
}
|
|
29
29
|
return [...grams].sort((left, right) => left.localeCompare(right));
|
|
30
30
|
}
|
|
31
|
-
export function getMatchSnippet(fields, query) {
|
|
31
|
+
export function getMatchSnippet(fields, query, queryNgrams = buildSearchNgrams([query])) {
|
|
32
32
|
const normalized = normalizeSearchText(fields.join(' '));
|
|
33
33
|
const lower = normalized.toLowerCase();
|
|
34
34
|
let start = lower.indexOf(query.toLowerCase());
|
|
35
35
|
let matchLength = query.length;
|
|
36
36
|
if (start === -1) {
|
|
37
|
-
const
|
|
37
|
+
const firstGram = queryNgrams.find((gram) => lower.includes(gram));
|
|
38
38
|
if (!firstGram) {
|
|
39
39
|
return truncateSearchMatchSnippet(normalized);
|
|
40
40
|
}
|
|
@@ -65,12 +65,27 @@ function parseFrontmatter(content) {
|
|
|
65
65
|
if (!content.startsWith('---')) {
|
|
66
66
|
return {};
|
|
67
67
|
}
|
|
68
|
-
const
|
|
68
|
+
const firstLineEnd = content.indexOf('\n');
|
|
69
|
+
if (firstLineEnd === -1) {
|
|
70
|
+
return {};
|
|
71
|
+
}
|
|
72
|
+
let end = -1;
|
|
73
|
+
let lineStart = firstLineEnd + 1;
|
|
74
|
+
while (lineStart < content.length) {
|
|
75
|
+
const lineEnd = content.indexOf('\n', lineStart);
|
|
76
|
+
const nextLineStart = lineEnd === -1 ? content.length : lineEnd + 1;
|
|
77
|
+
const line = content.slice(lineStart, lineEnd === -1 ? content.length : lineEnd).replace(/\r$/u, '');
|
|
78
|
+
if (line.trim() === '---') {
|
|
79
|
+
end = lineStart;
|
|
80
|
+
break;
|
|
81
|
+
}
|
|
82
|
+
lineStart = nextLineStart;
|
|
83
|
+
}
|
|
69
84
|
if (end === -1) {
|
|
70
85
|
return {};
|
|
71
86
|
}
|
|
72
87
|
const result = {};
|
|
73
|
-
const rawFrontmatter = content.slice(
|
|
88
|
+
const rawFrontmatter = content.slice(firstLineEnd + 1, end);
|
|
74
89
|
for (const line of rawFrontmatter.split(/\r?\n/)) {
|
|
75
90
|
const separatorIndex = line.indexOf(':');
|
|
76
91
|
if (separatorIndex === -1) {
|
|
@@ -5,7 +5,13 @@ export const MUSTFLOW_TEXT_MAX_BYTES = 1024 * 1024;
|
|
|
5
5
|
export const MUSTFLOW_TOML_MAX_BYTES = 256 * 1024;
|
|
6
6
|
export const MUSTFLOW_JSON_MAX_BYTES = 1024 * 1024;
|
|
7
7
|
export function mustflowProjectPath(projectRoot, relativePath) {
|
|
8
|
-
|
|
8
|
+
const normalizedRoot = path.resolve(projectRoot);
|
|
9
|
+
const resolvedPath = path.resolve(normalizedRoot, ...relativePath.split('/'));
|
|
10
|
+
const relative = path.relative(normalizedRoot, resolvedPath);
|
|
11
|
+
if (relative.startsWith('..') || path.isAbsolute(relative)) {
|
|
12
|
+
throw new Error(`Path escapes mustflow project root: ${relativePath}`);
|
|
13
|
+
}
|
|
14
|
+
return resolvedPath;
|
|
9
15
|
}
|
|
10
16
|
function missingPath(error) {
|
|
11
17
|
return error instanceof Error && 'code' in error && error.code === 'ENOENT';
|
|
@@ -14,7 +20,13 @@ export function readMustflowTextFile(projectRoot, relativePath, options = {}) {
|
|
|
14
20
|
return readUtf8FileInsideWithoutSymlinks(projectRoot, mustflowProjectPath(projectRoot, relativePath), { maxBytes: options.maxBytes ?? MUSTFLOW_TEXT_MAX_BYTES });
|
|
15
21
|
}
|
|
16
22
|
export function readMustflowTextFileResult(projectRoot, relativePath, options = {}) {
|
|
17
|
-
|
|
23
|
+
let filePath;
|
|
24
|
+
try {
|
|
25
|
+
filePath = mustflowProjectPath(projectRoot, relativePath);
|
|
26
|
+
}
|
|
27
|
+
catch (error) {
|
|
28
|
+
return { ok: false, exists: false, error: error instanceof Error ? error.message : String(error) };
|
|
29
|
+
}
|
|
18
30
|
if (!existsSync(filePath)) {
|
|
19
31
|
return { ok: false, exists: false, error: null };
|
|
20
32
|
}
|
package/dist/cli/lib/repo-map.js
CHANGED
|
@@ -193,6 +193,14 @@ const EXACT_ANCHOR_DESCRIPTIONS = new Map([
|
|
|
193
193
|
['.mustflow/docs/agent-workflow.md', 'Shared workflow policy for agent work.'],
|
|
194
194
|
['.mustflow/skills/INDEX.md', 'Index of available procedural skills.'],
|
|
195
195
|
]);
|
|
196
|
+
function spawnGitLsFiles(command, args, options) {
|
|
197
|
+
const result = spawnSync(command, [...args], options);
|
|
198
|
+
return {
|
|
199
|
+
status: result.status,
|
|
200
|
+
error: result.error,
|
|
201
|
+
stdout: result.stdout,
|
|
202
|
+
};
|
|
203
|
+
}
|
|
196
204
|
function getStringArray(value) {
|
|
197
205
|
return Array.isArray(value) ? value.filter((entry) => typeof entry === 'string') : [];
|
|
198
206
|
}
|
|
@@ -250,8 +258,7 @@ function classifyGitLsFilesFailure(result) {
|
|
|
250
258
|
return 'error';
|
|
251
259
|
}
|
|
252
260
|
export function discoverGitFilesForRepoMap(projectRoot, options = {}) {
|
|
253
|
-
const spawnGit = options.spawnGit ??
|
|
254
|
-
((command, args, spawnOptions) => spawnSync(command, [...args], spawnOptions));
|
|
261
|
+
const spawnGit = options.spawnGit ?? spawnGitLsFiles;
|
|
255
262
|
const result = spawnGit('git', ['ls-files', '-z'], {
|
|
256
263
|
cwd: projectRoot,
|
|
257
264
|
encoding: 'utf8',
|
|
@@ -413,7 +420,13 @@ function renderDirectoryAnchors(anchors) {
|
|
|
413
420
|
return lines;
|
|
414
421
|
}
|
|
415
422
|
function hasGitMarker(directoryPath) {
|
|
416
|
-
|
|
423
|
+
try {
|
|
424
|
+
const marker = lstatSync(path.join(directoryPath, '.git'));
|
|
425
|
+
return marker.isDirectory() || marker.isFile();
|
|
426
|
+
}
|
|
427
|
+
catch {
|
|
428
|
+
return false;
|
|
429
|
+
}
|
|
417
430
|
}
|
|
418
431
|
function isRealPathInside(parentRealPath, childRealPath) {
|
|
419
432
|
const relative = path.relative(parentRealPath, childRealPath);
|
|
@@ -304,11 +304,12 @@ export function getTemplateFiles(template, locale = template.manifest.defaultLoc
|
|
|
304
304
|
const commonPath = path.join(commonRoot, ...relativePath.split('/'));
|
|
305
305
|
const localizedPath = localePath && existsSync(localePath) ? localePath : undefined;
|
|
306
306
|
const fallbackLocalePath = sourceLocalePath && existsSync(sourceLocalePath) ? sourceLocalePath : undefined;
|
|
307
|
-
const
|
|
308
|
-
const
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
307
|
+
const commonSourcePath = existsSync(commonPath) ? commonPath : undefined;
|
|
308
|
+
const selectedSourcePath = localizedPath ?? fallbackLocalePath ?? commonSourcePath;
|
|
309
|
+
const content = selectedSourcePath && relativePath === '.mustflow/skills/INDEX.md'
|
|
310
|
+
? filterSkillIndexContent(readFileSync(selectedSourcePath, 'utf8'), selectedSkills)
|
|
311
|
+
: selectedSourcePath && relativePath === '.mustflow/skills/routes.toml'
|
|
312
|
+
? filterSkillRouteMetadataContent(readFileSync(selectedSourcePath, 'utf8'), selectedSkills)
|
|
312
313
|
: undefined;
|
|
313
314
|
if (localizedPath) {
|
|
314
315
|
return {
|
|
@@ -326,10 +327,10 @@ export function getTemplateFiles(template, locale = template.manifest.defaultLoc
|
|
|
326
327
|
content,
|
|
327
328
|
};
|
|
328
329
|
}
|
|
329
|
-
if (
|
|
330
|
+
if (commonSourcePath) {
|
|
330
331
|
return {
|
|
331
332
|
relativePath,
|
|
332
|
-
sourcePath:
|
|
333
|
+
sourcePath: commonSourcePath,
|
|
333
334
|
sourceKind: template.manifest.localesRoot ? 'common' : 'legacy',
|
|
334
335
|
content,
|
|
335
336
|
};
|
|
@@ -172,7 +172,7 @@ export const ALLOWED_TRANSLATION_POLICIES = new Set([
|
|
|
172
172
|
'update_source_mark_targets_stale',
|
|
173
173
|
'machine_translate_requires_review',
|
|
174
174
|
]);
|
|
175
|
-
export const RAW_COMMAND_FENCE_PATTERN = /```(?:sh|bash|shell|zsh|powershell|ps1|cmd)\s+[\s\S]*?```/
|
|
175
|
+
export const RAW_COMMAND_FENCE_PATTERN = /```(?:sh|bash|shell|zsh|powershell|ps1|cmd)\s+[\s\S]*?```/iu;
|
|
176
176
|
export const SKILL_COMMAND_PERMISSION_CLAIM_PATTERNS = [
|
|
177
177
|
/\b(?:this\s+skill|skill\s+documents?|skills?)\s+(?:authorizes?|grants?|allows?|permits?)\s+(?:agents?\s+)?(?:to\s+)?(?:run|execute)\b/iu,
|
|
178
178
|
/\b(?:agents?\s+)?(?:may|can|are\s+allowed\s+to|is\s+allowed\s+to|is\s+permitted\s+to|have\s+permission\s+to)\s+(?:run|execute)\s+(?:raw\s+)?(?:shell\s+)?commands?\b/iu,
|
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
import { createHash } from 'node:crypto';
|
|
2
|
-
import { existsSync, mkdirSync,
|
|
2
|
+
import { existsSync, mkdirSync, readdirSync, renameSync, rmSync, statSync, } from 'node:fs';
|
|
3
3
|
import path from 'node:path';
|
|
4
4
|
import { commandEffectsConflict, normalizeCommandEffects, } from './command-effects.js';
|
|
5
|
+
import { readUtf8FileInsideWithoutSymlinks, writeJsonFileInsideWithoutSymlinks, } from './safe-filesystem.js';
|
|
5
6
|
const ACTIVE_LOCK_SCHEMA_VERSION = '1';
|
|
6
7
|
const ACTIVE_LOCK_KIND = 'active_run_lock';
|
|
7
8
|
const LOCK_ROOT_RELATIVE_PATH = '.mustflow/state/locks';
|
|
@@ -9,9 +10,19 @@ const LOCK_MUTEX_STALE_MS = 30_000;
|
|
|
9
10
|
const LOCK_MUTEX_WAIT_MS = 1_000;
|
|
10
11
|
const LOCK_MUTEX_SLEEP_MS = 25;
|
|
11
12
|
const LOCK_MUTEX_SLEEP_BUFFER = new Int32Array(new SharedArrayBuffer(4));
|
|
12
|
-
const LOCK_MUTEX_RECOVERY_DIRECTORY = 'recovery';
|
|
13
|
+
const LOCK_MUTEX_RECOVERY_DIRECTORY = 'mutex.recovery';
|
|
14
|
+
const ACTIVE_LOCK_RECORD_MAX_BYTES = 256 * 1024;
|
|
15
|
+
const ACTIVE_LOCK_OWNER_MAX_BYTES = 16 * 1024;
|
|
13
16
|
function sleep(milliseconds) {
|
|
14
|
-
|
|
17
|
+
try {
|
|
18
|
+
Atomics.wait(LOCK_MUTEX_SLEEP_BUFFER, 0, 0, milliseconds);
|
|
19
|
+
}
|
|
20
|
+
catch {
|
|
21
|
+
const end = Date.now() + milliseconds;
|
|
22
|
+
while (Date.now() < end) {
|
|
23
|
+
// Fall back only for short mutex retry delays when Atomics.wait is unavailable on this runtime.
|
|
24
|
+
}
|
|
25
|
+
}
|
|
15
26
|
}
|
|
16
27
|
function sha256(value) {
|
|
17
28
|
return createHash('sha256').update(value).digest('hex');
|
|
@@ -26,7 +37,7 @@ function activeLockMutexDirectory(projectRoot) {
|
|
|
26
37
|
return path.join(activeLockRoot(projectRoot), 'mutex');
|
|
27
38
|
}
|
|
28
39
|
function activeLockMutexRecoveryDirectory(mutex) {
|
|
29
|
-
return path.join(mutex, LOCK_MUTEX_RECOVERY_DIRECTORY);
|
|
40
|
+
return path.join(path.dirname(mutex), LOCK_MUTEX_RECOVERY_DIRECTORY);
|
|
30
41
|
}
|
|
31
42
|
function normalizeEffect(effect) {
|
|
32
43
|
return {
|
|
@@ -121,9 +132,16 @@ function readActiveRecords(projectRoot) {
|
|
|
121
132
|
return [];
|
|
122
133
|
}
|
|
123
134
|
const records = [];
|
|
124
|
-
|
|
135
|
+
const entries = readdirSync(directory, { withFileTypes: true })
|
|
136
|
+
.filter((entry) => entry.isFile() && entry.name.endsWith('.json'))
|
|
137
|
+
.map((entry) => entry.name)
|
|
138
|
+
.sort();
|
|
139
|
+
for (const name of entries) {
|
|
125
140
|
try {
|
|
126
|
-
const
|
|
141
|
+
const recordPath = path.join(directory, name);
|
|
142
|
+
const record = parseRecord(JSON.parse(readUtf8FileInsideWithoutSymlinks(directory, recordPath, {
|
|
143
|
+
maxBytes: ACTIVE_LOCK_RECORD_MAX_BYTES,
|
|
144
|
+
})));
|
|
127
145
|
if (record) {
|
|
128
146
|
records.push(record);
|
|
129
147
|
}
|
|
@@ -153,9 +171,19 @@ function conflictDetail(current, active) {
|
|
|
153
171
|
}
|
|
154
172
|
function findConflicts(intentName, effects, records) {
|
|
155
173
|
const conflicts = [];
|
|
174
|
+
const effectsByLock = new Map();
|
|
175
|
+
for (const effect of effects) {
|
|
176
|
+
const existing = effectsByLock.get(effect.lock);
|
|
177
|
+
if (existing) {
|
|
178
|
+
existing.push(effect);
|
|
179
|
+
}
|
|
180
|
+
else {
|
|
181
|
+
effectsByLock.set(effect.lock, [effect]);
|
|
182
|
+
}
|
|
183
|
+
}
|
|
156
184
|
for (const record of records) {
|
|
157
185
|
for (const activeEffect of commandEffectsFromRecord(record)) {
|
|
158
|
-
for (const effect of
|
|
186
|
+
for (const effect of effectsByLock.get(activeEffect.lock) ?? []) {
|
|
159
187
|
if (!commandEffectsConflict(effect, activeEffect)) {
|
|
160
188
|
continue;
|
|
161
189
|
}
|
|
@@ -198,7 +226,9 @@ function createRecord(projectRoot, intentName, effects, commandHash) {
|
|
|
198
226
|
}
|
|
199
227
|
function readMutexOwner(ownerPath) {
|
|
200
228
|
try {
|
|
201
|
-
const owner = JSON.parse(
|
|
229
|
+
const owner = JSON.parse(readUtf8FileInsideWithoutSymlinks(path.dirname(ownerPath), ownerPath, {
|
|
230
|
+
maxBytes: ACTIVE_LOCK_OWNER_MAX_BYTES,
|
|
231
|
+
}));
|
|
202
232
|
if (typeof owner.started_at !== 'string' || typeof owner.token !== 'string') {
|
|
203
233
|
return null;
|
|
204
234
|
}
|
|
@@ -249,6 +279,24 @@ function beginMutexRecovery(mutex) {
|
|
|
249
279
|
rmSync(recoveryPath, { recursive: true, force: true });
|
|
250
280
|
};
|
|
251
281
|
}
|
|
282
|
+
function moveMutexAsideForRecovery(mutex, owner) {
|
|
283
|
+
const tokenSource = owner ? `${owner.pid}:${owner.startedAt}:${owner.token}` : `${process.pid}:${Date.now()}:${process.hrtime.bigint()}`;
|
|
284
|
+
const stalePath = path.join(path.dirname(mutex), `mutex.stale-${sha256(tokenSource)}`);
|
|
285
|
+
try {
|
|
286
|
+
rmSync(stalePath, { recursive: true, force: true });
|
|
287
|
+
renameSync(mutex, stalePath);
|
|
288
|
+
return stalePath;
|
|
289
|
+
}
|
|
290
|
+
catch {
|
|
291
|
+
try {
|
|
292
|
+
rmSync(stalePath, { recursive: true, force: true });
|
|
293
|
+
}
|
|
294
|
+
catch {
|
|
295
|
+
// Best-effort cleanup for a failed stale mutex move.
|
|
296
|
+
}
|
|
297
|
+
return null;
|
|
298
|
+
}
|
|
299
|
+
}
|
|
252
300
|
function recoverStaleMutexWithOwner(mutex, ownerPath, staleOwner) {
|
|
253
301
|
const releaseRecovery = beginMutexRecovery(mutex);
|
|
254
302
|
if (!releaseRecovery) {
|
|
@@ -258,7 +306,11 @@ function recoverStaleMutexWithOwner(mutex, ownerPath, staleOwner) {
|
|
|
258
306
|
if (!sameMutexOwner(staleOwner, readMutexOwner(ownerPath)) || !mutexOwnerIsStale(staleOwner)) {
|
|
259
307
|
return false;
|
|
260
308
|
}
|
|
261
|
-
|
|
309
|
+
const stalePath = moveMutexAsideForRecovery(mutex, staleOwner);
|
|
310
|
+
if (!stalePath) {
|
|
311
|
+
return false;
|
|
312
|
+
}
|
|
313
|
+
rmSync(stalePath, { recursive: true, force: true });
|
|
262
314
|
return true;
|
|
263
315
|
}
|
|
264
316
|
finally {
|
|
@@ -275,7 +327,11 @@ function recoverStaleMutexWithoutOwner(mutex) {
|
|
|
275
327
|
if (readMutexOwner(ownerPath) !== null) {
|
|
276
328
|
return false;
|
|
277
329
|
}
|
|
278
|
-
|
|
330
|
+
const stalePath = moveMutexAsideForRecovery(mutex, null);
|
|
331
|
+
if (!stalePath) {
|
|
332
|
+
return false;
|
|
333
|
+
}
|
|
334
|
+
rmSync(stalePath, { recursive: true, force: true });
|
|
279
335
|
return true;
|
|
280
336
|
}
|
|
281
337
|
catch {
|
|
@@ -291,13 +347,13 @@ function acquireMutex(projectRoot) {
|
|
|
291
347
|
const ownerPath = path.join(mutex, 'owner.json');
|
|
292
348
|
const ownerToken = sha256(`${process.pid}:${Date.now()}:${process.hrtime.bigint()}`);
|
|
293
349
|
mkdirSync(root, { recursive: true });
|
|
294
|
-
|
|
350
|
+
const startedAt = Date.now();
|
|
295
351
|
while (true) {
|
|
296
352
|
try {
|
|
297
353
|
mkdirSync(mutex);
|
|
298
354
|
const ownerRecord = { pid: process.pid, started_at: new Date().toISOString(), token: ownerToken };
|
|
299
355
|
try {
|
|
300
|
-
|
|
356
|
+
writeJsonFileInsideWithoutSymlinks(root, ownerPath, ownerRecord);
|
|
301
357
|
}
|
|
302
358
|
catch (error) {
|
|
303
359
|
rmSync(mutex, { recursive: true, force: true });
|
|
@@ -305,7 +361,9 @@ function acquireMutex(projectRoot) {
|
|
|
305
361
|
}
|
|
306
362
|
return () => {
|
|
307
363
|
try {
|
|
308
|
-
const owner = JSON.parse(
|
|
364
|
+
const owner = JSON.parse(readUtf8FileInsideWithoutSymlinks(path.dirname(ownerPath), ownerPath, {
|
|
365
|
+
maxBytes: ACTIVE_LOCK_OWNER_MAX_BYTES,
|
|
366
|
+
}));
|
|
309
367
|
if (Number(owner.pid) === ownerRecord.pid && owner.token === ownerRecord.token) {
|
|
310
368
|
rmSync(mutex, { recursive: true, force: true });
|
|
311
369
|
}
|
|
@@ -323,7 +381,6 @@ function acquireMutex(projectRoot) {
|
|
|
323
381
|
const owner = readMutexOwner(ownerPath);
|
|
324
382
|
if (owner) {
|
|
325
383
|
if (mutexOwnerIsStale(owner) && recoverStaleMutexWithOwner(mutex, ownerPath, owner)) {
|
|
326
|
-
startedAt = Date.now();
|
|
327
384
|
continue;
|
|
328
385
|
}
|
|
329
386
|
}
|
|
@@ -343,12 +400,10 @@ function acquireMutex(projectRoot) {
|
|
|
343
400
|
try {
|
|
344
401
|
const mutexStat = statSync(mutex);
|
|
345
402
|
if (Date.now() - mutexStat.mtimeMs > LOCK_MUTEX_STALE_MS && recoverStaleMutexWithoutOwner(mutex)) {
|
|
346
|
-
startedAt = Date.now();
|
|
347
403
|
continue;
|
|
348
404
|
}
|
|
349
405
|
}
|
|
350
406
|
catch {
|
|
351
|
-
startedAt = Date.now();
|
|
352
407
|
continue;
|
|
353
408
|
}
|
|
354
409
|
}
|
|
@@ -362,7 +417,8 @@ export function inspectActiveRunLocks(projectRoot, contract, intentName) {
|
|
|
362
417
|
const effects = normalizeCommandEffects(projectRoot, contract, intentName);
|
|
363
418
|
const records = readActiveRecords(projectRoot);
|
|
364
419
|
const staleRecords = records.map(staleRecordFor).filter((record) => record !== null);
|
|
365
|
-
const
|
|
420
|
+
const staleRecordIds = new Set(staleRecords.map((stale) => stale.runId));
|
|
421
|
+
const liveRecords = records.filter((record) => !staleRecordIds.has(record.run_id));
|
|
366
422
|
return {
|
|
367
423
|
conflicts: findConflicts(intentName, effects, liveRecords),
|
|
368
424
|
staleRecords,
|
|
@@ -371,7 +427,8 @@ export function inspectActiveRunLocks(projectRoot, contract, intentName) {
|
|
|
371
427
|
export function listActiveRunLocks(projectRoot) {
|
|
372
428
|
const records = readActiveRecords(projectRoot);
|
|
373
429
|
const staleRecords = records.map(staleRecordFor).filter((record) => record !== null);
|
|
374
|
-
const
|
|
430
|
+
const staleRecordIds = new Set(staleRecords.map((stale) => stale.runId));
|
|
431
|
+
const activeRecords = records.filter((record) => !staleRecordIds.has(record.run_id));
|
|
375
432
|
return {
|
|
376
433
|
records,
|
|
377
434
|
activeRecords,
|
|
@@ -404,14 +461,15 @@ export function acquireActiveRunLock(projectRoot, contract, intentName, options
|
|
|
404
461
|
removeRecord(projectRoot, staleRecord);
|
|
405
462
|
}
|
|
406
463
|
}
|
|
407
|
-
const
|
|
464
|
+
const staleRecordIds = new Set(staleRecords.map((stale) => stale.runId));
|
|
465
|
+
const liveRecords = records.filter((record) => !staleRecordIds.has(record.run_id));
|
|
408
466
|
const conflicts = findConflicts(intentName, effects, liveRecords);
|
|
409
467
|
if (conflicts.length > 0) {
|
|
410
468
|
return { ok: false, conflicts, recoveredStaleRecords: staleRecords };
|
|
411
469
|
}
|
|
412
470
|
const record = createRecord(projectRoot, intentName, effects, options.commandHash ?? null);
|
|
413
471
|
const recordPath = activeLockRecordPath(projectRoot, record.run_id);
|
|
414
|
-
|
|
472
|
+
writeJsonFileInsideWithoutSymlinks(activeLockDirectory(projectRoot), recordPath, record);
|
|
415
473
|
let released = false;
|
|
416
474
|
return {
|
|
417
475
|
ok: true,
|
|
@@ -41,6 +41,10 @@ function parseGitPorcelainStatusOutput(output) {
|
|
|
41
41
|
paths.push(filePath);
|
|
42
42
|
}
|
|
43
43
|
if (status.includes('R') || status.includes('C')) {
|
|
44
|
+
const targetPath = normalizePorcelainStatusPath(parts[index + 1] ?? '');
|
|
45
|
+
if (targetPath.length > 0) {
|
|
46
|
+
paths.push(targetPath);
|
|
47
|
+
}
|
|
44
48
|
index += 1;
|
|
45
49
|
}
|
|
46
50
|
}
|
|
@@ -41,7 +41,7 @@ export const LONG_RUNNING_COMMAND_TEXT_PATTERNS = [
|
|
|
41
41
|
/\bturbo\s+dev\b/iu,
|
|
42
42
|
/\btsx\s+(?:watch|--watch|-w)\b/iu,
|
|
43
43
|
/\bcargo\s+(?:watch|tauri\s+dev)\b/iu,
|
|
44
|
-
/\bzig\s+build\b(
|
|
44
|
+
/\bzig\s+build\b(?=[^\r\n;&|]*?(?:--watch(?:=|\b)|-w\b|\bwatch\b))/iu,
|
|
45
45
|
/\btauri\s+dev\b/iu,
|
|
46
46
|
/\bgh\s+(?:run\s+watch|codespace\s+ssh|codespace\s+logs\b.*(?:--follow|-f))\b/iu,
|
|
47
47
|
/\bdeno\s+(?:serve|task\s+(?:dev|start|serve|watch|preview)|run\b.*(?:--watch|-w))\b/iu,
|