fix(ultraplan-local): Bug 3 — wire frontmatter consistency check into /ultracontinue Phase 1.5

Step 8 of v3.4.1 plan.

commands/ultracontinue-local.md:
- New Phase 1.5 between Phase 1 and Phase 2 — runs the
  next-session-prompt-validator in --consistency mode when both candidates
  exist (plugin-root + project-dir). Refuses on producer mismatch with
  fresh candidates, downgrades stale candidate to a warning, downgrades
  >24h wall-clock drift to a soft warning.
- Anti-substitution rule applies — paths emitted as concrete tokens, not
  template placeholders.

lib/validators/next-session-prompt-validator.mjs:
- Sharpen NEXT_SESSION_PROMPT_PRODUCER_MISMATCH error message to include
  the literal "produced_by" field name so consumers (and operators) can
  trace the disagreement back to the YAML key.

tests/commands/ultracontinue.test.mjs:
- Test (Bug 3 prose) — Phase 1.5 header present, references validator,
  appears between Phase 1 and Phase 2 in document order.
- Test (Bug 3 e) — tmp project dir with state file + two prompt files
  with mismatched producers, both fresh relative to state.updated_at;
  CLI consistency mode exits non-zero, JSON stdout surfaces
  NEXT_SESSION_PROMPT_PRODUCER_MISMATCH with both paths and the
  "produced_by" token in the message.

Tests 346 -> 348 (+2).

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
Kjell Tore Guttormsen 2026-05-04 17:39:42 +02:00
commit 37108ae899
3 changed files with 125 additions and 1 deletions

View file

@ -15,6 +15,7 @@ import { mkdtempSync, mkdirSync, writeFileSync, readFileSync, rmSync } from 'nod
import { tmpdir } from 'node:os';
import { dirname, join } from 'node:path';
import { fileURLToPath } from 'node:url';
import { execFileSync } from 'node:child_process';
import { runHook } from '../helpers/hook-helper.mjs';
const HERE = dirname(fileURLToPath(import.meta.url));
@ -170,6 +171,80 @@ test('ultracontinue Bug 2 — pre-bash-executor ALLOWS resolved validator invoca
assert.strictEqual(code, 0, 'pre-bash-executor must not block resolved validator invocations');
});
// ---------------------------------------------------------------
// Step 8 — Bug 3 regression test (Phase 1.5 consistency wire-up)
// ---------------------------------------------------------------
test('ultracontinue Bug 3 — Phase 1.5 documents consistency check between Phase 1 and Phase 2', () => {
const cmd = readCommand();
// Phase 1.5 must exist literally in the prose between Phase 1 and Phase 2.
assert.match(cmd, /## Phase 1\.5 /, 'Phase 1.5 header must be present');
assert.match(cmd, /next-session-prompt-validator/, 'Phase 1.5 must invoke next-session-prompt-validator');
const phase15Idx = cmd.indexOf('## Phase 1.5 ');
const phase2Idx = cmd.indexOf('## Phase 2 ');
assert.ok(phase15Idx !== -1 && phase2Idx !== -1 && phase15Idx < phase2Idx,
'Phase 1.5 must appear before Phase 2');
});
test('ultracontinue Bug 3 (e) — CLI consistency mode flags producer mismatch in JSON output', () => {
const root = mkdtempSync(join(tmpdir(), 'ultracontinue-fm-'));
try {
const projectDir = join(root, '.claude', 'projects', '2026-05-04-fixture-c');
mkdirSync(projectDir, { recursive: true });
// State file (status: in_progress, updated_at = T-base)
const stateUpdatedAt = '2026-05-04T15:00:00.000Z';
writeFileSync(
join(projectDir, '.session-state.local.json'),
JSON.stringify({
schema_version: 1,
project: projectDir,
next_session_brief_path: join(projectDir, 'brief.md'),
next_session_label: 'Session 2',
status: 'in_progress',
updated_at: stateUpdatedAt,
}, null, 2),
);
// Project-dir prompt: produced_by ultraexecute-local at T-1
const projectPrompt = join(projectDir, 'NEXT-SESSION-PROMPT.local.md');
writeFileSync(projectPrompt,
'---\nproduced_by: ultraexecute-local\nproduced_at: 2026-05-04T15:30:00.000Z\n---\n\n# Session 2\n');
// Plugin-root prompt: produced_by graceful-handoff at T-0 (newer)
const pluginPrompt = join(root, 'NEXT-SESSION-PROMPT.local.md');
writeFileSync(pluginPrompt,
'---\nproduced_by: graceful-handoff\nproduced_at: 2026-05-04T15:31:00.000Z\n---\n\n# A2 master\n');
// Both fresh relative to state.updated_at → producer mismatch must hard-fail.
let exitCode = 0;
let stdout = '';
try {
stdout = execFileSync(process.execPath, [
join(ROOT, 'lib', 'validators', 'next-session-prompt-validator.mjs'),
'--json',
'--consistency',
projectPrompt,
pluginPrompt,
], { encoding: 'utf-8', cwd: ROOT });
} catch (e) {
exitCode = e.status;
stdout = e.stdout ? e.stdout.toString() : '';
}
assert.notEqual(exitCode, 0, 'consistency CLI must exit non-zero on producer mismatch');
const parsed = JSON.parse(stdout);
assert.equal(parsed.valid, false);
const mismatch = parsed.errors.find(e => e.code === 'NEXT_SESSION_PROMPT_PRODUCER_MISMATCH');
assert.ok(mismatch, 'must surface NEXT_SESSION_PROMPT_PRODUCER_MISMATCH error');
assert.match(mismatch.message, new RegExp(projectPrompt.replace(/[/\\]/g, '.')), 'error message must reference project-dir prompt path');
assert.match(mismatch.message, new RegExp(pluginPrompt.replace(/[/\\]/g, '.')), 'error message must reference plugin-root prompt path');
assert.match(mismatch.message, /produced_by/i, 'error message must mention produced_by');
} finally {
rmSync(root, { recursive: true, force: true });
}
});
test('ultracontinue Bug 2 — Phase 2 contains no {state-file-path} or any {curly-template} placeholder', () => {
// (d-2) Pattern D structure test. The fix must eliminate the
// {state-file-path} placeholder and any other {anything} curly-brace