From 4a9fa31812772f20a9b90d5aff3a326703944bba Mon Sep 17 00:00:00 2001 From: Ryder Sun Date: Sat, 7 Nov 2026 18:40:55 +0800 Subject: [PATCH] Provide verified outcomes to task graph reviewers Materialize audience-approved text artifacts and evidence summaries with bounded, redacted content so isolated reviews can inspect their supporting outcomes. Checks: task graph and human gate recovery tests; workspace reviewer integration test. --- ...local-workspace-auto-runner-task-graph.cjs | 33 +++++++++++++++++++ .../workspace/local-workspace-auto.test.cjs | 25 ++++++++++++++ 2 files changed, 58 insertions(+) diff --git a/desktop/src/workspace/local-workspace-auto-runner-task-graph.cjs b/desktop/src/workspace/local-workspace-auto-runner-task-graph.cjs index 85e8997..7016488 100644 --- a/desktop/src/workspace/local-workspace-auto-runner-task-graph.cjs +++ b/desktop/src/workspace/local-workspace-auto-runner-task-graph.cjs @@ -167,6 +167,35 @@ const taskGraphMethods = { ].join('\n') }, + taskGraphReviewOutcomeText(entries) { + const lines = [] + let remaining = 24000 + for (const entry of entries) { + if (!['artifact-ref', 'evidence-ref'].includes(entry.entryType)) continue + const id = entry.refs?.[0] + let label + let raw + if (entry.entryType === 'artifact-ref') { + const identity = this.v4ArtifactIdentity(id) + label = `Artifact ${id}` + raw = identity.artifact.contentRef?.mediaType === 'text/plain' + ? identity.content : '[Non-text artifact; content unavailable in this handoff.]' + } else { + const evidence = this.v4EvidenceRecord(id) + label = `Evidence ${id} (${evidence.kind}, ${evidence.level})` + raw = evidence.summary + } + const limit = Math.max(0, Math.min(6000, remaining)) + if (!limit) { lines.push('[Additional outcome content truncated.]'); break } + const content = publicCollaborationText(raw || '', limit + 1) + const truncated = content.length > limit + const shown = content.slice(0, limit) + lines.push(`${label}: ${shown}${truncated ? ' [Content truncated.]' : ''}`) + remaining -= shown.length + } + return lines.length ? `Verified outcome content:\n${lines.join('\n')}\nDo not assume truncated or unavailable content was reviewed.` : '' + }, + checkpointTaskGraph(group, controller, cursorInput, options = {}) { let taskGraph = parseTaskGraphCursor(cursorInput) taskGraph = parseTaskGraphCursor({ @@ -453,6 +482,10 @@ const taskGraphMethods = { throw new Error('LOCAL_WORKFLOW_INPUT_UNAVAILABLE') } const isolated = ['reviewer', 'arbiter'].includes(node.role) + if (isolated) { + const outcomes = this.taskGraphReviewOutcomeText(collaborationPackage.entries) + if (outcomes) collaborationPackage.text += `\n${outcomes}` + } const attachments = node.role === 'primary' && !controller.orchestration.attachmentRecipients.includes(node.agentKind) ? context.rootAttachments.map(attachment => attachment.path) diff --git a/desktop/test/workspace/local-workspace-auto.test.cjs b/desktop/test/workspace/local-workspace-auto.test.cjs index d61ce62..e44c202 100644 --- a/desktop/test/workspace/local-workspace-auto.test.cjs +++ b/desktop/test/workspace/local-workspace-auto.test.cjs @@ -342,6 +342,29 @@ test('task graph includes outcome references for both sides of a conflict', () = for (const ref of refs) assert.ok(handoff.text.includes(ref)) }) +test('task graph review materializes bounded verified outcome content without exposing secrets', () => { + const runner = { ...taskGraphMethods, + v4ArtifactIdentity: id => { + assert.equal(id, 'artifact-one') + return { artifact: { contentRef: { mediaType: 'text/plain' } }, content: `Result sk-test-12345678901234567890 ${'x'.repeat(7000)}` } + }, + v4EvidenceRecord: id => { + assert.equal(id, 'evidence-one') + return { kind: 'test-result', level: 'observed', summary: 'Verification succeeded' } + }, + } + const text = runner.taskGraphReviewOutcomeText([ + { entryType: 'artifact-ref', refs: ['artifact-one'] }, + { entryType: 'evidence-ref', refs: ['evidence-one'] }, + ]) + assert.match(text, /Result/) + assert.match(text, /Verification succeeded/) + assert.match(text, /truncated/i) + assert.doesNotMatch(text, /sk-test-12345678901234567890/) + assert.ok(text.length < 10000) + assert.throws(() => runner.taskGraphReviewOutcomeText([{ entryType: 'artifact-ref', refs: ['missing'] }])) +}) + test('task graph cannot accept an empty arbitration while conflicts remain', () => { const node = graphNode({ role: 'arbiter', agentKind: 'codex', terminal: true, acceptance: { requireConclusion: false, minArtifactRefs: 0, minEvidenceRefs: 0 } }) @@ -1381,6 +1404,8 @@ test('automatic Primary Reviewer Arbiter flow uses selective typed collaboration assert.match(calls[0].prompt, /Role: primary/) assert.match(calls[0].prompt, /Selected blackboard entries: \(none\)/) assert.match(calls[1].prompt, /Role: reviewer/) + assert.match(calls[1].prompt, /Verified outcome content:/) + assert.match(calls[1].prompt, /Evidence .*\(.*\)/) assert.match(calls[1].prompt, /The release is ready after tests\./) assert.match(calls[2].prompt, /Role: arbiter/) assert.match(calls[2].prompt, /\[conflict\]/)