brainclaw 1.26.2 → 1.28.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +13 -0
- package/dist/brainclaw-vscode.vsix +0 -0
- package/dist/cli/register-coordination.js +65 -1
- package/dist/commands/attempt-authority.js +80 -0
- package/dist/commands/harvest.js +140 -61
- package/dist/commands/loop.js +34 -0
- package/dist/commands/loops-handlers.js +143 -15
- package/dist/commands/mcp-catalog.js +52 -18
- package/dist/commands/mcp-schemas.generated.js +64 -0
- package/dist/commands/mcp-write-claims.js +128 -1
- package/dist/commands/mcp-write-coordination.js +149 -76
- package/dist/core/agent-capability.js +1 -1
- package/dist/core/agentrun-reconciler.js +148 -22
- package/dist/core/agentruns.js +254 -29
- package/dist/core/assignment-request-schema.js +7 -0
- package/dist/core/assignment-sweeper.js +5 -3
- package/dist/core/assignments.js +131 -33
- package/dist/core/claim-request-schema.js +7 -0
- package/dist/core/claims.js +53 -2
- package/dist/core/dispatch-status.js +16 -6
- package/dist/core/dispatcher.js +51 -51
- package/dist/core/entity-operations.js +20 -0
- package/dist/core/events.js +4 -0
- package/dist/core/execution-adapters.js +189 -14
- package/dist/core/execution-contract.js +345 -0
- package/dist/core/execution.js +130 -16
- package/dist/core/facade-schema.js +3 -0
- package/dist/core/harness-adapters/base.js +150 -0
- package/dist/core/harness-adapters/claude.js +39 -0
- package/dist/core/harness-adapters/codex.js +57 -0
- package/dist/core/harness-adapters/harvest.js +109 -0
- package/dist/core/harness-adapters/index.js +8 -0
- package/dist/core/harness-adapters/prompt-only.js +13 -0
- package/dist/core/harness-adapters/registry.js +48 -0
- package/dist/core/harness-adapters/result.js +33 -0
- package/dist/core/harness-adapters/types.js +2 -0
- package/dist/core/ideation-loop-close.js +25 -2
- package/dist/core/instruction-templates.js +3 -2
- package/dist/core/loop-turn-dispatch.js +235 -0
- package/dist/core/loops/artifact-contract.js +11 -0
- package/dist/core/loops/attempt-authority.js +496 -0
- package/dist/core/loops/attempt-generations.js +509 -0
- package/dist/core/loops/attempt-reservation.js +197 -35
- package/dist/core/loops/attempt-rollout.js +404 -0
- package/dist/core/loops/attempt-takeover.js +155 -0
- package/dist/core/loops/bootstrap-acquire.js +7 -3
- package/dist/core/loops/brief-assembly.js +21 -4
- package/dist/core/loops/evidence.js +188 -0
- package/dist/core/loops/facade-schema.js +75 -11
- package/dist/core/loops/gate-policy.js +533 -0
- package/dist/core/loops/impl-bind.js +91 -81
- package/dist/core/loops/index.js +9 -0
- package/dist/core/loops/iteration-engine.js +31 -19
- package/dist/core/loops/kind-policies.js +90 -0
- package/dist/core/loops/lock.js +71 -13
- package/dist/core/loops/reconcile-turn.js +237 -18
- package/dist/core/loops/result-reducers.js +113 -10
- package/dist/core/loops/store.js +34 -3
- package/dist/core/loops/turn-execution.js +480 -0
- package/dist/core/loops/types.js +127 -3
- package/dist/core/loops/verbs.js +335 -99
- package/dist/core/loops/verify-command.js +105 -20
- package/dist/core/loops/workspace-digest.js +54 -0
- package/dist/core/review-loop-close.js +25 -3
- package/dist/core/review-loop-turn-dispatch.js +210 -161
- package/dist/core/runtime-signals.js +62 -25
- package/dist/core/schema.js +40 -0
- package/dist/core/spawn-check.js +3 -2
- package/dist/core/upgrades/backup.js +27 -4
- package/dist/facts.js +9 -8
- package/dist/facts.json +8 -7
- package/docs/cli.md +49 -1
- package/docs/concepts/attempt-authority.md +407 -0
- package/docs/concepts/evidence-attestations.md +135 -0
- package/docs/concepts/execution-contract.md +166 -0
- package/docs/concepts/harness-adapters.md +166 -0
- package/docs/concepts/ideation-loop.md +5 -4
- package/docs/concepts/loop-engine.md +302 -113
- package/docs/index.md +4 -1
- package/docs/integrations/codex.md +3 -3
- package/docs/integrations/mcp.md +59 -5
- package/docs/loops/debug.md +144 -0
- package/docs/loops/ideation.md +158 -0
- package/docs/loops/implementation.md +174 -0
- package/docs/loops/research.md +136 -0
- package/docs/loops/review.md +200 -0
- package/docs/mcp-schema-changelog.md +18 -5
- package/package.json +1 -1
|
@@ -24,10 +24,16 @@
|
|
|
24
24
|
*/
|
|
25
25
|
import { spawnSync } from 'node:child_process';
|
|
26
26
|
import path from 'node:path';
|
|
27
|
+
import { loadAssignment } from '../assignments.js';
|
|
27
28
|
import { getLoop } from './store.js';
|
|
28
29
|
import { withLoopLock } from './lock.js';
|
|
29
|
-
import {
|
|
30
|
+
import { addArtifactWithEvidence } from './verbs.js';
|
|
30
31
|
import { artifactsInIteration } from './iteration-engine.js';
|
|
32
|
+
import { evidenceDigest } from './evidence.js';
|
|
33
|
+
import { eligibleArtifactsForPurpose } from './gate-policy.js';
|
|
34
|
+
import { captureWorkspaceDigest } from './workspace-digest.js';
|
|
35
|
+
import { findReservationByAssignmentId } from './attempt-reservation.js';
|
|
36
|
+
import { resolveTurnGenerationChain } from './attempt-generations.js';
|
|
31
37
|
import { VERIFY_DEFAULT_TIMEOUT_MS, LOOP_ARTIFACT_BODY_MAX_BYTES, } from './types.js';
|
|
32
38
|
/** VerifyReportBodySchema caps stdout_tail/stderr_tail at 1024. */
|
|
33
39
|
const TAIL_MAX = 1024;
|
|
@@ -48,7 +54,15 @@ export const defaultVerifyRunner = (config) => {
|
|
|
48
54
|
delete env[k];
|
|
49
55
|
}
|
|
50
56
|
const started = Date.now();
|
|
51
|
-
const
|
|
57
|
+
const requestedExecutable = config.command[0];
|
|
58
|
+
const npmCli = process.platform === 'win32' && (requestedExecutable === 'npm' || requestedExecutable === 'npx')
|
|
59
|
+
? path.join(path.dirname(process.execPath), 'node_modules', 'npm', 'bin', `${requestedExecutable}-cli.js`)
|
|
60
|
+
: undefined;
|
|
61
|
+
// Node cannot spawn .cmd shims with shell:false on Windows. Invoke npm's JS
|
|
62
|
+
// entrypoint through the current Node binary so the no-shell security contract holds.
|
|
63
|
+
const executable = npmCli ? process.execPath : requestedExecutable;
|
|
64
|
+
const commandArgs = npmCli ? [npmCli, ...config.command.slice(1)] : config.command.slice(1);
|
|
65
|
+
const r = spawnSync(executable, commandArgs, {
|
|
52
66
|
cwd: config.cwd,
|
|
53
67
|
env,
|
|
54
68
|
shell: false,
|
|
@@ -76,19 +90,52 @@ export const defaultVerifyRunner = (config) => {
|
|
|
76
90
|
};
|
|
77
91
|
};
|
|
78
92
|
/**
|
|
79
|
-
* Resolve the verify command for a loop.
|
|
80
|
-
*
|
|
81
|
-
* when the loop opted out (no `protocol.verify`).
|
|
93
|
+
* Resolve the verify command for a loop. Bound implementation lanes run only
|
|
94
|
+
* in their assignment worktree; legacy/unbound loops retain the project cwd.
|
|
95
|
+
* Returns `unconfigured` when the loop opted out (no `protocol.verify`).
|
|
82
96
|
*/
|
|
83
|
-
|
|
97
|
+
function assignmentWorktree(assignmentId, cwd) {
|
|
98
|
+
const assignment = loadAssignment(assignmentId, cwd);
|
|
99
|
+
const reservation = findReservationByAssignmentId(assignmentId, cwd);
|
|
100
|
+
const generation = reservation
|
|
101
|
+
? resolveTurnGenerationChain(cwd ?? reservation.store_root, reservation.turn_id)?.latest_generation
|
|
102
|
+
: undefined;
|
|
103
|
+
return generation?.workspace_path ?? assignment?.worktree_path;
|
|
104
|
+
}
|
|
105
|
+
export function resolveVerifyCommand(thread, cwd, slotId) {
|
|
84
106
|
const cfg = thread.protocol?.verify;
|
|
85
107
|
if (!cfg)
|
|
86
108
|
return { kind: 'unconfigured' };
|
|
109
|
+
let verifyCwd = path.resolve(cwd ?? process.cwd());
|
|
110
|
+
if (thread.kind === 'implementation') {
|
|
111
|
+
const selected = slotId ? thread.slots.find((slot) => slot.slot_id === slotId) : undefined;
|
|
112
|
+
if (slotId && !selected)
|
|
113
|
+
throw new Error(`verify: slot ${slotId} not found on loop ${thread.id}`);
|
|
114
|
+
const candidates = (selected ? [selected] : thread.slots)
|
|
115
|
+
.filter((slot) => slot.assignment_id)
|
|
116
|
+
.map((slot) => ({ slot, worktree_path: assignmentWorktree(slot.assignment_id, cwd) }))
|
|
117
|
+
.filter((entry) => entry.worktree_path);
|
|
118
|
+
if (!selected && candidates.length > 1) {
|
|
119
|
+
throw new Error(`verify: implementation loop ${thread.id} has multiple bound worktrees; pass slot_id to verify one lane deterministically`);
|
|
120
|
+
}
|
|
121
|
+
if (!selected && thread.slots.some((slot) => slot.lane) && candidates.length === 0) {
|
|
122
|
+
throw new Error(`verify: implementation loop ${thread.id} has bound lanes but no assignment worktree; dispatch and settle the execute turn first`);
|
|
123
|
+
}
|
|
124
|
+
if (selected?.lane && !selected.assignment_id) {
|
|
125
|
+
throw new Error(`verify: slot ${selected.slot_id} is bound to lane ${selected.lane} but has no assignment worktree; dispatch and settle the execute turn first`);
|
|
126
|
+
}
|
|
127
|
+
const candidate = candidates[0];
|
|
128
|
+
if (selected?.assignment_id && !candidate?.worktree_path) {
|
|
129
|
+
throw new Error(`verify: slot ${selected.slot_id} assignment ${selected.assignment_id} has no worktree_path`);
|
|
130
|
+
}
|
|
131
|
+
if (candidate?.worktree_path)
|
|
132
|
+
verifyCwd = path.resolve(candidate.worktree_path);
|
|
133
|
+
}
|
|
87
134
|
return {
|
|
88
135
|
kind: 'ok',
|
|
89
136
|
config: {
|
|
90
137
|
command: cfg.command,
|
|
91
|
-
cwd:
|
|
138
|
+
cwd: verifyCwd,
|
|
92
139
|
timeout_ms: cfg.timeout_ms ?? VERIFY_DEFAULT_TIMEOUT_MS,
|
|
93
140
|
},
|
|
94
141
|
};
|
|
@@ -115,9 +162,10 @@ function fitBody(report) {
|
|
|
115
162
|
}
|
|
116
163
|
return { ...report, stdout_tail: undefined, stderr_tail: undefined };
|
|
117
164
|
}
|
|
118
|
-
export function buildVerifyReportBody(config, result) {
|
|
165
|
+
export function buildVerifyReportBody(config, result, bindings) {
|
|
119
166
|
return fitBody({
|
|
120
167
|
command: config.command.join(' '),
|
|
168
|
+
command_argv: config.command,
|
|
121
169
|
exit_code: result.exit_code,
|
|
122
170
|
passed: result.passed,
|
|
123
171
|
duration_ms: result.duration_ms,
|
|
@@ -125,11 +173,24 @@ export function buildVerifyReportBody(config, result) {
|
|
|
125
173
|
timed_out: result.timed_out,
|
|
126
174
|
stdout_tail: result.stdout_tail || undefined,
|
|
127
175
|
stderr_tail: result.stderr_tail || undefined,
|
|
176
|
+
...bindings,
|
|
128
177
|
});
|
|
129
178
|
}
|
|
130
|
-
/** True when
|
|
131
|
-
function hasVerifyReportForIteration(thread, iteration) {
|
|
132
|
-
|
|
179
|
+
/** True when an authoritative, still-fresh engine report exists for this iteration. */
|
|
180
|
+
function hasVerifyReportForIteration(thread, iteration, lane) {
|
|
181
|
+
const reports = artifactsInIteration(thread, iteration).filter((artifact) => {
|
|
182
|
+
if (artifact.type !== 'verify_report')
|
|
183
|
+
return false;
|
|
184
|
+
if (!lane)
|
|
185
|
+
return true;
|
|
186
|
+
try {
|
|
187
|
+
return JSON.parse(artifact.body ?? '{}').lane === lane;
|
|
188
|
+
}
|
|
189
|
+
catch {
|
|
190
|
+
return false;
|
|
191
|
+
}
|
|
192
|
+
});
|
|
193
|
+
return eligibleArtifactsForPurpose(thread, reports, 'command_green').eligible.length > 0;
|
|
133
194
|
}
|
|
134
195
|
/**
|
|
135
196
|
* Run the configured verify command and record a deterministic `verify_report` for the
|
|
@@ -152,16 +213,23 @@ export function runVerify(input, cwd) {
|
|
|
152
213
|
const thread = getLoop(input.loop_id, cwd);
|
|
153
214
|
if (!thread)
|
|
154
215
|
throw new Error(`loop ${input.loop_id} not found`);
|
|
155
|
-
const
|
|
216
|
+
const inferredSlots = thread.kind === 'implementation' && !input.slot_id
|
|
217
|
+
? thread.slots.filter((slot) => slot.assignment_id && assignmentWorktree(slot.assignment_id, cwd))
|
|
218
|
+
: [];
|
|
219
|
+
const selectedSlot = input.slot_id
|
|
220
|
+
? thread.slots.find((slot) => slot.slot_id === input.slot_id)
|
|
221
|
+
: inferredSlots.length === 1 ? inferredSlots[0] : undefined;
|
|
222
|
+
const lane = selectedSlot?.lane;
|
|
223
|
+
const resolved = resolveVerifyCommand(thread, cwd, selectedSlot?.slot_id ?? input.slot_id);
|
|
156
224
|
if (resolved.kind === 'unconfigured')
|
|
157
225
|
return { state: 'unconfigured', thread };
|
|
158
226
|
const iteration = thread.iteration_count;
|
|
159
|
-
if (hasVerifyReportForIteration(thread, iteration))
|
|
227
|
+
if (hasVerifyReportForIteration(thread, iteration, lane))
|
|
160
228
|
return { state: 'deduped', thread };
|
|
161
229
|
// Snapshot the iteration + phase we are about to verify. The command tests THIS
|
|
162
230
|
// iteration's working tree; the report must be attributed to it even if a
|
|
163
231
|
// concurrent advance bumps the loop's iteration while we spawn (review F1).
|
|
164
|
-
return { state: 'run', thread, config: resolved.config, iteration, phase: thread.current_phase };
|
|
232
|
+
return { state: 'run', thread, config: resolved.config, iteration, phase: thread.current_phase, lane };
|
|
165
233
|
},
|
|
166
234
|
});
|
|
167
235
|
if (snapshot.state === 'unconfigured')
|
|
@@ -169,8 +237,13 @@ export function runVerify(input, cwd) {
|
|
|
169
237
|
if (snapshot.state === 'deduped')
|
|
170
238
|
return { thread: snapshot.thread, deduped: true };
|
|
171
239
|
// --- OUT OF LOCK: run the command (may take minutes). ---
|
|
172
|
-
const { config, iteration, phase } = snapshot;
|
|
173
|
-
const
|
|
240
|
+
const { config, iteration, phase, lane } = snapshot;
|
|
241
|
+
const command_digest = evidenceDigest({ command: config.command });
|
|
242
|
+
const workspaceBefore = captureWorkspaceDigest(config.cwd);
|
|
243
|
+
const runResult = runner(config);
|
|
244
|
+
const workspaceAfter = captureWorkspaceDigest(config.cwd);
|
|
245
|
+
const workspace_stable = workspaceBefore === workspaceAfter;
|
|
246
|
+
const reportAfterRun = buildVerifyReportBody(config, { ...runResult, passed: runResult.passed && workspace_stable }, { command_digest, workspace_digest: workspaceAfter, workspace_stable, lane });
|
|
174
247
|
// --- Lock scope 2: re-check idempotency (by SNAPSHOT iteration), then append. ---
|
|
175
248
|
return withLoopLock({
|
|
176
249
|
cwd,
|
|
@@ -184,12 +257,25 @@ export function runVerify(input, cwd) {
|
|
|
184
257
|
// Dedup on the SNAPSHOT iteration — a report for the iteration we verified already
|
|
185
258
|
// landed (a concurrent verify won). Checking the snapshot (not the current)
|
|
186
259
|
// iteration is what makes this correct after a concurrent advance (review F1).
|
|
187
|
-
if (hasVerifyReportForIteration(thread, iteration)) {
|
|
188
|
-
return { thread, report, deduped: true };
|
|
260
|
+
if (hasVerifyReportForIteration(thread, iteration, lane)) {
|
|
261
|
+
return { thread, report: reportAfterRun, deduped: true };
|
|
189
262
|
}
|
|
190
|
-
|
|
263
|
+
// Close the final out-of-lock race: the bytes verified above must still be the
|
|
264
|
+
// bytes present at the evidence commit boundary. A later gate independently
|
|
265
|
+
// repeats this freshness check so a post-commit mutation also fails closed.
|
|
266
|
+
const workspaceAtCommit = captureWorkspaceDigest(config.cwd);
|
|
267
|
+
const commitStable = workspace_stable && workspaceAtCommit === workspaceAfter;
|
|
268
|
+
const report = buildVerifyReportBody(config, { ...runResult, passed: runResult.passed && commitStable }, { command_digest, workspace_digest: workspaceAtCommit, workspace_stable: commitStable, lane });
|
|
269
|
+
const updated = addArtifactWithEvidence({
|
|
191
270
|
id: input.loop_id,
|
|
192
271
|
actor: input.actor,
|
|
272
|
+
evidence_context: {
|
|
273
|
+
channel: 'verify_command',
|
|
274
|
+
producer_kind: 'engine',
|
|
275
|
+
producer_id: 'brainclaw:verify-command',
|
|
276
|
+
command_digest,
|
|
277
|
+
workspace_digest: workspaceAtCommit,
|
|
278
|
+
},
|
|
193
279
|
artifact: {
|
|
194
280
|
// Stamp the SNAPSHOT phase + iteration so the report is attributed to the
|
|
195
281
|
// iteration whose code it actually tested — never a later iteration a
|
|
@@ -198,7 +284,6 @@ export function runVerify(input, cwd) {
|
|
|
198
284
|
iteration,
|
|
199
285
|
type: 'verify_report',
|
|
200
286
|
body: JSON.stringify(report),
|
|
201
|
-
produced_by: 'engine',
|
|
202
287
|
},
|
|
203
288
|
}, cwd);
|
|
204
289
|
const art = updated.artifacts[updated.artifacts.length - 1];
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import crypto from 'node:crypto';
|
|
2
|
+
import { spawnSync } from 'node:child_process';
|
|
3
|
+
import fs from 'node:fs';
|
|
4
|
+
import path from 'node:path';
|
|
5
|
+
function fallbackFiles(root, current = root) {
|
|
6
|
+
const files = [];
|
|
7
|
+
for (const entry of fs.readdirSync(current, { withFileTypes: true })) {
|
|
8
|
+
if (entry.name === '.brainclaw' || entry.name === '.git' || entry.name === 'node_modules')
|
|
9
|
+
continue;
|
|
10
|
+
const absolute = path.join(current, entry.name);
|
|
11
|
+
if (entry.isDirectory())
|
|
12
|
+
files.push(...fallbackFiles(root, absolute));
|
|
13
|
+
else if (entry.isFile())
|
|
14
|
+
files.push(path.relative(root, absolute));
|
|
15
|
+
}
|
|
16
|
+
return files;
|
|
17
|
+
}
|
|
18
|
+
/** Hash the exact tracked/untracked workspace bytes at a gate boundary. */
|
|
19
|
+
export function captureWorkspaceDigest(cwd) {
|
|
20
|
+
const rootResult = spawnSync('git', ['-C', cwd, 'rev-parse', '--show-toplevel'], {
|
|
21
|
+
encoding: 'utf8', shell: false, maxBuffer: 1024 * 1024,
|
|
22
|
+
});
|
|
23
|
+
const gitRoot = rootResult.status === 0 ? rootResult.stdout.trim() : undefined;
|
|
24
|
+
let files;
|
|
25
|
+
let root;
|
|
26
|
+
if (gitRoot) {
|
|
27
|
+
root = path.resolve(gitRoot);
|
|
28
|
+
const listed = spawnSync('git', ['-C', root, 'ls-files', '--cached', '--others', '--exclude-standard', '-z'], {
|
|
29
|
+
encoding: 'buffer', shell: false, maxBuffer: 32 * 1024 * 1024,
|
|
30
|
+
});
|
|
31
|
+
files = listed.status === 0
|
|
32
|
+
? listed.stdout.toString('utf8').split('\0').filter(Boolean)
|
|
33
|
+
: fallbackFiles(root);
|
|
34
|
+
}
|
|
35
|
+
else {
|
|
36
|
+
root = path.resolve(cwd);
|
|
37
|
+
files = fallbackFiles(root);
|
|
38
|
+
}
|
|
39
|
+
const hash = crypto.createHash('sha256').update(root.normalize('NFC'));
|
|
40
|
+
for (const relative of [...new Set(files)].sort()) {
|
|
41
|
+
const absolute = path.resolve(root, relative);
|
|
42
|
+
try {
|
|
43
|
+
const stat = fs.lstatSync(absolute);
|
|
44
|
+
if (!stat.isFile())
|
|
45
|
+
continue;
|
|
46
|
+
hash.update('\0').update(relative.normalize('NFC')).update('\0').update(fs.readFileSync(absolute));
|
|
47
|
+
}
|
|
48
|
+
catch {
|
|
49
|
+
hash.update('\0missing\0').update(relative.normalize('NFC'));
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
return hash.digest('hex');
|
|
53
|
+
}
|
|
54
|
+
//# sourceMappingURL=workspace-digest.js.map
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { getLoop } from './loops/store.js';
|
|
2
|
-
import {
|
|
2
|
+
import { completeTurnWithEvidence, advance } from './loops/verbs.js';
|
|
3
3
|
import { withLoopLock } from './loops/lock.js';
|
|
4
4
|
import { LOOP_ARTIFACT_BODY_MAX_BYTES } from './loops/types.js';
|
|
5
5
|
/** review-loop:lop_xxx → the loop id (mirrors assignment-reconciler.ts). */
|
|
@@ -93,8 +93,19 @@ export function closeReviewLoopFromLaneResult(assignment, lane, actor, cwd, opti
|
|
|
93
93
|
if (verdict === 'approve') {
|
|
94
94
|
if (slot) {
|
|
95
95
|
// isVerdictAccepted fires reviewer_green ONLY on an "accepted…" body.
|
|
96
|
-
|
|
96
|
+
completeTurnWithEvidence({
|
|
97
97
|
id: loopId, slot_id: slot.slot_id, actor,
|
|
98
|
+
evidence_context: {
|
|
99
|
+
channel: 'complete_turn',
|
|
100
|
+
producer_kind: 'slot',
|
|
101
|
+
producer_id: slot.slot_id,
|
|
102
|
+
agent_id: slot.agent_id,
|
|
103
|
+
slot_id: slot.slot_id,
|
|
104
|
+
slot_role: slot.role,
|
|
105
|
+
turn_id: slot.current_turn_id,
|
|
106
|
+
assignment_id: slot.assignment_id ?? assignment.id,
|
|
107
|
+
claim_id: slot.claim_id,
|
|
108
|
+
},
|
|
98
109
|
// pln#639 BUG-2 — the phase the slot was DISPATCHED in, not the
|
|
99
110
|
// loop's phase at close time. Same defect as the ideation closer;
|
|
100
111
|
// fixed here too because this is the far more travelled path.
|
|
@@ -145,8 +156,19 @@ export function closeReviewLoopFromLaneResult(assignment, lane, actor, cwd, opti
|
|
|
145
156
|
// is a planned follow-up, so a human drives it. (No re-dispatch means no
|
|
146
157
|
// worktree reuse, so the claim is released by harvest as usual.)
|
|
147
158
|
const symmetric = loop.protocol?.review_mode === 'symmetric';
|
|
148
|
-
|
|
159
|
+
completeTurnWithEvidence({
|
|
149
160
|
id: loopId, slot_id: slot.slot_id, actor,
|
|
161
|
+
evidence_context: {
|
|
162
|
+
channel: 'complete_turn',
|
|
163
|
+
producer_kind: 'slot',
|
|
164
|
+
producer_id: slot.slot_id,
|
|
165
|
+
agent_id: slot.agent_id,
|
|
166
|
+
slot_id: slot.slot_id,
|
|
167
|
+
slot_role: slot.role,
|
|
168
|
+
turn_id: slot.current_turn_id,
|
|
169
|
+
assignment_id: slot.assignment_id ?? assignment.id,
|
|
170
|
+
claim_id: slot.claim_id,
|
|
171
|
+
},
|
|
150
172
|
// pln#639 BUG-2 — dispatch phase, not close-time phase (see above).
|
|
151
173
|
artifact: { phase: slot.phase ?? loop.current_phase, type: 'verdict', body: capVerdictBody('changes-requested', detail) },
|
|
152
174
|
}, cwd);
|