Add active slice handoff contract

This commit is contained in:
OpenClaw Bot
2026-05-27 16:05:12 +02:00
parent 92a086824c
commit 460f088a8b
3 changed files with 57 additions and 3 deletions
+1 -1
View File
@@ -53,7 +53,7 @@ Candidate trace note: candidate-level `sourceItemId` / `traceId`, `sourceTitle`
Soft Scattermind labels are accepted at the bridge boundary so Scattermind does not need to use harsh verdict copy in its own product surface. Lens text can say `Continue first`, `Make tangible`, `Try next`, `Evidence next`, `Hold for later`, or `Set aside`; Build Order objects and direct bridge/envelope sections can use matching camel/snake-case keys such as `continueFirst`, `evidenceNext`, `holdForLater`, and `setAside`. Ranker maps those to `doFirst / validateNext / defer / park` while preserving the softer original label in `sourceQuote` or candidate source trace.
Lane safety note: explicit Scattermind `defer` / `park` hints are hard rails, not mild suggestions. Source `nonGoals` / `avoid` guardrails are also hard enough to keep conflicting candidates out of Do first / Validate next even when their local scoring hints look attractive; the result will mark the lane source as `source-non-goal` so the handoff can explain that the candidate needs guardrail resolution before active work. Handoff `source.requiresSourceTrace` is true only when a real source artifact/title is present; plain idea-only ranking still warns about a missing artifact ID when it carries prompt provenance, but it does not spam source-section/evidence warnings meant for Scattermind artifacts. Handoff `readiness` now gives downstream bridge consumers a deterministic gate: `ready`, `usable-with-warnings`, `needs-source-context`, or `blocked`, with blockers and next checks for missing evidence, source trace, duplicate IDs, or active source-non-goal conflicts. For tired first-screen users, `brief.decisionReceipt` repeats the one active move, first proof step, evidence question, held-back items, source anchor, and the handoff rule that only Do first is active; use it as the compact result strip before showing the full lane board. For low-friction handoff, `/api/rank-feedback` also detects a raw Scattermind/Concept Map JSON object pasted into `idea`, `ideaText`, `optionsText`, or wrapper keys such as `payload`; it expands that object before ranking and reports `input.embeddedPayloadSource` so the public form can accept copy/paste exports without a custom import screen.
Lane safety note: explicit Scattermind `defer` / `park` hints are hard rails, not mild suggestions. Source `nonGoals` / `avoid` guardrails are also hard enough to keep conflicting candidates out of Do first / Validate next even when their local scoring hints look attractive; the result will mark the lane source as `source-non-goal` so the handoff can explain that the candidate needs guardrail resolution before active work. Handoff `source.requiresSourceTrace` is true only when a real source artifact/title is present; plain idea-only ranking still warns about a missing artifact ID when it carries prompt provenance, but it does not spam source-section/evidence warnings meant for Scattermind artifacts. Handoff `readiness` now gives downstream bridge consumers a deterministic gate: `ready`, `usable-with-warnings`, `needs-source-context`, or `blocked`, with blockers and next checks for missing evidence, source trace, duplicate IDs, or active source-non-goal conflicts. Handoff `activeSlice` (`ranker-active-slice-v1`) is the compact machine-readable continuation unit: one active item, its proof/evidence/success/kill signals, source anchor, held-back items, readiness status, and the rule that only this slice is build-ready. For tired first-screen users, `brief.decisionReceipt` repeats the one active move, first proof step, evidence question, held-back items, source anchor, and the handoff rule that only Do first is active; use it as the compact result strip before showing the full lane board. For low-friction handoff, `/api/rank-feedback` also detects a raw Scattermind/Concept Map JSON object pasted into `idea`, `ideaText`, `optionsText`, or wrapper keys such as `payload`; it expands that object before ranking and reports `input.embeddedPayloadSource` so the public form can accept copy/paste exports without a custom import screen.
Recommended payload shape:
+9
View File
@@ -105,6 +105,15 @@ try {
assert.ok(data.brief.whatWouldChangeRanking.some(item => /evidence fails|re-run the order/i.test(item)));
assert.ok(Array.isArray(data.brief.assumptions));
assert.equal(data.handoff.readiness.status, 'ready');
assert.equal(data.handoff.activeSlice.schema, 'ranker-active-slice-v1');
assert.equal(data.handoff.activeSlice.item.id, data.buildOrder.doFirst[0]);
assert.match(data.handoff.activeSlice.proof.nextStep, /manual proof/i);
assert.equal(data.handoff.activeSlice.source.artifactId, 'snapshot_123');
assert.equal(data.handoff.activeSlice.source.sourceSection, 'concept-map.nextMoves');
assert.ok(data.handoff.activeSlice.notNow.some(item => item.id === 'export'));
assert.match(data.handoff.activeSlice.rule, /Only this active slice is build-ready/);
assert.match(data.handoff.copyableText, /## Active slice/);
assert.match(data.handoff.copyableText, /- Do not start yet:/);
assert.equal(data.handoff.readiness.sourceComplete, true);
assert.ok(data.handoff.readiness.nextChecks.some(item => /Do first item/i.test(item)));
assert.match(data.handoff.copyableText, /# Ranker build-order handoff/);
+47 -2
View File
@@ -1396,7 +1396,41 @@ function handoffReadinessFor({ ranked = [], provenance = {}, warnings = [], expe
};
}
function copyableHandoffText({ ranked = [], provenance = {}, decisionContext = {}, readiness = {} }) {
function activeSliceFor({ ranked = [], provenance = {}, readiness = {} }) {
const active = ranked.find(item => item.lane?.id === 'do') || ranked.find(item => item.lane?.id === 'test') || ranked[0] || null;
const heldBack = ranked.filter(item => item.id !== active?.id && ['test', 'defer', 'park'].includes(item.lane?.id)).slice(0, 4);
if (!active) return null;
return {
schema: 'ranker-active-slice-v1',
item: {
id: active.id,
title: active.title,
lane: active.lane?.id || 'do',
laneLabel: active.lane?.label || 'Do first',
reason: reasonFor(active),
},
proof: {
nextStep: nextStepFor(active),
evidenceQuestion: evidenceQuestionFor(active),
successSignal: successSignalFor(active),
killSignal: killSignalFor(active),
},
source: {
artifactId: provenance?.artifactId || '',
conceptMapId: provenance?.conceptMapId || '',
snapshotTitle: provenance?.snapshotTitle || '',
sourceSection: active.provenance?.sourceSection || '',
sourceId: active.provenance?.sourceId || '',
sourceTitle: active.provenance?.sourceTitle || '',
sourceQuote: active.provenance?.sourceQuote || '',
},
notNow: heldBack.map(item => ({ id: item.id, title: item.title, lane: item.lane?.id || 'defer', reason: reasonFor(item) })),
readinessStatus: readiness?.status || '',
rule: 'Only this active slice is build-ready. Do not promote Validate next / Defer / Park items until evidence changes and Ranker is rerun.',
};
}
function copyableHandoffText({ ranked = [], provenance = {}, decisionContext = {}, readiness = {}, activeSlice = null }) {
const lanes = [
['do', 'Do first'],
['test', 'Validate next'],
@@ -1438,11 +1472,20 @@ function copyableHandoffText({ ranked = [], provenance = {}, decisionContext = {
'',
];
});
const activeSliceLines = activeSlice ? [
'## Active slice',
`- Move: ${activeSlice.item.title}`,
`- Proof: ${activeSlice.proof.nextStep}`,
`- Evidence question: ${activeSlice.proof.evidenceQuestion}`,
activeSlice.source.sourceSection ? `- Source: ${[activeSlice.source.sourceSection, activeSlice.source.sourceId].filter(Boolean).join(' · ')}` : '',
activeSlice.notNow.length ? `- Do not start yet: ${activeSlice.notNow.map(item => item.title).join('; ')}` : '',
].filter(Boolean).join('\n') : '';
return [
'# Ranker build-order handoff',
sourceLine ? `Source: ${sourceLine}` : '',
sourceContextLine,
readiness?.status ? `Readiness: ${readiness.status}${readiness.summary || ''}` : '',
activeSliceLines,
contextLines.length ? ['## Carried context', ...contextLines.map(item => `- ${item}`)].join('\n') : '',
...itemLines,
'Rule: only the Do first item is active. Re-rank after evidence changes; do not quietly turn this into a workspace/dashboard backlog.',
@@ -1479,6 +1522,7 @@ function createHandoffContract({ ranked, provenance, decisionContext }) {
});
const uniqueWarnings = [...new Set(warnings)];
const readiness = handoffReadinessFor({ ranked, provenance, warnings: uniqueWarnings, expectsSourceTrace });
const activeSlice = activeSliceFor({ ranked, provenance, readiness });
return {
schema: 'rank-feedback-result-v1',
@@ -1503,7 +1547,8 @@ function createHandoffContract({ ranked, provenance, decisionContext }) {
itemTrace,
warnings: uniqueWarnings,
readiness,
copyableText: copyableHandoffText({ ranked, provenance, decisionContext, readiness }),
activeSlice,
copyableText: copyableHandoffText({ ranked, provenance, decisionContext, readiness, activeSlice }),
};
}