From f44d5c3cc3bbe9c04c2390c56ae07ffed56e3468 Mon Sep 17 00:00:00 2001 From: OpenClaw Bot Date: Wed, 27 May 2026 20:22:25 +0200 Subject: [PATCH] Preserve Scattermind action-thread signals --- scripts/check-rank-feedback.mjs | 30 ++++++++++++++++++++++++++++++ server.js | 13 ++++++++++++- 2 files changed, 42 insertions(+), 1 deletion(-) diff --git a/scripts/check-rank-feedback.mjs b/scripts/check-rank-feedback.mjs index 16ba20b..194d6ad 100644 --- a/scripts/check-rank-feedback.mjs +++ b/scripts/check-rank-feedback.mjs @@ -1107,6 +1107,36 @@ try { assert.ok(privateReadingEnvelope.input.decisionContext.nonGoals.includes('Avoid accounts, saved calendars, and payment dashboards until one workshop has real interest')); assert.deepEqual(privateReadingEnvelope.handoff.warnings, []); + const actionThreadSignalsResponse = await fetch(`${base}/api/rank-feedback`, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ + reference_code: 'SM-THREAD-SIGNALS', + working_name: 'Game concept thread signals', + ideaText: 'A tiny driving bullet-heaven game needs a first playable prototype order.', + context: 'Solo builder. Manual playable proof first. Do not let this become a dashboard or account system.', + mode: 'mvp', + threads_to_hold: [ + 'Start with a five-minute greybox drive loop. Success signal: one player asks for another run. Failure signal: they cannot read threats while steering.', + 'Test enemy pressure with one obstacle wave before adding upgrades. Green flag: players change path without explanation. Red flag: they ignore the wave and wait for UI prompts.', + 'Do not let this become a saved-board dashboard with accounts and subscription tiers.', + ], + questions_to_sit_with: ['Can a player understand the loop without a tutorial?'], + }), + }); + assert.equal(actionThreadSignalsResponse.status, 200); + const actionThreadSignals = await actionThreadSignalsResponse.json(); + assert.equal(actionThreadSignals.input.optionCount, 3); + assert.equal(actionThreadSignals.ranked[0].id, 'action-thread-1'); + assert.equal(actionThreadSignals.ranked[0].factors.successSignal, 'one player asks for another run'); + assert.equal(actionThreadSignals.ranked[0].factors.killSignal, 'they cannot read threats while steering'); + assert.equal(actionThreadSignals.ranked.find(item => item.id === 'action-thread-2').factors.successSignal, 'players change path without explanation'); + assert.equal(actionThreadSignals.ranked.find(item => item.id === 'action-thread-2').factors.killSignal, 'they ignore the wave and wait for UI prompts'); + assert.equal(actionThreadSignals.ranked.find(item => item.id === 'action-thread-3').lane.id, 'park'); + assert.equal(actionThreadSignals.ranked.find(item => item.id === 'action-thread-3').lane.source, 'hint'); + assert.ok(actionThreadSignals.input.decisionContext.nonGoals.some(item => /Do not let this become a dashboard/i.test(item))); + assert.deepEqual(actionThreadSignals.handoff.warnings, []); + const softLabelLensResponse = await fetch(`${base}/api/rank-feedback`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, diff --git a/server.js b/server.js index 7f4a9a5..75cc3d1 100644 --- a/server.js +++ b/server.js @@ -1241,13 +1241,20 @@ function optionsFromProofLensText(text = '', sourceSection = 'concept-map.lenses } function laneFromActionThread(text = '') { - if (/\b(probably noise|set aside|park|parking lot|do not build|don't build|not worth|distraction)\b/i.test(text)) return 'park'; + if (/\b(probably noise|set aside|park|parking lot|do not build|don't build|do not let this become|don't let this become|not worth|distraction)\b/i.test(text)) return 'park'; if (/\b(defer|not yet|later|hold for later|after proof|wait until)\b/i.test(text)) return 'defer'; if (/^(manual|start|ship|build|show|turn one)\b/i.test(text)) return 'do-first'; if (/\b(test|validate|proof|ask|interview|observe|learn|evidence|signal)\b/i.test(text)) return 'validate-next'; return ''; } +function signalFromThreadText(text = '', labels = []) { + const pattern = labels.map(label => label.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')).join('|'); + if (!pattern) return ''; + const match = cleanMultiline(text, 520).match(new RegExp(`\\b(?:${pattern})\\b\\s*(?:is|if|:|-|—|–)?\\s*([^.;\\n]{8,180})`, 'i')); + return cleanText(match?.[1] || '', 180); +} + function optionsFromActionThreads(items = [], sourceSection = 'concept-map.threadsToHold', sourceTitle = 'Action thread') { if (!Array.isArray(items)) return []; return items.slice(0, 8).map((item, index) => { @@ -1255,6 +1262,8 @@ function optionsFromActionThreads(items = [], sourceSection = 'concept-map.threa const objectItem = objectFrom(item); const text = cleanMultiline(raw || objectItem.text || objectItem.content || objectItem.thread || objectItem.action || objectItem.title || '', 420); const lane = laneFromActionThread(text); + const successSignal = signalFromThreadText(text, ['success signal', 'green flag', 'working if', 'working when']); + const failureSignal = signalFromThreadText(text, ['failure signal', 'red flag', 'failing if', 'failing when', 'stop if']); return { id: `action-thread-${index + 1}`, action: titleFromBuildOrderFragment(text), @@ -1262,6 +1271,8 @@ function optionsFromActionThreads(items = [], sourceSection = 'concept-map.threa evidence: /\b(evidence|signal|proof|test|validate|ask|observe)\b/i.test(text) ? text : 'What smallest real-world signal would prove this action deserves the active build slot?', + greenFlag: successSignal, + redFlag: failureSignal, suggestedLane: lane, rankerHints: lane === 'do-first' ? { value: 8, effort: 2, confidence: 7, urgency: 7, risk: 2 }