mustflow 2.39.1 → 2.58.1
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/tech.js +60 -4
- 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/npm-version-check.js +36 -0
- 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
|
@@ -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,
|
|
@@ -546,6 +546,6 @@ export function validateCommandContractStrictDefaults(projectRoot, commandsToml)
|
|
|
546
546
|
issues.push(...validateCommandEnvInheritanceWarnings(commandsToml));
|
|
547
547
|
issues.push(...validateProjectLocalBinWarnings(projectRoot, commandsToml));
|
|
548
548
|
issues.push(...validateCommandEffects(projectRoot, commandsToml));
|
|
549
|
-
issues.push(...validateCommandEffectLockWarnings(commandsToml));
|
|
549
|
+
issues.push(...validateCommandEffectLockWarnings(projectRoot, commandsToml));
|
|
550
550
|
return issues;
|
|
551
551
|
}
|
package/dist/core/command-cwd.js
CHANGED
|
@@ -13,10 +13,21 @@ export function resolveSafeProjectCwd(projectRoot, rawCwd) {
|
|
|
13
13
|
const cwd = rawCwd ?? '.';
|
|
14
14
|
const resolved = path.resolve(projectRoot, cwd);
|
|
15
15
|
const rootRealPath = realpathSync.native(projectRoot);
|
|
16
|
-
|
|
16
|
+
try {
|
|
17
|
+
if (!existsSync(resolved) || !statSync(resolved).isDirectory()) {
|
|
18
|
+
throw new Error();
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
catch {
|
|
22
|
+
throw new Error(`Intent cwd must stay inside the current root and resolve to an existing directory: ${cwd}`);
|
|
23
|
+
}
|
|
24
|
+
let cwdRealPath;
|
|
25
|
+
try {
|
|
26
|
+
cwdRealPath = realpathSync.native(resolved);
|
|
27
|
+
}
|
|
28
|
+
catch {
|
|
17
29
|
throw new Error(`Intent cwd must stay inside the current root and resolve to an existing directory: ${cwd}`);
|
|
18
30
|
}
|
|
19
|
-
const cwdRealPath = realpathSync.native(resolved);
|
|
20
31
|
if (!isInsideOrEqual(rootRealPath, cwdRealPath)) {
|
|
21
32
|
throw new Error(`Intent cwd must stay inside the current root and resolve to an existing directory: ${cwd}`);
|
|
22
33
|
}
|
|
@@ -161,11 +161,16 @@ export function validateCommandEffects(projectRoot, commandsToml) {
|
|
|
161
161
|
}
|
|
162
162
|
return issues;
|
|
163
163
|
}
|
|
164
|
-
export function validateCommandEffectLockWarnings(commandsToml) {
|
|
164
|
+
export function validateCommandEffectLockWarnings(projectRoot, commandsToml) {
|
|
165
165
|
const issues = [];
|
|
166
166
|
if (!commandsToml || !isRecord(commandsToml.intents)) {
|
|
167
167
|
return issues;
|
|
168
168
|
}
|
|
169
|
+
const commandContract = {
|
|
170
|
+
defaults: isRecord(commandsToml.defaults) ? commandsToml.defaults : {},
|
|
171
|
+
intents: commandsToml.intents,
|
|
172
|
+
resources: isRecord(commandsToml.resources) ? commandsToml.resources : {},
|
|
173
|
+
};
|
|
169
174
|
const lockToIntents = new Map();
|
|
170
175
|
for (const [intentName, intent] of Object.entries(commandsToml.intents)) {
|
|
171
176
|
if (!isRecord(intent)) {
|
|
@@ -177,9 +182,22 @@ export function validateCommandEffectLockWarnings(commandsToml) {
|
|
|
177
182
|
Array.isArray(intent.effects)) {
|
|
178
183
|
continue;
|
|
179
184
|
}
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
185
|
+
let effects;
|
|
186
|
+
try {
|
|
187
|
+
effects = normalizeCommandEffects(projectRoot, commandContract, intentName).filter((effect) => effect.source === 'writes');
|
|
188
|
+
}
|
|
189
|
+
catch {
|
|
190
|
+
continue;
|
|
191
|
+
}
|
|
192
|
+
for (const effect of effects) {
|
|
193
|
+
const lock = effect.lock;
|
|
194
|
+
const intents = lockToIntents.get(lock);
|
|
195
|
+
if (intents) {
|
|
196
|
+
intents.push(intentName);
|
|
197
|
+
}
|
|
198
|
+
else {
|
|
199
|
+
lockToIntents.set(lock, [intentName]);
|
|
200
|
+
}
|
|
183
201
|
}
|
|
184
202
|
}
|
|
185
203
|
for (const [lock, intents] of lockToIntents) {
|
package/dist/core/command-env.js
CHANGED
|
@@ -105,14 +105,16 @@ function removeProjectLocalBinFromPath(env, projectRoot) {
|
|
|
105
105
|
const pathKey = getPathEnvKey(env);
|
|
106
106
|
const currentPath = env[pathKey];
|
|
107
107
|
if (!currentPath) {
|
|
108
|
-
return env;
|
|
108
|
+
return { ...env };
|
|
109
109
|
}
|
|
110
110
|
const localBinPath = path.join(projectRoot, 'node_modules', '.bin');
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
111
|
+
return {
|
|
112
|
+
...env,
|
|
113
|
+
[pathKey]: currentPath
|
|
114
|
+
.split(path.delimiter)
|
|
115
|
+
.filter((entry) => entry.length > 0 && !sameResolvedPath(entry, localBinPath))
|
|
116
|
+
.join(path.delimiter),
|
|
117
|
+
};
|
|
116
118
|
}
|
|
117
119
|
export function resolveCommandEnv(contract, intent) {
|
|
118
120
|
const policy = readEnvPolicy(intent) ?? readEnvPolicy(contract.defaults) ?? DEFAULT_COMMAND_ENV_POLICY;
|
|
@@ -84,6 +84,10 @@ function globToRegExp(pattern) {
|
|
|
84
84
|
expression += '[^/]*';
|
|
85
85
|
continue;
|
|
86
86
|
}
|
|
87
|
+
if (character === '?') {
|
|
88
|
+
expression += '[^/]';
|
|
89
|
+
continue;
|
|
90
|
+
}
|
|
87
91
|
expression += escapeRegExp(character);
|
|
88
92
|
}
|
|
89
93
|
return new RegExp(`${expression}$`, 'u');
|
|
@@ -219,10 +223,32 @@ function evaluateArtifactFreshness(projectRoot, declaration, satisfyIntent, cont
|
|
|
219
223
|
satisfyIntent,
|
|
220
224
|
};
|
|
221
225
|
}
|
|
222
|
-
|
|
226
|
+
let artifactMtime;
|
|
227
|
+
try {
|
|
228
|
+
artifactMtime = statSync(artifactPath).mtimeMs;
|
|
229
|
+
}
|
|
230
|
+
catch {
|
|
231
|
+
return {
|
|
232
|
+
kind: declaration.kind,
|
|
233
|
+
label: declaration.label,
|
|
234
|
+
status: 'missing',
|
|
235
|
+
detail: `artifact "${artifact}" is missing.`,
|
|
236
|
+
path: null,
|
|
237
|
+
artifact,
|
|
238
|
+
sources: declaration.sources,
|
|
239
|
+
newestSource: null,
|
|
240
|
+
satisfyIntent,
|
|
241
|
+
};
|
|
242
|
+
}
|
|
223
243
|
let newest = null;
|
|
224
244
|
for (const source of sourceFiles) {
|
|
225
|
-
|
|
245
|
+
let mtime;
|
|
246
|
+
try {
|
|
247
|
+
mtime = statSync(path.join(projectRoot, ...source.split('/'))).mtimeMs;
|
|
248
|
+
}
|
|
249
|
+
catch {
|
|
250
|
+
continue;
|
|
251
|
+
}
|
|
226
252
|
if (!newest || mtime > newest.mtime) {
|
|
227
253
|
newest = { source, mtime };
|
|
228
254
|
}
|
|
@@ -219,7 +219,7 @@ export function createVerifyCompletionVerdict(input) {
|
|
|
219
219
|
failed_intents: normalizedInput.failedIntents,
|
|
220
220
|
skipped_intents: normalizedInput.skippedIntents,
|
|
221
221
|
receipt_count: normalizedInput.receiptCount,
|
|
222
|
-
gap_count: normalizedInput.
|
|
222
|
+
gap_count: normalizedInput.gapCount ?? 0,
|
|
223
223
|
source_anchor_risk_count: normalizedInput.sourceAnchorRiskCount ?? 0,
|
|
224
224
|
scope_diff_risk_count: normalizedInput.scopeDiffRiskCount ?? 0,
|
|
225
225
|
repeated_failure_count: normalizedInput.repeatedFailureCount ?? 0,
|
|
@@ -21,6 +21,7 @@ function gitList(projectRoot, args) {
|
|
|
21
21
|
encoding: 'buffer',
|
|
22
22
|
env: createCommandEnv(projectRoot, { policy: 'minimal', allowlist: [] }),
|
|
23
23
|
stdio: ['ignore', 'pipe', 'pipe'],
|
|
24
|
+
timeout: 30_000,
|
|
24
25
|
windowsHide: true,
|
|
25
26
|
});
|
|
26
27
|
if (result.status !== 0) {
|
|
@@ -71,19 +72,22 @@ function detectLineEnding(buffer) {
|
|
|
71
72
|
return crlfCount === lfCount ? 'crlf' : 'mixed';
|
|
72
73
|
}
|
|
73
74
|
function normalizeLf(buffer) {
|
|
74
|
-
const bytes =
|
|
75
|
+
const bytes = Buffer.allocUnsafe(buffer.length);
|
|
76
|
+
let writeIndex = 0;
|
|
75
77
|
for (let index = 0; index < buffer.length; index += 1) {
|
|
76
78
|
const byte = buffer[index];
|
|
77
79
|
if (byte === 0x0d) {
|
|
78
|
-
bytes
|
|
80
|
+
bytes[writeIndex] = 0x0a;
|
|
81
|
+
writeIndex += 1;
|
|
79
82
|
if (buffer[index + 1] === 0x0a) {
|
|
80
83
|
index += 1;
|
|
81
84
|
}
|
|
82
85
|
continue;
|
|
83
86
|
}
|
|
84
|
-
bytes
|
|
87
|
+
bytes[writeIndex] = byte;
|
|
88
|
+
writeIndex += 1;
|
|
85
89
|
}
|
|
86
|
-
return
|
|
90
|
+
return bytes.subarray(0, writeIndex);
|
|
87
91
|
}
|
|
88
92
|
export function inspectLineEndings(projectRoot, mode, options = {}) {
|
|
89
93
|
const root = path.resolve(projectRoot);
|
|
@@ -14,7 +14,15 @@ function tempFilePath(targetPath) {
|
|
|
14
14
|
return path.join(path.dirname(targetPath), `.${path.basename(targetPath)}.${suffix}.tmp`);
|
|
15
15
|
}
|
|
16
16
|
function sleep(milliseconds) {
|
|
17
|
-
|
|
17
|
+
try {
|
|
18
|
+
Atomics.wait(WRITE_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 retry delays when Atomics.wait is unavailable on this runtime.
|
|
24
|
+
}
|
|
25
|
+
}
|
|
18
26
|
}
|
|
19
27
|
function isRetryableWindowsRenameError(error) {
|
|
20
28
|
if (process.platform !== 'win32' || !error || typeof error !== 'object' || !('code' in error)) {
|
|
@@ -131,7 +131,13 @@ export function validateSourceAnchorsInProject(projectRoot) {
|
|
|
131
131
|
const anchorsById = new Map();
|
|
132
132
|
for (const relativePath of sourceFiles) {
|
|
133
133
|
const absolutePath = path.join(projectRoot, ...relativePath.split('/'));
|
|
134
|
-
|
|
134
|
+
let content;
|
|
135
|
+
try {
|
|
136
|
+
content = readFileSync(absolutePath, 'utf8');
|
|
137
|
+
}
|
|
138
|
+
catch {
|
|
139
|
+
continue;
|
|
140
|
+
}
|
|
135
141
|
const anchors = parseSourceAnchorsInContent(relativePath, content);
|
|
136
142
|
issues.push(...validateSourceAnchorDensity(relativePath, content, anchors));
|
|
137
143
|
for (const anchor of anchors) {
|
|
@@ -321,10 +321,16 @@ export function collectSourceAnchorSummaries(projectRoot) {
|
|
|
321
321
|
const anchors = [];
|
|
322
322
|
for (const relativePath of listSourceAnchorFiles(projectRoot)) {
|
|
323
323
|
const filePath = path.join(projectRoot, ...relativePath.split('/'));
|
|
324
|
-
|
|
324
|
+
let content;
|
|
325
|
+
try {
|
|
326
|
+
if (!existsSync(filePath) || !statSync(filePath).isFile()) {
|
|
327
|
+
continue;
|
|
328
|
+
}
|
|
329
|
+
content = readFileSync(filePath, 'utf8');
|
|
330
|
+
}
|
|
331
|
+
catch {
|
|
325
332
|
continue;
|
|
326
333
|
}
|
|
327
|
-
const content = readFileSync(filePath, 'utf8');
|
|
328
334
|
for (const anchor of parseSourceAnchorsInContent(relativePath, content)) {
|
|
329
335
|
if (!anchor.idValid || sourceAnchorTextContainsSecretLike(anchor.rawText)) {
|
|
330
336
|
continue;
|
|
@@ -19,12 +19,18 @@ function toScheduleEffect(effect) {
|
|
|
19
19
|
function isObject(value) {
|
|
20
20
|
return !!value && typeof value === 'object';
|
|
21
21
|
}
|
|
22
|
+
function isMissingPathError(error) {
|
|
23
|
+
return error instanceof Error && 'code' in error && error.code === 'ENOENT';
|
|
24
|
+
}
|
|
22
25
|
function readJsonFile(projectRoot, filePath) {
|
|
23
26
|
try {
|
|
24
27
|
return JSON.parse(readUtf8FileInsideWithoutSymlinks(projectRoot, filePath, { maxBytes: MUSTFLOW_JSON_MAX_BYTES }));
|
|
25
28
|
}
|
|
26
|
-
catch {
|
|
27
|
-
|
|
29
|
+
catch (error) {
|
|
30
|
+
if (isMissingPathError(error)) {
|
|
31
|
+
return null;
|
|
32
|
+
}
|
|
33
|
+
throw error;
|
|
28
34
|
}
|
|
29
35
|
}
|
|
30
36
|
function getUndeclaredWriteIntent(value) {
|