@smartmemory/compose 0.2.10-beta → 0.2.11-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 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
- return stratum.parallelDone(dispFlowId, dispStepId, taskResults, mergeStatus);
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.10-beta",
3
+ "version": "0.2.11-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",