Mechanical rename produced by scripts/msd-rename.cjs: gsd/Gsd/GSD -> msd/Msd/MSD across contents and paths, upstream package/repo coordinates -> @golem15/msd-core and golem15com/msd-core. Deep links into upstream history, sibling upstream packages, the GSD-2 import feature, CHANGELOG.md and .changeset/ are kept as-is. Hand edits on top: MSD block-letter banner and logos, LICENSE copyright line, package/plugin identity, regenerated lockfile, install-tree fixtures, derived registries and benchmark baseline; migration checksum baseline re-locked (MSD keeps its own install state, so no install had applied the old sums); sort-order and regex-escaped expectations in tests adjusted.
155 lines
6.7 KiB
JavaScript
155 lines
6.7 KiB
JavaScript
// allow-test-rule: source-text-is-the-product — see #2650, #4079
|
|
// Workflow markdown is the installed orchestration contract.
|
|
|
|
'use strict';
|
|
|
|
/**
|
|
* #4079 — background-wait pattern lets the orchestrator reach for the host's
|
|
* `ScheduleWakeup` tool with partial arguments.
|
|
*
|
|
* On Claude Code, a `ScheduleWakeup` tool is surfaced in-session (the /loop
|
|
* dynamic-pacing surface). While a MSD workflow waits on a background subagent
|
|
* (researcher / pattern-mapper synchronous Agent() stop-and-wait, the
|
|
* planner/checker/revision `msd_stall_watch` polling waits, or the manager
|
|
* dashboard's background dispatch), the orchestrator model could spontaneously
|
|
* literalize "I'll wait" by calling `ScheduleWakeup` — a tool MSD never
|
|
* documents — with incomplete arguments, surfacing the host's red validation
|
|
* error: "`prompt` is required when `stop` is not true."
|
|
*
|
|
* The fix is contract-level, not code-level: every wait-instruction site now
|
|
* carries an explicit prohibition — never call `ScheduleWakeup` or any host
|
|
* wake/sleep-scheduling tool to literalize a wait; the sanctioned mechanisms
|
|
* (blocking Agent() return, `msd_stall_watch` bash polling) ARE the wait.
|
|
*
|
|
* These are content-contract tests over the shipped workflow text (same
|
|
* "source text is the product" pattern as tests/plan-phase-stall-detection
|
|
* .test.cjs) — they fail on any future edit that drops the guard.
|
|
*
|
|
* Seam: msd-core/workflows/plan-phase.md,
|
|
* msd-core/workflows/plan-phase/steps/stall-detection-helpers.md,
|
|
* msd-core/workflows/manager.md
|
|
*/
|
|
|
|
const { describe, test } = require('node:test');
|
|
const assert = require('node:assert/strict');
|
|
const path = require('node:path');
|
|
|
|
const { readFileNormalized } = require('./helpers.cjs');
|
|
|
|
const REPO_ROOT = path.join(__dirname, '..');
|
|
const PLAN_PHASE_PATH = path.join(REPO_ROOT, 'msd-core', 'workflows', 'plan-phase.md');
|
|
const STALL_HELPERS_PATH = path.join(
|
|
REPO_ROOT, 'msd-core', 'workflows', 'plan-phase', 'steps', 'stall-detection-helpers.md'
|
|
);
|
|
const MANAGER_PATH = path.join(REPO_ROOT, 'msd-core', 'workflows', 'manager.md');
|
|
|
|
// The phase6 shrink-only line for plan-phase.md (tests/phase6-capstone-
|
|
// conformance.test.cjs PRE_PHASE6) — mirrored here so a guard sentence can
|
|
// never quietly push the file past it. #3771/#3916 raised the authoritative
|
|
// PRE_PHASE6 value (94519 -> 96700 -> 98300) for the REVISION_CONFLICT
|
|
// persistence/routing gate before this file's own next-merge landed; keep
|
|
// this mirror equal to that constant, not a stale snapshot of it.
|
|
const PLAN_PHASE_PHASE6_LINE = 98300;
|
|
|
|
function lfByteCount(p) {
|
|
return Buffer.byteLength(readFileNormalized(p), 'utf-8');
|
|
}
|
|
|
|
/** Split plan-phase.md into its ORCHESTRATOR RULE blocks. */
|
|
function orchestratorRules(content) {
|
|
return content.split('\n')
|
|
.filter((line) => line.includes('ORCHESTRATOR RULE'))
|
|
.map((line) => line.trim());
|
|
}
|
|
|
|
describe('#4079 background-wait wake-tool guard', () => {
|
|
test('every synchronous stop-and-wait ORCHESTRATOR RULE in plan-phase.md forbids ScheduleWakeup (#4079)', () => {
|
|
const content = readFileNormalized(PLAN_PHASE_PATH);
|
|
const stopAndWaitRules = orchestratorRules(content).filter(
|
|
(rule) => /stop working on this task immediately/.test(rule)
|
|
);
|
|
assert.ok(
|
|
stopAndWaitRules.length >= 2,
|
|
`expected at least 2 stop-and-wait ORCHESTRATOR RULEs (researcher + pattern mapper), found ${stopAndWaitRules.length}`
|
|
);
|
|
const missing = stopAndWaitRules.filter((rule) => !/ScheduleWakeup/.test(rule));
|
|
assert.deepEqual(
|
|
missing,
|
|
[],
|
|
`every synchronous stop-and-wait rule must forbid ScheduleWakeup (${missing.length} do not). ` +
|
|
'A model told only to "wait" reaches for any in-session scheduling tool with partial args (#4079).'
|
|
);
|
|
});
|
|
|
|
test('stall-detection-helpers.md forbids ScheduleWakeup between msd_stall_watch cycles (#4079)', () => {
|
|
const content = readFileNormalized(STALL_HELPERS_PATH);
|
|
assert.match(
|
|
content,
|
|
/ScheduleWakeup/,
|
|
'the stall-wait fragment (read+executed at step 7.99 before every stall-watch wait) ' +
|
|
'must name ScheduleWakeup so the prohibition is loaded exactly when the wait happens'
|
|
);
|
|
assert.match(
|
|
content,
|
|
/msd_stall_watch[\s\S]{0,400}ScheduleWakeup|ScheduleWakeup[\s\S]{0,400}msd_stall_watch/,
|
|
'the prohibition must be tied to the msd_stall_watch wait cycle, not float disconnected'
|
|
);
|
|
assert.match(
|
|
content,
|
|
/never part of|not part of|Do NOT call/i,
|
|
'the fragment must state the wait tool is never part of the sanctioned wait'
|
|
);
|
|
});
|
|
|
|
test('manager.md background-dispatch wait rules forbid ScheduleWakeup (#4079)', () => {
|
|
const content = readFileNormalized(MANAGER_PATH);
|
|
const dispatchRules = content.split('\n')
|
|
.filter((line) => line.includes('ORCHESTRATOR RULE — BACKGROUND DISPATCH'))
|
|
.map((line) => line.trim());
|
|
assert.ok(
|
|
dispatchRules.length >= 2,
|
|
`expected at least 2 BACKGROUND DISPATCH rules (plan + execute), found ${dispatchRules.length}`
|
|
);
|
|
const missing = dispatchRules.filter((rule) => !/ScheduleWakeup/.test(rule));
|
|
assert.deepEqual(
|
|
missing,
|
|
[],
|
|
'every background-dispatch wait rule must forbid ScheduleWakeup — the dashboard wait ' +
|
|
'is exactly where the model literalizes "I\'ll wait" with a partial-args wake call (#4079)'
|
|
);
|
|
});
|
|
|
|
test('plan-phase.md still uses msd_stall_watch at all three stall-watch sites (negative space)', () => {
|
|
const content = readFileNormalized(PLAN_PHASE_PATH);
|
|
const stallWatchRules = orchestratorRules(content).filter(
|
|
(rule) => rule.includes('msd_stall_watch')
|
|
);
|
|
assert.ok(
|
|
stallWatchRules.length >= 3,
|
|
`expected >= 3 msd_stall_watch ORCHESTRATOR RULEs (planner, checker, revision), found ${stallWatchRules.length}`
|
|
);
|
|
// The sanctioned mechanism is unchanged — the guard only closes the escape hatch.
|
|
assert.match(content, /marker_received/);
|
|
assert.match(content, /stalled/);
|
|
});
|
|
|
|
test('plan-phase.md stays under the phase6 shrink-only line (#4079 guard)', () => {
|
|
const bytes = lfByteCount(PLAN_PHASE_PATH);
|
|
assert.ok(
|
|
bytes < PLAN_PHASE_PHASE6_LINE,
|
|
`plan-phase.md is ${bytes} LF bytes — must stay < ${PLAN_PHASE_PHASE6_LINE} ` +
|
|
'(tests/phase6-capstone-conformance.test.cjs PRE_PHASE6)'
|
|
);
|
|
});
|
|
|
|
test('manager.md background-dispatch semantics unchanged (FLATTEN gate preserved)', () => {
|
|
const content = readFileNormalized(MANAGER_PATH);
|
|
assert.match(content, /dispatch-should-flatten/, 'FLATTEN resolution must be preserved');
|
|
assert.match(
|
|
content,
|
|
/Return to the dashboard immediately and wait for the background agent to report back/,
|
|
'the dashboard-return wait semantics must be preserved'
|
|
);
|
|
});
|
|
});
|