opencode-plugin-flow 6.2.0 → 6.3.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/CHANGELOG.md CHANGED
@@ -6,6 +6,32 @@ One short entry per release, written for users deciding whether to upgrade.
6
6
 
7
7
  No changes yet.
8
8
 
9
+ ## [6.3.0] - 2026-07-21
10
+
11
+ Reviewer-owned submission lore strengthens independent review without turning
12
+ Flow back into a heavy orchestration framework:
13
+
14
+ - The reserved reviewer now submits its own result through
15
+ `flow_feature_complete`. The host authorizes new completions by agent identity,
16
+ while exact previously accepted requests remain read-only replays for active
17
+ Session v5 workflows.
18
+ - Pending-review recovery is source-aware: current assignments can be
19
+ redispatched after interruption, stale source routes to reset and full
20
+ revalidation, and fingerprint failures fail closed with repair guidance.
21
+ - Final reviewers receive the approved plan targets and validation intent,
22
+ completed feature IDs, and only assignment-linked evidence. Broad validation
23
+ is treated as a coverage claim; durable commands exclude secrets, and raw
24
+ output is reduced to completeness plus a digest.
25
+ - Closed-world permission tests and the pinned OpenCode host smoke cover every
26
+ registered Flow tool. Documentation distinguishes lifecycle mutation from
27
+ fail-closed quarantine maintenance.
28
+
29
+ Install or update:
30
+
31
+ ```bash
32
+ opencode plugin opencode-plugin-flow@6.3.0 --global --force
33
+ ```
34
+
9
35
  ## [6.2.0] - 2026-07-21
10
36
 
11
37
  Low-friction bounded-wave lore keeps Flow small while making parallel
package/README.md CHANGED
@@ -16,11 +16,11 @@ parallel before it validates and reviews the combined result.
16
16
  Install the exact npm release through OpenCode:
17
17
 
18
18
  ```bash
19
- opencode plugin opencode-plugin-flow@6.2.0 --global --force
19
+ opencode plugin opencode-plugin-flow@6.3.0 --global --force
20
20
  ```
21
21
 
22
22
  Omit `--global` for project scope. Exact version pins do not update
23
- automatically. To update, replace `6.2.0` with the new release and rerun the
23
+ automatically. To update, replace `6.3.0` with the new release and rerun the
24
24
  command.
25
25
 
26
26
  Before upgrading from Flow v5 or earlier, finish or explicitly close any active
@@ -32,7 +32,7 @@ The equivalent manual project configuration is:
32
32
  ```json
33
33
  {
34
34
  "$schema": "https://opencode.ai/config.json",
35
- "plugin": ["opencode-plugin-flow@6.2.0"]
35
+ "plugin": ["opencode-plugin-flow@6.3.0"]
36
36
  }
37
37
  ```
38
38
 
package/dist/index.js CHANGED
@@ -7,10 +7,12 @@ description: Manage a Flow goal from planning through implementation, validation
7
7
  # Flow
8
8
 
9
9
  Flow is a small state ledger around ordinary coding work. The root manager owns
