@smartmemory/compose 0.2.10-beta → 0.2.12-beta
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/lib/build.js +98 -3
- package/lib/stratum-mcp-client.js +3 -1
- package/package.json +1 -1
package/lib/build.js
CHANGED
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
* Gates resolved via CLI readline prompt.
|
|
9
9
|
*/
|
|
10
10
|
|
|
11
|
-
import { readFileSync, writeFileSync, existsSync, mkdirSync, unlinkSync, renameSync, mkdtempSync, rmSync } from 'node:fs';
|
|
11
|
+
import { readFileSync, writeFileSync, existsSync, mkdirSync, unlinkSync, renameSync, mkdtempSync, rmSync, symlinkSync } from 'node:fs';
|
|
12
12
|
import { join, resolve, dirname } from 'node:path';
|
|
13
13
|
import { fileURLToPath } from 'node:url';
|
|
14
14
|
import { homedir, tmpdir } from 'node:os';
|
|
@@ -3323,6 +3323,67 @@ export function buildMergeConflictBounce(taskId, error, files) {
|
|
|
3323
3323
|
};
|
|
3324
3324
|
}
|
|
3325
3325
|
|
|
3326
|
+
/**
|
|
3327
|
+
* COMP-PAR-MERGE-QUEUE-CONSUMER: best-effort list of files changed in a worktree
|
|
3328
|
+
* (tracked + untracked), for a gate_failed bounce's `files`. Never throws.
|
|
3329
|
+
*/
|
|
3330
|
+
function gateChangedFiles(cwd) {
|
|
3331
|
+
const out = [];
|
|
3332
|
+
const seen = new Set();
|
|
3333
|
+
for (const cmd of [
|
|
3334
|
+
'git -c core.hooksPath=/dev/null diff --name-only HEAD',
|
|
3335
|
+
'git -c core.hooksPath=/dev/null ls-files --others --exclude-standard',
|
|
3336
|
+
]) {
|
|
3337
|
+
try {
|
|
3338
|
+
const res = execSync(cmd, { cwd, encoding: 'utf-8', timeout: 30000, stdio: 'pipe' });
|
|
3339
|
+
for (const line of res.split('\n')) {
|
|
3340
|
+
const f = line.trim();
|
|
3341
|
+
if (f && !seen.has(f)) { seen.add(f); out.push(f); }
|
|
3342
|
+
}
|
|
3343
|
+
} catch { /* best-effort */ }
|
|
3344
|
+
}
|
|
3345
|
+
return out;
|
|
3346
|
+
}
|
|
3347
|
+
|
|
3348
|
+
/**
|
|
3349
|
+
* COMP-PAR-MERGE-QUEUE-CONSUMER: run a per-task pre-merge gate inside a Compose-run
|
|
3350
|
+
* task worktree (the consumer-dispatch mirror of Stratum's worktree.run_pre_merge_gate).
|
|
3351
|
+
* Best-effort symlinks node_modules from base (the worktree is bare). Each command
|
|
3352
|
+
* runs in `cwd` with `timeoutMs`; the first non-zero exit (or launch failure) returns a
|
|
3353
|
+
* structured gate_failed bounce record (caller stamps task_id). All-pass ⇒ null.
|
|
3354
|
+
*/
|
|
3355
|
+
export function runPreMergeGateLocal(cwd, commands, baseCwd, timeoutMs) {
|
|
3356
|
+
if (!Array.isArray(commands) || commands.length === 0) return null;
|
|
3357
|
+
// Best-effort node_modules symlink (bare worktree lacks gitignored deps).
|
|
3358
|
+
if (baseCwd) {
|
|
3359
|
+
try {
|
|
3360
|
+
const baseNm = join(baseCwd, 'node_modules');
|
|
3361
|
+
const wtNm = join(cwd, 'node_modules');
|
|
3362
|
+
if (existsSync(baseNm) && !existsSync(wtNm)) symlinkSync(baseNm, wtNm, 'dir');
|
|
3363
|
+
} catch { /* non-fatal */ }
|
|
3364
|
+
}
|
|
3365
|
+
for (const cmd of commands) {
|
|
3366
|
+
try {
|
|
3367
|
+
execSync(cmd, { cwd, encoding: 'utf-8', timeout: timeoutMs, stdio: 'pipe' });
|
|
3368
|
+
} catch (err) {
|
|
3369
|
+
// execSync throws on non-zero exit, ENOENT, or timeout.
|
|
3370
|
+
const exitCode = (typeof err.status === 'number') ? err.status : null;
|
|
3371
|
+
const out = (err.stdout != null ? String(err.stdout) : '');
|
|
3372
|
+
const errOut = (err.stderr != null ? String(err.stderr) : '');
|
|
3373
|
+
const sep = (out && errOut) ? '\n' : '';
|
|
3374
|
+
const excerpt = (out + sep + errOut + (out || errOut ? '' : (err.message ?? ''))).slice(-2048);
|
|
3375
|
+
return {
|
|
3376
|
+
reason: 'gate_failed',
|
|
3377
|
+
command: cmd,
|
|
3378
|
+
exit_code: exitCode,
|
|
3379
|
+
files: gateChangedFiles(cwd),
|
|
3380
|
+
excerpt,
|
|
3381
|
+
};
|
|
3382
|
+
}
|
|
3383
|
+
}
|
|
3384
|
+
return null;
|
|
3385
|
+
}
|
|
3386
|
+
|
|
3326
3387
|
function applyTaskDiffsToBaseCwd(tasks, diffMap, baseCwd, streamWriter, stepId, patchDir) {
|
|
3327
3388
|
if (diffMap.size === 0) {
|
|
3328
3389
|
return { mergeStatus: 'clean', appliedFiles: [], conflictedTaskId: null, conflictError: null, conflictFiles: [] };
|
|
@@ -3681,6 +3742,26 @@ async function executeParallelDispatch(
|
|
|
3681
3742
|
});
|
|
3682
3743
|
|
|
3683
3744
|
if (worktreeIsolation && worktreePaths.has(taskId)) {
|
|
3745
|
+
// COMP-PAR-MERGE-QUEUE-CONSUMER: run the per-task pre-merge gate in the
|
|
3746
|
+
// worktree BEFORE capturing the diff. A gate failure fails the task,
|
|
3747
|
+
// records a gate_failed bounce, and skips diff capture so the bad work
|
|
3748
|
+
// never merges. No-op when no gate is configured (byte-identical).
|
|
3749
|
+
const gateCmds = dispatchResponse.pre_merge_verify ?? [];
|
|
3750
|
+
if (gateCmds.length > 0) {
|
|
3751
|
+
const gateTimeout = STEP_TIMEOUT_MS[dispStepId] ?? DEFAULT_TIMEOUT_MS;
|
|
3752
|
+
const bounce = runPreMergeGateLocal(wtPath, gateCmds, baseCwd, gateTimeout);
|
|
3753
|
+
if (bounce) {
|
|
3754
|
+
bounce.task_id = taskId;
|
|
3755
|
+
if (streamWriter) {
|
|
3756
|
+
streamWriter.write({
|
|
3757
|
+
type: 'build_error', stepId: taskId,
|
|
3758
|
+
message: `pre-merge gate failed for ${taskId}: ${bounce.command} (exit ${bounce.exit_code ?? '?'}). Diff NOT merged.`,
|
|
3759
|
+
});
|
|
3760
|
+
}
|
|
3761
|
+
return { taskId, status: 'failed', error: `pre_merge_verify failed: ${bounce.command}`, gateBounce: bounce };
|
|
3762
|
+
}
|
|
3763
|
+
}
|
|
3764
|
+
|
|
3684
3765
|
const diskQuotaMB = dispatchResponse.diskQuotaMB ?? 500;
|
|
3685
3766
|
try {
|
|
3686
3767
|
const duOut = execSync(`du -sk "${wtPath}"`, { encoding: 'utf-8', timeout: 10000, stdio: 'pipe' }).trim();
|
|
@@ -3734,11 +3815,15 @@ async function executeParallelDispatch(
|
|
|
3734
3815
|
})
|
|
3735
3816
|
);
|
|
3736
3817
|
|
|
3818
|
+
// COMP-PAR-MERGE-QUEUE-CONSUMER: collect structured bounce records (gate-failed
|
|
3819
|
+
// here, merge-conflict below) to surface on the parallelDone envelope.
|
|
3820
|
+
const bouncedTasks = [];
|
|
3737
3821
|
const taskResults = settled.map(outcome => {
|
|
3738
3822
|
if (outcome.status === 'rejected') {
|
|
3739
3823
|
return { task_id: 'unknown', status: 'failed', error: String(outcome.reason) };
|
|
3740
3824
|
}
|
|
3741
|
-
const { taskId, status, result, error } = outcome.value;
|
|
3825
|
+
const { taskId, status, result, error, gateBounce } = outcome.value;
|
|
3826
|
+
if (gateBounce) bouncedTasks.push(gateBounce);
|
|
3742
3827
|
return status === 'complete'
|
|
3743
3828
|
? { task_id: taskId, status: 'complete', result }
|
|
3744
3829
|
: { task_id: taskId, status: 'failed', error };
|
|
@@ -3765,6 +3850,10 @@ async function executeParallelDispatch(
|
|
|
3765
3850
|
taskResults[idx].status = 'failed';
|
|
3766
3851
|
taskResults[idx].error = `merge conflict: ${result.conflictError}`;
|
|
3767
3852
|
}
|
|
3853
|
+
// COMP-PAR-MERGE-QUEUE-CONSUMER: structured merge-conflict bounce.
|
|
3854
|
+
bouncedTasks.push(
|
|
3855
|
+
buildMergeConflictBounce(result.conflictedTaskId, result.conflictError, result.conflictFiles),
|
|
3856
|
+
);
|
|
3768
3857
|
}
|
|
3769
3858
|
}
|
|
3770
3859
|
|
|
@@ -3784,7 +3873,13 @@ async function executeParallelDispatch(
|
|
|
3784
3873
|
});
|
|
3785
3874
|
}
|
|
3786
3875
|
|
|
3787
|
-
|
|
3876
|
+
// COMP-PAR-MERGE-QUEUE-CONSUMER: when there are bounce records, send a structured
|
|
3877
|
+
// merge_status {status, bounced_tasks} so Stratum threads them onto the failure
|
|
3878
|
+
// envelope (and derives readable violations). Bare string otherwise — byte-identical.
|
|
3879
|
+
const mergeArg = bouncedTasks.length > 0
|
|
3880
|
+
? { status: mergeStatus, bounced_tasks: bouncedTasks }
|
|
3881
|
+
: mergeStatus;
|
|
3882
|
+
return stratum.parallelDone(dispFlowId, dispStepId, taskResults, mergeArg);
|
|
3788
3883
|
}
|
|
3789
3884
|
|
|
3790
3885
|
async function startFresh(stratum, specYaml, featureCode, description, dataDir, templateName, mode = 'feature') {
|
|
@@ -394,7 +394,9 @@ export class StratumMcpClient {
|
|
|
394
394
|
* @param {string} flowId
|
|
395
395
|
* @param {string} stepId
|
|
396
396
|
* @param {Array<{task_id: string, status: string, result?: object, error?: string}>} taskResults
|
|
397
|
-
* @param {'clean'|'conflict'|'fallback'|'manual_required'} mergeStatus
|
|
397
|
+
* @param {'clean'|'conflict'|'fallback'|'manual_required'|{status:string,bounced_tasks?:object[]}} mergeStatus
|
|
398
|
+
* COMP-PAR-MERGE-QUEUE-CONSUMER: either a bare status string (back-compat) or a
|
|
399
|
+
* structured object carrying gate/conflict bounce records. Serializes as-is.
|
|
398
400
|
* @returns {Promise<object>} Next dispatch response
|
|
399
401
|
*/
|
|
400
402
|
async parallelDone(flowId, stepId, taskResults, mergeStatus) {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@smartmemory/compose",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.12-beta",
|
|
4
4
|
"description": "Structured AI dev pipeline — goal-to-product orchestration with gates, iteration loops, and feature lifecycle management.",
|
|
5
5
|
"author": "SmartMemory",
|
|
6
6
|
"license": "MIT",
|