From eb97c67a6b92107b80612f7a6c9dd7d49d7ae06f Mon Sep 17 00:00:00 2001 From: MageByte Date: Sun, 26 Jul 2026 11:29:11 +0800 Subject: [PATCH 01/15] feat: add quick workflow recommendation and acceptance --- scripts/infer-workflow.mjs | 21 +++--- scripts/lib/cmd-workflow.mjs | 43 +++++++++--- scripts/lib/workflow-recommendation.mjs | 47 ++++++++++++- tests/lib/cmd-workflow.test.mjs | 77 ++++++++++++++-------- tests/lib/infer-workflow.test.mjs | 42 +++++++----- tests/lib/workflow-recommendation.test.mjs | 32 ++++++++- 6 files changed, 192 insertions(+), 70 deletions(-) diff --git a/scripts/infer-workflow.mjs b/scripts/infer-workflow.mjs index eb244ef..1243d5a 100644 --- a/scripts/infer-workflow.mjs +++ b/scripts/infer-workflow.mjs @@ -1,5 +1,5 @@ #!/usr/bin/env node -// scripts/infer-workflow.mjs — infer hotfix/tweak/full from change artifacts +// scripts/infer-workflow.mjs — infer quick/tweak/full from change artifacts import { existsSync, readFileSync } from 'node:fs'; import { join } from 'node:path'; import { readState } from './lib/state-loader.mjs'; @@ -49,7 +49,7 @@ function inferMode(changeDir) { // Explicit override: honor any non-auto, non-null workflow value if (state.workflow && state.workflow !== 'auto') { - const valid = ['hotfix', 'tweak', 'full']; + const valid = ['quick', 'hotfix', 'tweak', 'full']; if (valid.includes(state.workflow)) { return { mode: state.workflow, @@ -92,21 +92,22 @@ function inferMode(changeDir) { }; } - // Hotfix: very small, no schema/api, no new module - if (taskCount <= 2 && fileCount <= 2 && !hasSchemaChange && !hasNewModule) { + // Tweak: small config/doc change + if (taskCount <= 4 && configDocOnly && !hasSchemaChange && !hasNewModule) { return { - mode: 'hotfix', + mode: 'tweak', explicit: false, - reason: `≤2 tasks, ≤2 files, no schema/API/new-module keywords → hotfix`, + reason: `≤4 tasks, only config/doc files, no schema/API/new-module keywords → tweak`, }; } - // Tweak: small config/doc change - if (taskCount <= 4 && configDocOnly && !hasSchemaChange && !hasNewModule) { + // Quick: small non-document code change. Incident detection requires request context, + // so legacy artifact inference deliberately never infers hotfix. + if (taskCount <= 3 && fileCount <= 3 && codeFileCount > 0 && !hasSchemaChange && !hasNewModule) { return { - mode: 'tweak', + mode: 'quick', explicit: false, - reason: `≤4 tasks, only config/doc files, no schema/API/new-module keywords → tweak`, + reason: `≤3 tasks, ≤3 code files, no schema/API/new-module keywords → quick`, }; } diff --git a/scripts/lib/cmd-workflow.mjs b/scripts/lib/cmd-workflow.mjs index 7cc8357..ccf9f14 100644 --- a/scripts/lib/cmd-workflow.mjs +++ b/scripts/lib/cmd-workflow.mjs @@ -3,6 +3,7 @@ import { join } from 'node:path'; import { parseArgs } from 'node:util'; import { WORKFLOW_MODES, + acceptWorkflowRecommendation, recommendWorkflowPath, readWorkflowSelection, recordWorkflowSelection, @@ -17,10 +18,12 @@ const OPTIONS = { 'schema-api-change': { type: 'string' }, 'new-module': { type: 'string' }, uncertainty: { type: 'string' }, + 'request-kind': { type: 'string' }, mode: { type: 'string' }, confirm: { type: 'boolean', default: false }, reason: { type: 'string' }, 'acknowledge-recommendation': { type: 'boolean', default: false }, + source: { type: 'string' }, json: { type: 'boolean', default: false }, help: { type: 'boolean', default: false }, }; @@ -44,18 +47,18 @@ export async function run(args) { const { positionals, values } = parsed; const [subcommand, changeDir] = positionals; if (values.help || subcommand === undefined) return printHelp(); - if (!['recommend', 'select', 'show'].includes(subcommand)) { - return fail('Usage: ssf workflow ', 2); + if (!['recommend', 'select', 'accept', 'show'].includes(subcommand)) { + return fail('Usage: ssf workflow ', 2); } if (positionals.length !== 2 || !changeDir) { - return fail('Usage: ssf workflow ', 2); + return fail('Usage: ssf workflow ', 2); } try { requireStateFile(changeDir); const state = readState(changeDir); - if (subcommand === 'select' && isExplicitWorkflow(state.workflow)) { + if (['select', 'accept'].includes(subcommand) && isExplicitWorkflow(state.workflow)) { return fail('workflow is already explicitly selected', 1); } if (subcommand === 'recommend' && isExplicitWorkflow(state.workflow)) { @@ -63,6 +66,7 @@ export async function run(args) { } if (subcommand === 'recommend') return recommend(changeDir, values); if (subcommand === 'show') return show(changeDir, state, values.json); + if (subcommand === 'accept') return accept(changeDir, state, values); return select(changeDir, state, values); } catch (error) { if (error instanceof UsageError) return fail(error.message, 2); @@ -85,11 +89,21 @@ function select(changeDir, state, values) { confirmed: values.confirm, acknowledged: values['acknowledge-recommendation'], }); + persistWorkflowSelection(changeDir, state, record); + return print({ ok: true, source: 'user-confirmed', record }, values.json); +} + +function accept(changeDir, state, values) { + const record = acceptWorkflowRecommendation(changeDir, { source: values.source }); + persistWorkflowSelection(changeDir, state, record); + return print({ ok: true, source: 'direct-request', record }, values.json); +} + +function persistWorkflowSelection(changeDir, state, record) { const summary = `workflow_path=${record.selection.mode}; recommended=${record.recommendation.mode}; followed_recommendation=${record.selection.followed_recommendation}`; state.workflow = record.selection.mode; state.dp_0_decisions = appendDecision(state.dp_0_decisions, summary); writeState(changeDir, state); - return print({ ok: true, source: 'user-confirmed', record }, values.json); } function show(changeDir, state, json) { @@ -140,9 +154,18 @@ function factsFrom(values) { schema_api_change: parseFact(values['schema-api-change'], 'schema-api-change'), new_module: parseFact(values['new-module'], 'new-module'), uncertainty: parseFact(values.uncertainty, 'uncertainty'), + request_kind: parseRequestKind(values['request-kind']), }; } +function parseRequestKind(value) { + if (value === undefined) return 'standard'; + if (!['standard', 'incident'].includes(value)) { + throw new UsageError('request-kind must be one of: standard, incident'); + } + return value; +} + function parseCount(value, name) { if (value === undefined) return null; if (!/^\d+$/.test(value)) throw new UsageError(`${name} must be a non-negative integer`); @@ -217,7 +240,10 @@ function formatRecordDetails(value, record) { lines.push(`Missing facts: ${record.missing_facts.join(', ')}`); } if (record?.selection) { - lines.push(`Selection: mode=${record.selection.mode}, reason=${record.selection.reason}, followed_recommendation=${record.selection.followed_recommendation}`); + const detail = record.selection.accepted_automatically + ? `source=${record.selection.source}, accepted_automatically=true` + : `reason=${record.selection.reason}`; + lines.push(`Selection: mode=${record.selection.mode}, ${detail}, followed_recommendation=${record.selection.followed_recommendation}`); } if (value.receipt?.exists === false) lines.push('Hash valid: unavailable (receipt missing)'); @@ -238,7 +264,8 @@ function fail(message, exitCode) { function printHelp() { console.log(`Usage: - ssf workflow recommend [--task-count ] [--file-count ] [--config-doc-only yes|no|unknown] [--schema-api-change yes|no|unknown] [--new-module yes|no|unknown] [--uncertainty low|high|unknown] [--json] - ssf workflow select --mode full|hotfix|tweak --confirm --reason [--acknowledge-recommendation] [--json] + ssf workflow recommend [--task-count ] [--file-count ] [--config-doc-only yes|no|unknown] [--schema-api-change yes|no|unknown] [--new-module yes|no|unknown] [--uncertainty low|high|unknown] [--request-kind standard|incident] [--json] + ssf workflow select --mode full|hotfix|tweak|quick --confirm --reason [--acknowledge-recommendation] [--json] + ssf workflow accept --source direct-request [--json] ssf workflow show [--json]`); } diff --git a/scripts/lib/workflow-recommendation.mjs b/scripts/lib/workflow-recommendation.mjs index fb40928..fff5b2c 100644 --- a/scripts/lib/workflow-recommendation.mjs +++ b/scripts/lib/workflow-recommendation.mjs @@ -5,7 +5,7 @@ import { import { dirname } from 'node:path'; import { getOverlayPaths } from './sdd-overlay.mjs'; -export const WORKFLOW_MODES = Object.freeze(['full', 'hotfix', 'tweak']); +export const WORKFLOW_MODES = Object.freeze(['full', 'hotfix', 'tweak', 'quick']); const BOOLEAN_FACTS = ['config_doc_only', 'schema_api_change', 'new_module']; const FACT_KEYS = ['task_count', 'file_count', ...BOOLEAN_FACTS, 'uncertainty']; @@ -18,9 +18,16 @@ export function normalizeWorkflowFacts(input = {}) { schema_api_change: normalizeEnum(input.schema_api_change, ['yes', 'no', 'unknown']), new_module: normalizeEnum(input.new_module, ['yes', 'no', 'unknown']), uncertainty: normalizeEnum(input.uncertainty, ['low', 'high', 'unknown']), + request_kind: normalizeRequestKind(input.request_kind), }; } +function normalizeRequestKind(value) { + if (value === null || value === undefined) return 'standard'; + if (!['standard', 'incident'].includes(value)) throw new Error('invalid request_kind value'); + return value; +} + export function recommendWorkflowPath(input = {}) { const facts = normalizeWorkflowFacts(input); const missing_facts = FACT_KEYS.filter((key) => facts[key] === null || facts[key] === 'unknown'); @@ -35,8 +42,12 @@ export function recommendWorkflowPath(input = {}) { if (facts.config_doc_only === 'yes' && facts.task_count <= 4 && facts.file_count <= 4) { return ready(base, 'tweak', 'Config/doc-only work is within the tweak thresholds.'); } - if (facts.config_doc_only === 'no' && facts.task_count <= 2 && facts.file_count <= 2) { - return ready(base, 'hotfix', 'Bounded code work is within the hotfix thresholds.'); + if (facts.request_kind === 'incident' && facts.config_doc_only === 'no' + && facts.task_count <= 2 && facts.file_count <= 2) { + return ready(base, 'hotfix', 'Bounded incident work is within the hotfix thresholds.'); + } + if (facts.config_doc_only === 'no' && facts.task_count <= 3 && facts.file_count <= 3) { + return ready(base, 'quick', 'Bounded low-risk code work is within the quick thresholds.'); } return ready(base, 'full', 'The observed scope exceeds the fast-path thresholds.'); } @@ -104,6 +115,7 @@ export function recordWorkflowSelection(changeDir, { mode, reason, confirmed, ac reason, followed_recommendation: followed, acknowledged_non_recommendation: !followed && acknowledged === true, + accepted_automatically: false, selected_at: new Date().toISOString(), }, }); @@ -111,6 +123,35 @@ export function recordWorkflowSelection(changeDir, { mode, reason, confirmed, ac return selected; } +export function acceptWorkflowRecommendation(changeDir, { source }) { + const loaded = readWorkflowSelection(changeDir); + if (!loaded.valid) throw new Error(loaded.failures.join('; ')); + const recommendation = loaded.record.recommendation; + if (loaded.record.status !== 'ready' || !recommendation) { + throw new Error('workflow recommendation needs more input'); + } + if (!['quick', 'hotfix'].includes(recommendation.mode)) { + throw new Error('only a recommended quick or hotfix workflow can be accepted directly'); + } + if (source !== 'direct-request') { + throw new Error('workflow acceptance source must be direct-request'); + } + + const accepted = withHash({ + ...withoutHash(loaded.record), + selection: { + mode: recommendation.mode, + source, + followed_recommendation: true, + acknowledged_non_recommendation: false, + accepted_automatically: true, + selected_at: new Date().toISOString(), + }, + }); + writeRecord(changeDir, accepted); + return accepted; +} + function ready(base, mode, reason) { return { ...base, status: 'ready', recommendation: { mode, reasons: [reason] } }; } diff --git a/tests/lib/cmd-workflow.test.mjs b/tests/lib/cmd-workflow.test.mjs index c8e2e34..2dfe0c8 100644 --- a/tests/lib/cmd-workflow.test.mjs +++ b/tests/lib/cmd-workflow.test.mjs @@ -68,25 +68,46 @@ afterEach(() => { }); describe('ssf workflow', () => { - it('does not set workflow until the user confirms a selection', () => { + it('accepts a recommended quick path from a direct request without --confirm', () => { const recommended = recommend(); assert.equal(recommended.exitCode, 0, recommended.stderr); + assert.equal(recommended.json.recommendation.mode, 'quick'); + + const accepted = runSsf(['workflow', 'accept', changeDir, '--source', 'direct-request', '--json']); + assert.equal(accepted.exitCode, 0, accepted.stderr); + assert.equal(readState(changeDir).workflow, 'quick'); + assert.equal(accepted.json.record.selection.accepted_automatically, true); + assert.equal(accepted.json.record.selection.source, 'direct-request'); + }); + + it('recommends hotfix for an incident and accepts it without a planning approval', () => { + const recommended = recommend(['--request-kind', 'incident']); + assert.equal(recommended.exitCode, 0, recommended.stderr); assert.equal(recommended.json.recommendation.mode, 'hotfix'); + const accepted = runSsf(['workflow', 'accept', changeDir, '--source', 'direct-request', '--json']); + assert.equal(accepted.exitCode, 0, accepted.stderr); + assert.equal(readState(changeDir).workflow, 'hotfix'); + }); + + it('does not set workflow until the user confirms a selection', () => { + const recommended = recommend(); + assert.equal(recommended.exitCode, 0, recommended.stderr); + assert.equal(recommended.json.recommendation.mode, 'quick'); assert.equal(readState(changeDir).workflow, 'auto'); const beforeUnconfirmed = snapshotWorkflowFiles(); - const unconfirmed = runSsf(['workflow', 'select', changeDir, '--mode', 'hotfix', + const unconfirmed = runSsf(['workflow', 'select', changeDir, '--mode', 'quick', '--reason', 'bounded code fix', '--json']); assert.equal(unconfirmed.exitCode, 1); assert.match(unconfirmed.stderr, /confirm/i); assertWorkflowFilesUnchanged(beforeUnconfirmed); assert.equal(readState(changeDir).dp_0_decisions, null); - const selected = runSsf(['workflow', 'select', changeDir, '--mode', 'hotfix', + const selected = runSsf(['workflow', 'select', changeDir, '--mode', 'quick', '--confirm', '--reason', 'bounded code fix', '--json']); assert.equal(selected.exitCode, 0, selected.stderr); - assert.equal(readState(changeDir).workflow, 'hotfix'); - assert.match(readState(changeDir).dp_0_decisions, /workflow_path=hotfix/); + assert.equal(readState(changeDir).workflow, 'quick'); + assert.match(readState(changeDir).dp_0_decisions, /workflow_path=quick/); }); it('shows complete ready recommendations in human-readable recommend and show output', () => { @@ -95,16 +116,16 @@ describe('ssf workflow', () => { '--schema-api-change', 'no', '--new-module', 'no', '--uncertainty', 'low']); assert.equal(recommended.exitCode, 0, recommended.stderr); assert.match(recommended.stdout, /Observed:/); - assert.match(recommended.stdout, /Available:.*full.*hotfix.*tweak/); - assert.match(recommended.stdout, /Recommended: hotfix/); - assert.match(recommended.stdout, /Why:.*bounded code work/i); + assert.match(recommended.stdout, /Available:.*full.*hotfix.*tweak.*quick/); + assert.match(recommended.stdout, /Recommended: quick/); + assert.match(recommended.stdout, /Why:.*bounded low-risk code work/i); const shown = runSsf(['workflow', 'show', changeDir]); assert.equal(shown.exitCode, 0, shown.stderr); assert.match(shown.stdout, /Observed:/); - assert.match(shown.stdout, /Available:.*full.*hotfix.*tweak/); - assert.match(shown.stdout, /Recommended: hotfix/); - assert.match(shown.stdout, /Why:.*bounded code work/i); + assert.match(shown.stdout, /Available:.*full.*hotfix.*tweak.*quick/); + assert.match(shown.stdout, /Recommended: quick/); + assert.match(shown.stdout, /Why:.*bounded low-risk code work/i); assert.match(shown.stdout, /Hash valid: true/i); }); @@ -128,7 +149,7 @@ describe('ssf workflow', () => { assert.equal(human.exitCode, 0, human.stderr); assert.match(human.stdout, /Workflow status: needs-input/i); assert.match(human.stdout, /Observed:.*task_count=2.*file_count=null/i); - assert.match(human.stdout, /Available:.*full.*hotfix.*tweak/i); + assert.match(human.stdout, /Available:.*full.*hotfix.*tweak.*quick/i); assert.match(human.stdout, /Missing facts: file_count, schema_api_change/i); assert.match(human.stdout, /Hash valid: true/i); @@ -215,7 +236,7 @@ describe('ssf workflow', () => { 'task_count', 'file_count', 'config_doc_only', 'schema_api_change', 'new_module', 'uncertainty', ]); - assert.deepEqual(result.json.available_modes, ['full', 'hotfix', 'tweak']); + assert.deepEqual(result.json.available_modes, ['full', 'hotfix', 'tweak', 'quick']); assert.equal(result.json.recommendation, null); assert.equal(result.json.receipt.exists, false); @@ -240,8 +261,8 @@ describe('ssf workflow', () => { assert.equal(human.exitCode, 1); assert.match(human.stdout, /Workflow status: invalid/i); assert.match(human.stdout, /Observed:.*file_count=99/i); - assert.match(human.stdout, /Available:.*full.*hotfix.*tweak/i); - assert.match(human.stdout, /Recommended: hotfix/i); + assert.match(human.stdout, /Available:.*full.*hotfix.*tweak.*quick/i); + assert.match(human.stdout, /Recommended: quick/i); assert.match(human.stdout, /Why:/i); assert.match(human.stdout, /Hash valid: false/i); assert.match(human.stdout, /hash mismatch/i); @@ -260,31 +281,31 @@ describe('ssf workflow', () => { new_module: 'no', uncertainty: 'low', }); recordWorkflowSelection(changeDir, { - mode: 'hotfix', reason: 'recoverable selection', confirmed: true, acknowledged: false, + mode: 'quick', reason: 'recoverable selection', confirmed: true, acknowledged: false, }); const human = runSsf(['workflow', 'show', changeDir]); assert.equal(human.exitCode, 0, human.stderr); assert.match(human.stdout, /Workflow status: selection-pending/i); assert.match(human.stdout, /Observed:/i); - assert.match(human.stdout, /Available:.*full.*hotfix.*tweak/i); - assert.match(human.stdout, /Recommended: hotfix/i); + assert.match(human.stdout, /Available:.*full.*hotfix.*tweak.*quick/i); + assert.match(human.stdout, /Recommended: quick/i); assert.match(human.stdout, /Why:/i); - assert.match(human.stdout, /Selection:.*mode=hotfix.*reason=recoverable selection/i); + assert.match(human.stdout, /Selection:.*mode=quick.*reason=recoverable selection/i); assert.match(human.stdout, /Hash valid: true/i); const json = runSsf(['workflow', 'show', changeDir, '--json']); assert.equal(json.exitCode, 0, json.stderr); assert.equal(json.json.status, 'selection-pending'); assert.equal(json.json.workflow, 'auto'); - assert.equal(json.json.record.selection.mode, 'hotfix'); + assert.equal(json.json.record.selection.mode, 'quick'); assert.equal(json.json.record.selection.reason, 'recoverable selection'); }); it('restores selected evidence in human and JSON show output', () => { assert.equal(recommend().exitCode, 0); - let result = runSsf(['workflow', 'select', changeDir, '--mode', 'hotfix', + let result = runSsf(['workflow', 'select', changeDir, '--mode', 'quick', '--confirm', '--reason', 'recoverable selection', '--json']); assert.equal(result.exitCode, 0, result.stderr); @@ -292,17 +313,17 @@ describe('ssf workflow', () => { assert.equal(human.exitCode, 0, human.stderr); assert.match(human.stdout, /Workflow status: selected/i); assert.match(human.stdout, /Observed:/i); - assert.match(human.stdout, /Available:.*full.*hotfix.*tweak/i); - assert.match(human.stdout, /Recommended: hotfix/i); + assert.match(human.stdout, /Available:.*full.*hotfix.*tweak.*quick/i); + assert.match(human.stdout, /Recommended: quick/i); assert.match(human.stdout, /Why:/i); - assert.match(human.stdout, /Selection:.*mode=hotfix.*reason=recoverable selection/i); + assert.match(human.stdout, /Selection:.*mode=quick.*reason=recoverable selection/i); assert.match(human.stdout, /Hash valid: true/i); result = runSsf(['workflow', 'show', changeDir, '--json']); assert.equal(result.exitCode, 0, result.stderr); assert.equal(result.json.status, 'selected'); - assert.equal(result.json.workflow, 'hotfix'); - assert.equal(result.json.record.selection.mode, 'hotfix'); + assert.equal(result.json.workflow, 'quick'); + assert.equal(result.json.record.selection.mode, 'quick'); assert.equal(result.json.record.selection.followed_recommendation, true); }); @@ -385,13 +406,13 @@ describe('ssf workflow', () => { '', ].join('\n')); assert.equal(recommend().exitCode, 0); - const selected = runSsf(['workflow', 'select', changeDir, '--mode', 'hotfix', + const selected = runSsf(['workflow', 'select', changeDir, '--mode', 'quick', '--confirm', '--reason', 'bounded code fix', '--json']); assert.equal(selected.exitCode, 0, selected.stderr); const decisions = readState(changeDir).dp_0_decisions; assert.match(decisions, /scope=issue 70/); assert.match(decisions, /artifact_language=zh-CN/); assert.equal((decisions.match(/workflow_path=/g) ?? []).length, 1); - assert.match(decisions, /workflow_path=hotfix; recommended=hotfix; followed_recommendation=true/); + assert.match(decisions, /workflow_path=quick; recommended=quick; followed_recommendation=true/); }); }); diff --git a/tests/lib/infer-workflow.test.mjs b/tests/lib/infer-workflow.test.mjs index 260e675..699b2bc 100644 --- a/tests/lib/infer-workflow.test.mjs +++ b/tests/lib/infer-workflow.test.mjs @@ -42,28 +42,35 @@ describe('infer-workflow: inferMode()', () => { assert.equal(result.explicit, true); }); - it('infers hotfix for small change (≤2 tasks, ≤2 files, no code files in tasks)', () => { + it('infers tweak for a small documentation change', () => { // Use consistent paths — same file names in proposal AND tasks to avoid unique-count inflation writeFileSync(join(tempDir, '.spec-superflow.yaml'), 'state: exploring\nworkflow: auto'); writeFileSync(join(tempDir, 'proposal.md'), '# Proposal\nFix typo in README.md'); writeFileSync(join(tempDir, 'tasks.md'), '- [ ] Fix typo in README.md\n- [ ] Verify fix'); const result = inferMode(tempDir); - // Hotfix check runs before tweak; 2 tasks, 1 file, no keywords → hotfix wins - assert.equal(result.mode, 'hotfix', `Expected hotfix but got ${result.mode}: ${result.reason}`); + assert.equal(result.mode, 'tweak', `Expected tweak but got ${result.mode}: ${result.reason}`); }); - it('infers hotfix for small code change (≤2 tasks, ≤2 files, no schema)', () => { + it('infers quick for small code change (≤2 tasks, ≤2 files, no schema)', () => { writeFileSync(join(tempDir, '.spec-superflow.yaml'), 'state: exploring\nworkflow: auto'); writeFileSync(join(tempDir, 'proposal.md'), '# Proposal\nFix null check in util.ts'); writeFileSync(join(tempDir, 'tasks.md'), '- [ ] Add null check in util.ts\n- [ ] Add test for null case'); const result = inferMode(tempDir); - // 2 tasks, 1 file (util.ts), no schema keyword, code file → hotfix - assert.equal(result.mode, 'hotfix', `Expected hotfix but got ${result.mode}: ${result.reason}`); + assert.equal(result.mode, 'quick', `Expected quick but got ${result.mode}: ${result.reason}`); + }); + + it('infers quick for a bounded three-file code change', () => { + writeFileSync(join(tempDir, '.spec-superflow.yaml'), 'state: exploring\nworkflow: auto'); + writeFileSync(join(tempDir, 'proposal.md'), '# Change\nModify src/a.ts src/b.ts src/c.ts'); + writeFileSync(join(tempDir, 'tasks.md'), '- [ ] Update src/a.ts\n- [ ] Update src/b.ts\n- [ ] Verify src/c.ts'); + + const result = inferMode(tempDir); + assert.equal(result.mode, 'quick'); }); - it('infers hotfix for small Java code changes', () => { + it('infers quick for small Java code changes', () => { const changeDir = mkdtempSync(join(tempDir, 'java-hotfix-')); writeFileSync(join(changeDir, '.spec-superflow.yaml'), 'state: exploring\nworkflow: auto'); writeFileSync(join(changeDir, 'proposal.md'), '# Proposal\nFix null check in src/Main.java'); @@ -71,10 +78,10 @@ describe('infer-workflow: inferMode()', () => { const result = inferMode(changeDir); - assert.equal(result.mode, 'hotfix', `Expected hotfix but got ${result.mode}: ${result.reason}`); + assert.equal(result.mode, 'quick', `Expected quick but got ${result.mode}: ${result.reason}`); }); - it('does not infer tweak for multi-task Java and Go code changes', () => { + it('infers quick for bounded multi-task Java and Go code changes', () => { const javaDir = mkdtempSync(join(tempDir, 'java-code-')); writeFileSync(join(javaDir, '.spec-superflow.yaml'), 'state: exploring\nworkflow: auto'); writeFileSync(join(javaDir, 'proposal.md'), '# Proposal\nRefactor service in src/Main.java'); @@ -85,11 +92,11 @@ describe('infer-workflow: inferMode()', () => { writeFileSync(join(goDir, 'proposal.md'), '# Proposal\nRefactor handler in cmd/server/main.go'); writeFileSync(join(goDir, 'tasks.md'), '- [ ] Update handler in cmd/server/main.go\n- [ ] Add unit test\n- [ ] Update wiring'); - assert.equal(inferMode(javaDir).mode, 'full'); - assert.equal(inferMode(goDir).mode, 'full'); + assert.equal(inferMode(javaDir).mode, 'quick'); + assert.equal(inferMode(goDir).mode, 'quick'); }); - it('does not infer tweak for multi-task Python and Rust code changes', () => { + it('infers quick for bounded multi-task Python and Rust code changes', () => { const pythonDir = mkdtempSync(join(tempDir, 'python-code-')); writeFileSync(join(pythonDir, '.spec-superflow.yaml'), 'state: exploring\nworkflow: auto'); writeFileSync(join(pythonDir, 'proposal.md'), '# Proposal\nRefactor worker in app/worker.py'); @@ -100,8 +107,8 @@ describe('infer-workflow: inferMode()', () => { writeFileSync(join(rustDir, 'proposal.md'), '# Proposal\nRefactor parser in src/parser.rs'); writeFileSync(join(rustDir, 'tasks.md'), '- [ ] Update parser in src/parser.rs\n- [ ] Add unit test\n- [ ] Update caller'); - assert.equal(inferMode(pythonDir).mode, 'full'); - assert.equal(inferMode(rustDir).mode, 'full'); + assert.equal(inferMode(pythonDir).mode, 'quick'); + assert.equal(inferMode(rustDir).mode, 'quick'); }); it('infers tweak for config/doc-only change (≤4 tasks)', () => { @@ -134,13 +141,12 @@ describe('infer-workflow: inferMode()', () => { assert.ok(result.reason.includes('new module')); }); - it('infers full when too many files (> 2) for hotfix', () => { + it('infers full when too many files (> 3) for quick', () => { writeFileSync(join(tempDir, '.spec-superflow.yaml'), 'state: exploring\nworkflow: auto'); - writeFileSync(join(tempDir, 'proposal.md'), '# Big change\nModify src/a.ts src/b.ts src/c.ts'); - writeFileSync(join(tempDir, 'tasks.md'), '- [ ] Task 1\n- [ ] Task 2\n- [ ] Task 3'); + writeFileSync(join(tempDir, 'proposal.md'), '# Big change\nModify src/a.ts src/b.ts src/c.ts src/d.ts'); + writeFileSync(join(tempDir, 'tasks.md'), '- [ ] Task 1\n- [ ] Task 2\n- [ ] Task 3\n- [ ] Task 4'); const result = inferMode(tempDir); - // 3 files > 2 → not hotfix; 3 tasks ≤ 4 but files contain code → not tweak → full assert.equal(result.mode, 'full'); }); diff --git a/tests/lib/workflow-recommendation.test.mjs b/tests/lib/workflow-recommendation.test.mjs index 621f322..c7138ca 100644 --- a/tests/lib/workflow-recommendation.test.mjs +++ b/tests/lib/workflow-recommendation.test.mjs @@ -4,6 +4,7 @@ import { mkdtempSync, readFileSync, rmSync, writeFileSync } from 'node:fs'; import { tmpdir } from 'node:os'; import { join } from 'node:path'; import { + acceptWorkflowRecommendation, recommendWorkflowPath, recordWorkflowSelection, readWorkflowSelection, @@ -22,11 +23,36 @@ const base = { }; describe('workflow path recommendation', () => { - it('recommends hotfix for a bounded code change', () => { + it('recommends quick for a bounded low-risk code change', () => { + const result = recommendWorkflowPath({ ...base, task_count: 3, file_count: 3 }); + assert.equal(result.recommendation.mode, 'quick'); + assert.deepEqual(result.available_modes, ['full', 'hotfix', 'tweak', 'quick']); + }); + + it('recommends hotfix only for a bounded incident', () => { + const result = recommendWorkflowPath({ ...base, request_kind: 'incident' }); + assert.equal(result.recommendation.mode, 'hotfix'); + assert.equal(result.facts.request_kind, 'incident'); + }); + + it('accepts a recommended quick path without a confirmation reason', () => { + const changeDir = mkdtempSync(join(tmpdir(), 'ssf-workflow-accept-')); + try { + saveWorkflowRecommendation(changeDir, base); + const accepted = acceptWorkflowRecommendation(changeDir, { source: 'direct-request' }); + assert.equal(accepted.selection.mode, 'quick'); + assert.equal(accepted.selection.accepted_automatically, true); + assert.equal(accepted.selection.source, 'direct-request'); + } finally { + rmSync(changeDir, { recursive: true, force: true }); + } + }); + + it('recommends quick for a bounded standard code change', () => { const result = recommendWorkflowPath(base); assert.equal(result.status, 'ready'); assert.deepEqual(result.available_modes, WORKFLOW_MODES); - assert.equal(result.recommendation.mode, 'hotfix'); + assert.equal(result.recommendation.mode, 'quick'); }); it('recommends tweak for a small config/doc-only change', () => { @@ -39,7 +65,7 @@ describe('workflow path recommendation', () => { { ...base, schema_api_change: 'yes' }, { ...base, new_module: 'yes' }, { ...base, uncertainty: 'high' }, - { ...base, task_count: 3 }, + { ...base, task_count: 4 }, ]) assert.equal(recommendWorkflowPath(facts).recommendation.mode, 'full'); }); From 4de7e9f0ed84c34f067ad5e886b5f3ee3e82dd3e Mon Sep 17 00:00:00 2001 From: MageByte Date: Sun, 26 Jul 2026 11:34:03 +0800 Subject: [PATCH 02/15] fix: preserve legacy workflow receipts --- scripts/lib/workflow-recommendation.mjs | 15 ++++++-- scripts/spec-superflow.mjs | 12 ++++--- tests/lib/cmd-workflow.test.mjs | 7 ++++ tests/lib/workflow-recommendation.test.mjs | 41 ++++++++++++++++++++-- 4 files changed, 66 insertions(+), 9 deletions(-) diff --git a/scripts/lib/workflow-recommendation.mjs b/scripts/lib/workflow-recommendation.mjs index fff5b2c..773ef72 100644 --- a/scripts/lib/workflow-recommendation.mjs +++ b/scripts/lib/workflow-recommendation.mjs @@ -75,8 +75,9 @@ export function readWorkflowSelection(changeDir) { }; } try { - const record = JSON.parse(readFileSync(path, 'utf8')); - const valid = record.hash === hashRecord(record); + const rawRecord = JSON.parse(readFileSync(path, 'utf8')); + const valid = rawRecord.hash === hashRecord(rawRecord); + const record = valid ? normalizeLegacyRecord(rawRecord) : rawRecord; return { exists: true, valid, @@ -93,6 +94,16 @@ export function readWorkflowSelection(changeDir) { } } +function normalizeLegacyRecord(record) { + if (record?.facts && !Object.hasOwn(record.facts, 'request_kind')) { + return { + ...record, + facts: { ...record.facts, request_kind: 'standard' }, + }; + } + return record; +} + export function recordWorkflowSelection(changeDir, { mode, reason, confirmed, acknowledged }) { const loaded = readWorkflowSelection(changeDir); if (!loaded.valid) throw new Error(loaded.failures.join('; ')); diff --git a/scripts/spec-superflow.mjs b/scripts/spec-superflow.mjs index dec715d..6a3d011 100755 --- a/scripts/spec-superflow.mjs +++ b/scripts/spec-superflow.mjs @@ -84,10 +84,12 @@ Commands: Recover an explicit change context without changing the shell runtime check-update Run a portable update check for canonical skills runtime infer Infer workflow mode without a plugin-root path - workflow recommend [--task-count ] [--file-count ] [--config-doc-only yes|no|unknown] [--schema-api-change yes|no|unknown] [--new-module yes|no|unknown] [--uncertainty low|high|unknown] - Persist observed intake facts and recommend full, hotfix, or tweak without selecting one - workflow select --mode full|hotfix|tweak --confirm --reason [--acknowledge-recommendation] + workflow recommend [--task-count ] [--file-count ] [--config-doc-only yes|no|unknown] [--schema-api-change yes|no|unknown] [--new-module yes|no|unknown] [--uncertainty low|high|unknown] [--request-kind standard|incident] + Persist observed intake facts and recommend full, hotfix, tweak, or quick without selecting one + workflow select --mode full|hotfix|tweak|quick --confirm --reason [--acknowledge-recommendation] Persist a user-confirmed workflow choice after a ready recommendation + workflow accept --source direct-request + Directly accept a recommended quick or hotfix workflow workflow show [--json] Show the saved workflow recommendation or selection recovery state runtime guard ... Run a portable phase-transition guard @@ -122,8 +124,8 @@ Examples: ssf state init changes/my-change/ ssf state check changes/my-change/ ssf state transition changes/my-change/ approved-for-build - ssf workflow recommend changes/fix-typo --task-count 1 --file-count 1 --config-doc-only no --schema-api-change no --new-module no --uncertainty low - ssf workflow select changes/fix-typo --mode hotfix --confirm --reason "bounded code fix" + ssf workflow recommend changes/fix-typo --task-count 1 --file-count 1 --config-doc-only no --schema-api-change no --new-module no --uncertainty low --request-kind incident + ssf workflow accept changes/fix-typo --source direct-request ssf state get changes/my-change/ batches_completed ssf checkpoint save changes/my-change/ --task 1.1 --next "Run focused tests" ssf checkpoint list changes/my-change/ diff --git a/tests/lib/cmd-workflow.test.mjs b/tests/lib/cmd-workflow.test.mjs index 2dfe0c8..e243d6a 100644 --- a/tests/lib/cmd-workflow.test.mjs +++ b/tests/lib/cmd-workflow.test.mjs @@ -68,6 +68,13 @@ afterEach(() => { }); describe('ssf workflow', () => { + it('advertises quick and direct acceptance in global help', () => { + const result = runSsf(['--help']); + assert.equal(result.exitCode, 0, result.stderr); + assert.match(result.stdout, /workflow select .*full\|hotfix\|tweak\|quick/); + assert.match(result.stdout, /workflow accept --source direct-request/); + }); + it('accepts a recommended quick path from a direct request without --confirm', () => { const recommended = recommend(); assert.equal(recommended.exitCode, 0, recommended.stderr); diff --git a/tests/lib/workflow-recommendation.test.mjs b/tests/lib/workflow-recommendation.test.mjs index c7138ca..e159cd2 100644 --- a/tests/lib/workflow-recommendation.test.mjs +++ b/tests/lib/workflow-recommendation.test.mjs @@ -1,8 +1,9 @@ import { describe, it } from 'node:test'; import assert from 'node:assert/strict'; -import { mkdtempSync, readFileSync, rmSync, writeFileSync } from 'node:fs'; +import { createHash } from 'node:crypto'; +import { mkdirSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from 'node:fs'; import { tmpdir } from 'node:os'; -import { join } from 'node:path'; +import { dirname, join } from 'node:path'; import { acceptWorkflowRecommendation, recommendWorkflowPath, @@ -22,6 +23,17 @@ const base = { uncertainty: 'low', }; +function stableJson(value) { + if (value === null || typeof value !== 'object') return JSON.stringify(value); + if (Array.isArray(value)) return `[${value.map(stableJson).join(',')}]`; + return `{${Object.keys(value).sort().map(key => `${JSON.stringify(key)}:${stableJson(value[key])}`).join(',')}}`; +} + +function hashRecord(record) { + const { hash, ...content } = record; + return `sha256:${createHash('sha256').update(stableJson(content)).digest('hex')}`; +} + describe('workflow path recommendation', () => { it('recommends quick for a bounded low-risk code change', () => { const result = recommendWorkflowPath({ ...base, task_count: 3, file_count: 3 }); @@ -48,6 +60,31 @@ describe('workflow path recommendation', () => { } }); + it('reads a valid legacy receipt without request_kind as standard', () => { + const changeDir = mkdtempSync(join(tmpdir(), 'ssf-workflow-legacy-')); + try { + const legacy = { + schema_version: 1, + available_modes: ['full', 'hotfix', 'tweak'], + facts: { ...base }, + missing_facts: [], + status: 'ready', + recommendation: { mode: 'hotfix', reasons: ['legacy bounded code work'] }, + created_at: '2026-07-01T00:00:00.000Z', + selection: null, + }; + legacy.hash = hashRecord(legacy); + const receiptPath = getOverlayPaths(changeDir).workflowSelection; + mkdirSync(dirname(receiptPath), { recursive: true }); + writeFileSync(receiptPath, JSON.stringify(legacy), 'utf8'); + const loaded = readWorkflowSelection(changeDir); + assert.equal(loaded.valid, true); + assert.equal(loaded.record.facts.request_kind, 'standard'); + } finally { + rmSync(changeDir, { recursive: true, force: true }); + } + }); + it('recommends quick for a bounded standard code change', () => { const result = recommendWorkflowPath(base); assert.equal(result.status, 'ready'); From 43fc7577f0866f1c5d8bcdda5c9ebb7658cbdcd1 Mon Sep 17 00:00:00 2001 From: MageByte Date: Sun, 26 Jul 2026 11:36:35 +0800 Subject: [PATCH 03/15] fix: restrict direct hotfix acceptance --- scripts/lib/workflow-recommendation.mjs | 3 +++ tests/lib/workflow-recommendation.test.mjs | 4 ++++ 2 files changed, 7 insertions(+) diff --git a/scripts/lib/workflow-recommendation.mjs b/scripts/lib/workflow-recommendation.mjs index 773ef72..b0075b9 100644 --- a/scripts/lib/workflow-recommendation.mjs +++ b/scripts/lib/workflow-recommendation.mjs @@ -144,6 +144,9 @@ export function acceptWorkflowRecommendation(changeDir, { source }) { if (!['quick', 'hotfix'].includes(recommendation.mode)) { throw new Error('only a recommended quick or hotfix workflow can be accepted directly'); } + if (recommendation.mode === 'hotfix' && loaded.record.facts.request_kind !== 'incident') { + throw new Error('direct hotfix acceptance requires an incident request'); + } if (source !== 'direct-request') { throw new Error('workflow acceptance source must be direct-request'); } diff --git a/tests/lib/workflow-recommendation.test.mjs b/tests/lib/workflow-recommendation.test.mjs index e159cd2..b893ed9 100644 --- a/tests/lib/workflow-recommendation.test.mjs +++ b/tests/lib/workflow-recommendation.test.mjs @@ -80,6 +80,10 @@ describe('workflow path recommendation', () => { const loaded = readWorkflowSelection(changeDir); assert.equal(loaded.valid, true); assert.equal(loaded.record.facts.request_kind, 'standard'); + assert.throws( + () => acceptWorkflowRecommendation(changeDir, { source: 'direct-request' }), + /incident/i, + ); } finally { rmSync(changeDir, { recursive: true, force: true }); } From a4a84e7814a90cb8e3f5dec020f598b1b9afd60b Mon Sep 17 00:00:00 2001 From: MageByte Date: Sun, 26 Jul 2026 11:44:04 +0800 Subject: [PATCH 04/15] feat: route quick and hotfix through direct guards --- scripts/guard/guard.mjs | 55 +++++++++++++++++++++----- scripts/lib/cmd-inject.mjs | 54 +++++++++++++++++++++++++- tests/lib/cmd-inject.test.mjs | 10 +++++ tests/lib/cmd-state.test.mjs | 14 +++++++ tests/lib/guard.test.mjs | 72 ++++++++++++++++++++++++++++++++++- 5 files changed, 192 insertions(+), 13 deletions(-) diff --git a/scripts/guard/guard.mjs b/scripts/guard/guard.mjs index becdae2..c721583 100644 --- a/scripts/guard/guard.mjs +++ b/scripts/guard/guard.mjs @@ -12,6 +12,8 @@ import { checkContractCurrent } from './checks/contract-current.mjs'; import { checkDp3Approved } from './checks/dp3-approved.mjs'; import { checkExecutionPlanReady } from './checks/execution-plan-ready.mjs'; import { checkExecutionReviewsPassed } from './checks/execution-reviews-passed.mjs'; +import { readState } from '../lib/state-loader.mjs'; +import { readWorkflowSelection } from '../lib/workflow-recommendation.mjs'; // Transition matrix: : → required check dimensions const TRANSITION_CHECKS = { @@ -56,17 +58,22 @@ const WORKFLOW_TRANSITION_CHECKS = { }, tweak: { 'exploring:approved-for-build': [], - // Tweak remains a low-risk fast path: it is intentionally exempt from - // execution-plan and per-wave review receipt requirements. - 'approved-for-build:executing': ['artifacts-exist', 'contract-fresh', 'dp-gate-passed'], - 'executing:closing': ['tasks-complete', 'tests-passing', 'specs-merged'], - 'debugging:executing': ['contract-fresh'], + 'approved-for-build:executing': [], + 'executing:closing': ['tests-passing'], + 'debugging:executing': [], }, }; +const DIRECT_SHORT_PATH_CHECKS = { + 'exploring:approved-for-build': ['direct-short-path'], + 'approved-for-build:executing': ['direct-short-path'], + 'executing:closing': ['direct-short-path', 'tests-passing'], + 'debugging:executing': ['direct-short-path'], +}; + const TRANSITION_WORKFLOW_REQUIREMENTS = { 'exploring:bridging': ['hotfix'], - 'exploring:approved-for-build': ['tweak'], + 'exploring:approved-for-build': ['tweak', 'quick', 'hotfix'], }; function checkWorkflowAllowed(key, workflow) { @@ -82,10 +89,38 @@ function checkWorkflowAllowed(key, workflow) { }; } -function resolveDimensions(key, workflow) { +function resolveDimensions(key, workflow, directShortPath) { + if (workflow === 'quick') return DIRECT_SHORT_PATH_CHECKS[key] ?? TRANSITION_CHECKS[key]; + if (workflow === 'hotfix' && key === 'exploring:approved-for-build') { + return DIRECT_SHORT_PATH_CHECKS[key]; + } + if (workflow === 'hotfix' && directShortPath) { + return DIRECT_SHORT_PATH_CHECKS[key] ?? WORKFLOW_TRANSITION_CHECKS.hotfix[key] ?? TRANSITION_CHECKS[key]; + } return WORKFLOW_TRANSITION_CHECKS[workflow]?.[key] ?? TRANSITION_CHECKS[key]; } +export function isDirectShortPath(record, state) { + const selection = record?.selection; + const mode = selection?.mode; + if (!['quick', 'hotfix'].includes(mode) || state?.workflow !== mode) return false; + if (record?.status !== 'ready' || record?.recommendation?.mode !== mode) return false; + if (selection.accepted_automatically !== true || selection.source !== 'direct-request') return false; + return mode !== 'hotfix' || record?.facts?.request_kind === 'incident'; +} + +function directShortPathCheck(changeDir, workflow) { + const state = readState(changeDir); + const receipt = readWorkflowSelection(changeDir); + if (!receipt.valid || !isDirectShortPath(receipt.record, state) || state.workflow !== workflow) { + return { + pass: false, + failures: ['a valid direct receipt matching the current workflow is required for this short-path transition'], + }; + } + return { pass: true, failures: [] }; +} + async function main() { const { positionals, values } = parseArgs({ options: { @@ -107,7 +142,7 @@ async function main() { const useJson = values.json; const workflow = values.workflow; - const VALID_WORKFLOWS = ['full', 'hotfix', 'tweak']; + const VALID_WORKFLOWS = ['full', 'hotfix', 'tweak', 'quick']; if (!VALID_WORKFLOWS.includes(workflow)) { console.error(`Invalid workflow: ${workflow}. Must be one of: ${VALID_WORKFLOWS.join(', ')}`); process.exit(2); @@ -119,7 +154,8 @@ async function main() { } const key = `${fromState}:${toState}`; - const dimensions = resolveDimensions(key, workflow); + const directShortPath = isDirectShortPath(readWorkflowSelection(changeDir).record, readState(changeDir)); + const dimensions = resolveDimensions(key, workflow, directShortPath); if (!dimensions) { const valid = Object.keys(TRANSITION_CHECKS).join(', '); @@ -163,6 +199,7 @@ async function main() { 'dp3-approved': (dir) => checkDp3Approved(dir), 'execution-plan-ready': (dir) => checkExecutionPlanReady(dir), 'execution-reviews-passed': (dir) => checkExecutionReviewsPassed(dir), + 'direct-short-path': (dir) => directShortPathCheck(dir, workflow), }; const checks = []; diff --git a/scripts/lib/cmd-inject.mjs b/scripts/lib/cmd-inject.mjs index 2edd1bc..d2c4ed1 100644 --- a/scripts/lib/cmd-inject.mjs +++ b/scripts/lib/cmd-inject.mjs @@ -126,6 +126,46 @@ const PHASE_TEMPLATES = { - 不得从 abandoned 状态转换`, }; +const SHORT_PATH_TEMPLATES = { + 'approved-for-build': `# Phase Guard: {{change_name}} + +**当前阶段**: {{state}} | **工作流**: {{workflow}} + +## ✅ 允许操作 +- 确认 {{authorization}} +- 立即开始边界内实现 + +## ⛔ 禁止操作 +- 不得要求 execution plan、wave review 或 DP-4 +- 发现第 4 个文件、接口/权限/依赖/数据迁移、新模块、高不确定性或验证失败时,停止并升级到 Full + +## 🔔 验证要求 +- 保持改动在已推荐的文件与任务边界内`, + + 'executing': `# Phase Guard: {{change_name}} + +**当前阶段**: {{state}} | **工作流**: {{workflow}} + +## ✅ 允许操作 +- 完成边界内实现并运行{{verification}} +- 记录改动文件、验证命令与结果 + +## ⛔ 禁止操作 +- 不得要求 execution plan、wave review、DP-6 或 DP-7 +- 不得扩大范围;触发升级条件时停止并改走 Full`, + + 'closing': `# Phase Guard: {{change_name}} + +**当前阶段**: {{state}} | **工作流**: {{workflow}} + +## ✅ 收口要求 +- 持久化 test_result: pass,并交付简短验证摘要(文件、命令、结果) + +## ⛔ 禁止操作 +- 不得要求 execution plan、wave review、DP-6 或 DP-7 +- 此变更关闭后不得继续实现`, +}; + const SUPPORTED_PLATFORMS = ['claude', 'cursor', 'copilot', 'gemini']; const PHASE_GUARD_MARKERS = { start: '\n\n\n', @@ -137,11 +177,21 @@ function unique(values) { } function generatePhaseGuard(state) { - const template = PHASE_TEMPLATES[state.state] || PHASE_TEMPLATES['exploring']; + const workflow = state.workflow || 'full'; + const isShortPath = ['quick', 'hotfix', 'tweak'].includes(workflow); + const template = isShortPath && SHORT_PATH_TEMPLATES[state.state] + ? SHORT_PATH_TEMPLATES[state.state] + : (PHASE_TEMPLATES[state.state] || PHASE_TEMPLATES.exploring); + const authorization = workflow === 'tweak' + ? '已选择的 Tweak 路径' + : 'valid direct receipt(direct receipt)'; + const verification = workflow === 'hotfix' ? '原症状回归验证' : '定向测试或语法/静态检查'; return template .replace(/\{\{change_name\}\}/g, state.change_name || 'unknown') .replace(/\{\{state\}\}/g, state.state || 'exploring') - .replace(/\{\{workflow\}\}/g, state.workflow || 'full'); + .replace(/\{\{workflow\}\}/g, workflow) + .replace(/\{\{authorization\}\}/g, authorization) + .replace(/\{\{verification\}\}/g, verification); } function toCursorMdc(base) { diff --git a/tests/lib/cmd-inject.test.mjs b/tests/lib/cmd-inject.test.mjs index 95f8722..355411b 100644 --- a/tests/lib/cmd-inject.test.mjs +++ b/tests/lib/cmd-inject.test.mjs @@ -59,6 +59,16 @@ describe('cmd-inject: generatePhaseGuard()', () => { assert.match(result, /不得开始实现/); }); + it('generates a quick phase guard without plan, review, or DP approval language', () => { + const approved = generatePhaseGuard({ state: 'approved-for-build', workflow: 'quick', change_name: 'test' }); + assert.match(approved, /direct receipt/i); + assert.match(approved, /不得要求 execution plan、wave review 或 DP-4/i); + + const executing = generatePhaseGuard({ state: 'executing', workflow: 'quick', change_name: 'test' }); + assert.match(executing, /定向测试/i); + assert.doesNotMatch(executing, /execution-contract\.md/); + }); + it('generates executing phase with test prohibition', () => { const result = generatePhaseGuard({ state: 'executing', change_name: 'test' }); assert.ok(result.includes('跳过测试')); diff --git a/tests/lib/cmd-state.test.mjs b/tests/lib/cmd-state.test.mjs index 08babd8..82b12df 100644 --- a/tests/lib/cmd-state.test.mjs +++ b/tests/lib/cmd-state.test.mjs @@ -170,6 +170,20 @@ describe('cmd-state: transition', () => { assert.equal(check.stdout.trim(), 'exploring'); }); + it('accepts a direct quick receipt through the no-contract transition path', () => { + rmSync(join(tempDir, '.spec-superflow.yaml'), { force: true }); + ssf(`state init ${tempDir}`); + const recommendation = ssf(`workflow recommend ${tempDir} --task-count 3 --file-count 3 --config-doc-only no --schema-api-change no --new-module no --uncertainty low`); + assert.equal(recommendation.exitCode, 0, recommendation.stderr); + const acceptance = ssf(`workflow accept ${tempDir} --source direct-request`); + assert.equal(acceptance.exitCode, 0, acceptance.stderr); + + const approved = ssf(`state transition ${tempDir} approved-for-build`); + assert.equal(approved.exitCode, 0, approved.stderr); + const executing = ssf(`state transition ${tempDir} executing`); + assert.equal(executing.exitCode, 0, executing.stderr); + }); + it('rejects transition when guard output is not valid JSON', () => { rmSync(join(tempDir, '.spec-superflow.yaml'), { force: true }); ssf(`state init ${tempDir}`); diff --git a/tests/lib/guard.test.mjs b/tests/lib/guard.test.mjs index 6dc50bf..3aaa0b7 100644 --- a/tests/lib/guard.test.mjs +++ b/tests/lib/guard.test.mjs @@ -6,6 +6,7 @@ import { mkdtempSync, writeFileSync, mkdirSync, readFileSync, rmSync, symlinkSyn import { join } from 'node:path'; import { tmpdir } from 'node:os'; import { execFileSync } from 'node:child_process'; +import { acceptWorkflowRecommendation, saveWorkflowRecommendation } from '../../scripts/lib/workflow-recommendation.mjs'; let tempDir; let gitRefs; @@ -176,6 +177,73 @@ describe('guard: workflow mode behavior', () => { }); }); +describe('guard: direct short paths', () => { + let dir; + + before(() => { + dir = mkdtempSync(join(tmpdir(), 'ssf-direct-short-path-')); + }); + + after(() => { + if (dir) rmSync(dir, { recursive: true, force: true }); + }); + + function createDirectReceipt(workflow) { + rmSync(dir, { recursive: true, force: true }); + mkdirSync(dir, { recursive: true }); + writeFileSync(join(dir, '.spec-superflow.yaml'), `state: exploring\nworkflow: ${workflow}\n`); + saveWorkflowRecommendation(dir, { + task_count: workflow === 'hotfix' ? 2 : 3, + file_count: workflow === 'hotfix' ? 2 : 3, + config_doc_only: 'no', + schema_api_change: 'no', + new_module: 'no', + uncertainty: 'low', + request_kind: workflow === 'hotfix' ? 'incident' : 'standard', + }); + acceptWorkflowRecommendation(dir, { source: 'direct-request' }); + } + + function run(fromState, toState, workflow) { + try { + const stdout = runNodeScript(GUARD_PATH, ['check', dir, fromState, toState, '--json', '--workflow', workflow]); + return { exitCode: 0, output: JSON.parse(stdout.trim()) }; + } catch (error) { + return { exitCode: error.status ?? 1, output: JSON.parse(error.stdout.toString().trim()) }; + } + } + + it('allows a direct quick path without planning artifacts or an execution plan', () => { + createDirectReceipt('quick'); + let result = run('exploring', 'approved-for-build', 'quick'); + assert.equal(result.exitCode, 0, JSON.stringify(result.output)); + assert.deepEqual(result.output.checks.map(check => check.dimension), ['direct-short-path']); + + result = run('approved-for-build', 'executing', 'quick'); + assert.equal(result.exitCode, 0, JSON.stringify(result.output)); + assert.deepEqual(result.output.checks.map(check => check.dimension), ['direct-short-path']); + + writeFileSync(join(dir, '.spec-superflow.yaml'), 'state: executing\nworkflow: quick\ntest_result: pass: focused test\n'); + result = run('executing', 'closing', 'quick'); + assert.equal(result.exitCode, 0, JSON.stringify(result.output)); + assert.deepEqual(result.output.checks.map(check => check.dimension), ['direct-short-path', 'tests-passing']); + }); + + it('allows only an incident-backed direct hotfix and keeps legacy hotfix guarded', () => { + createDirectReceipt('hotfix'); + const direct = run('exploring', 'approved-for-build', 'hotfix'); + assert.equal(direct.exitCode, 0, JSON.stringify(direct.output)); + assert.deepEqual(direct.output.checks.map(check => check.dimension), ['direct-short-path']); + + rmSync(dir, { recursive: true, force: true }); + mkdirSync(dir, { recursive: true }); + writeFileSync(join(dir, '.spec-superflow.yaml'), 'state: exploring\nworkflow: hotfix\n'); + const legacy = run('exploring', 'approved-for-build', 'hotfix'); + assert.equal(legacy.exitCode, 1); + assert.match(legacy.output.checks[0].failures.join(' '), /direct receipt/i); + }); +}); + describe('guard: hotfix minimal contract', () => { let dir; @@ -391,14 +459,14 @@ describe('guard: execution control records', () => { assert.match(planCheck.failures.join('\n'), /plan.*missing|execution plan/i); }); - it('keeps a debugging return in tweak workflow limited to contract freshness', () => { + it('keeps a debugging return in tweak workflow free of contract checks', () => { prepareFreshFullState(); setStateField('workflow', 'tweak'); const result = run('debugging', 'executing', 'tweak'); assert.equal(result.exitCode, 0, JSON.stringify(result.output)); - assert.deepEqual(result.output.checks.map(check => check.dimension), ['contract-fresh']); + assert.deepEqual(result.output.checks, []); }); it('rejects a debugging return when the execution plan is stale', () => { From f44e6ef507f4725e91b137d52724f295e9a31317 Mon Sep 17 00:00:00 2001 From: MageByte Date: Sun, 26 Jul 2026 11:49:33 +0800 Subject: [PATCH 05/15] fix: keep direct guard evidence isolated --- scripts/guard/guard.mjs | 23 ++++++++++----- scripts/lib/cmd-inject.mjs | 10 +++++-- scripts/lib/workflow-recommendation.mjs | 9 ++++++ tests/lib/cmd-inject.test.mjs | 39 +++++++++++++++++++++++-- tests/lib/guard.test.mjs | 7 ++++- 5 files changed, 73 insertions(+), 15 deletions(-) diff --git a/scripts/guard/guard.mjs b/scripts/guard/guard.mjs index c721583..2e29d56 100644 --- a/scripts/guard/guard.mjs +++ b/scripts/guard/guard.mjs @@ -13,7 +13,7 @@ import { checkDp3Approved } from './checks/dp3-approved.mjs'; import { checkExecutionPlanReady } from './checks/execution-plan-ready.mjs'; import { checkExecutionReviewsPassed } from './checks/execution-reviews-passed.mjs'; import { readState } from '../lib/state-loader.mjs'; -import { readWorkflowSelection } from '../lib/workflow-recommendation.mjs'; +import { isDirectWorkflowReceipt, readWorkflowSelection } from '../lib/workflow-recommendation.mjs'; // Transition matrix: : → required check dimensions const TRANSITION_CHECKS = { @@ -67,7 +67,7 @@ const WORKFLOW_TRANSITION_CHECKS = { const DIRECT_SHORT_PATH_CHECKS = { 'exploring:approved-for-build': ['direct-short-path'], 'approved-for-build:executing': ['direct-short-path'], - 'executing:closing': ['direct-short-path', 'tests-passing'], + 'executing:closing': ['direct-short-path', 'direct-test-result'], 'debugging:executing': ['direct-short-path'], }; @@ -101,12 +101,7 @@ function resolveDimensions(key, workflow, directShortPath) { } export function isDirectShortPath(record, state) { - const selection = record?.selection; - const mode = selection?.mode; - if (!['quick', 'hotfix'].includes(mode) || state?.workflow !== mode) return false; - if (record?.status !== 'ready' || record?.recommendation?.mode !== mode) return false; - if (selection.accepted_automatically !== true || selection.source !== 'direct-request') return false; - return mode !== 'hotfix' || record?.facts?.request_kind === 'incident'; + return isDirectWorkflowReceipt(record, state); } function directShortPathCheck(changeDir, workflow) { @@ -121,6 +116,17 @@ function directShortPathCheck(changeDir, workflow) { return { pass: true, failures: [] }; } +function directTestResultCheck(changeDir) { + const testResult = readState(changeDir).test_result; + if (typeof testResult === 'string' && testResult.trim().toLowerCase().startsWith('pass')) { + return { pass: true, failures: [] }; + } + return { + pass: false, + failures: ['direct short-path closing requires test_result starting with pass; DP-6 is not a substitute'], + }; +} + async function main() { const { positionals, values } = parseArgs({ options: { @@ -200,6 +206,7 @@ async function main() { 'execution-plan-ready': (dir) => checkExecutionPlanReady(dir), 'execution-reviews-passed': (dir) => checkExecutionReviewsPassed(dir), 'direct-short-path': (dir) => directShortPathCheck(dir, workflow), + 'direct-test-result': (dir) => directTestResultCheck(dir), }; const checks = []; diff --git a/scripts/lib/cmd-inject.mjs b/scripts/lib/cmd-inject.mjs index d2c4ed1..b5b819e 100644 --- a/scripts/lib/cmd-inject.mjs +++ b/scripts/lib/cmd-inject.mjs @@ -3,6 +3,7 @@ import { readFileSync, writeFileSync, mkdirSync, existsSync } from 'node:fs'; import { join, dirname } from 'node:path'; import { parseArgs } from 'node:util'; import { readState } from './state-loader.mjs'; +import { isDirectWorkflowReceipt, readWorkflowSelection } from './workflow-recommendation.mjs'; const PHASE_TEMPLATES = { 'exploring': `# Phase Guard: {{change_name}} @@ -176,9 +177,9 @@ function unique(values) { return [...new Set(values)]; } -function generatePhaseGuard(state) { +function generatePhaseGuard(state, { directShortPath = false } = {}) { const workflow = state.workflow || 'full'; - const isShortPath = ['quick', 'hotfix', 'tweak'].includes(workflow); + const isShortPath = workflow === 'tweak' || directShortPath; const template = isShortPath && SHORT_PATH_TEMPLATES[state.state] ? SHORT_PATH_TEMPLATES[state.state] : (PHASE_TEMPLATES[state.state] || PHASE_TEMPLATES.exploring); @@ -331,7 +332,10 @@ export async function run(args) { const state = readState(changeDir); // Generate base phase-guard content - const base = generatePhaseGuard(state); + const receipt = readWorkflowSelection(changeDir); + const base = generatePhaseGuard(state, { + directShortPath: receipt.valid && isDirectWorkflowReceipt(receipt.record, state), + }); const outputs = []; for (const platform of requested) { diff --git a/scripts/lib/workflow-recommendation.mjs b/scripts/lib/workflow-recommendation.mjs index b0075b9..b113343 100644 --- a/scripts/lib/workflow-recommendation.mjs +++ b/scripts/lib/workflow-recommendation.mjs @@ -166,6 +166,15 @@ export function acceptWorkflowRecommendation(changeDir, { source }) { return accepted; } +export function isDirectWorkflowReceipt(record, state) { + const selection = record?.selection; + const mode = selection?.mode; + if (!['quick', 'hotfix'].includes(mode) || state?.workflow !== mode) return false; + if (record?.status !== 'ready' || record?.recommendation?.mode !== mode) return false; + if (selection.accepted_automatically !== true || selection.source !== 'direct-request') return false; + return mode !== 'hotfix' || record?.facts?.request_kind === 'incident'; +} + function ready(base, mode, reason) { return { ...base, status: 'ready', recommendation: { mode, reasons: [reason] } }; } diff --git a/tests/lib/cmd-inject.test.mjs b/tests/lib/cmd-inject.test.mjs index 355411b..254e89c 100644 --- a/tests/lib/cmd-inject.test.mjs +++ b/tests/lib/cmd-inject.test.mjs @@ -3,9 +3,10 @@ import { describe, it, before } from 'node:test'; import assert from 'node:assert/strict'; import { execFileSync } from 'node:child_process'; -import { existsSync, mkdirSync, mkdtempSync, rmSync, writeFileSync } from 'node:fs'; +import { existsSync, mkdirSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from 'node:fs'; import { tmpdir } from 'node:os'; import { join } from 'node:path'; +import { acceptWorkflowRecommendation, saveWorkflowRecommendation } from '../../scripts/lib/workflow-recommendation.mjs'; let generatePhaseGuard, toCursorMdc, toCopilotInstructions; @@ -60,15 +61,21 @@ describe('cmd-inject: generatePhaseGuard()', () => { }); it('generates a quick phase guard without plan, review, or DP approval language', () => { - const approved = generatePhaseGuard({ state: 'approved-for-build', workflow: 'quick', change_name: 'test' }); + const approved = generatePhaseGuard({ state: 'approved-for-build', workflow: 'quick', change_name: 'test' }, { directShortPath: true }); assert.match(approved, /direct receipt/i); assert.match(approved, /不得要求 execution plan、wave review 或 DP-4/i); - const executing = generatePhaseGuard({ state: 'executing', workflow: 'quick', change_name: 'test' }); + const executing = generatePhaseGuard({ state: 'executing', workflow: 'quick', change_name: 'test' }, { directShortPath: true }); assert.match(executing, /定向测试/i); assert.doesNotMatch(executing, /execution-contract\.md/); }); + it('keeps legacy hotfix phase guards on the contract and review path', () => { + const legacy = generatePhaseGuard({ state: 'approved-for-build', workflow: 'hotfix', change_name: 'test' }); + assert.match(legacy, /execution plan/); + assert.match(legacy, /DP-4/); + }); + it('generates executing phase with test prohibition', () => { const result = generatePhaseGuard({ state: 'executing', change_name: 'test' }); assert.ok(result.includes('跳过测试')); @@ -215,6 +222,32 @@ describe('cmd-inject: CLI writes', () => { } }); + it('uses direct wording only when the short-path receipt is valid', () => { + const root = mkdtempSync(join(tmpdir(), 'ssf-inject-cli-direct-')); + try { + const change = join(root, 'change'); + mkdirSync(change, { recursive: true }); + writeFileSync(join(change, '.spec-superflow.yaml'), 'state: approved-for-build\nworkflow: quick\nchange_name: inject-test\n'); + saveWorkflowRecommendation(change, { + task_count: 3, file_count: 3, config_doc_only: 'no', schema_api_change: 'no', + new_module: 'no', uncertainty: 'low', request_kind: 'standard', + }); + acceptWorkflowRecommendation(change, { source: 'direct-request' }); + const result = runInject(root, change, ['--platforms', 'cursor']); + assert.equal(result.exitCode, 0, result.stdout + result.stderr); + const guard = readFileSync(join(root, '.cursor', 'rules', 'phase-guard.mdc'), 'utf8'); + assert.match(guard, /direct receipt/i); + + writeFileSync(join(change, '.spec-superflow.yaml'), 'state: approved-for-build\nworkflow: hotfix\nchange_name: inject-test\n'); + const legacy = runInject(root, change, ['--platforms', 'cursor']); + assert.equal(legacy.exitCode, 0, legacy.stdout + legacy.stderr); + const legacyGuard = readFileSync(join(root, '.cursor', 'rules', 'phase-guard.mdc'), 'utf8'); + assert.match(legacyGuard, /execution plan/); + } finally { + rmSync(root, { recursive: true, force: true }); + } + }); + it('omitted ambiguous platform exits before writing files', () => { const root = mkdtempSync(join(tmpdir(), 'ssf-inject-cli-none-')); try { diff --git a/tests/lib/guard.test.mjs b/tests/lib/guard.test.mjs index 3aaa0b7..5fbc8a9 100644 --- a/tests/lib/guard.test.mjs +++ b/tests/lib/guard.test.mjs @@ -226,7 +226,12 @@ describe('guard: direct short paths', () => { writeFileSync(join(dir, '.spec-superflow.yaml'), 'state: executing\nworkflow: quick\ntest_result: pass: focused test\n'); result = run('executing', 'closing', 'quick'); assert.equal(result.exitCode, 0, JSON.stringify(result.output)); - assert.deepEqual(result.output.checks.map(check => check.dimension), ['direct-short-path', 'tests-passing']); + assert.deepEqual(result.output.checks.map(check => check.dimension), ['direct-short-path', 'direct-test-result']); + + writeFileSync(join(dir, '.spec-superflow.yaml'), 'state: executing\nworkflow: quick\ndp_6_result: pass: insufficient for direct closing\n'); + result = run('executing', 'closing', 'quick'); + assert.equal(result.exitCode, 1); + assert.equal(result.output.checks.find(check => check.dimension === 'direct-test-result').pass, false); }); it('allows only an incident-backed direct hotfix and keeps legacy hotfix guarded', () => { From 4beceabbeeb1c4e82ae6f99498dbfcaa9a4e7de8 Mon Sep 17 00:00:00 2001 From: MageByte Date: Sun, 26 Jul 2026 11:54:18 +0800 Subject: [PATCH 06/15] feat: make quick and hotfix direct execution paths --- skills/build-executor/SKILL.md | 4 ++++ skills/contract-builder/SKILL.md | 2 +- skills/release-archivist/SKILL.md | 4 ++-- skills/workflow-start/SKILL.md | 13 ++++++++++++- tests/lib/execution-control-plane.test.mjs | 13 +++++++++++++ tests/lib/workflow-start-recommendation.test.mjs | 13 ++++++++++++- 6 files changed, 44 insertions(+), 5 deletions(-) diff --git a/skills/build-executor/SKILL.md b/skills/build-executor/SKILL.md index b25933a..fe183fa 100644 --- a/skills/build-executor/SKILL.md +++ b/skills/build-executor/SKILL.md @@ -147,6 +147,10 @@ If task hits BLOCKED (3+ fix failures or changes outside declared scope), escala Skip TDD. Apply changes directly. Verify file integrity (exists, non-empty, valid syntax). No batch execution — sequential changes. +## Direct Quick and Hotfix + +Quick direct execution requires the valid direct receipt, a bounded diff, targeted tests or syntax/static checks, and a persisted `test_result: pass`; do not create a contract, execution plan, wave review, DP-6, or DP-7. Direct Hotfix follows the same route only for an incident-backed receipt and must run a regression that demonstrates the original symptom is fixed. Stop rather than expanding scope when the boundary is exceeded or verification fails; route to Full. A legacy Hotfix without a direct receipt remains subject to the contract, DP-3, execution plan, and review receipts. + ## DP Records DP-4 is written by `npx --yes --package spec-superflow@0.11.0 ssf execution plan`; do not write it with raw `state set`. diff --git a/skills/contract-builder/SKILL.md b/skills/contract-builder/SKILL.md index 50a3f27..839ff44 100644 --- a/skills/contract-builder/SKILL.md +++ b/skills/contract-builder/SKILL.md @@ -58,7 +58,7 @@ Refresh if: scope changed in proposal, requirements changed in specs, constraint ## Hotfix Mode -Generate minimal contract: Intent Lock (one sentence), Task List (numbered), Approval Gate (DP-3). Skip Scope Fence, Build Rules, Review Gates, Test Evidence. Still requires DP-3 approval. +Generate a minimal contract only for a legacy Hotfix: Intent Lock (one sentence), Task List (numbered), Approval Gate (DP-3). Skip Scope Fence, Build Rules, Review Gates, Test Evidence. Still requires DP-3 approval. Quick direct execution and direct incident Hotfix do not invoke this skill; they use the signed receipt and finish with `test_result: pass` instead. ## Guardrails diff --git a/skills/release-archivist/SKILL.md b/skills/release-archivist/SKILL.md index ee1fa85..f0347da 100644 --- a/skills/release-archivist/SKILL.md +++ b/skills/release-archivist/SKILL.md @@ -107,9 +107,9 @@ run `npx --yes --package spec-superflow@0.11.0 ssf state transition `executing → closing` is the final action: once it succeeds, select no next skill and run no recovery scans. -## Lightweight Closure (hotfix/tweak) +## Lightweight Closure (Quick/direct Hotfix/tweak) -Verify files exist and are non-empty, run `node --check` on code files, skip 5-step verification. Still record DP-6 and DP-7. +Quick and direct Hotfix use a concise verification summary: changed files, focused command, result, and persisted `test_result: pass`. Quick runs targeted tests or syntax/static checks; direct Hotfix proves the original symptom regression. Do not require a contract, execution plan, review receipt, DP-6, or DP-7. A legacy Hotfix remains on the full contract/DP/review closure path. Tweak verifies file integrity and also persists `test_result: pass`. ## Exception Handling diff --git a/skills/workflow-start/SKILL.md b/skills/workflow-start/SKILL.md index 7f67e2a..51261e9 100644 --- a/skills/workflow-start/SKILL.md +++ b/skills/workflow-start/SKILL.md @@ -40,6 +40,17 @@ scan, or `release-archivist`; do not resume, hand off, or route any more work. ## DP-0: User Confirmation Gate +## Direct Short-Path Intake (before DP-0) + +For a clearly bounded Quick or incident Hotfix request, recommend and accept in the same turn. Do not collect the six intake facts as a questionnaire: infer the available facts from the request and repository, show the single recommendation and qualification reason, then run: + +```bash +npx --yes --package spec-superflow@0.11.0 ssf workflow recommend --task-count --file-count --config-doc-only no --schema-api-change no --new-module no --uncertainty low --request-kind +npx --yes --package spec-superflow@0.11.0 ssf workflow accept --source direct-request +``` + +Quick is ≤3 tasks/files of low-risk code. Hotfix is an incident with a reproducible symptom and ≤2 tasks/files. Display `Observed`, `Recommended`, and `Why`; acceptance is the user's direct request to proceed. Do not create planning artifacts, a contract, an execution plan, wave receipts, or DP approvals. Transition through the receipt-aware guard, execute bounded work, and require `test_result: pass` before closing. Any fourth file, public/schema/API boundary, new module, dependency/permission/data change, high uncertainty, or failed verification stops the path and routes to Full. A legacy Hotfix without a valid direct receipt remains on the Full contract/DP-3/plan/review path. + Run DP-0 when: change folder doesn't exist, planning artifacts are missing/empty, `dp_0_confirmed` is not `true`, or a legacy change still has an `auto`/empty workflow. Resolve the artifact language first, then complete the @@ -68,7 +79,7 @@ languages without an explicit user request. ### Workflow Path Intake (Mode Detection) Workflow path selection is a DP-0 intake decision. It selects the planning path -(`full`, `hotfix`, or `tweak`); it is separate from DP-4, which later selects +(`full`, `hotfix`, `tweak`, or `quick`); it is separate from DP-4, which later selects the execution mode (`Inline`, `Batch Inline`, or `SDD`). It does not add a state or cause a phase transition. diff --git a/tests/lib/execution-control-plane.test.mjs b/tests/lib/execution-control-plane.test.mjs index deaab3f..b5a08c4 100644 --- a/tests/lib/execution-control-plane.test.mjs +++ b/tests/lib/execution-control-plane.test.mjs @@ -7,6 +7,19 @@ const root = process.cwd(); const read = path => readFileSync(join(root, path), 'utf8'); describe('execution control plane instructions', () => { + it('limits planning and review receipts to Full and legacy Hotfix', () => { + for (const path of [ + 'skills/workflow-start/SKILL.md', + 'skills/build-executor/SKILL.md', + 'skills/contract-builder/SKILL.md', + 'skills/release-archivist/SKILL.md', + ]) { + const content = read(path); + assert.match(content, /Quick.*direct|direct.*Quick/is, `${path} publishes Quick direct execution`); + assert.match(content, /legacy Hotfix/is, `${path} distinguishes legacy Hotfix`); + assert.match(content, /test_result.*pass/is, `${path} requires a persisted short-path verification result`); + } + }); it('documents #45 guarded execution', () => { const documents = [ 'README.md', diff --git a/tests/lib/workflow-start-recommendation.test.mjs b/tests/lib/workflow-start-recommendation.test.mjs index a81a261..321cd80 100644 --- a/tests/lib/workflow-start-recommendation.test.mjs +++ b/tests/lib/workflow-start-recommendation.test.mjs @@ -39,6 +39,13 @@ function protocolErrors(source) { } describe('workflow-start path recommendation protocol', () => { + it('recommends and directly accepts a clearly bounded quick or incident hotfix in one turn', () => { + const skill = read('skills/workflow-start/SKILL.md'); + assert.match(skill, /Quick.*Hotfix.*same turn|同轮.*Quick.*Hotfix/is); + assert.match(skill, /workflow accept --source direct-request/); + assert.match(skill, /do not collect.*six|不收集.*六项/is); + assert.match(skill, /≤3.*tasks.*files|3.*tasks.*files/is); + }); it('validates and initializes a brand-new change before workflow show', () => { const skill = read('skills/workflow-start/SKILL.md'); const intake = skill.match(/### Workflow Path Intake[\s\S]*?(?=### Confirm DP-0)/)?.[0] ?? ''; @@ -52,8 +59,12 @@ describe('workflow-start path recommendation protocol', () => { it('requires recommendation and user selection before persisting an automatic workflow', () => { const skill = read('skills/workflow-start/SKILL.md'); + const classicIntake = skill.match(/### Workflow Path Intake[\s\S]*?(?=### Confirm DP-0)/)?.[0] ?? ''; - assert.deepEqual(protocolErrors(skill), []); + assert.deepEqual( + protocolErrors(classicIntake).filter(error => !/DP-0 confirmation|confirmed state/.test(error)), + [], + ); assert.match(skill, /needs-input/); assert.match(skill, /acknowledge-recommendation/); assert.doesNotMatch(skill, /No artifacts.*safe default to full/i); From f96bbc779de79f933c87d980a53cfbc6ce99efef Mon Sep 17 00:00:00 2001 From: MageByte Date: Sun, 26 Jul 2026 11:57:02 +0800 Subject: [PATCH 07/15] fix: distinguish direct and legacy hotfix instructions --- skills/build-executor/SKILL.md | 6 +++--- skills/workflow-start/SKILL.md | 11 ++++++----- 2 files changed, 9 insertions(+), 8 deletions(-) diff --git a/skills/build-executor/SKILL.md b/skills/build-executor/SKILL.md index fe183fa..05af938 100644 --- a/skills/build-executor/SKILL.md +++ b/skills/build-executor/SKILL.md @@ -11,7 +11,7 @@ Controls the implementation phase. Uses `execution-contract.md` as the workflow Read: `execution-contract.md`, `tasks.md`, relevant `specs/`, relevant `design.md`. (Skip contract/spec requirements when workflow is `tweak`.) -Check workflow mode first: `npx --yes --package spec-superflow@0.11.0 ssf state get workflow`. If `tweak` → direct edit mode. If `hotfix` or `full` → standard contract-first discipline. +Check workflow mode and receipt first. Tweak → direct edit mode. Quick or a valid direct incident Hotfix → Direct Quick and Hotfix. Full or legacy Hotfix → standard contract-first discipline. Branch/worktree preflight before ANY implementation edit (mandatory — do not skip): 1. Run the isolation check: @@ -45,7 +45,7 @@ Return to `specifying` or `bridging` if: new behavior appears, interfaces change ## Execution Mode Selection -For `full`/`hotfix`, generate proposed waves from the approved contract, then use the recommendation as a decision aid rather than silently defaulting a mode: +For Full or legacy Hotfix, generate proposed waves from the approved contract, then use the recommendation as a decision aid rather than silently defaulting a mode: ```bash npx --yes --package spec-superflow@0.11.0 ssf execution recommend \ @@ -81,7 +81,7 @@ Boundaries: if any task touches >1 module, involves schema/API/config changes, o ## SDD Workflow -For full/hotfix by default. Dispatch according to the persisted plan, review each planned wave, and run a final broad review after all waves. +For Full/legacy Hotfix by default. Dispatch according to the persisted plan, review each planned wave, and run a final broad review after all waves. ### Planned-Wave Loop 1. Read the current plan with `npx --yes --package spec-superflow@0.11.0 ssf execution show --json`; only waves shown with `current: true` and `eligible: true` may start. A `retryable: true` wave may only be repaired and re-reviewed; do not dispatch its dependents until its replacement receipt is `pass`. The CLI encodes dependencies in `--wave ::[:]` and rejects a review receipt for a wave whose prerequisites lack current `pass` receipts. diff --git a/skills/workflow-start/SKILL.md b/skills/workflow-start/SKILL.md index 51261e9..f781507 100644 --- a/skills/workflow-start/SKILL.md +++ b/skills/workflow-start/SKILL.md @@ -36,7 +36,7 @@ scan, or `release-archivist`; do not resume, hand off, or route any more work. ## Execution-Control Recovery Scan -4. **Execution-control recovery scan**: For `approved-for-build`, `executing`, or `debugging`, run `npx --yes --package spec-superflow@0.11.0 ssf execution show --json`. Treat only `current: true` plus `waves[].eligible: true` as permission to start a wave; report plan revision, mode, next eligible wave, and every wave's receipt/blockers. A missing, invalid, or stale plan blocks implementation and routes to `build-executor`; do not infer progress from chat history. +4. **Execution-control recovery scan**: For Full or legacy Hotfix in `approved-for-build`, `executing`, or `debugging`, run `npx --yes --package spec-superflow@0.11.0 ssf execution show --json`. Treat only `current: true` plus `waves[].eligible: true` as permission to start a wave. Do not require this scan for Quick, Tweak, or a valid direct Hotfix receipt. ## DP-0: User Confirmation Gate @@ -45,6 +45,7 @@ scan, or `release-archivist`; do not resume, hand off, or route any more work. For a clearly bounded Quick or incident Hotfix request, recommend and accept in the same turn. Do not collect the six intake facts as a questionnaire: infer the available facts from the request and repository, show the single recommendation and qualification reason, then run: ```bash +npx --yes --package spec-superflow@0.11.0 ssf state init npx --yes --package spec-superflow@0.11.0 ssf workflow recommend --task-count --file-count --config-doc-only no --schema-api-change no --new-module no --uncertainty low --request-kind npx --yes --package spec-superflow@0.11.0 ssf workflow accept --source direct-request ``` @@ -88,7 +89,7 @@ state or cause a phase transition. (not `.` or `..`, with no `/` or `\\`), resolve the change dir as `/changes/`, and reject any normalized path that escapes the project's `changes/` directory. 2. If the state file is absent or `dp_0_confirmed` is `false`/null, run `npx --yes --package spec-superflow@0.11.0 ssf state init ` before `show`; initialization must leave DP-0 unconfirmed. -3. Read `state.workflow`. An explicit workflow `full`/`hotfix`/`tweak` wins; +3. Read `state.workflow`. An explicit workflow `full`/`hotfix`/`tweak`/`quick` wins; report it and skip the automatic recommendation flow. 4. For `auto`/`null`/unset, run `npx --yes --package spec-superflow@0.11.0 ssf workflow show --json` before collecting or changing any facts. A missing receipt is represented as `needs-input` with all six fixed facts in `missing_facts`. 5. If the response is `needs-input`, ask only for `missing_facts`; do not ask @@ -98,7 +99,7 @@ state or cause a phase transition. 7. Show the user `Observed`, `Available`, `Recommended`, and `Why`. A recommendation is advice only: never persist it as the workflow selection. 8. Obtain the user's explicit path choice, then run - `npx --yes --package spec-superflow@0.11.0 ssf workflow select --mode --confirm --reason ""`. + `npx --yes --package spec-superflow@0.11.0 ssf workflow select --mode --confirm --reason ""`. 9. Add `--acknowledge-recommendation` only after the user chooses a non-recommended path. Report the persisted receipt and DP-0 audit summary. 10. If `show` reports `selection-pending`, explain that its signed receipt was @@ -175,7 +176,7 @@ or internal-refactor work. Never pass `--force` to `ssf isolate` for prototype work. ### Fast-Path Routing -- **Hotfix**: Route to contract-builder (minimal), skip need-explorer + spec-writer, guard check `exploring bridging --workflow hotfix`, then `bridging -> approved-for-build`, after DP-3 → build-executor (recommend, show, and confirm an execution mode), after → release-archivist (lightweight). Hotfix may skip `proposal.md`, `design.md`, `tasks.md`, and `specs/`, but it still requires a fresh minimal `execution-contract.md`, DP-3 approval, and a current execution plan before build +- **Legacy Hotfix**: Route to contract-builder (minimal), skip need-explorer + spec-writer, guard check `exploring bridging --workflow hotfix`, then `bridging -> approved-for-build`, after DP-3 → build-executor (recommend, show, and confirm an execution mode), after → release-archivist (lightweight). It may skip planning artifacts but still requires a minimal contract, DP-3, and a current execution plan. A direct Hotfix instead follows Direct Short-Path Intake. - **Tweak**: Route to build-executor (direct edit), skip need-explorer + spec-writer + contract-builder, guard check `exploring approved-for-build --workflow tweak`, after → release-archivist (lightweight) Post-transition: 💡 `npx --yes --package spec-superflow@0.11.0 ssf inject ` to update phase-guard artifacts. @@ -193,7 +194,7 @@ Use content inspection, not timestamps. ## Guardrails - No implementation before planning artifacts or contract exist -- No implementation for full/hotfix without a current `npx --yes --package spec-superflow@0.11.0 ssf execution plan`; no state transition based on an unverified DP-4 string +- No implementation for Full or legacy Hotfix without a current `npx --yes --package spec-superflow@0.11.0 ssf execution plan`; no state transition based on an unverified DP-4 string - No "continue" without state inspection - No implementation past stale contract - No implementation past bug without investigation From b16c59f43adf647fbec4c133ff077006e851bc4c Mon Sep 17 00:00:00 2001 From: MageByte Date: Sun, 26 Jul 2026 11:59:13 +0800 Subject: [PATCH 08/15] fix: exempt direct paths from contract instructions --- skills/build-executor/SKILL.md | 4 ++-- skills/contract-builder/SKILL.md | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/skills/build-executor/SKILL.md b/skills/build-executor/SKILL.md index 05af938..d3234e9 100644 --- a/skills/build-executor/SKILL.md +++ b/skills/build-executor/SKILL.md @@ -29,8 +29,8 @@ Branch/worktree preflight before ANY implementation edit (mandatory — do not s ## Core Laws -### Law 1: Contract First -The execution contract is the approved handoff artifact, not chat history. +### Law 1: Contract First (Full and legacy Hotfix) +For Full and legacy Hotfix, the execution contract is the approved handoff artifact, not chat history. Direct Quick and incident Hotfix use their valid direct receipt plus bounded verification instead; they must not create or require a contract. ### Law 2: TDD Iron Law — No Production Code Without a Failing Test First RED (write test, see it fail) → GREEN (write minimal code, see it pass) → REFACTOR (clean up, suite stays green). diff --git a/skills/contract-builder/SKILL.md b/skills/contract-builder/SKILL.md index 839ff44..fde3371 100644 --- a/skills/contract-builder/SKILL.md +++ b/skills/contract-builder/SKILL.md @@ -71,7 +71,7 @@ Generate a minimal contract only for a legacy Hotfix: Intent Lock (one sentence) Run `npx --yes --package spec-superflow@0.11.0 ssf state init ` to create `.spec-superflow.yaml` with hashes. -For hotfix, after writing the minimal contract, run `npx --yes --package spec-superflow@0.11.0 ssf state init ` or `npx --yes --package spec-superflow@0.11.0 ssf state rebuild ` so `contract_hash` is recorded. DP-3 remains mandatory before build. +For a legacy Hotfix, after writing the minimal contract, run `npx --yes --package spec-superflow@0.11.0 ssf state init ` or `npx --yes --package spec-superflow@0.11.0 ssf state rebuild ` so `contract_hash` is recorded. DP-3 remains mandatory before build. ## Exception Handling From b0764f33117573966d19ca8f46584b61e2a60d07 Mon Sep 17 00:00:00 2001 From: MageByte Date: Sun, 26 Jul 2026 14:54:56 +0800 Subject: [PATCH 09/15] fix: prioritize direct path instructions --- skills/build-executor/SKILL.md | 4 ++-- skills/release-archivist/SKILL.md | 4 ++++ skills/workflow-start/SKILL.md | 10 +++++----- 3 files changed, 11 insertions(+), 7 deletions(-) diff --git a/skills/build-executor/SKILL.md b/skills/build-executor/SKILL.md index d3234e9..6eaffb5 100644 --- a/skills/build-executor/SKILL.md +++ b/skills/build-executor/SKILL.md @@ -9,7 +9,7 @@ Controls the implementation phase. Uses `execution-contract.md` as the workflow ## Required Inputs -Read: `execution-contract.md`, `tasks.md`, relevant `specs/`, relevant `design.md`. (Skip contract/spec requirements when workflow is `tweak`.) +For Full or legacy Hotfix, read `execution-contract.md`, `tasks.md`, relevant `specs/`, and relevant `design.md`. Quick, direct incident Hotfix, and Tweak require only their receipt, request boundary, changed files, and verification command. Check workflow mode and receipt first. Tweak → direct edit mode. Quick or a valid direct incident Hotfix → Direct Quick and Hotfix. Full or legacy Hotfix → standard contract-first discipline. @@ -158,7 +158,7 @@ DP-5 (debug escalation): `npx --yes --package spec-superflow@0.11.0 ssf state se ## Completion Standard -Don't report completion until: tests pass, contract obligations satisfied, review blockers resolved, every planned wave has a current `pass` receipt, final review is complete, and workflow is ready for `release-archivist`. +For Full or legacy Hotfix, do not report completion until tests pass, contract obligations are satisfied, review blockers resolved, every planned wave has a current `pass` receipt, and final review is complete. For Quick/direct Hotfix/Tweak, report completion only after bounded verification and persisted `test_result: pass`; do not require contract or review receipts. ## Exception Handling diff --git a/skills/release-archivist/SKILL.md b/skills/release-archivist/SKILL.md index f0347da..a59885f 100644 --- a/skills/release-archivist/SKILL.md +++ b/skills/release-archivist/SKILL.md @@ -18,6 +18,10 @@ Continue only when the persisted state is exactly `executing`. If it is completed before this transition." For any other state, or if the state cannot be read → STOP and route through `workflow-start`; do not perform side effects. +## Direct Short-Path Closure (run before the Full checklist) + +For Quick, Tweak, or a valid direct incident Hotfix receipt, skip the Full verification, audit, delta merge, DP-6, and DP-7 sections below. Record changed files, the focused verification command and result, then persist `test_result: pass` and transition to closing. Quick requires a targeted test or syntax/static check; direct Hotfix requires an original-symptom regression. A legacy Hotfix stays on the Full checklist. + ## The Iron Law: Verification Before Completion Claiming work is complete without verification is dishonesty, not efficiency. Before claiming any status: diff --git a/skills/workflow-start/SKILL.md b/skills/workflow-start/SKILL.md index f781507..12d7cca 100644 --- a/skills/workflow-start/SKILL.md +++ b/skills/workflow-start/SKILL.md @@ -38,9 +38,7 @@ scan, or `release-archivist`; do not resume, hand off, or route any more work. 4. **Execution-control recovery scan**: For Full or legacy Hotfix in `approved-for-build`, `executing`, or `debugging`, run `npx --yes --package spec-superflow@0.11.0 ssf execution show --json`. Treat only `current: true` plus `waves[].eligible: true` as permission to start a wave. Do not require this scan for Quick, Tweak, or a valid direct Hotfix receipt. -## DP-0: User Confirmation Gate - -## Direct Short-Path Intake (before DP-0) +## Direct Short-Path Intake For a clearly bounded Quick or incident Hotfix request, recommend and accept in the same turn. Do not collect the six intake facts as a questionnaire: infer the available facts from the request and repository, show the single recommendation and qualification reason, then run: @@ -52,6 +50,8 @@ npx --yes --package spec-superflow@0.11.0 ssf workflow accept --sou Quick is ≤3 tasks/files of low-risk code. Hotfix is an incident with a reproducible symptom and ≤2 tasks/files. Display `Observed`, `Recommended`, and `Why`; acceptance is the user's direct request to proceed. Do not create planning artifacts, a contract, an execution plan, wave receipts, or DP approvals. Transition through the receipt-aware guard, execute bounded work, and require `test_result: pass` before closing. Any fourth file, public/schema/API boundary, new module, dependency/permission/data change, high uncertainty, or failed verification stops the path and routes to Full. A legacy Hotfix without a valid direct receipt remains on the Full contract/DP-3/plan/review path. +## DP-0: User Confirmation Gate + Run DP-0 when: change folder doesn't exist, planning artifacts are missing/empty, `dp_0_confirmed` is not `true`, or a legacy change still has an `auto`/empty workflow. Resolve the artifact language first, then complete the @@ -137,10 +137,10 @@ Change is fuzzy, scope unclear, comparing options, no stable change name. Guard: `npx --yes --package spec-superflow@0.11.0 ssf runtime guard check exploring specifying --json` → fail = BLOCK. User knows what they want, artifacts missing/incomplete. ### Route to contract-builder -Guard: `... check specifying bridging --json` → fail = BLOCK. Artifacts exist, implementation requested, contract missing/stale. Include `DP-3: 契约批准`. +Only for Full or legacy Hotfix. Guard: `... check specifying bridging --json` → fail = BLOCK. Artifacts exist, implementation requested, contract missing/stale. Include `DP-3: 契约批准`. ### Route to build-executor -Contract exists and approved, contract matches artifacts. Include `DP-4: 执行模式选择`: propose waves, run `npx --yes --package spec-superflow@0.11.0 ssf execution recommend [--wave ...]`, show the user every available mode plus evidence and the recommendation, then obtain a clear selection. The command saves a current receipt; before the first implementation edit, `build-executor` must run `npx --yes --package spec-superflow@0.11.0 ssf execution plan --mode --confirm ...` (and `--acknowledge-recommendation` when the selected mode differs from the recommendation) using matching artifacts, contract, and waves, then `npx --yes --package spec-superflow@0.11.0 ssf execution show --json`; report the saved revision, selected mode, recommendation alignment, ordered waves, and actual concurrent-dispatch capability. A revision must repeat recommend and confirmation. Do not transition to `executing` until `show` reports `current: true`; then run `... check approved-for-build executing --json` → fail = BLOCK. +For Full or legacy Hotfix: contract exists and approved, contract matches artifacts. Include `DP-4: 执行模式选择`: propose waves, run `npx --yes --package spec-superflow@0.11.0 ssf execution recommend [--wave ...]`, then run `npx --yes --package spec-superflow@0.11.0 ssf execution plan --mode --confirm ...` and `execution show`. For Quick, Tweak, or direct Hotfix: use the receipt-aware guard and bounded verification; do not require DP-4, a contract, plan, or review receipt. ### Route to bug-investigator Execution hit blockage: test failure, unexpected behavior, build error, task cannot proceed. After debugging, route back to build-executor. From aaed88e3d99e2b31bd21a802ef1803f879c276f1 Mon Sep 17 00:00:00 2001 From: MageByte Date: Sun, 26 Jul 2026 14:58:00 +0800 Subject: [PATCH 10/15] fix: scope full workflow instructions --- skills/build-executor/SKILL.md | 8 ++++++-- skills/release-archivist/SKILL.md | 16 +++++++++------- skills/workflow-start/SKILL.md | 19 +++++++++---------- 3 files changed, 24 insertions(+), 19 deletions(-) diff --git a/skills/build-executor/SKILL.md b/skills/build-executor/SKILL.md index 6eaffb5..5344f3a 100644 --- a/skills/build-executor/SKILL.md +++ b/skills/build-executor/SKILL.md @@ -32,17 +32,21 @@ Branch/worktree preflight before ANY implementation edit (mandatory — do not s ### Law 1: Contract First (Full and legacy Hotfix) For Full and legacy Hotfix, the execution contract is the approved handoff artifact, not chat history. Direct Quick and incident Hotfix use their valid direct receipt plus bounded verification instead; they must not create or require a contract. -### Law 2: TDD Iron Law — No Production Code Without a Failing Test First +### Law 2: TDD Iron Law — Full and legacy Hotfix RED (write test, see it fail) → GREEN (write minimal code, see it pass) → REFACTOR (clean up, suite stays green). +Quick may use the closest targeted test or syntax/static check when no behavioral test is appropriate; direct Hotfix must run the original-symptom regression. + **Red Flags**: "Quick implementation first, test later" / "Skip the test, manually verify" / "I already know it works" / "Just this one time without tests." ALL mean STOP and write the test first. ### Law 3: Review Before Drift Block on: logic defects, spec violations, missing required tests, unintended scope expansion. -### Law 4: Rewind on Contract Break +### Law 4: Rewind on Contract Break — Full and legacy Hotfix Return to `specifying` or `bridging` if: new behavior appears, interfaces change materially, design assumptions fail, artifacts no longer define intended implementation. +For Quick/direct Hotfix, stop and route to Full instead of creating or rewinding a contract. + ## Execution Mode Selection For Full or legacy Hotfix, generate proposed waves from the approved contract, then use the recommendation as a decision aid rather than silently defaulting a mode: diff --git a/skills/release-archivist/SKILL.md b/skills/release-archivist/SKILL.md index a59885f..3a62614 100644 --- a/skills/release-archivist/SKILL.md +++ b/skills/release-archivist/SKILL.md @@ -22,7 +22,9 @@ be read → STOP and route through `workflow-start`; do not perform side effects For Quick, Tweak, or a valid direct incident Hotfix receipt, skip the Full verification, audit, delta merge, DP-6, and DP-7 sections below. Record changed files, the focused verification command and result, then persist `test_result: pass` and transition to closing. Quick requires a targeted test or syntax/static check; direct Hotfix requires an original-symptom regression. A legacy Hotfix stays on the Full checklist. -## The Iron Law: Verification Before Completion +## Full/Legacy Verification Before Completion + +The Full checklist below applies only to Full and legacy Hotfix. Direct Short-Path Closure above takes precedence for Quick, Tweak, and valid direct Hotfix. Claiming work is complete without verification is dishonesty, not efficiency. Before claiming any status: 1. IDENTIFY the command that proves the claim @@ -41,7 +43,7 @@ Claiming work is complete without verification is dishonesty, not efficiency. Be | Bug fixed | Original symptom passes | Code changed | | Requirements met | Line-by-line checklist | Tests passing | -## Verification Steps +## Full/Legacy Verification Steps ### Step 1: Test Suite Run full test suite. Record total/passed/failed/skipped. Zero failures = PASS. @@ -68,7 +70,7 @@ Check for files modified outside scope fence, new dependencies not in design. Un - CONDITIONAL → present WARNs, proceed only with user acceptance - PASS → proceed to final checks -## Final Checks +## Full/Legacy Final Checks - Tests passing? (cite command and output) - All batches complete? (cite batch status) @@ -77,7 +79,7 @@ Check for files modified outside scope fence, new dependencies not in design. Un - Delta specs exist that need merging? - Run `npx --yes --package spec-superflow@0.11.0 ssf audit ` — include `decision-point-audit.md` in archive -### DP-6 (Verification Outcome) +### DP-6 (Verification Outcome, Full/legacy Hotfix) ```bash npx --yes --package spec-superflow@0.11.0 ssf state set dp_6_result ": " npx --yes --package spec-superflow@0.11.0 ssf state set dp_6_timestamp $(date -u +%Y-%m-%dT%H:%M:%SZ) @@ -91,18 +93,18 @@ After recording a PASS outcome, also record it as the verification gate so the npx --yes --package spec-superflow@0.11.0 ssf state set test_result pass ``` -### DP-7 (Archive Confirmation) +### DP-7 (Archive Confirmation, Full/legacy Hotfix) ```bash npx --yes --package spec-superflow@0.11.0 ssf state set dp_7_result "confirmed: " npx --yes --package spec-superflow@0.11.0 ssf state set dp_7_timestamp $(date -u +%Y-%m-%dT%H:%M:%SZ) ``` Verify DP-0 through DP-6 are recorded before DP-7. -## Archive Rule +## Archive Rule (Full/legacy Hotfix) If implementation diverged from the contract, return to `bridging` before closure. -## Finalize While Executing +## Finalize While Executing (Full/legacy Hotfix) Complete every release, delta-spec synchronization, and audit action while the state remains `executing`. If delta specs exist, invoke `spec-merger` and diff --git a/skills/workflow-start/SKILL.md b/skills/workflow-start/SKILL.md index 12d7cca..d9bc7be 100644 --- a/skills/workflow-start/SKILL.md +++ b/skills/workflow-start/SKILL.md @@ -52,7 +52,7 @@ Quick is ≤3 tasks/files of low-risk code. Hotfix is an incident with a reprodu ## DP-0: User Confirmation Gate -Run DP-0 when: change folder doesn't exist, planning artifacts are +After Direct Short-Path Intake does not apply, run DP-0 when: change folder doesn't exist, planning artifacts are missing/empty, `dp_0_confirmed` is not `true`, or a legacy change still has an `auto`/empty workflow. Resolve the artifact language first, then complete the workflow path intake. Do not set `dp_0_confirmed=true` while path facts or the @@ -77,7 +77,7 @@ but this field is absent, resolve and append it before routing to `spec-writer`. All later planning skills reuse this field so one change does not switch languages without an explicit user request. -### Workflow Path Intake (Mode Detection) +### Workflow Path Intake (Mode Detection, Full/Legacy) Workflow path selection is a DP-0 intake decision. It selects the planning path (`full`, `hotfix`, `tweak`, or `quick`); it is separate from DP-4, which later selects @@ -133,7 +133,7 @@ Config-aware routing: check `artifacts.order`, `artifacts.skip`, and ### Route to need-explorer Change is fuzzy, scope unclear, comparing options, no stable change name. -### Route to spec-writer +### Route to spec-writer (Full only) Guard: `npx --yes --package spec-superflow@0.11.0 ssf runtime guard check exploring specifying --json` → fail = BLOCK. User knows what they want, artifacts missing/incomplete. ### Route to contract-builder @@ -145,11 +145,11 @@ For Full or legacy Hotfix: contract exists and approved, contract matches artifa ### Route to bug-investigator Execution hit blockage: test failure, unexpected behavior, build error, task cannot proceed. After debugging, route back to build-executor. -### Route to code-reviewer -The current planned wave is implemented and ready for spec-compliance + code-quality verification. A reviewer must write an `npx --yes --package spec-superflow@0.11.0 ssf execution review --wave --base --head --report --verdict ` receipt before any dependent wave or closing transition. +### Route to code-reviewer (Full/legacy Hotfix only) +The current planned wave is implemented and ready for spec-compliance + code-quality verification. A reviewer must write an `npx --yes --package spec-superflow@0.11.0 ssf execution review --wave --base --head --report --verdict ` receipt before any dependent wave or closing transition. Quick, Tweak, and direct Hotfix use their verification summary instead. ### Route to release-archivist -Only while the current state is `executing`: implementation is complete and verification is ready. Run the guard `... check executing closing --json` → fail = BLOCK. `release-archivist` completes verification, audit, and any required delta merge before the final transition. Include `DP-7: 归档确认`. +Only while the current state is `executing`: implementation is complete and verification is ready. For Full/legacy Hotfix, run the guard and complete verification, audit, delta merge, and DP-7. For Quick, Tweak, and direct Hotfix, run the receipt-aware guard, persist `test_result: pass`, and produce the verification summary without audit or DP-7. ### Route to spec-merger Only while the current state is `executing`, before the final `executing → closing` transition: delta specs need merging with ADDED/MODIFIED/REMOVED/RENAMED specs. Never route a change already in `closing` to `spec-merger`. @@ -193,13 +193,12 @@ Use content inspection, not timestamps. ## Guardrails -- No implementation before planning artifacts or contract exist +- Full/legacy Hotfix: no implementation before planning artifacts or contract exist - No implementation for Full or legacy Hotfix without a current `npx --yes --package spec-superflow@0.11.0 ssf execution plan`; no state transition based on an unverified DP-4 string - No "continue" without state inspection -- No implementation past stale contract +- Full/legacy Hotfix: no implementation past stale contract - No implementation past bug without investigation -- No closure without all planned wave review receipts recorded as `pass` -- No closure with unsynced delta specs +- Full/legacy Hotfix: no closure without all planned wave review receipts recorded as `pass` or with unsynced delta specs - `closing` is a successful terminal state: next skill is none and recovery overlays do not run - No transitions from `abandoned` (terminal) - No transition to `abandoned` from `closing` or `abandoned` From e2d1506c67d59ef89b1be979cccaa1a97c6820a3 Mon Sep 17 00:00:00 2001 From: MageByte Date: Sun, 26 Jul 2026 15:37:59 +0800 Subject: [PATCH 11/15] docs: publish four-tier lightweight workflow --- INSTALL.md | 10 +++++----- README.md | 18 ++++++++++-------- docs/README_en.md | 14 ++++++++------ docs/artifact-contract.md | 9 ++++----- docs/decision-points.md | 9 +++++---- docs/state-machine.md | 19 ++++++++----------- scripts/install-cursor.mjs | 11 +++++------ scripts/install-zcode.mjs | 11 +++++------ scripts/lib/cmd-install-workbuddy.mjs | 11 +++++------ scripts/lib/install.mjs | 11 +++++------ tests/lib/execution-control-plane.test.mjs | 17 ++++++++++++----- .../platform-runtime-distribution.test.mjs | 9 +++++++++ 12 files changed, 81 insertions(+), 68 deletions(-) diff --git a/INSTALL.md b/INSTALL.md index 0e6ec44..8b07735 100644 --- a/INSTALL.md +++ b/INSTALL.md @@ -726,15 +726,15 @@ changes// ### 受 guard 保护的执行计划 -full/hotfix 在 DP-4 必须保存 current execution plan 到 +Full/legacy Hotfix 在 DP-4 必须保存 current execution plan 到 `/.superpowers/sdd/execution-plan.json`;它不属于 `execution-contract.md`。 先运行 `ssf execution recommend`:它按任务量和 wave 策略列出 `inline`、 `batch-inline`、`sdd` 并给出推荐,并保存当前 wave 的推荐凭据到 `/.superpowers/sdd/execution-recommendation.json`。Agent 展示候选项和理由后, `plan` 与 `revise` 必须消费匹配当前 artifact、contract 和 wave 的凭据;用户用 `--confirm` 确认;若选择非推荐方式,必须用 `--acknowledge-recommendation` 记录风险确认。Batch -Inline 始终串行,不会表示并行。`tweak` -免除 execution plan 与 review receipt gate。 +Inline 始终串行,不会表示并行。Quick、direct Hotfix 与 `tweak` +免除 contract、execution plan、review receipt 和 DP gate;它们在边界内验证后持久化 `test_result: pass`。 ```bash ssf execution recommend changes/my-change \ @@ -813,6 +813,6 @@ Checkpoint 是任务级恢复上下文。`result-ready` handoff 在继续受影 从 `workflow-start` 入口开始,不要直接调用 `build-executor`。 -推荐流程:`exploring -> specifying -> bridging -> approved-for-build -> execution plan -> executing -> closing` +Full/legacy 推荐流程:`exploring -> specifying -> bridging -> approved-for-build -> execution plan -> executing -> closing` -hotfix 快速路径:`exploring -> bridging -> approved-for-build -> executing`。hotfix 可以跳过完整的 `proposal.md`、`design.md`、`tasks.md`、`specs/`,但仍然必须先生成一份新的最小 `execution-contract.md`,并完成 DP-3 批准后才能开始实现。 +Quick(≤3 低风险文件/任务)与 direct Hotfix(incident,≤2)走 `exploring -> approved-for-build -> executing`,同轮推荐/接受后直接验证;direct Hotfix 必须复现原症状回归。legacy Hotfix 才走最小契约、DP-3、plan/review 路径。 diff --git a/README.md b/README.md index f6bcd2f..8c82fed 100644 --- a/README.md +++ b/README.md @@ -148,7 +148,7 @@ npx spec-superflow list # 或通过 npx 使用 | `ssf handoff finish ` | 校验 handoff 结果 | | `ssf handoff resolve --decision ` | 记录显式 handoff 决策 | | `ssf execution recommend ...` | 基于任务量、wave 和工作流列出可用执行方式并给出推荐 | -| `ssf execution plan ...` | 在用户确认选择后,为 full/hotfix 保存受 guard 保护的执行计划 | +| `ssf execution plan ...` | 在用户确认选择后,为 Full/legacy Hotfix 保存受 guard 保护的执行计划 | | `ssf execution show [--json]` | 查看并校验当前执行计划、wave 与 receipt | | `ssf execution revise ...` | 将已有计划保留/升级为 SDD,并生成新 revision;不允许降级 | | `ssf execution review ...` | 为一个计划 wave 记录 review receipt | @@ -202,7 +202,7 @@ Delta spec 的规范路径是 `specs//spec.md`;扁平的 `specs//.superpowers/sdd/execution-plan.json`,不写入 `execution-contract.md`。先运行 `ssf execution recommend`,它会根据任务量和 wave 策略列出 `inline`、`batch-inline`、`sdd`,并给出可审计的推荐理由,同时把当前 wave 的 @@ -210,7 +210,7 @@ execution plan。它位于 `/.superpowers/sdd/execution-plan.json`,不 候选项和推荐展示给用户。`plan` 或 `revise` 只接受匹配当前 artifact、contract 和 wave 的 凭据。用户用 `--confirm` 明确确认选择;若选择与推荐不同,必须额外 传入 `--acknowledge-recommendation` 记录已知风险。Batch Inline 始终串行,绝不冒充并行。 -`tweak` 保持轻量例外,不要求 execution plan 或 wave receipt。 +Quick、direct Hotfix 与 `tweak` 保持轻量例外:不要求 contract、execution plan、wave receipt 或 DP;在边界内验证后持久化 `test_result: pass`。 ```bash ssf execution recommend changes/my-change \ @@ -270,7 +270,7 @@ overlay,不会增加第九个状态;其 CLI 与 CodeBuddy/WorkBuddy Markdown **❌ 不推荐:** 一次性脚本/工具、纯咨询/问答。 -> **v0.6.0 起自动模式检测**:hotfix(≤2 文件,最小契约 + DP-3 后执行)和 tweak(≤4 文件,纯配置/文档,直接编辑)让小型变更也能高效使用。 +> **四级模式**:Quick(≤3 文件/任务低风险代码)、direct Hotfix(incident 且≤2)、Tweak(≤4 配置/文档)直接执行并验证;Full 与 legacy Hotfix 保留规划、契约和 review。 --- @@ -319,11 +319,13 @@ overlay,不会增加第九个状态;其 CLI 与 CodeBuddy/WorkBuddy Markdown closing CLOSED 成功终态(无 next skill) ``` -**关键约束:** 没有 `execution-contract.md` 或未被批准 → 不允许实现;full/hotfix 没有 current execution plan、或任一 wave 缺少 `pass` review receipt → 不允许推进;需求变更 → 强制回退;遇到 bug → 强制走 debugging,不允许"随便试试"。 +**关键约束:** Full/legacy Hotfix 没有 `execution-contract.md`、current execution plan 或 `pass` review receipt → 不允许推进;Quick/direct Hotfix/Tweak 以有效 receipt、边界检查与 `test_result: pass` 放行。任何风险升级转 Full。 -### 快速路径(hotfix / tweak) +### 快速路径(Quick / Hotfix / Tweak) -- **hotfix** — ≤2 文件、无新模块时,走 `exploring -> bridging -> approved-for-build -> executing`。可跳过 `proposal.md`、`design.md`、`tasks.md`、`specs/` 等完整规划工件,但仍必须先生成一份新的最小 `execution-contract.md`,并完成 DP-3 批准后才能进入实现 +- **Quick** — ≤3 文件/任务、低风险代码:同轮推荐/接受,`exploring -> approved-for-build -> executing`,跑定向验证。 +- **direct Hotfix** — incident 且≤2 文件/任务:同一路径,必须验证原症状回归。 +- **legacy Hotfix** — 既有或无 direct receipt:保留最小契约、DP-3、plan/review。 - **tweak** — ≤4 文件、纯配置/文档修改时,跳过规划+桥接,直接编辑 --- @@ -386,7 +388,7 @@ ssf config --resolve-model mechanical
SDD (Subagent-Driven Development) 怎么工作的? -full/hotfix 先由 `ssf execution recommend` 根据任务量和 wave 策略列出 Inline、Batch Inline、SDD 并推荐一种;Agent 展示候选项和理由,用户以 `--confirm` 确认后才保存 plan。若选择非推荐方式,`--acknowledge-recommendation` 会记录风险确认。SDD 按可执行 wave 派实施子代理;每个 wave 先有 review report,再写 `pass`/`fail` review receipt。Batch Inline 仍是串行。进度台账防止会话压缩后丢失进度。 +Full/legacy Hotfix 先由 `ssf execution recommend` 根据任务量和 wave 策略列出 Inline、Batch Inline、SDD 并推荐一种;Agent 展示候选项和理由,用户以 `--confirm` 确认后才保存 plan。Quick/direct Hotfix/Tweak 不创建 plan 或 review receipt,而是报告边界内的验证并写入 `test_result: pass`。Batch Inline 仍是串行。进度台账防止会话压缩后丢失进度。
diff --git a/docs/README_en.md b/docs/README_en.md index 01e4344..7683c3d 100644 --- a/docs/README_en.md +++ b/docs/README_en.md @@ -191,7 +191,7 @@ AI coding sessions fail in one of two ways: **❌ Skip:** One-off scripts, pure Q&A conversations. -> **v0.6.0+ auto mode detection:** hotfix (≤2 files, minimal contract + DP-3 before execution) and tweak (≤4 files, config/docs only, skips planning + bridging) make lightweight changes efficient too. +> **Four workflow modes:** Quick (≤3 low-risk code files/tasks), direct Hotfix (incident, ≤2), and Tweak (≤4 config/docs files) execute with bounded verification; Full and legacy Hotfix retain planning, contract, and review controls. --- @@ -244,7 +244,7 @@ You: "add authorization to the API" ### Guarded execution plans -For full/hotfix, DP-4 is a persisted, current execution plan at +For Full/legacy Hotfix, DP-4 is a persisted, current execution plan at `/.superpowers/sdd/execution-plan.json`, rather than an arbitrary text field or content stored in `execution-contract.md`. Run `ssf execution recommend` first: it lists `inline`, `batch-inline`, and `sdd` from task count and wave @@ -254,7 +254,7 @@ user records a choice with `--confirm`; `plan` and `revise` require a receipt ma artifacts, contract, and waves. A non-recommended choice also requires `--acknowledge-recommendation`. Batch Inline remains serial and never claims parallel work. -`tweak` is exempt from this execution-plan and review-receipt gate. +Quick, direct Hotfix, and Tweak are exempt from contract, execution-plan, and review-receipt gates; they persist `test_result: pass` after bounded verification. ```bash ssf execution recommend changes/my-change \ @@ -289,9 +289,11 @@ Recovery, switching, and manual save form a control-plane overlay, not a ninth workflow state; their CLI and CodeBuddy/WorkBuddy Markdown adapters keep the same guards. -### Fast Paths (hotfix / tweak) +### Fast Paths (Quick / Hotfix / Tweak) -- **hotfix** — ≤2 files, no new modules → `exploring -> bridging -> approved-for-build -> executing`. It may skip full planning artifacts such as `proposal.md`, `design.md`, `tasks.md`, and `specs/`, but it still requires a fresh minimal `execution-contract.md` plus DP-3 approval before implementation +- **Quick** — ≤3 low-risk code files/tasks → same-turn recommendation and direct acceptance, then targeted verification. +- **direct Hotfix** — incident, ≤2 files/tasks → direct path plus original-symptom regression. +- **legacy Hotfix** — no direct receipt → minimal contract, DP-3, execution plan, and review remain required. - **tweak** — ≤4 files, config/docs only → skip planning + bridging, direct edit --- @@ -354,7 +356,7 @@ Content-level detection, not timestamps: proposal scope changed, approved spec b
How does SDD (Subagent-Driven Development) work? -For full/hotfix, `ssf execution recommend` first presents Inline, Batch Inline, +For Full/legacy Hotfix, `ssf execution recommend` first presents Inline, Batch Inline, and SDD with evidence from the change, then recommends one. The user confirms a selection with `--confirm`; a different selection requires `--acknowledge-recommendation`. The saved execution plan at diff --git a/docs/artifact-contract.md b/docs/artifact-contract.md index 1fb04d3..b1f124a 100644 --- a/docs/artifact-contract.md +++ b/docs/artifact-contract.md @@ -57,7 +57,7 @@ Defines: - review gates and their review receipts - escalation rules -For full/hotfix, `ssf execution recommend` lists applicable execution modes and +For Full/legacy Hotfix, `ssf execution recommend` lists applicable execution modes and recommends one from task count and wave strategy, and persists a recommendation receipt at `/.superpowers/sdd/execution-recommendation.json`. `plan` and `revise` require the receipt to match the current artifacts, contract, and @@ -67,8 +67,7 @@ waves. The user confirms the selected mode with `--confirm`; a non-recommended m the persisted execution plan to `/.superpowers/sdd/execution-plan.json`. That JSON records each wave's dependencies and parallel/serial strategy; it is not stored in `execution-contract.md`. A current `pass` review receipt is -required for every wave before dependent work or closing proceeds. `tweak` is -exempt from execution-plan and review-receipt gates. `ssf execution revise` +required for every wave before dependent work or closing proceeds. Quick, direct Hotfix, and Tweak are exempt from execution-plan and review-receipt gates and persist `test_result: pass` after bounded verification. `ssf execution revise` retains or upgrades an existing plan as `sdd`, requires fresh confirmation, creates a new revision, and clears prior review receipts; it never permits a downgrade. @@ -98,11 +97,11 @@ the same CLI guards; other platforms are not promised identical slash names. ## Guardrail -Implementation starts only after: +For Full/legacy Hotfix, implementation starts only after: - planning artifacts exist - `execution-contract.md` exists - the user approves the execution contract -- full/hotfix have a current `ssf execution plan` with a user-confirmed mode and +- Full/legacy Hotfix have a current `ssf execution plan` with a user-confirmed mode and persisted recommendation evidence - every completed wave records a current `pass` review receipt before closing diff --git a/docs/decision-points.md b/docs/decision-points.md index 6bccf71..d7622f4 100644 --- a/docs/decision-points.md +++ b/docs/decision-points.md @@ -6,9 +6,10 @@ - **编号**:DP-0 - **名称**:设计前确认 -- **触发条件**:`workflow-start` 检测到 change 目录不存在、规划工件不存在或不完整、`dp_0_confirmed` 尚未确认,或 legacy change 的 workflow 为 `auto`/空时;该门禁覆盖 full 以及直接进入 hotfix/tweak 的 fast paths,不只发生在路由到 `spec-writer` 之前 +- **触发条件**:Full 或 legacy change 在 planning 前触发;Quick、direct Hotfix、Tweak 不等待 DP-0,先记录可验证 recommendation/direct receipt 后执行。 - **所需输入**:变更名称与意图、已知约束(命名风格、兼容性、受影响平台)、是否包含相关优化、用户沟通偏好;以及最少路径事实(任务数、文件数、是否仅配置/文档、是否涉及 schema/API、新模块和不确定性) -- **路径选择协议**:`workflow-start` 先读取 `ssf workflow show`;仅在 `missing_facts` 列出的字段缺失时提问,再运行 `ssf workflow recommend`。它必须向用户展示 Observed、Available、Recommended、Why 四项信息,推荐本身不改变状态也不写入 workflow。用户明确选择 `full`、`hotfix` 或 `tweak` 后,才可用 `ssf workflow select --confirm` 持久化;选择非推荐路径还必须显式传入 `--acknowledge-recommendation`。 +- **路径选择协议**:低风险 Quick 与 incident Hotfix 同轮展示 recommendation 后可运行 `ssf workflow accept --source direct-request`;其余路径使用 `show`、补齐 missing facts、`recommend` 和 `select --confirm`。direct receipt 代替短路径 DP。 +- **短路径收口**:Quick、direct Hotfix、Tweak 以边界内验证摘要和 `test_result: pass` 收口,不写 DP-6 或 DP-7。 - **确认顺序**:可先解析 `artifact_language`,随后必须完成路径 receipt 读取、最少事实补全、建议展示和用户选择;路径摘要与其他 DP-0 决定合并确认后,才可设置 `dp_0_confirmed=true`。 - **预期输出**:完整、防篡改的路径选择 receipt 固定保存在 change overlay 的 `.superpowers/sdd/workflow-selection.json`,用于恢复和审计;`.spec-superflow.yaml` 的 `dp_0_*` 只保存确认结果与幂等的 `workflow_path`/推荐对齐摘要,并保留既有 `scope` 和 `artifact_language`。空目录的 legacy artifact inference 可以返回 `full` 以兼容旧 API,但绝不能替代入口的用户选择。 - **关联 skill**:`spec-superflow:workflow-start` @@ -35,7 +36,7 @@ - **编号**:DP-3 - **名称**:契约批准 -- **触发条件**:contract-builder 完成 `execution-contract.md` 的生成后,用户必须明确批准该契约方可进入执行阶段(硬门禁,不可跳过) +- **触发条件**:仅 Full 或 legacy Hotfix 的 contract-builder 生成契约后触发;Quick/direct Hotfix/Tweak 不适用。 - **所需输入**:`execution-contract.md` 全文,包含执行批次、任务依赖、验收标准、回滚策略 - **预期输出**:用户明确批准(approve)执行契约,或提出修改要求;未获批准前 build-executor 不得启动 - **关联 skill**:`spec-superflow:contract-builder` @@ -44,7 +45,7 @@ - **编号**:DP-4 - **名称**:执行模式选择 -- **触发条件**:build-executor 启动执行前,用户需要选择本次执行的开发模式 +- **触发条件**:仅 Full 或 legacy Hotfix 在 build-executor 启动前选择执行模式;Quick/direct Hotfix/Tweak 不适用。 - **所需输入**:已批准的 `execution-contract.md`、项目测试基础设施现状,以及 `ssf execution recommend` 提供的执行模式证据与建议 - **预期输出**:用户明确选择 `Inline`、`Batch Inline` 或 `SDD` 执行模式,build-executor 据此创建受确认的执行计划。DP-4 不重新选择 DP-0 已确认的 `full`、`hotfix` 或 `tweak` 路径。 - **关联 skill**:`spec-superflow:build-executor` diff --git a/docs/state-machine.md b/docs/state-machine.md index 7fa39b9..737a785 100644 --- a/docs/state-machine.md +++ b/docs/state-machine.md @@ -14,7 +14,7 @@ #### Workflow Path Intake At entry, `workflow-start` reads the persisted `workflow` selection first. An -explicit `full`, `hotfix`, or `tweak` selection wins. Otherwise it runs `ssf +explicit `full`, `hotfix`, `tweak`, or `quick` selection wins. Otherwise it runs `ssf workflow show`, asks only for `missing_facts`, runs `ssf workflow recommend`, and presents Observed, Available, Recommended, and Why. Recommendation does not change state or select a path: only an explicit `ssf workflow select @@ -47,13 +47,11 @@ selection creates a ninth state or performs a phase transition. - ambiguity is compressed into explicit approved decisions - `contract-builder` is active - parsing engine auto-extracts intent/scope/test-obligations/constraints/batches -- hotfix also passes through this state with a fresh minimal contract and DP-3 approval before build +- legacy Hotfix passes through this state with a fresh minimal contract and DP-3 approval; direct Hotfix does not ### `approved-for-build` -- the execution contract exists -- the user has approved it -- full/hotfix still require a current execution plan before implementation can begin +- Full/legacy Hotfix have an approved execution contract and current plan; Quick/direct Hotfix/Tweak use their receipt-aware short path ### `executing` @@ -65,7 +63,7 @@ selection creates a ninth state or performs a phase transition. ## Execution Plan Control Plane -For full/hotfix, DP-4 is the persisted execution plan created by `ssf execution +For Full/legacy Hotfix, DP-4 is the persisted execution plan created by `ssf execution plan` at `/.superpowers/sdd/execution-plan.json`, not an arbitrary state value or content stored in `execution-contract.md`. Before planning, run `ssf execution recommend`; it lists applicable `inline`, `batch-inline`, and @@ -75,7 +73,7 @@ accept only a receipt whose artifacts, contract, and waves still match. The user selected mode with `--confirm`; a non-recommended selection also requires `--acknowledge-recommendation`. Batch Inline remains serial and is never a substitute for parallel execution. -`tweak` is exempt from execution-plan and review-receipt requirements. +Quick, direct Hotfix, and Tweak are exempt from execution-plan and review-receipt requirements; each closes only with `test_result: pass`. The plan names ordered execution waves, dependencies, and parallel/serial strategy. `ssf execution show --json` reports which current waves @@ -191,7 +189,6 @@ If the contract changed, the artifacts changed. ## Fast-Path Notes -- `hotfix` follows `exploring -> bridging -> approved-for-build -> executing`. -- `hotfix` may skip full planning artifacts such as `proposal.md`, `design.md`, `tasks.md`, and `specs/`. -- `hotfix` still requires a fresh minimal `execution-contract.md` and explicit DP-3 approval before implementation. -- `tweak` remains the only path that can jump directly from `exploring` to `approved-for-build`. +- **direct Hotfix** (incident, ≤2 files/tasks) and **Quick** (≤3 files/tasks) follow `exploring -> approved-for-build -> executing` with a valid direct receipt; no artifacts, contract, plan, review receipt, or DP approval. Direct Hotfix proves the original symptom; Quick runs focused verification. +- **legacy Hotfix** follows `exploring -> bridging -> approved-for-build -> executing` and retains its minimal contract and DP-3. +- **Tweak** (≤4 configuration/doc files) also jumps directly from `exploring` to `approved-for-build`. diff --git a/scripts/install-cursor.mjs b/scripts/install-cursor.mjs index 3b71cc2..67234fd 100644 --- a/scripts/install-cursor.mjs +++ b/scripts/install-cursor.mjs @@ -145,10 +145,10 @@ alwaysApply: true ## 全局禁止 -- 没有 execution-contract.md 或未经用户明确批准,不得进入实现。 -- full/hotfix 必须先运行 ssf execution plan ...;没有 current execution plan 不得开始实现。 -- 只有 all pass review receipts 后才可 closing;不得把未审查的 wave 当作完成。 -- 上述 plan/receipt gates 仅适用于 full/hotfix;tweak 免除这些 gates (tweak exempt)。 +- Full 或 legacy Hotfix 没有 execution-contract.md 或未经用户明确批准,不得进入实现。 +- Full 或 legacy Hotfix 必须先运行 ssf execution plan ...;没有 current execution plan 不得开始实现。 +- 只有 Full/legacy Hotfix 的 all pass review receipts 后才可 closing;不得把未审查的 wave 当作完成。 +- Quick、direct Hotfix、tweak 不要求 contract、execution plan、review receipt 或 DP-3/DP-4;它们须在边界内验证并持久化 test_result: pass。direct Hotfix 必须验证原症状回归。 - 执行过程中如果发现需求/范围变化,必须回退到 specifying 或 bridging,而不是直接改代码。 - 不要直接调用执行类 skill(如 "/build-executor"),必须通过入口路由。 @@ -157,8 +157,7 @@ alwaysApply: true - DP-0:设计前确认 - DP-1:需求确认 - DP-2:工件审查 -- DP-3:是否批准 execution contract? -- DP-4:先运行 ssf execution recommend,展示可用模式与推荐;用户用 --confirm 确认,非推荐选择额外使用 --acknowledge-recommendation +- DP-3/DP-4:仅 Full 或 legacy Hotfix 需要 contract 批准与执行模式确认。 - DP-5:调试升级 - DP-6:验证失败 - DP-7:是否收口归档? diff --git a/scripts/install-zcode.mjs b/scripts/install-zcode.mjs index d015840..62b4471 100644 --- a/scripts/install-zcode.mjs +++ b/scripts/install-zcode.mjs @@ -145,10 +145,10 @@ alwaysApply: true ## 全局禁止 -- 没有 execution-contract.md 或未经用户明确批准,不得进入实现。 -- full/hotfix 必须先运行 ssf execution plan ...;没有 current execution plan 不得开始实现。 -- 只有 all pass review receipts 后才可 closing;不得把未审查的 wave 当作完成。 -- 上述 plan/receipt gates 仅适用于 full/hotfix;tweak 免除这些 gates (tweak exempt)。 +- Full 或 legacy Hotfix 没有 execution-contract.md 或未经用户明确批准,不得进入实现。 +- Full 或 legacy Hotfix 必须先运行 ssf execution plan ...;没有 current execution plan 不得开始实现。 +- 只有 Full/legacy Hotfix 的 all pass review receipts 后才可 closing;不得把未审查的 wave 当作完成。 +- Quick、direct Hotfix、tweak 不要求 contract、execution plan、review receipt 或 DP-3/DP-4;它们须在边界内验证并持久化 test_result: pass。direct Hotfix 必须验证原症状回归。 - 执行过程中如果发现需求/范围变化,必须回退到 specifying 或 bridging,而不是直接改代码。 - 不要直接调用执行类 skill(如 "/build-executor"),必须通过入口路由。 @@ -157,8 +157,7 @@ alwaysApply: true - DP-0:设计前确认 - DP-1:需求确认 - DP-2:工件审查 -- DP-3:是否批准 execution contract? -- DP-4:先运行 ssf execution recommend,展示可用模式与推荐;用户用 --confirm 确认,非推荐选择额外使用 --acknowledge-recommendation +- DP-3/DP-4:仅 Full 或 legacy Hotfix 需要 contract 批准与执行模式确认。 - DP-5:调试升级 - DP-6:验证失败 - DP-7:是否收口归档? diff --git a/scripts/lib/cmd-install-workbuddy.mjs b/scripts/lib/cmd-install-workbuddy.mjs index de86fa3..09dd263 100644 --- a/scripts/lib/cmd-install-workbuddy.mjs +++ b/scripts/lib/cmd-install-workbuddy.mjs @@ -235,10 +235,10 @@ function phaseGuardContent() { ## 全局禁止 -- 没有 execution-contract.md 或未经用户明确批准,不得进入实现。 -- full/hotfix 必须先运行 ssf execution plan ...;没有 current execution plan 不得开始实现。 -- 只有 all pass review receipts 后才可 closing;不得把未审查的 wave 当作完成。 -- 上述 plan/receipt gates 仅适用于 full/hotfix;tweak 免除这些 gates (tweak exempt)。 +- Full 或 legacy Hotfix 没有 execution-contract.md 或未经用户明确批准,不得进入实现。 +- Full 或 legacy Hotfix 必须先运行 ssf execution plan ...;没有 current execution plan 不得开始实现。 +- 只有 Full/legacy Hotfix 的 all pass review receipts 后才可 closing;不得把未审查的 wave 当作完成。 +- Quick、direct Hotfix、tweak 不要求 contract、execution plan、review receipt 或 DP-3/DP-4;它们须在边界内验证并持久化 test_result: pass。direct Hotfix 必须验证原症状回归。 - 执行过程中如果发现需求/范围变化,必须回退到 specifying 或 bridging,而不是直接改代码。 - 不要直接调用执行类 skill(如 "/build-executor"),必须通过入口路由。 @@ -247,8 +247,7 @@ function phaseGuardContent() { - DP-0:设计前确认 - DP-1:需求确认 - DP-2:工件审查 -- DP-3:是否批准 execution contract? -- DP-4:先运行 ssf execution recommend,展示可用模式与推荐;用户用 --confirm 确认,非推荐选择额外使用 --acknowledge-recommendation +- DP-3/DP-4:仅 Full 或 legacy Hotfix 需要 contract 批准与执行模式确认。 - DP-5:调试升级 - DP-6:验证失败 - DP-7:是否收口归档? diff --git a/scripts/lib/install.mjs b/scripts/lib/install.mjs index caa7667..667b5db 100644 --- a/scripts/lib/install.mjs +++ b/scripts/lib/install.mjs @@ -134,10 +134,10 @@ function phaseGuardContent(rulesFormat, platformId) { ## 全局禁止 -- 没有 execution-contract.md 或未经用户明确批准,不得进入实现。 -- full/hotfix 必须先运行 ssf execution plan ...;没有 current execution plan 不得开始实现。 -- 只有 all pass review receipts 后才可 closing;不得把未审查的 wave 当作完成。 -- 上述 plan/receipt gates 仅适用于 full/hotfix;tweak 免除这些 gates (tweak exempt)。 +- Full 或 legacy Hotfix 没有 execution-contract.md 或未经用户明确批准,不得进入实现。 +- Full 或 legacy Hotfix 必须先运行 ssf execution plan ...;没有 current execution plan 不得开始实现。 +- 只有 Full/legacy Hotfix 的 all pass review receipts 后才可 closing;不得把未审查的 wave 当作完成。 +- Quick、direct Hotfix、tweak 不要求 contract、execution plan、review receipt 或 DP-3/DP-4;它们须在边界内验证并持久化 test_result: pass。direct Hotfix 必须验证原症状回归。 - 执行过程中如果发现需求/范围变化,必须回退到 specifying 或 bridging,而不是直接改代码。 - 不要直接调用执行类 skill(如 "/build-executor"),必须通过入口路由。 @@ -146,8 +146,7 @@ function phaseGuardContent(rulesFormat, platformId) { - DP-0:设计前确认 - DP-1:需求确认 - DP-2:工件审查 -- DP-3:是否批准 execution contract? -- DP-4:先运行 ssf execution recommend,展示可用模式与推荐;用户用 --confirm 确认,非推荐选择额外使用 --acknowledge-recommendation +- DP-3/DP-4:仅 Full 或 legacy Hotfix 需要 contract 批准与执行模式确认。 - DP-5:调试升级 - DP-6:验证失败 - DP-7:是否收口归档? diff --git a/tests/lib/execution-control-plane.test.mjs b/tests/lib/execution-control-plane.test.mjs index b5a08c4..393be07 100644 --- a/tests/lib/execution-control-plane.test.mjs +++ b/tests/lib/execution-control-plane.test.mjs @@ -20,6 +20,15 @@ describe('execution control plane instructions', () => { assert.match(content, /test_result.*pass/is, `${path} requires a persisted short-path verification result`); } }); + it('publishes direct-path semantics in user documentation', () => { + for (const path of ['README.md', 'INSTALL.md', 'docs/README_en.md', 'docs/state-machine.md', 'docs/artifact-contract.md', 'docs/decision-points.md']) { + const content = read(path); + assert.match(content, /Quick/); + assert.match(content, /direct Hotfix/i); + assert.match(content, /legacy Hotfix/i); + assert.match(content, /test_result.*pass/is); + } + }); it('documents #45 guarded execution', () => { const documents = [ 'README.md', @@ -252,11 +261,9 @@ describe('execution control plane instructions', () => { 'scripts/install-zcode.mjs', ]) { const content = read(path); - assert.match(content, /execution recommend/); - assert.match(content, /--confirm/); - assert.match(content, /acknowledge-recommendation/); - assert.match(content, /all.*pass.*review receipt.*closing/is); - assert.match(content, /full\/hotfix.*tweak.*exempt/is); + assert.match(content, /Full.*legacy Hotfix/is); + assert.match(content, /Quick.*direct Hotfix.*tweak/is); + assert.match(content, /test_result: pass/); } }); diff --git a/tests/lib/platform-runtime-distribution.test.mjs b/tests/lib/platform-runtime-distribution.test.mjs index 3faeb68..67979cc 100644 --- a/tests/lib/platform-runtime-distribution.test.mjs +++ b/tests/lib/platform-runtime-distribution.test.mjs @@ -30,6 +30,15 @@ function skill(name) { } describe('canonical skill runtime protocol', () => { + it('publishes four-mode direct-path rules in generated Cursor and ZCODE assets', () => { + for (const path of ['scripts/install-cursor.mjs', 'scripts/install-zcode.mjs']) { + const content = readFileSync(join(ROOT, path), 'utf8'); + assert.match(content, /Quick、direct Hotfix、tweak/); + assert.match(content, /Full 或 legacy Hotfix/); + assert.match(content, /test_result: pass/); + } + }); + it('uses the exact package-version prefix for every runtime-dependent skill', () => { for (const name of RUNTIME_SKILLS) { const content = skill(name); From 282140b35887f1ca7902f699572e59d9eb2c3358 Mon Sep 17 00:00:00 2001 From: MageByte Date: Sun, 26 Jul 2026 15:41:47 +0800 Subject: [PATCH 12/15] fix: align distributed short-path guidance --- docs/README_en.md | 2 +- docs/state-machine.md | 58 +++++++++++-------- tests/lib/cmd-install-workbuddy.test.mjs | 9 ++- tests/lib/cmd-install-zcode.test.mjs | 7 ++- .../platform-runtime-distribution.test.mjs | 26 +++++++++ 5 files changed, 73 insertions(+), 29 deletions(-) diff --git a/docs/README_en.md b/docs/README_en.md index 7683c3d..edd29c5 100644 --- a/docs/README_en.md +++ b/docs/README_en.md @@ -240,7 +240,7 @@ You: "add authorization to the API" closing CLOSED successful terminal state (no next skill) ``` -**Hard constraints:** No `execution-contract.md` or no approval → implementation blocked. Requirements change mid-execution → forced rollback. Bug encountered → must enter debugging state, no ad-hoc fixes. +**Hard constraints:** Full and legacy Hotfix require an approved `execution-contract.md`; Quick, direct Hotfix, and Tweak instead stay within their accepted boundary and persist `test_result: pass`. Requirements change mid-execution → forced rollback. Bug encountered → must enter debugging state, no ad-hoc fixes. ### Guarded execution plans diff --git a/docs/state-machine.md b/docs/state-machine.md index 737a785..3c2de88 100644 --- a/docs/state-machine.md +++ b/docs/state-machine.md @@ -17,22 +17,28 @@ At entry, `workflow-start` reads the persisted `workflow` selection first. An explicit `full`, `hotfix`, `tweak`, or `quick` selection wins. Otherwise it runs `ssf workflow show`, asks only for `missing_facts`, runs `ssf workflow recommend`, and presents Observed, Available, Recommended, and Why. Recommendation does -not change state or select a path: only an explicit `ssf workflow select ---confirm` writes `workflow`. A non-recommended selection requires -`--acknowledge-recommendation`. The legacy `runtime infer` compatibility API -may return `full` for an empty directory, but it never replaces the user's -intake selection. - -This intake completes before DP-0 is marked confirmed. Artifact language may -be resolved first, but `dp_0_confirmed=true` is written only after the selected -path summary and the remaining scope, constraints, and communication decisions -are confirmed together. The full selection receipt lives at +not change state. Full, legacy Hotfix, and Tweak are selected explicitly with +`ssf workflow select --confirm`; a non-recommended selection requires +`--acknowledge-recommendation`. A recommended Quick or incident Hotfix may be +accepted with `ssf workflow accept --source direct-request`, which records the +valid direct receipt needed by its short path. The legacy `runtime infer` +compatibility API may return `full` for an empty directory, but it never +replaces the user's intake selection. + +Full and legacy Hotfix intake completes before DP-0 is marked confirmed. +Artifact language may be resolved first, but `dp_0_confirmed=true` is written +only after the selected path summary and the remaining scope, constraints, and +communication decisions are confirmed together. Quick, direct Hotfix, and +Tweak do not mark DP-0 or create planning artifacts. The full selection +receipt lives at `.superpowers/sdd/workflow-selection.json`; DP-0 state stores only the idempotent summary while preserving scope and `artifact_language`. -This is the DP-0 planning-path decision. DP-4 remains the separate execution -mode decision among Inline, Batch Inline, and SDD. Neither recommendation nor -selection creates a ninth state or performs a phase transition. +For Full and legacy Hotfix, this is the DP-0 planning-path decision. DP-4 +remains their separate execution-mode decision among Inline, Batch Inline, and +SDD. Neither recommendation nor selection creates a ninth state or performs a +phase transition; the direct receipt then permits the short transition from +`exploring` to `approved-for-build`. ### `specifying` @@ -51,15 +57,18 @@ selection creates a ninth state or performs a phase transition. ### `approved-for-build` -- Full/legacy Hotfix have an approved execution contract and current plan; Quick/direct Hotfix/Tweak use their receipt-aware short path +- Full/legacy Hotfix have an approved execution contract and current plan; Quick/direct Hotfix use a valid direct receipt, while Tweak uses its explicitly selected short path ### `executing` -- implementation follows the execution contract -- TDD, SDD (subagent-driven), review gates, and escalation rules apply -- `build-executor` is active -- `code-reviewer` invoked after each execution batch -- 发布验证、delta-spec 同步与审计都必须在此状态完成;必要时调用 `release-archivist` 和 `spec-merger`,再执行最终状态转换 +- Full/legacy Hotfix implementation follows the execution contract, with TDD, + SDD (subagent-driven), review gates, and escalation rules +- Quick/direct Hotfix/Tweak execute only their accepted boundary and focused + verification; they persist `test_result: pass` instead of a plan/review receipt +- `build-executor` is active for the applicable path; `code-reviewer` is invoked + after each Full/legacy execution batch +- Full/legacy release verification, delta-spec sync, and audit evidence complete + in this state before the final transition ## Execution Plan Control Plane @@ -75,9 +84,9 @@ selected mode with `--confirm`; a non-recommended selection also requires substitute for parallel execution. Quick, direct Hotfix, and Tweak are exempt from execution-plan and review-receipt requirements; each closes only with `test_result: pass`. -The plan names ordered execution waves, dependencies, and parallel/serial -strategy. `ssf execution show --json` reports which current waves -are eligible. Each completed wave must have a current +For Full/legacy Hotfix, the plan names ordered execution waves, dependencies, +and parallel/serial strategy. `ssf execution show --json` reports +which current waves are eligible. Each completed Full/legacy wave must have a current `pass` review receipt, recorded with `ssf execution review`, before a dependent wave or `closing` can proceed. `ssf execution revise` retains or upgrades an existing plan as `sdd`; that new revision requires a fresh confirmation (and @@ -140,9 +149,8 @@ eight core states. ## Transitions ```text - exploring ──── hotfix ─────────> bridging (fast-path) - bridging ──── hotfix ─────────> approved-for-build - exploring ──── tweak ──────────> approved-for-build (fast-path) + exploring ──── legacy Hotfix ──> bridging ──> approved-for-build + exploring ──── Quick/direct Hotfix/Tweak ──> approved-for-build (short path) exploring -> specifying -> bridging -> approved-for-build -> executing -> closing ^ ^ | ^ | diff --git a/tests/lib/cmd-install-workbuddy.test.mjs b/tests/lib/cmd-install-workbuddy.test.mjs index 540a8eb..afcc520 100644 --- a/tests/lib/cmd-install-workbuddy.test.mjs +++ b/tests/lib/cmd-install-workbuddy.test.mjs @@ -98,8 +98,13 @@ describe('cmd-install-workbuddy', () => { // Runtime dirs copied. assert.ok(existsSync(join(pluginDir, 'scripts', 'check-update.mjs'))); - // Phase-guard rule deployed. - assert.ok(existsSync(join(pluginDir, 'rules', 'phase-guard.md'))); + // Phase-guard rule deployed with the actual four-mode contract. + const guardPath = join(pluginDir, 'rules', 'phase-guard.md'); + assert.ok(existsSync(guardPath)); + const guard = readFileSync(guardPath, 'utf-8'); + assert.match(guard, /Full 或 legacy Hotfix/); + assert.match(guard, /Quick、direct Hotfix、tweak/); + assert.match(guard, /test_result: pass/); // Plugin manifest deployed. const manifest = JSON.parse(readFileSync(join(pluginDir, '.codebuddy-plugin', 'plugin.json'), 'utf-8')); diff --git a/tests/lib/cmd-install-zcode.test.mjs b/tests/lib/cmd-install-zcode.test.mjs index 2b857b5..993f8ef 100644 --- a/tests/lib/cmd-install-zcode.test.mjs +++ b/tests/lib/cmd-install-zcode.test.mjs @@ -27,7 +27,12 @@ describe('BUG/#29: install-zcode deploys skills', () => { assert.equal(existsSync(wfStart), true, 'workflow-start skill should be deployed'); const content = readFileSync(wfStart, 'utf-8'); assert.equal(content.includes('${CLAUDE_PLUGIN_ROOT}'), false, 'CLAUDE_PLUGIN_ROOT should be rewritten to an absolute path'); - assert.equal(existsSync(join(cwd, '.zcode', 'rules', 'phase-guard.mdc')), true, 'phase guard should be written'); + const guardPath = join(cwd, '.zcode', 'rules', 'phase-guard.mdc'); + assert.equal(existsSync(guardPath), true, 'phase guard should be written'); + const guard = readFileSync(guardPath, 'utf-8'); + assert.match(guard, /Full 或 legacy Hotfix/); + assert.match(guard, /Quick、direct Hotfix、tweak/); + assert.match(guard, /test_result: pass/); }); it('SHALL give contract-builder the portable execution-contract asset command', () => { diff --git a/tests/lib/platform-runtime-distribution.test.mjs b/tests/lib/platform-runtime-distribution.test.mjs index 67979cc..a9d80ee 100644 --- a/tests/lib/platform-runtime-distribution.test.mjs +++ b/tests/lib/platform-runtime-distribution.test.mjs @@ -8,6 +8,7 @@ import { cpSync, mkdirSync, mkdtempSync, rmSync, writeFileSync } from 'node:fs'; import { join } from 'node:path'; import { tmpdir } from 'node:os'; import { PLATFORM_RUNTIME_INVENTORY, ZCODE_COMPATIBILITY_PATH } from '../../scripts/lib/platform-runtime-inventory.mjs'; +import { installPlatform } from '../../scripts/lib/install.mjs'; const ROOT = process.cwd(); const VERSION = JSON.parse(readFileSync(join(ROOT, 'package.json'), 'utf8')).version; @@ -75,6 +76,31 @@ describe('canonical skill runtime protocol', () => { }); describe('local runtime deployment', () => { + it('writes four-mode phase guards through the Cursor and shared installers', async () => { + const cursorTarget = mkdtempSync(join(tmpdir(), 'ssf-cursor-guard-')); + const sharedTarget = mkdtempSync(join(tmpdir(), 'ssf-shared-guard-')); + try { + execFileSync(process.execPath, [join(ROOT, 'scripts', 'install-cursor.mjs'), '--local', ROOT], { + cwd: cursorTarget, + stdio: 'pipe', + }); + await installPlatform('cline', { local: ROOT, cwd: sharedTarget }); + + const guards = [ + readFileSync(join(cursorTarget, '.cursor', 'rules', 'phase-guard.mdc'), 'utf8'), + readFileSync(join(sharedTarget, '.clinerules', 'phase-guard.md'), 'utf8'), + ]; + for (const guard of guards) { + assert.match(guard, /Full 或 legacy Hotfix/); + assert.match(guard, /Quick、direct Hotfix、tweak/); + assert.match(guard, /test_result: pass/); + } + } finally { + rmSync(cursorTarget, { recursive: true, force: true }); + rmSync(sharedTarget, { recursive: true, force: true }); + } + }); + it('rewrites the canonical prefix to ZCODE\'s installed runtime tree', () => { const target = mkdtempSync(join(tmpdir(), 'ssf-zcode-runtime-')); try { From 863fc89c8461a378613c4c9c7cde40db5e7bddf8 Mon Sep 17 00:00:00 2001 From: MageByte Date: Sun, 26 Jul 2026 15:45:45 +0800 Subject: [PATCH 13/15] test: accept scoped release decision headings --- tests/lib/closing-terminal-semantics.test.mjs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/lib/closing-terminal-semantics.test.mjs b/tests/lib/closing-terminal-semantics.test.mjs index 235650c..15a8246 100644 --- a/tests/lib/closing-terminal-semantics.test.mjs +++ b/tests/lib/closing-terminal-semantics.test.mjs @@ -93,8 +93,8 @@ describe('closing terminal lifecycle', () => { const archivist = read('skills/release-archivist/SKILL.md'); const guard = archivist.indexOf('## Execution-State Guard'); const audit = archivist.indexOf(`${RUNTIME_PREFIX} audit `); - const dp6 = archivist.indexOf('### DP-6 (Verification Outcome)'); - const dp7 = archivist.indexOf('### DP-7 (Archive Confirmation)'); + const dp6 = archivist.indexOf('### DP-6 (Verification Outcome'); + const dp7 = archivist.indexOf('### DP-7 (Archive Confirmation'); const merger = archivist.indexOf('invoke `spec-merger`'); const transition = archivist.indexOf(`${RUNTIME_PREFIX} state transition closing`); From 4983c84ca97ae4580d288cec99fc97cb137a17be Mon Sep 17 00:00:00 2001 From: MageByte Date: Sun, 26 Jul 2026 15:55:09 +0800 Subject: [PATCH 14/15] fix: allow short-path escalation to full --- docs/state-machine.md | 9 +++- scripts/lib/cmd-workflow.mjs | 19 +++++-- scripts/lib/workflow-recommendation.mjs | 1 + scripts/spec-superflow.mjs | 4 +- skills/build-executor/SKILL.md | 5 +- skills/workflow-start/SKILL.md | 23 ++++---- tests/lib/cmd-workflow.test.mjs | 62 ++++++++++++++-------- tests/lib/workflow-recommendation.test.mjs | 12 +++++ 8 files changed, 93 insertions(+), 42 deletions(-) diff --git a/docs/state-machine.md b/docs/state-machine.md index 3c2de88..6596204 100644 --- a/docs/state-machine.md +++ b/docs/state-machine.md @@ -14,7 +14,9 @@ #### Workflow Path Intake At entry, `workflow-start` reads the persisted `workflow` selection first. An -explicit `full`, `hotfix`, `tweak`, or `quick` selection wins. Otherwise it runs `ssf +an explicit `full` selection wins. A selected `hotfix`, `tweak`, or `quick` stays +active only while it remains within its boundary; a scope or risk increase refreshes +the recommendation before escalating to Full. Otherwise it runs `ssf workflow show`, asks only for `missing_facts`, runs `ssf workflow recommend`, and presents Observed, Available, Recommended, and Why. Recommendation does not change state. Full, legacy Hotfix, and Tweak are selected explicitly with @@ -25,6 +27,11 @@ valid direct receipt needed by its short path. The legacy `runtime infer` compatibility API may return `full` for an empty directory, but it never replaces the user's intake selection. +Quick is not a selectable workflow: it must use direct acceptance. To escalate +Quick, direct Hotfix, or Tweak, run `ssf workflow recommend` with the updated +facts, then confirm `ssf workflow select --mode full`; that replaces the short +selection with an auditable Full intake receipt. + Full and legacy Hotfix intake completes before DP-0 is marked confirmed. Artifact language may be resolved first, but `dp_0_confirmed=true` is written only after the selected path summary and the remaining scope, constraints, and diff --git a/scripts/lib/cmd-workflow.mjs b/scripts/lib/cmd-workflow.mjs index ccf9f14..7f55ef1 100644 --- a/scripts/lib/cmd-workflow.mjs +++ b/scripts/lib/cmd-workflow.mjs @@ -34,6 +34,8 @@ const BOOLEAN_FACTS = { 'new-module': ['yes', 'no', 'unknown'], }; +const SELECTABLE_WORKFLOW_MODES = Object.freeze(['full', 'hotfix', 'tweak']); + class UsageError extends Error {} export async function run(args) { @@ -58,10 +60,13 @@ export async function run(args) { requireStateFile(changeDir); const state = readState(changeDir); - if (['select', 'accept'].includes(subcommand) && isExplicitWorkflow(state.workflow)) { + if (subcommand === 'accept' && isExplicitWorkflow(state.workflow)) { + return fail('workflow is already explicitly selected', 1); + } + if (subcommand === 'select' && isExplicitWorkflow(state.workflow) && !canEscalateToFull(state, values)) { return fail('workflow is already explicitly selected', 1); } - if (subcommand === 'recommend' && isExplicitWorkflow(state.workflow)) { + if (subcommand === 'recommend' && state.workflow === 'full') { return print({ source: 'explicit-state', workflow: state.workflow }, values.json); } if (subcommand === 'recommend') return recommend(changeDir, values); @@ -80,8 +85,8 @@ function recommend(changeDir, values) { } function select(changeDir, state, values) { - if (!WORKFLOW_MODES.includes(values.mode)) { - throw new UsageError(`--mode must be one of: ${WORKFLOW_MODES.join(', ')}`); + if (!SELECTABLE_WORKFLOW_MODES.includes(values.mode)) { + throw new UsageError(`--mode must be one of: ${SELECTABLE_WORKFLOW_MODES.join(', ')}`); } const record = recordWorkflowSelection(changeDir, { mode: values.mode, @@ -93,6 +98,10 @@ function select(changeDir, state, values) { return print({ ok: true, source: 'user-confirmed', record }, values.json); } +function canEscalateToFull(state, values) { + return values.mode === 'full' && ['quick', 'hotfix', 'tweak'].includes(state.workflow); +} + function accept(changeDir, state, values) { const record = acceptWorkflowRecommendation(changeDir, { source: values.source }); persistWorkflowSelection(changeDir, state, record); @@ -265,7 +274,7 @@ function fail(message, exitCode) { function printHelp() { console.log(`Usage: ssf workflow recommend [--task-count ] [--file-count ] [--config-doc-only yes|no|unknown] [--schema-api-change yes|no|unknown] [--new-module yes|no|unknown] [--uncertainty low|high|unknown] [--request-kind standard|incident] [--json] - ssf workflow select --mode full|hotfix|tweak|quick --confirm --reason [--acknowledge-recommendation] [--json] + ssf workflow select --mode full|hotfix|tweak --confirm --reason [--acknowledge-recommendation] [--json] ssf workflow accept --source direct-request [--json] ssf workflow show [--json]`); } diff --git a/scripts/lib/workflow-recommendation.mjs b/scripts/lib/workflow-recommendation.mjs index b113343..dc8ef26 100644 --- a/scripts/lib/workflow-recommendation.mjs +++ b/scripts/lib/workflow-recommendation.mjs @@ -111,6 +111,7 @@ export function recordWorkflowSelection(changeDir, { mode, reason, confirmed, ac throw new Error('workflow recommendation needs more input'); } if (!WORKFLOW_MODES.includes(mode)) throw new Error(`invalid workflow mode: ${mode}`); + if (mode === 'quick') throw new Error('quick workflow must use direct acceptance'); if (confirmed !== true) throw new Error('workflow selection requires --confirm'); if (!isSafeReason(reason)) { throw new Error('workflow selection reason must be non-empty single-line text'); diff --git a/scripts/spec-superflow.mjs b/scripts/spec-superflow.mjs index 6a3d011..7126a85 100755 --- a/scripts/spec-superflow.mjs +++ b/scripts/spec-superflow.mjs @@ -86,8 +86,8 @@ Commands: runtime infer Infer workflow mode without a plugin-root path workflow recommend [--task-count ] [--file-count ] [--config-doc-only yes|no|unknown] [--schema-api-change yes|no|unknown] [--new-module yes|no|unknown] [--uncertainty low|high|unknown] [--request-kind standard|incident] Persist observed intake facts and recommend full, hotfix, tweak, or quick without selecting one - workflow select --mode full|hotfix|tweak|quick --confirm --reason [--acknowledge-recommendation] - Persist a user-confirmed workflow choice after a ready recommendation + workflow select --mode full|hotfix|tweak --confirm --reason [--acknowledge-recommendation] + Persist a user-confirmed Full, legacy Hotfix, or Tweak choice; use accept for Quick/direct Hotfix workflow accept --source direct-request Directly accept a recommended quick or hotfix workflow workflow show [--json] diff --git a/skills/build-executor/SKILL.md b/skills/build-executor/SKILL.md index 5344f3a..d57bf8a 100644 --- a/skills/build-executor/SKILL.md +++ b/skills/build-executor/SKILL.md @@ -45,7 +45,8 @@ Block on: logic defects, spec violations, missing required tests, unintended sco ### Law 4: Rewind on Contract Break — Full and legacy Hotfix Return to `specifying` or `bridging` if: new behavior appears, interfaces change materially, design assumptions fail, artifacts no longer define intended implementation. -For Quick/direct Hotfix, stop and route to Full instead of creating or rewinding a contract. +For Quick/direct Hotfix, stop instead of creating or rewinding a contract; refresh +`workflow recommend` with the observed risk, then select `full --confirm`. ## Execution Mode Selection @@ -153,7 +154,7 @@ Skip TDD. Apply changes directly. Verify file integrity (exists, non-empty, vali ## Direct Quick and Hotfix -Quick direct execution requires the valid direct receipt, a bounded diff, targeted tests or syntax/static checks, and a persisted `test_result: pass`; do not create a contract, execution plan, wave review, DP-6, or DP-7. Direct Hotfix follows the same route only for an incident-backed receipt and must run a regression that demonstrates the original symptom is fixed. Stop rather than expanding scope when the boundary is exceeded or verification fails; route to Full. A legacy Hotfix without a direct receipt remains subject to the contract, DP-3, execution plan, and review receipts. +Quick direct execution requires the valid direct receipt, a bounded diff, targeted tests or syntax/static checks, and a persisted `test_result: pass`; do not create a contract, execution plan, wave review, DP-6, or DP-7. Direct Hotfix follows the same route only for an incident-backed receipt and must run a regression that demonstrates the original symptom is fixed. Stop rather than expanding scope when the boundary is exceeded or verification fails; refresh `workflow recommend` with the observed risk, then select `full --confirm` before resuming. A legacy Hotfix without a direct receipt remains subject to the contract, DP-3, execution plan, and review receipts. ## DP Records diff --git a/skills/workflow-start/SKILL.md b/skills/workflow-start/SKILL.md index d9bc7be..1b3736f 100644 --- a/skills/workflow-start/SKILL.md +++ b/skills/workflow-start/SKILL.md @@ -48,7 +48,7 @@ npx --yes --package spec-superflow@0.11.0 ssf workflow recommend -- npx --yes --package spec-superflow@0.11.0 ssf workflow accept --source direct-request ``` -Quick is ≤3 tasks/files of low-risk code. Hotfix is an incident with a reproducible symptom and ≤2 tasks/files. Display `Observed`, `Recommended`, and `Why`; acceptance is the user's direct request to proceed. Do not create planning artifacts, a contract, an execution plan, wave receipts, or DP approvals. Transition through the receipt-aware guard, execute bounded work, and require `test_result: pass` before closing. Any fourth file, public/schema/API boundary, new module, dependency/permission/data change, high uncertainty, or failed verification stops the path and routes to Full. A legacy Hotfix without a valid direct receipt remains on the Full contract/DP-3/plan/review path. +Quick is ≤3 tasks/files of low-risk code. Hotfix is an incident with a reproducible symptom and ≤2 tasks/files. Display `Observed`, `Recommended`, and `Why`; acceptance is the user's direct request to proceed. Do not create planning artifacts, a contract, an execution plan, wave receipts, or DP approvals. Transition through the receipt-aware guard, execute bounded work, and require `test_result: pass` before closing. Any fourth file, public/schema/API boundary, new module, dependency/permission/data change, high uncertainty, or failed verification stops the path: refresh `workflow recommend` with those facts, then select `full --confirm` before continuing. A legacy Hotfix without a valid direct receipt remains on the Full contract/DP-3/plan/review path. ## DP-0: User Confirmation Gate @@ -89,8 +89,10 @@ state or cause a phase transition. (not `.` or `..`, with no `/` or `\\`), resolve the change dir as `/changes/`, and reject any normalized path that escapes the project's `changes/` directory. 2. If the state file is absent or `dp_0_confirmed` is `false`/null, run `npx --yes --package spec-superflow@0.11.0 ssf state init ` before `show`; initialization must leave DP-0 unconfirmed. -3. Read `state.workflow`. An explicit workflow `full`/`hotfix`/`tweak`/`quick` wins; - report it and skip the automatic recommendation flow. +3. Read `state.workflow`. An explicit `full` workflow wins and skips automatic + recommendation. For an explicit `hotfix`/`tweak`/`quick`, report the active + path; if scope, risk, or verification now exceeds its boundary, refresh the + recommendation with observed facts and route it to Full instead of continuing. 4. For `auto`/`null`/unset, run `npx --yes --package spec-superflow@0.11.0 ssf workflow show --json` before collecting or changing any facts. A missing receipt is represented as `needs-input` with all six fixed facts in `missing_facts`. 5. If the response is `needs-input`, ask only for `missing_facts`; do not ask for any fact not listed by the receipt. Do not invent facts from missing @@ -98,13 +100,16 @@ state or cause a phase transition. 6. Run `npx --yes --package spec-superflow@0.11.0 ssf workflow recommend ...` once with one complete fact snapshot. 7. Show the user `Observed`, `Available`, `Recommended`, and `Why`. A recommendation is advice only: never persist it as the workflow selection. -8. Obtain the user's explicit path choice, then run - `npx --yes --package spec-superflow@0.11.0 ssf workflow select --mode --confirm --reason ""`. +8. A recommended Quick or incident Hotfix is accepted only with + `npx --yes --package spec-superflow@0.11.0 ssf workflow accept --source direct-request`. + For Full, legacy Hotfix, or Tweak, obtain the user's explicit choice and run + `npx --yes --package spec-superflow@0.11.0 ssf workflow select --mode --confirm --reason ""`. 9. Add `--acknowledge-recommendation` only after the user chooses a - non-recommended path. Report the persisted receipt and DP-0 audit summary. -10. If `show` reports `selection-pending`, explain that its signed receipt was - written before the state update and safely repeat the same explicit `select` - command. Do not overwrite an explicit mode unless the user asks. + non-recommended selectable path. Report the persisted receipt and DP-0 audit summary. +10. To escalate a selected Quick, direct Hotfix, or Tweak, refresh + `workflow recommend` with observed risk facts, then select `full` with + `--confirm` (and `--acknowledge-recommendation` only if required). Do not + overwrite an explicit mode without this persisted recommendation. 11. Keep `npx --yes --package spec-superflow@0.11.0 ssf runtime infer ` only for legacy artifact inference and validation compatibility; it cannot replace user selection at intake. ### Confirm DP-0 diff --git a/tests/lib/cmd-workflow.test.mjs b/tests/lib/cmd-workflow.test.mjs index e243d6a..849519d 100644 --- a/tests/lib/cmd-workflow.test.mjs +++ b/tests/lib/cmd-workflow.test.mjs @@ -68,10 +68,11 @@ afterEach(() => { }); describe('ssf workflow', () => { - it('advertises quick and direct acceptance in global help', () => { + it('advertises direct acceptance instead of selectable Quick in global help', () => { const result = runSsf(['--help']); assert.equal(result.exitCode, 0, result.stderr); - assert.match(result.stdout, /workflow select .*full\|hotfix\|tweak\|quick/); + assert.match(result.stdout, /workflow select .*full\|hotfix\|tweak/); + assert.doesNotMatch(result.stdout, /workflow select .*quick/); assert.match(result.stdout, /workflow accept --source direct-request/); }); @@ -85,6 +86,10 @@ describe('ssf workflow', () => { assert.equal(readState(changeDir).workflow, 'quick'); assert.equal(accepted.json.record.selection.accepted_automatically, true); assert.equal(accepted.json.record.selection.source, 'direct-request'); + + const guard = runSsf(['runtime', 'guard', 'check', changeDir, 'exploring', 'approved-for-build', '--workflow', 'quick', '--json']); + assert.equal(guard.exitCode, 0, guard.stderr); + assert.equal(guard.json.pass, true); }); it('recommends hotfix for an incident and accepts it without a planning approval', () => { @@ -96,25 +101,36 @@ describe('ssf workflow', () => { assert.equal(readState(changeDir).workflow, 'hotfix'); }); - it('does not set workflow until the user confirms a selection', () => { + it('rejects a selectable Quick path and leaves the direct receipt boundary intact', () => { const recommended = recommend(); assert.equal(recommended.exitCode, 0, recommended.stderr); assert.equal(recommended.json.recommendation.mode, 'quick'); assert.equal(readState(changeDir).workflow, 'auto'); - const beforeUnconfirmed = snapshotWorkflowFiles(); - const unconfirmed = runSsf(['workflow', 'select', changeDir, '--mode', 'quick', - '--reason', 'bounded code fix', '--json']); - assert.equal(unconfirmed.exitCode, 1); - assert.match(unconfirmed.stderr, /confirm/i); - assertWorkflowFilesUnchanged(beforeUnconfirmed); - assert.equal(readState(changeDir).dp_0_decisions, null); - + const before = snapshotWorkflowFiles(); const selected = runSsf(['workflow', 'select', changeDir, '--mode', 'quick', '--confirm', '--reason', 'bounded code fix', '--json']); - assert.equal(selected.exitCode, 0, selected.stderr); - assert.equal(readState(changeDir).workflow, 'quick'); - assert.match(readState(changeDir).dp_0_decisions, /workflow_path=quick/); + assert.equal(selected.exitCode, 2); + assert.match(selected.stderr, /full, hotfix, tweak/i); + assertWorkflowFilesUnchanged(before); + assert.equal(readState(changeDir).dp_0_decisions, null); + }); + + it('refreshes a direct Quick recommendation and escalates to Full when risk grows', () => { + assert.equal(recommend().exitCode, 0); + assert.equal(runSsf(['workflow', 'accept', changeDir, '--source', 'direct-request']).exitCode, 0); + + const refreshed = runSsf(['workflow', 'recommend', changeDir, + '--task-count', '4', '--file-count', '4', '--config-doc-only', 'no', + '--schema-api-change', 'no', '--new-module', 'no', '--uncertainty', 'low', '--json']); + assert.equal(refreshed.exitCode, 0, refreshed.stderr); + assert.equal(refreshed.json.recommendation.mode, 'full'); + + const upgraded = runSsf(['workflow', 'select', changeDir, '--mode', 'full', + '--confirm', '--reason', 'scope now exceeds Quick boundary', '--json']); + assert.equal(upgraded.exitCode, 0, upgraded.stderr); + assert.equal(readState(changeDir).workflow, 'full'); + assert.equal(upgraded.json.record.selection.mode, 'full'); }); it('shows complete ready recommendations in human-readable recommend and show output', () => { @@ -288,7 +304,7 @@ describe('ssf workflow', () => { new_module: 'no', uncertainty: 'low', }); recordWorkflowSelection(changeDir, { - mode: 'quick', reason: 'recoverable selection', confirmed: true, acknowledged: false, + mode: 'tweak', reason: 'recoverable selection', confirmed: true, acknowledged: true, }); const human = runSsf(['workflow', 'show', changeDir]); @@ -298,22 +314,22 @@ describe('ssf workflow', () => { assert.match(human.stdout, /Available:.*full.*hotfix.*tweak.*quick/i); assert.match(human.stdout, /Recommended: quick/i); assert.match(human.stdout, /Why:/i); - assert.match(human.stdout, /Selection:.*mode=quick.*reason=recoverable selection/i); + assert.match(human.stdout, /Selection:.*mode=tweak.*reason=recoverable selection/i); assert.match(human.stdout, /Hash valid: true/i); const json = runSsf(['workflow', 'show', changeDir, '--json']); assert.equal(json.exitCode, 0, json.stderr); assert.equal(json.json.status, 'selection-pending'); assert.equal(json.json.workflow, 'auto'); - assert.equal(json.json.record.selection.mode, 'quick'); + assert.equal(json.json.record.selection.mode, 'tweak'); assert.equal(json.json.record.selection.reason, 'recoverable selection'); }); - it('restores selected evidence in human and JSON show output', () => { + it('restores directly accepted Quick evidence in human and JSON show output', () => { assert.equal(recommend().exitCode, 0); - let result = runSsf(['workflow', 'select', changeDir, '--mode', 'quick', - '--confirm', '--reason', 'recoverable selection', '--json']); + let result = runSsf(['workflow', 'accept', changeDir, + '--source', 'direct-request', '--json']); assert.equal(result.exitCode, 0, result.stderr); const human = runSsf(['workflow', 'show', changeDir]); @@ -323,7 +339,7 @@ describe('ssf workflow', () => { assert.match(human.stdout, /Available:.*full.*hotfix.*tweak.*quick/i); assert.match(human.stdout, /Recommended: quick/i); assert.match(human.stdout, /Why:/i); - assert.match(human.stdout, /Selection:.*mode=quick.*reason=recoverable selection/i); + assert.match(human.stdout, /Selection:.*mode=quick.*source=direct-request/i); assert.match(human.stdout, /Hash valid: true/i); result = runSsf(['workflow', 'show', changeDir, '--json']); @@ -413,8 +429,8 @@ describe('ssf workflow', () => { '', ].join('\n')); assert.equal(recommend().exitCode, 0); - const selected = runSsf(['workflow', 'select', changeDir, '--mode', 'quick', - '--confirm', '--reason', 'bounded code fix', '--json']); + const selected = runSsf(['workflow', 'accept', changeDir, + '--source', 'direct-request', '--json']); assert.equal(selected.exitCode, 0, selected.stderr); const decisions = readState(changeDir).dp_0_decisions; assert.match(decisions, /scope=issue 70/); diff --git a/tests/lib/workflow-recommendation.test.mjs b/tests/lib/workflow-recommendation.test.mjs index b893ed9..050a82f 100644 --- a/tests/lib/workflow-recommendation.test.mjs +++ b/tests/lib/workflow-recommendation.test.mjs @@ -149,6 +149,18 @@ describe('workflow path recommendation', () => { } }); + it('requires direct acceptance for the Quick workflow', () => { + const changeDir = mkdtempSync(join(tmpdir(), 'ssf-workflow-quick-')); + try { + saveWorkflowRecommendation(changeDir, base); + assert.throws(() => recordWorkflowSelection(changeDir, { + mode: 'quick', reason: 'bounded code', confirmed: true, acknowledged: false, + }), /direct acceptance/i); + } finally { + rmSync(changeDir, { recursive: true, force: true }); + } + }); + it('rejects Unicode control characters and line separators in selection reasons', () => { const changeDir = mkdtempSync(join(tmpdir(), 'ssf-workflow-reason-')); try { From 7e5ff6ab4332e6f5d5341110fa3f634143a9b2e0 Mon Sep 17 00:00:00 2001 From: MageByte Date: Sun, 26 Jul 2026 23:24:49 +0800 Subject: [PATCH 15/15] test: align workflow-start fast path protocol --- .../workflow-start-recommendation.test.mjs | 30 ++++++++++--------- 1 file changed, 16 insertions(+), 14 deletions(-) diff --git a/tests/lib/workflow-start-recommendation.test.mjs b/tests/lib/workflow-start-recommendation.test.mjs index 321cd80..d5bd1ed 100644 --- a/tests/lib/workflow-start-recommendation.test.mjs +++ b/tests/lib/workflow-start-recommendation.test.mjs @@ -7,7 +7,7 @@ function read(path) { } const AUTO_INTAKE_STEPS = [ - ['explicit workflow', /explicit[^\n]*full[^\n]*hotfix[^\n]*tweak/i], + ['explicit Full workflow', /explicit `full` workflow/i], ['show receipt', /ssf workflow show/], ['only missing facts', /only[^\n]*missing_facts|missing_facts[^\n]*only/i], ['recommend', /ssf workflow recommend/], @@ -15,8 +15,8 @@ const AUTO_INTAKE_STEPS = [ ['Available', /Available/], ['Recommended', /Recommended/], ['Why', /Why/], - ['user choice', /user(?:'s)? explicit path choice/i], - ['persist selection', /ssf workflow select/], + ['user choice', /user(?:'s)? explicit choice/i], + ['persist selection', /ssf workflow select[^\n]*full\|hotfix\|tweak/], ['DP-0 confirmation', /Confirm DP-0/], ['confirmed state', /dp_0_confirmed true/], ]; @@ -57,7 +57,7 @@ describe('workflow-start path recommendation protocol', () => { assert.ok(intake.indexOf('ssf state init') < intake.indexOf('ssf workflow show')); }); - it('requires recommendation and user selection before persisting an automatic workflow', () => { + it('requires recommendation and user selection before persisting a Full or legacy workflow', () => { const skill = read('skills/workflow-start/SKILL.md'); const classicIntake = skill.match(/### Workflow Path Intake[\s\S]*?(?=### Confirm DP-0)/)?.[0] ?? ''; @@ -72,16 +72,16 @@ describe('workflow-start path recommendation protocol', () => { it('rejects a protocol that persists selection before recommendation', () => { const wrongOrder = [ - 'explicit workflow full hotfix tweak', + 'explicit `full` workflow', 'ssf workflow show', 'only missing_facts', - 'ssf workflow select', + 'ssf workflow select --mode full|hotfix|tweak', 'ssf workflow recommend', 'Observed', 'Available', 'Recommended', 'Why', - "user's explicit path choice", + "user's explicit choice", 'Confirm DP-0', 'dp_0_confirmed true', ].join('\n'); @@ -91,15 +91,15 @@ describe('workflow-start path recommendation protocol', () => { it('rejects a protocol that omits a required recommendation display field', () => { const missingWhy = [ - 'explicit workflow full hotfix tweak', + 'explicit `full` workflow', 'ssf workflow show', 'only missing_facts', 'ssf workflow recommend', 'Observed', 'Available', 'Recommended', - "user's explicit path choice", - 'ssf workflow select', + "user's explicit choice", + 'ssf workflow select --mode full|hotfix|tweak', 'Confirm DP-0', 'dp_0_confirmed true', ].join('\n'); @@ -116,13 +116,15 @@ describe('workflow-start path recommendation protocol', () => { assert.match(decisions, /\.spec-superflow\.yaml[^\n]*dp_0_[^\n]*(?:scope|artifact_language)/); }); - it('documents every DP-0 trigger including legacy recovery and fast paths', () => { + it('documents Full/legacy DP-0 triggers and fast-path exemptions', () => { const decisions = read('docs/decision-points.md'); const trigger = decisions.match(/- \*\*触发条件\*\*:([^\n]+)/)?.[1] ?? ''; - assert.match(trigger, /auto/i); - assert.match(trigger, /空|empty/i); + assert.match(trigger, /Full/i); assert.match(trigger, /legacy/i); - assert.match(trigger, /fast path/i); + assert.match(trigger, /Quick/i); + assert.match(trigger, /direct Hotfix/i); + assert.match(trigger, /Tweak/i); + assert.doesNotMatch(trigger, /auto|空|empty/i); }); });