10
- the session, every state-changing \`flow_*\` call, integration, validation,
11
- review dispatch, reset, and closure. Bounded \`flow-worker\` instances may
12
- contribute disjoint work inside the active feature. The reserved \`flow-reviewer\`
13
- independently reviews; it never edits or mutates Flow state.
10
+ the session, integration, validation, review dispatch, reset, closure, and every
11
+ lifecycle mutation except review submission. Bounded \`flow-worker\`
12
+ instances may contribute disjoint work inside the active feature. The reserved
13
+ \`flow-reviewer\` independently reviews and submits its own result through
14
+ \`flow_feature_complete\`; it cannot edit the workspace or make any other
15
+ lifecycle mutation.
14
16
 
15
17
  ## Route from status
16
18
 
@@ -37,9 +39,10 @@ report that the plugin is not fully loaded instead of simulating state changes.
37
39
  - Only one durable feature run is active at a time. Conversation-local worker
38
40
  waves do not create additional runs or Flow state.
39
41
  - Work stays inside the active feature and preserves unrelated user changes.
40
- - A passing feature needs successful current-source validation and one
41
- independent reviewer assignment. The final feature uses broad validation and
42
- a final review; it does not add a second review pass.
42
+ - A passing feature needs successful current-source validation and one result
43
+ submitted directly by the assigned independent reviewer. The final feature
44
+ uses broad validation and a final review; it does not add a second review
45
+ pass.
43
46
  - A failed review is recorded honestly. Any retry is a fresh run with full
44
47
  validation and review.
45
48
  - Use runtime revisions and operation ids for ordering and idempotency. Supply
@@ -53,7 +56,13 @@ report that the plugin is not fully loaded instead of simulating state changes.
53
56
 
54
57
  On confusion or interruption, read compact status and follow its next action.
55
58
  Use execution status for the active feature and reviewer status for a returned
56
- assignment id. If status is closed with \`archiveRetry\`, call
59
+ assignment id. Redispatch a pending assignment after interruption or an
60
+ unconfirmed reviewer return; the manager never invents or submits a verdict. If
61
+ completion reports \`Workspace content changed after review started\`, call
62
+ \`flow_feature_reset\`; that source-stale assignment must not be redispatched.
63
+ Start a fresh run and repeat full validation and review. After the reviewer
64
+ returns, read compact status to learn the durable outcome. If status is closed
65
+ with \`archiveRetry\`, call
57
66
  \`flow_session_close\` with that projected request byte-for-byte; do not create a
58
67
  new operation id or revision. Never infer completion, retry count, or closure
59
68
  from prose.
@@ -126,26 +135,30 @@ document in the repository unless the user explicitly requests one.
126
135
  // skills/flow-review/SKILL.md
127
136
  var SKILL_default3 = `---
128
137
  name: flow-review
129
- description: Independently review one runtime-owned Flow assignment. Reserved for the read-only flow-reviewer; managers dispatch assignments but do not perform this review themselves.
138
+ description: Independently review one runtime-owned Flow assignment. Reserved for the workspace-read-only flow-reviewer; managers dispatch assignments but do not perform this review themselves.
130
139
  ---
131
140
 
132
141
  # Flow Review
133
142
 
