mirror of
https://github.com/pbakaus/impeccable.git
synced 2026-09-15 23:56:29 +03:00
Require verbatim skip quote for seed pause and fix hidden doc shell.
Direction rolls on projects without DESIGN.md now refuse until the user accepts document --seed or re-runs with --seed-declined carrying their actual skip answer; IMPECCABLE_SEED_DECLINED=1 remains the unattended harness escape. Hide the design context shell when [hidden] so it does not reserve viewport space. Extend concept-seed tests for the seed gate. AI-assisted. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -65,6 +65,10 @@ body.dcx-open { overflow: hidden; background: linear-gradient(180deg, var(--ks-l
|
||||
padding: var(--dcx-gap) clamp(14px, 2vw, 26px) clamp(16px, 2.2vw, 28px);
|
||||
}
|
||||
|
||||
/* The author display above would defeat the hidden attribute the shell ships
|
||||
with, leaving an invisible 100svh box under the questionnaire. */
|
||||
.dcx-shell[hidden] { display: none; }
|
||||
|
||||
.dcx-grid {
|
||||
position: relative;
|
||||
flex: 1 1 0;
|
||||
|
||||
@@ -47,6 +47,7 @@
|
||||
*
|
||||
* Usage:
|
||||
* node scripts/concept-seed.mjs --scope direction --mode persuade
|
||||
* node scripts/concept-seed.mjs --scope direction --mode persuade --seed-declined="<user's verbatim skip answer>"
|
||||
* node scripts/concept-seed.mjs --scope surface --mode operate --from <key>
|
||||
* node scripts/concept-seed.mjs --scope surface --mode operate --grain flow
|
||||
* node scripts/concept-seed.mjs --scope direction --candidate-count 6
|
||||
@@ -55,6 +56,16 @@
|
||||
* node scripts/concept-seed.mjs --chosen <challenger-id> --kind challenger --from <key> --scope direction
|
||||
* node scripts/concept-seed.mjs --kind assigned --from <key> --scope direction
|
||||
*
|
||||
* --seed-declined records that the user was offered the document --seed
|
||||
* questionnaire on a no-DESIGN.md project and skipped it. The flag carries
|
||||
* evidence: its value is the user's verbatim skip answer, quoted from the
|
||||
* conversation. Without it, a --scope direction roll on a project with no
|
||||
* DESIGN.md refuses to deal and prints the pause directive instead; a bare
|
||||
* or empty flag refuses the same way (a real live session self-passed the
|
||||
* boolean form without asking, which is why the flag demands the quote).
|
||||
* Fabricating or paraphrasing the quote is a contract violation, not a
|
||||
* shortcut.
|
||||
*
|
||||
* --grain names how much of the product is in play: product, flow, view, or
|
||||
* region. A docs site, an onboarding flow, a landing page and a data table are
|
||||
* four different amounts of product and want different compositions. Grain is a
|
||||
@@ -87,6 +98,10 @@
|
||||
* IMPECCABLE_CATALOG_DIR — directory holding the four catalog JSON files.
|
||||
* IMPECCABLE_API_URL — roll API base (default https://impeccable.style/api).
|
||||
* IMPECCABLE_NO_TELEMETRY — disables the choice ping (DO_NOT_TRACK also honored).
|
||||
* IMPECCABLE_SEED_DECLINED — set to 1 to bypass the seed pause without a
|
||||
* quote; the unattended escape for eval and CI harnesses that
|
||||
* legitimately roll direction on a no-DESIGN.md workspace. Never for
|
||||
* attended sessions; the bypass is logged to stderr so it stays auditable.
|
||||
*/
|
||||
|
||||
import crypto from 'node:crypto';
|
||||
@@ -736,6 +751,21 @@ if (isMainModule()) {
|
||||
const candidateCountIdx = args.indexOf('--candidate-count');
|
||||
const chosenIdx = args.indexOf('--chosen');
|
||||
const kindIdx = args.indexOf('--kind');
|
||||
// --seed-declined carries evidence: the user's verbatim skip answer, in
|
||||
// either --seed-declined="<answer>" or --seed-declined <answer> form. A
|
||||
// bare or empty flag does not count; a live session self-passed the
|
||||
// boolean form without asking the user, so the flag demands the quote.
|
||||
let seedDeclinedAnswer = null;
|
||||
for (let i = 0; i < args.length; i += 1) {
|
||||
if (args[i] === '--seed-declined') {
|
||||
const next = args[i + 1];
|
||||
seedDeclinedAnswer = next !== undefined && !next.startsWith('--') ? next : '';
|
||||
} else if (args[i].startsWith('--seed-declined=')) {
|
||||
seedDeclinedAnswer = args[i].slice('--seed-declined='.length);
|
||||
}
|
||||
}
|
||||
const seedDeclinedByEnv = process.env.IMPECCABLE_SEED_DECLINED === '1';
|
||||
const seedDeclined = Boolean(seedDeclinedAnswer && seedDeclinedAnswer.trim()) || seedDeclinedByEnv;
|
||||
try {
|
||||
if (chosenIdx !== -1 || kindIdx !== -1) {
|
||||
// Choice ping: always exits 0, telemetry must never fail a design flow.
|
||||
@@ -776,7 +806,8 @@ if (isMainModule()) {
|
||||
// rolled directions with no PRODUCT.md, so nothing grounded the fusion).
|
||||
// The --chosen branch above stays ungated; telemetry never blocks.
|
||||
const { loadContext } = await import('./context.mjs');
|
||||
if (!loadContext(process.cwd()).hasProduct) {
|
||||
const ctx = loadContext(process.cwd());
|
||||
if (!ctx.hasProduct) {
|
||||
process.stdout.write([
|
||||
'NO_PRODUCT_MD: the dice stay in the cup until product truth exists.',
|
||||
'Complete the init ask round and write PRODUCT.md first (reference/init.md), then re-run this exact command.',
|
||||
@@ -784,6 +815,31 @@ if (isMainModule()) {
|
||||
].join(' ') + '\n');
|
||||
process.exit(1);
|
||||
}
|
||||
// Mechanical seed-pause gate: prose alone did not keep a model from
|
||||
// rolling a direction before offering the seed questionnaire (a real
|
||||
// session read a stale reference file and dealt straight after init),
|
||||
// and a boolean flag did not either (the next session self-passed it
|
||||
// without asking). A direction roll invents the visual world, so on a
|
||||
// project with no DESIGN.md the user gets the choice first; a seed
|
||||
// DESIGN.md counts as present, so a post-questionnaire re-entry never
|
||||
// re-asks. The flag now carries the user's verbatim skip answer.
|
||||
const scopeArg = scopeIdx !== -1 ? args[scopeIdx + 1] : 'surface';
|
||||
if (scopeArg === 'direction' && !ctx.hasDesign && !seedDeclined) {
|
||||
process.stdout.write([
|
||||
'NO_DESIGN_MD: the dice stay in the cup until the user answers the seed question.',
|
||||
'First action: create a tracked todo "Ask user: document --seed or skip" with the harness todo tool and start no other todo until it is answered; with no todo tool, state this gate to the user in chat before anything else.',
|
||||
'Then ask one question: recommend `document --seed`, the guided interview plus browser questionnaire (reference/document.md, seed mode), because a world built from the user\'s own choices beats one assigned to them; offer the skip in the same breath.',
|
||||
'User accepts: run document seed mode; its seed DESIGN.md reads as an established world, so no direction roll happens.',
|
||||
'User skips: re-run this exact command with --seed-declined="<their verbatim skip answer>" quoting the user\'s actual words from this conversation.',
|
||||
'The original build request is never a skip answer, a bare or empty flag refuses again, and a fabricated or paraphrased quote is a contract violation.',
|
||||
].join(' ') + '\n');
|
||||
process.exit(1);
|
||||
}
|
||||
if (scopeArg === 'direction' && !ctx.hasDesign && seedDeclinedByEnv && !(seedDeclinedAnswer && seedDeclinedAnswer.trim())) {
|
||||
// Auditable trace for the unattended escape; stderr keeps the seed
|
||||
// output clean for the agent.
|
||||
process.stderr.write('seed pause bypassed via IMPECCABLE_SEED_DECLINED (unattended harness escape)\n');
|
||||
}
|
||||
process.stdout.write(await renderConceptSeed({
|
||||
scope: scopeIdx !== -1 ? args[scopeIdx + 1] : 'surface',
|
||||
key: fromIdx !== -1
|
||||
|
||||
@@ -532,7 +532,9 @@ describe('init gate', () => {
|
||||
const gateRun = (cwd) => spawnSync(process.execPath, [SCRIPT, '--scope', 'direction', '--from', 'gate-test'], {
|
||||
cwd,
|
||||
encoding: 'utf-8',
|
||||
env: { ...process.env, IMPECCABLE_CATALOG_DIR: FIXTURE_DIR, IMPECCABLE_CONTEXT_DIR: '' },
|
||||
// Blank the escape hatch so a developer shell carrying it never turns
|
||||
// the refusal assertions green for the wrong reason.
|
||||
env: { ...process.env, IMPECCABLE_CATALOG_DIR: FIXTURE_DIR, IMPECCABLE_CONTEXT_DIR: '', IMPECCABLE_SEED_DECLINED: '' },
|
||||
});
|
||||
|
||||
it('refuses to deal when no PRODUCT.md exists and routes to init', () => {
|
||||
@@ -547,11 +549,75 @@ describe('init gate', () => {
|
||||
it('deals normally once PRODUCT.md exists', () => {
|
||||
const dir = mkdtempSync(path.join(tmpdir(), 'concept-seed-product-'));
|
||||
writeFileSync(path.join(dir, 'PRODUCT.md'), '# Test Product\n\n## Register\n\nbrand\n');
|
||||
// DESIGN.md keeps this case about the product gate; the seed pause has
|
||||
// its own suite below.
|
||||
writeFileSync(path.join(dir, 'DESIGN.md'), '# Design\n\nEstablished world.\n');
|
||||
const result = gateRun(dir);
|
||||
assert.equal(result.status, 0);
|
||||
assert.doesNotMatch(result.stdout, /NO_PRODUCT_MD/);
|
||||
});
|
||||
|
||||
// The seed pause: a direction roll invents the visual world, so a project
|
||||
// with PRODUCT.md but no DESIGN.md gets the questionnaire offer before the
|
||||
// deal. The refusal is mechanical because prose alone did not stop a real
|
||||
// session from rolling straight after init.
|
||||
const seedGateDir = (design = null) => {
|
||||
const dir = mkdtempSync(path.join(tmpdir(), 'concept-seed-seedgate-'));
|
||||
writeFileSync(path.join(dir, 'PRODUCT.md'), '# Test Product\n\n## Platform\n\nweb\n');
|
||||
if (design !== null) writeFileSync(path.join(dir, 'DESIGN.md'), design);
|
||||
return dir;
|
||||
};
|
||||
|
||||
it('refuses a direction roll with no DESIGN.md and routes to the seed questionnaire offer', () => {
|
||||
const result = gateRun(seedGateDir());
|
||||
assert.equal(result.status, 1);
|
||||
assert.match(result.stdout, /NO_DESIGN_MD/);
|
||||
assert.match(result.stdout, /document --seed/);
|
||||
assert.match(result.stdout, /--seed-declined/);
|
||||
assert.doesNotMatch(result.stdout, /ASSIGNED INDEX/);
|
||||
});
|
||||
|
||||
it('deals a direction once --seed-declined records the user skip', () => {
|
||||
const dir = seedGateDir();
|
||||
const result = spawnSync(process.execPath, [SCRIPT, '--scope', 'direction', '--from', 'gate-test', '--seed-declined'], {
|
||||
cwd: dir,
|
||||
encoding: 'utf-8',
|
||||
env: { ...process.env, IMPECCABLE_CATALOG_DIR: FIXTURE_DIR, IMPECCABLE_CONTEXT_DIR: '' },
|
||||
});
|
||||
assert.equal(result.status, 0);
|
||||
assert.doesNotMatch(result.stdout, /NO_DESIGN_MD/);
|
||||
assert.match(result.stdout, /ASSIGNED INDEX/);
|
||||
});
|
||||
|
||||
it('honors IMPECCABLE_SEED_DECLINED=1 for harnesses that cannot thread the flag', () => {
|
||||
const dir = seedGateDir();
|
||||
const result = spawnSync(process.execPath, [SCRIPT, '--scope', 'direction', '--from', 'gate-test'], {
|
||||
cwd: dir,
|
||||
encoding: 'utf-8',
|
||||
env: { ...process.env, IMPECCABLE_CATALOG_DIR: FIXTURE_DIR, IMPECCABLE_CONTEXT_DIR: '', IMPECCABLE_SEED_DECLINED: '1' },
|
||||
});
|
||||
assert.equal(result.status, 0);
|
||||
assert.doesNotMatch(result.stdout, /NO_DESIGN_MD/);
|
||||
});
|
||||
|
||||
it('treats a seed DESIGN.md as present so a post-questionnaire re-entry never re-asks', () => {
|
||||
const seedDesign = '# Design\n\n<!-- SEED: established with the user before implementation; re-run /impeccable document once there\'s code to capture the actual tokens and components. -->\n';
|
||||
const result = gateRun(seedGateDir(seedDesign));
|
||||
assert.equal(result.status, 0);
|
||||
assert.doesNotMatch(result.stdout, /NO_DESIGN_MD/);
|
||||
});
|
||||
|
||||
it('never seed-gates a surface roll', () => {
|
||||
const dir = seedGateDir();
|
||||
const result = spawnSync(process.execPath, [SCRIPT, '--scope', 'surface', '--from', 'gate-test'], {
|
||||
cwd: dir,
|
||||
encoding: 'utf-8',
|
||||
env: { ...process.env, IMPECCABLE_CATALOG_DIR: FIXTURE_DIR, IMPECCABLE_CONTEXT_DIR: '' },
|
||||
});
|
||||
assert.equal(result.status, 0);
|
||||
assert.doesNotMatch(result.stdout, /NO_DESIGN_MD/);
|
||||
});
|
||||
|
||||
it('never gates the choice ping', () => {
|
||||
const dir = mkdtempSync(path.join(tmpdir(), 'concept-seed-ping-'));
|
||||
const result = spawnSync(process.execPath, [SCRIPT, '--chosen', 'assigned', '--from', 'gate-test'], {
|
||||
@@ -809,7 +875,7 @@ describe('API roll path', () => {
|
||||
const result = await new Promise((resolveRun, rejectRun) => {
|
||||
const child = spawn(NODE, [
|
||||
'--import', pathToFileURL(preloadPath).href,
|
||||
SCRIPT, '--scope', 'direction', '--mode', 'persuade', '--from', 'api-test',
|
||||
SCRIPT, '--scope', 'direction', '--mode', 'persuade', '--from', 'api-test', '--seed-declined',
|
||||
], {
|
||||
cwd: dir,
|
||||
env: {
|
||||
|
||||
Reference in New Issue
Block a user