Accept Scattermind action thread fallbacks

This commit is contained in:
OpenClaw Bot
2026-05-27 16:11:38 +02:00
parent 460f088a8b
commit 421913dc2c
2 changed files with 75 additions and 1 deletions
+33 -1
View File
@@ -1222,7 +1222,39 @@ try {
assert.equal(softDirectLaneAliases.handoff.readiness.status, 'ready'); assert.equal(softDirectLaneAliases.handoff.readiness.status, 'ready');
assert.deepEqual(softDirectLaneAliases.handoff.warnings, []); assert.deepEqual(softDirectLaneAliases.handoff.warnings, []);
console.log(JSON.stringify({ ok: true, top: data.ranked[0].id, hintedTop: hinted.ranked[0].id, actionTop: actions.ranked[0].id, nestedConceptTop: nestedConcept.ranked[0].id, nonGoalTop: nonGoal.ranked[0].id, structuredContextTop: structuredContext.ranked[0].id, lensOnlyTop: lensOnly.ranked[0].id, scattermindPaidShapeTop: scattermindPaidShape.ranked[0].id, mergedContextTop: mergedContext.ranked[0].id, embeddedJsonTop: embeddedJson.ranked[0].id, fencedJsonTop: fencedJson.ranked[0].id, embeddedSnapshotTop: embeddedSnapshot.ranked[0].id, sourceExcerptTop: sourceExcerpt.ranked[0].id, snakeCaseBridgeTop: snakeCaseBridge.ranked[0].id, nextStepsAliasTop: nextStepsAlias.ranked[0].id, summaryGuardrailTop: summaryGuardrail.ranked[0].id, bridgeEnvelopeTop: bridgeEnvelope.ranked[0].id, directEnvelopeSectionsTop: directEnvelopeSections.ranked[0].id, softDirectLaneAliasesTop: softDirectLaneAliases.ranked[0].id, duplicateIds: duplicateIds.ranked.map(item => item.id), readiness: data.handoff.readiness.status, provenance: data.input.provenance, buildOrder: data.buildOrder }, null, 2)); const threadsFallbackResponse = await fetch(`${base}/api/rank-feedback`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
reference_code: 'SM-THREADS-1',
working_name: 'Thread-only Concept Map',
opening_reflection: 'Scattermind produced action threads but the Build Order lens did not use labels.',
lenses: {
risk: 'Avoid saved workspaces, accounts, and dashboards before the thread proof works.',
channel: 'These notes discuss sequence, but they do not contain explicit Build first / Test manually labels.',
},
threads_to_hold: [
'Manual thread proof: turn one Concept Map into a source-traced build order preview for a tired user.',
'Validate copyable handoff by asking one user whether the copied brief tells them what to do next.',
'Defer export polish until the first proof says the handoff is understandable.',
'Probably noise: saved workspace dashboard with accounts and collaboration before proof.',
],
mode: 'mvp',
}),
});
assert.equal(threadsFallbackResponse.status, 200);
const threadsFallback = await threadsFallbackResponse.json();
assert.equal(threadsFallback.input.provenance.artifactId, 'SM-THREADS-1');
assert.equal(threadsFallback.input.optionCount, 4);
assert.equal(threadsFallback.ranked[0].id, 'action-thread-1');
assert.equal(threadsFallback.ranked[0].provenance.sourceSection, 'threadsToHold');
assert.match(threadsFallback.ranked[0].factors.evidenceNeeded, /smallest real-world signal|Manual thread proof/i);
assert.equal(threadsFallback.ranked.find(item => item.id === 'action-thread-3').lane.id, 'defer');
assert.equal(threadsFallback.ranked.find(item => item.id === 'action-thread-4').lane.id, 'park');
assert.equal(threadsFallback.handoff.readiness.status, 'ready');
assert.deepEqual(threadsFallback.handoff.warnings, []);
console.log(JSON.stringify({ ok: true, top: data.ranked[0].id, hintedTop: hinted.ranked[0].id, actionTop: actions.ranked[0].id, nestedConceptTop: nestedConcept.ranked[0].id, nonGoalTop: nonGoal.ranked[0].id, structuredContextTop: structuredContext.ranked[0].id, lensOnlyTop: lensOnly.ranked[0].id, scattermindPaidShapeTop: scattermindPaidShape.ranked[0].id, mergedContextTop: mergedContext.ranked[0].id, embeddedJsonTop: embeddedJson.ranked[0].id, fencedJsonTop: fencedJson.ranked[0].id, embeddedSnapshotTop: embeddedSnapshot.ranked[0].id, sourceExcerptTop: sourceExcerpt.ranked[0].id, snakeCaseBridgeTop: snakeCaseBridge.ranked[0].id, nextStepsAliasTop: nextStepsAlias.ranked[0].id, summaryGuardrailTop: summaryGuardrail.ranked[0].id, bridgeEnvelopeTop: bridgeEnvelope.ranked[0].id, directEnvelopeSectionsTop: directEnvelopeSections.ranked[0].id, softDirectLaneAliasesTop: softDirectLaneAliases.ranked[0].id, threadsFallbackTop: threadsFallback.ranked[0].id, duplicateIds: duplicateIds.ranked.map(item => item.id), readiness: data.handoff.readiness.status, provenance: data.input.provenance, buildOrder: data.buildOrder }, null, 2));
} finally { } finally {
server.kill('SIGTERM'); server.kill('SIGTERM');
} }
+42
View File
@@ -937,6 +937,42 @@ function optionsFromBuildOrderText(text = '', sourceSection = 'concept-map.lense
}).filter(item => item.action); }).filter(item => item.action);
} }
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(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 optionsFromActionThreads(items = [], sourceSection = 'concept-map.threadsToHold', sourceTitle = 'Action thread') {
if (!Array.isArray(items)) return [];
return items.slice(0, 8).map((item, index) => {
const raw = typeof item === 'string' || typeof item === 'number' ? String(item) : '';
const objectItem = objectFrom(item);
const text = cleanMultiline(raw || objectItem.text || objectItem.content || objectItem.thread || objectItem.action || objectItem.title || '', 420);
const lane = laneFromActionThread(text);
return {
id: `action-thread-${index + 1}`,
action: titleFromBuildOrderFragment(text),
why: text,
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?',
suggestedLane: lane,
rankerHints: lane === 'do-first'
? { value: 8, effort: 2, confidence: 7, urgency: 7, risk: 2 }
: lane === 'validate-next'
? { value: 7, effort: 3, confidence: 6, urgency: 5, risk: 3 }
: undefined,
sourceSection,
sourceItemId: `${sourceSection}#${index + 1}`,
sourceTitle: cleanText(objectItem.sourceTitle || objectItem.source_title || sourceTitle, 140),
sourceExcerpt: text,
};
}).filter(item => item.action);
}
function optionsFromBody(body = {}) { function optionsFromBody(body = {}) {
const envelope = bridgeEnvelopeFrom(body); const envelope = bridgeEnvelopeFrom(body);
const featureSet = featureSetFrom(body); const featureSet = featureSetFrom(body);
@@ -1037,6 +1073,12 @@ function optionsFromBody(body = {}) {
); );
const buildOrderOptions = optionsFromBuildOrderText(buildOrderText, 'concept-map.lenses.channel', buildOrderSourceTitle); const buildOrderOptions = optionsFromBuildOrderText(buildOrderText, 'concept-map.lenses.channel', buildOrderSourceTitle);
if (buildOrderOptions.length) return normalizeCandidateGroup([{ items: buildOrderOptions, sourceSection: 'concept-map.lenses.channel' }]); if (buildOrderOptions.length) return normalizeCandidateGroup([{ items: buildOrderOptions, sourceSection: 'concept-map.lenses.channel' }]);
const actionThreadOptions = optionsFromActionThreads(
body.threads_to_hold || body.threadsToHold || body.actionThreads || body.action_threads || conceptMap.threads_to_hold || conceptMap.threadsToHold || conceptMap.actionThreads || conceptMap.action_threads,
conceptMap.threads_to_hold || conceptMap.threadsToHold || conceptMap.actionThreads || conceptMap.action_threads ? 'concept-map.threadsToHold' : 'threadsToHold',
'Thread to hold'
);
if (actionThreadOptions.length >= 2) return normalizeCandidateGroup([{ items: actionThreadOptions, sourceSection: 'concept-map.threadsToHold' }]);
if (Array.isArray(body.options)) { if (Array.isArray(body.options)) {
return normalizeOptionIds(body.options.slice(0, 24).map((item, index) => normalizeFeatureOption(item, index, 'option', 'options')).filter(item => item.title)); return normalizeOptionIds(body.options.slice(0, 24).map((item, index) => normalizeFeatureOption(item, index, 'option', 'options')).filter(item => item.title));
} }