134
143
  You are the independent \`flow-reviewer\`. Review the assigned work; do not fix
135
144
  it. You may read relevant files and supplied evidence, but must not edit files,
136
- read outside the workspace, run commands, launch workers, or call any
137
- state-changing \`flow_*\` tool. Only the root manager records the result.
145
+ read outside the workspace, run commands, or launch workers. You may call only
146
+ \`flow_status\` to read this assignment and \`flow_feature_complete\` to submit its
147
+ exact result. The latter is your sole lifecycle mutation.
138
148
 
139
149
  ## Recover the assignment
140
150
 
141
151
  When given an assignment id, call only
142
152
  \`flow_status { request: { view: "reviewer", assignmentId: "..." } }\`. Use its
143
- bounded packet and approved-plan context instead of reconstructing feature,
144
- source, revision, validation, or lifecycle data from conversation memory.
153
+ bounded packet, assignment-linked validations, approved-plan context, and
154
+ completed feature IDs instead of reconstructing feature, source,
155
+ revision, validation, or lifecycle data from conversation memory.
145
156
 
146
- If the assignment or evidence required to justify a verdict is unavailable,
147
- return a failed result with a blocking evidence-gap finding. Never invent
148
- validation, identity, or time.
157
+ If the reviewer projection is available but evidence required to justify a
158
+ verdict is missing, submit a failed result with a blocking evidence-gap finding.
159
+ If the assignment itself is unavailable, report that failure without another
160
+ state change so the manager can inspect compact status. Never invent validation,
161
+ identity, revision, or time.
149
162
 
150
163
  ## Review
151
164
 
@@ -160,6 +173,12 @@ the manager's summary. Check that:
160
173
  - persistence, concurrency, security, migration, package, UI, and recovery
161
174
  risks were examined when relevant.
162
175
 
176
+ Validation scope is a claim. Treat \`broad\` as adequate only when the durable
177
+ command is the repository's canonical applicable gate or a justified equivalent
178
+ for the delivered state. Flow deliberately projects no raw command output; use
179
+ the durable command, exit code, completeness, digest, source binding, and your
180
+ workspace inspection. A weak or unclear coverage claim is an evidence gap.
181
+
163
182
  For a final assignment, also trace every approved requirement and feature to
164
183
  the delivered result, inspect broad validation, and confirm docs, commands,
165
184
  package surfaces, and remaining gaps are consistent with completion. The final
@@ -170,25 +189,40 @@ approved outcome; otherwise use \`advisory\`. Every blocker needs a precise
170
189
  summary. Every blocker must cite a changed artifact and location, or identify
171
190
  the exact missing evidence or unmet approved requirement in \`evidence\`.
172
191
 
173
- ## Return one result
192
+ ## Submit one result
174
193
 
175
- Return exactly one assignment result:
194
+ Call \`flow_feature_complete\` directly with the assignment id, current reviewer
195
+ projection revision and feature id, a fresh operation id, a concise summary,
196
+ and exactly one assignment result:
176
197
 
177
198
  \`\`\`json
178
199
  {
179
- "assignmentId": "review-assignment:runtime-id",
180
- "verdict": "passed",
181
- "findings": [],
182
- "terminalDisposition": "submitted"
200
+ "request": {
201
+ "operationId": "review-submit:fresh-id",
202
+ "expectedRevision": 12,
203
+ "featureId": "approved-feature-id",
204
+ "assignmentId": "review-assignment:runtime-id",
205
+ "summary": "Concise reviewed outcome.",
206
+ "result": {
207
+ "verdict": "passed",
208
+ "findings": [],
209
+ "terminalDisposition": "submitted"
210
+ }
211
+ }
183
212
  }
184
213
  \`\`\`
185
214
 
186
215
  Each finding contains \`severity\`, \`summary\`, and optional \`evidence\`. Use
187
- \`verdict: "failed"\` whenever any blocking finding remains. The manager copies
188
- \`verdict\`, \`findings\`, and \`terminalDisposition\` under
189
- \`flow_feature_complete.request.result\` and supplies \`assignmentId\` beside that
190
- result. Do not return or invent run ids, revisions, source hashes, validation
191
- records, timestamps, review modes, or attempt fields.
216
+ \`verdict: "failed"\` whenever any blocking finding remains. Do not return or
217
+ invent run ids, source hashes, validation records, timestamps, review modes, or
218
+ attempt fields. Never ask the manager to copy or submit your verdict.
219
+
220
+ After the tool returns, report its durable outcome concisely. If submission
221
+ fails, report the exact failure. For \`Workspace content changed after review
222
+ started\`, tell the manager to reset the feature and do not recommend redispatch;
223
+ the source-stale assignment cannot complete. After an interruption with no
224
+ accepted result, the manager may recover the pending assignment. Never
225
+ downgrade, fabricate, or hand off a result for manager submission.
192
226
 
193
227
  Approve only what you actually inspected. Missing coverage is a finding, never
194
228
  a reason to lower the bar.
@@ -202,10 +236,11 @@ description: Implement, validate, independently review, and record one approved
202
236
 
203
237
  # Flow Run
204
238
 
205
- Work on exactly one approved feature. The root manager owns the session, every
206
- state-changing \`flow_*\` call, integration, validation, review dispatch, reset,
207
- and closure. Bounded \`flow-worker\` instances may contribute disjoint work; the
208
- reserved \`flow-reviewer\` owns the independent review.
239
+ Work on exactly one approved feature. The root manager owns the session,
240
+ integration, validation, review dispatch, reset, closure, and every
241
+ lifecycle mutation except review submission. Bounded \`flow-worker\`
242
+ instances may contribute disjoint work; the reserved \`flow-reviewer\` owns the
243
+ independent review and submits its own result.
209
244
 
210
245
  ## Start and scope
211
246
 
@@ -271,17 +306,25 @@ checks from the changed behavior and risk:
271
306
  sufficient alone only for genuinely mechanical or documentation-only work.
272
307
  - UI claims need visual inspection when a runnable target is available.
273
308
  - Cleanup claims need behavior-preservation evidence, not formatting alone.
309
+ - \`scope: "broad"\` is a claim about coverage, not a stronger label. Use it only
310
+ for the repository's canonical applicable gate or a justified equivalent
311
+ that covers the delivered repository state.
274
312
 
275
313
  Immediately before each Bash command used as evidence, call
276
314
  \`flow_validation_start\` with the current revision, feature id, the exact
277
315
  command, and \`scope\` (\`focused\` or \`broad\`). Run that byte-for-byte command next
278
316
  and inspect its complete outcome. Flow records the host-observed result directly
279
- in the session; do not copy host-observed fields into a later request.
317
+ in the session; do not copy host-observed fields into a later request. The exact
318
+ command is durable, so never inline tokens, passwords, credentials, or other
319
+ secrets. Raw output is deliberately neither persisted nor projected: the
320
+ durable evidence is the command, exit code, output completeness, and output
321
+ digest, while the manager must inspect the live output.
280
322
 
281
323
  Use focused validation for ordinary features. For the final feature, run the
282
324
  repository's broad applicable gate after the last relevant edit. A source edit
283
325
  invalidates earlier applicability. Failed or unavailable checks are blockers,
284
- not passing evidence.
326
+ not passing evidence. If the canonical gate cannot run, explain why the chosen
327
+ equivalent is broad enough; otherwise record the narrower evidence as focused.
285
328
 
286
329
  ## Review and record
287
330
 
@@ -295,9 +338,19 @@ automatically and derives \`feature\` versus \`final\` review from plan progress
295
338
  callers do not supply the review kind.
296
339
 
297
340
  Dispatch the returned assignment only to the reserved \`flow-reviewer\`. Do not
298
- perform the independent review in manager context. Submit its exact assignment
299
- result through \`flow_feature_complete\` with fresh guards. A pass completes the
300
- feature; a blocking finding records a blocked outcome.
341
+ perform the independent review in manager context and never copy or submit its
342
+ verdict. The reviewer reads its assignment, inspects the workspace, and calls
343
+ \`flow_feature_complete\` directly; the runtime verifies the calling agent. The
344
+ reviewer remains workspace-read-only and may make only this exact result
345
+ submission as its sole lifecycle mutation.
346
+
347
+ After the reviewer returns, read compact status rather than treating prose as
348
+ the outcome. Redispatch the same pending assignment after interruption or an
349
+ unconfirmed reviewer return. If submission reports \`Workspace content changed
350
+ after review started\`, call \`flow_feature_reset\` and do not redispatch that
351
+ source-stale assignment; start a fresh run and repeat full validation and
352
+ review. Never fabricate a verdict. A submitted pass completes the feature; a
353
+ submitted blocking finding records a blocked outcome.
301
354
 
302
355
  If repair is authorized after a failed review, reset the feature, fix it, and
303
356
  repeat full validation and full review in a fresh run.
@@ -415,7 +468,7 @@ function compileFlowPromptSurface(surface) {
415
468
  return [
416
469
  "# Flow review command",
417
470
  "",
418
- "Run this assignment only as the reserved `flow-reviewer`. The reviewer is independent and read-only; it must not edit files or mutate Flow state.",
471
+ "Run this assignment only as the reserved `flow-reviewer`. The reviewer is independent and workspace-read-only; it may read reviewer status and submit only its own result through `flow_feature_complete`.",
419
472
  "",
420
473
  "Assignment: $ARGUMENTS"
421
474
  ].join(`
@@ -436,7 +489,7 @@ var FLOW_CORE_AGENTS = {
436
489
  "flow-reviewer": {
437
490
  mode: "subagent",
438
491
  hidden: true,
439
- description: "Independent read-only reviewer for one Flow assignment.",
492
+ description: "Independent workspace-read-only reviewer that submits one Flow result.",
440
493
  prompt: compileFlowPromptSurface("flow-reviewer"),
441
494
  permission: {
442
495
  edit: "deny",
@@ -445,7 +498,8 @@ var FLOW_CORE_AGENTS = {
445
498
  skill: "deny",
446
499
  task: { "*": "deny" },
447
500
  "flow_*": "deny",
448
- flow_status: "allow"
501
+ flow_status: "allow",
502
+ flow_feature_complete: "allow"
449
503
  }
450
504
  },
451
505
  "flow-worker": {
@@ -486,7 +540,7 @@ var FLOW_CORE_COMMANDS = {
486
540
  template: compileFlowPromptSurface("flow-run")
487
541
  },
488
542
  "flow-review": {
489
- description: "Run one independent read-only Flow review",
543
+ description: "Run one independent workspace-read-only Flow review",
490
544
  agent: "flow-reviewer",
491
545
  subtask: true,
492
546
  template: compileFlowPromptSurface("flow-review")
@@ -1453,6 +1507,7 @@ function boundedText(label, options) {
1453
1507
  }
1454
1508
  var FeatureIdSchema = z.string().max(MAX_SESSION_ID_LENGTH).regex(FEATURE_ID_PATTERN, FEATURE_ID_MESSAGE);
1455
1509
  var OperationIdSchema = z.string().min(1).max(128).regex(/^[a-zA-Z0-9][a-zA-Z0-9._:-]*$/);
1510
+ var ReviewAssignmentIdSchema = z.string().min(1).max(256);
1456
1511
  var RevisionSchema = z.number().int().safe().nonnegative();
1457
1512
  var SourceDigestSchema = z.custom((value) => typeof value === "string" && /^sha256:[a-f0-9]{64}$/.test(value), "Expected a sha256: content digest.");
1458
1513
  var PlanFeatureSchema = z.object({
@@ -1508,7 +1563,7 @@ var ArtifactSchema = z.object({
1508
1563
  path: boundedText("Artifact path", { maxBytes: MAX_PATH_BYTES }).refine(isArtifactPath, ARTIFACT_PATH_MESSAGE)
1509
1564
  }).strict();
1510
1565
  var ReviewAssignmentSchema = z.object({
1511
- id: z.string().min(1).max(256),
1566
+ id: ReviewAssignmentIdSchema,
1512
1567
  operationId: OperationIdSchema,
1513
1568
  featureId: FeatureIdSchema,
1514
1569
  runId: z.string().min(1).max(256),
@@ -1606,7 +1661,7 @@ var FeatureCompleteInputSchema = z.object({
1606
1661
  request: z.object({
1607
1662
  ...guarded,
1608
1663
  featureId: FeatureIdSchema,
1609
- assignmentId: z.string().min(1).max(256),
1664
+ assignmentId: ReviewAssignmentIdSchema,
1610
1665
  summary: boundedText("Feature result summary"),
1611
1666
  result: PublicReviewResultSchema
1612
1667
  }).strict()
@@ -1637,7 +1692,7 @@ var StatusInputSchema = z.object({
1637
1692
  z.object({ view: z.literal("execution") }).strict(),
1638
1693
  z.object({
1639
1694
  view: z.literal("reviewer"),
1640
- assignmentId: z.string().min(1).max(256)
1695
+ assignmentId: ReviewAssignmentIdSchema
1641
1696
  }).strict()
1642
1697
  ])
1643
1698
  }).strict();
@@ -2472,7 +2527,10 @@ function featureProgress(session) {
2472
2527
  const completed = session.plan?.features.filter((feature) => isFeatureComplete(session, feature.id)).length ?? 0;
2473
2528
  return { completed, total, remaining: total - completed };
2474
2529
  }
2475
- function nextAction(session) {
2530
+ function activePendingReview(session) {
2531
+ return activeRun(session)?.reviews.find((review) => review.result === null) ?? null;
2532
+ }
2533
+ function nextAction(session, pendingReviewSourceStale = false) {
2476
2534
  const status = sessionStatus(session);
2477
2535
  if (status === "planning") {
2478
2536
  return session.plan ? "flow_plan_approve" : "flow_plan_save";
@@ -2488,16 +2546,16 @@ function nextAction(session) {
2488
2546
  const run = activeRun(session);
2489
2547
  if (!run)
2490
2548
  return "flow_status";
2491
- const pending = run.reviews.find((review) => review.result === null);
2492
- if (pending)
2493
- return "dispatch-flow-reviewer";
2549
+ if (activePendingReview(session)) {
2550
+ return pendingReviewSourceStale ? "flow_feature_reset" : "dispatch-flow-reviewer";
2551
+ }
2494
2552
  const finalRun = session.plan?.features.every((feature) => feature.id === run.featureId || isFeatureComplete(session, feature.id)) ?? false;
2495
2553
  const hasPassingValidation = run.validations.some((validation) => validation.exitCode === 0 && validation.outputComplete && (!finalRun || validation.scope === "broad"));
2496
2554
  if (!hasPassingValidation)
2497
2555
  return "flow_validation_start";
2498
2556
  return "flow_review_start";
2499
2557
  }
2500
- function compactProjection(session) {
2558
+ function compactProjection(session, pendingReviewSourceStale = false) {
2501
2559
  const run = activeRun(session);
2502
2560
  const retryRequest = closureRetryRequest(session);
2503
2561
  if (session.closure && !retryRequest) {
@@ -2512,7 +2570,7 @@ function compactProjection(session) {
2512
2570
  activeFeatureId: run?.featureId ?? null,
2513
2571
  activeRunId: run?.id ?? null,
2514
2572
  progress: featureProgress(session),
2515
- nextAction: nextAction(session),
2573
+ nextAction: nextAction(session, pendingReviewSourceStale),
2516
2574
  archiveRetry: retryRequest ? { request: retryRequest } : null
2517
2575
  };
2518
2576
  }
@@ -2524,11 +2582,11 @@ function archivedProjection(session) {
2524
2582
  archived: true
2525
2583
  };
2526
2584
  }
2527
- function executionProjection(session) {
2585
+ function executionProjection(session, pendingReviewSourceStale = false) {
2528
2586
  const run = activeRun(session);
2529
2587
  const feature = session.plan?.features.find((item) => item.id === run?.featureId);
2530
2588
  return {
2531
- ...compactProjection(session),
2589
+ ...compactProjection(session, pendingReviewSourceStale),
2532
2590
  view: "execution",
2533
2591
  goal: session.goal,
2534
2592
  feature: feature ?? null,
@@ -2553,6 +2611,7 @@ function reviewerProjection(session, assignmentId) {
2553
2611
  }
2554
2612
  const feature = session.plan?.features.find((item) => item.id === assignment?.featureId);
2555
2613
  const plan = session.plan;
2614
+ const assignedValidationIds = new Set(assignment.validationIds);
2556
2615
  return {
2557
2616
  view: "reviewer",
2558
2617
  sessionId: session.id,
@@ -2567,26 +2626,30 @@ function reviewerProjection(session, assignmentId) {
2567
2626
  id: candidate.id,
2568
2627
  title: candidate.title,
2569
2628
  summary: candidate.summary,
2629
+ targets: [...candidate.targets],
2630
+ validation: [...candidate.validation],
2570
2631
  dependsOn: [...candidate.dependsOn]
2571
2632
  }))
2572
2633
  } : null,
2573
2634
  feature: feature ?? null,
2574
2635
  assignment,
2575
2636
  artifactsChanged: run.artifactsChanged,
2576
- validations: run.validations,
2637
+ validations: run.validations.filter((validation) => assignedValidationIds.has(validation.id)),
2577
2638
  completedFeatureIds: plan?.features.filter((candidate) => isFeatureComplete(session, candidate.id)).map((candidate) => candidate.id) ?? []
2578
2639
  };
2579
2640
  }
2580
- function project(session, request) {
2581
- if (request.view === "compact")
2582
- return compactProjection(session);
2583
- if (request.view === "execution")
2584
- return executionProjection(session);
2641
+ function project(session, request, pendingReviewSourceStale = false) {
2642
+ if (request.view === "compact") {
2643
+ return compactProjection(session, pendingReviewSourceStale);
2644
+ }
2645
+ if (request.view === "execution") {
2646
+ return executionProjection(session, pendingReviewSourceStale);
2647
+ }
2585
2648
  if (request.view === "reviewer") {
2586
2649
  return reviewerProjection(session, request.assignmentId);
2587
2650
  }
2588
2651
  return {
2589
- ...compactProjection(session),
2652
+ ...compactProjection(session, pendingReviewSourceStale),
2590
2653
  view: "detail",
2591
2654
  goal: session.goal,
2592
2655
  plan: session.plan,
@@ -2603,6 +2666,12 @@ function operationResult(session, operationId, replayed, entity) {
2603
2666
  ...entity === undefined ? {} : { entity }
2604
2667
  };
2605
2668
  }
2669
+ function featureCompleteResponse(session, request, run, replayed) {
2670
+ return ok(request.result.verdict === "passed" ? "Feature completed." : "Feature blocked by review.", {
2671
+ operation: operationResult(session, request.operationId, replayed, run),
2672
+ projection: compactProjection(session)
2673
+ });
2674
+ }
2606
2675
  async function loadExactArchivedClose(transaction, request) {
2607
2676
  const archived = await transaction.loadArchive(request.sessionId);
2608
2677
  const operation = archived?.operations.find((item) => item.id === request.operationId);
@@ -2660,6 +2729,33 @@ function createFlowService(repository, environment) {
2660
2729
  }
2661
2730
  });
2662
2731
  }
2732
+ if (request.view !== "reviewer" && activePendingReview(session)) {
2733
+ return await repository.transact(async (transaction) => {
2734
+ const current = await transaction.load();
2735
+ if (!current) {
2736
+ return ok("No active Flow session.", {
2737
+ projection: {
2738
+ view: request.view,
2739
+ status: "idle",
2740
+ revision: 0,
2741
+ nextAction: "flow_plan_save"
2742
+ }
2743
+ });
2744
+ }
2745
+ const pending = activePendingReview(current);
2746
+ let pendingReviewSourceStale = false;
2747
+ if (pending) {
2748
+ try {
2749
+ pendingReviewSourceStale = await transaction.computeSourceDigest() !== pending.sourceDigest;
2750
+ } catch (error) {
2751
+ return errorResponse(error, "Repair workspace fingerprinting before recovering this pending review. Do not redispatch the assignment until its source can be checked.");
2752
+ }
2753
+ }
2754
+ return ok("Flow status loaded.", {
2755
+ projection: project(current, request, pendingReviewSourceStale)
2756
+ });
2757
+ });
2758
+ }
2663
2759
  return ok("Flow status loaded.", {
2664
2760
  projection: project(session, request)
2665
2761
  });
@@ -2770,20 +2866,36 @@ function createFlowService(repository, environment) {
2770
2866
  if (!assignment)
2771
2867
  throw new Error("Unknown review assignment.");
2772
2868
  if (await transaction.computeSourceDigest() !== assignment.sourceDigest) {
2773
- throw new Error("Workspace content changed after review started; reset and rerun validation and review.");
2869
+ return errorResponse(new Error("Workspace content changed after review started."), "Call flow_feature_reset, start a fresh run, and repeat full validation and review. Do not redispatch this source-stale assignment.");
2774
2870
  }
2775
2871
  }
2776
2872
  const result = completeFeature(session, request);
2777
2873
  await transaction.save(result.session);
2778
- return ok(request.result.verdict === "passed" ? "Feature completed." : "Feature blocked by review.", {
2779
- operation: operationResult(result.session, request.operationId, result.replayed, result.value),
2780
- projection: compactProjection(result.session)
2781
- });
2874
+ return featureCompleteResponse(result.session, request, result.value, result.replayed);
2782
2875
  });
2783
2876
  } catch (error) {
2784
2877
  return errorResponse(error);
2785
2878
  }
2786
2879
  },
2880
+ async featureCompleteReplay(input) {
2881
+ try {
2882
+ const request = FeatureCompleteInputSchema.parse(input).request;
2883
+ const session = await repository.read();
2884
+ if (!session)
2885
+ throw new Error("No active Flow session exists.");
2886
+ const priorOperation = session.operations.find((operation) => operation.id === request.operationId);
2887
+ if (priorOperation?.kind !== "feature-complete" || priorOperation.inputDigest !== operationInputDigest(request)) {
2888
+ throw new Error("Only the Flow reviewer may submit a new feature completion; other agents may replay only an exact previously accepted request.");
2889
+ }
2890
+ const result = completeFeature(session, request);
2891
+ if (!result.replayed) {
2892
+ throw new Error("Expected an exact feature-completion replay.");
2893
+ }
2894
+ return featureCompleteResponse(result.session, request, result.value, true);
2895
+ } catch (error) {
2896
+ return errorResponse(error);
2897
+ }
2898
+ },
2787
2899
  async featureReset(input) {
2788
2900
  try {
2789
2901
  const request = FeatureResetInputSchema.parse(input).request;
@@ -2850,6 +2962,7 @@ var flowPlanApprove = (workspace, input) => service(workspace).planApprove(input
2850
2962
  var flowRunStart = (workspace, input) => service(workspace).runStart(input);
2851
2963
  var flowReviewStart = (workspace, input) => service(workspace).reviewStart(input);
2852
2964
  var flowFeatureComplete = (workspace, input) => service(workspace).featureComplete(input);
2965
+ var flowFeatureCompleteReplay = (workspace, input) => service(workspace).featureCompleteReplay(input);
2853
2966
  var flowFeatureReset = (workspace, input) => service(workspace).featureReset(input);
2854
2967
  var flowSessionClose = (workspace, input) => service(workspace).sessionClose(input);
2855
2968
 
@@ -2863,6 +2976,7 @@ function boundedHostText(label, options) {
2863
2976
  var text = boundedHostText("Text");
2864
2977
  var featureId = host.string().max(MAX_SESSION_ID_LENGTH).regex(/^[a-z0-9]+(?:-[a-z0-9]+)*$/);
2865
2978
  var operationId = host.string().min(1).max(128).regex(/^[a-zA-Z0-9][a-zA-Z0-9._:-]*$/);
2979
+ var reviewAssignmentId = host.string().min(1).max(256);
2866
2980
  var revision = host.number().int().safe().nonnegative();
2867
2981
  var guard = { operationId, expectedRevision: revision };
2868
2982
  var artifact = host.object({
@@ -2909,7 +3023,10 @@ var StatusArgs = {
2909
3023
  host.object({ view: host.literal("compact") }).strict(),
2910
3024
  host.object({ view: host.literal("detail") }).strict(),
2911
3025
  host.object({ view: host.literal("execution") }).strict(),
2912
- host.object({ view: host.literal("reviewer"), assignmentId: text.max(256) }).strict()
3026
+ host.object({
3027
+ view: host.literal("reviewer"),
3028
+ assignmentId: reviewAssignmentId
3029
+ }).strict()
2913
3030
  ])
2914
3031
  };
2915
3032
  var PlanSaveArgs = {
@@ -2942,7 +3059,7 @@ var FeatureCompleteArgs = {
2942
3059
  request: host.object({
2943
3060
  ...guard,
2944
3061
  featureId,
2945
- assignmentId: text.max(256),
3062
+ assignmentId: reviewAssignmentId,
2946
3063
  summary: text,
2947
3064
  result: reviewResult
2948
3065
  }).strict()
@@ -2984,6 +3101,12 @@ function executeMutation(context, validation, handler) {
2984
3101
  validation.cancel(context.sessionID);
2985
3102
  return execute(context, handler);
2986
3103
  }
3104
+ function executeReviewerMutation(context, validation, handler, replayHandler) {
3105
+ if (context.agent !== "flow-reviewer") {
3106
+ return execute(context, replayHandler);
3107
+ }
3108
+ return executeMutation(context, validation, handler);
3109
+ }
2987
3110
  function createTools(_ctx, options) {
2988
3111
  return {
2989
3112
  flow_guidance: tool({
@@ -3039,9 +3162,9 @@ function createTools(_ctx, options) {
3039
3162
  execute: (args, context) => executeMutation(context, options.validation, (workspace) => flowReviewStart(workspace, args))
3040
3163
  }),
3041
3164
  flow_feature_complete: tool({
3042
- description: "Atomically record the pending review result and feature outcome.",
3165
+ description: "Submit a pending review result; only the reviewer may create a new completion, while exact accepted requests remain replayable.",
3043
3166
  args: FeatureCompleteArgs,
3044
- execute: (args, context) => executeMutation(context, options.validation, (workspace) => flowFeatureComplete(workspace, args))
3167
+ execute: (args, context) => executeReviewerMutation(context, options.validation, (workspace) => flowFeatureComplete(workspace, args), (workspace) => flowFeatureCompleteReplay(workspace, args))
3045
3168
  }),
3046
3169
  flow_feature_reset: tool({
3047
3170
  description: "Reset a feature and dependents for a fresh full retry.",
@@ -3307,4 +3430,4 @@ export {
3307
3430
  plugin_default as default
3308
3431
  };
3309
3432
 
3310
- //# debugId=48BDB7D8B1F2A60764756E2164756E21
3433
+ //# debugId=E5E0649B3D35337264756E2164756E21