opencode-plugin-flow 6.2.0 → 6.3.1

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,52 @@ One short entry per release, written for users deciding whether to upgrade.
6
6
 
7
7
  No changes yet.
8
8
 
9
+ ## [6.3.1] - 2026-07-21
10
+
11
+ Read-only retry lore closes the recovery and documentation gaps in 6.3.0:
12
+
13
+ - While Session v5 remains active, every caller with tool access now receives an
14
+ exact accepted completion through the read-only replay path before a new
15
+ reviewer submission is considered. Reviewer retries no longer cancel
16
+ validation capture or write session state.
17
+ - Replay documentation now states the active Session v5 boundary, and release
18
+ language accurately describes secret avoidance as manager guidance rather
19
+ than runtime filtering or redaction.
20
+ - The documentation contract keeps the rolling `Unreleased` changelog heading
21
+ without requiring its contents to remain `No changes yet.`
22
+
23
+ Install or update:
24
+
25
+ ```bash
26
+ opencode plugin opencode-plugin-flow@6.3.1 --global --force
27
+ ```
28
+
29
+ ## [6.3.0] - 2026-07-21
30
+
31
+ Reviewer-owned submission lore strengthens independent review without turning
32
+ Flow back into a heavy orchestration framework:
33
+
34
+ - The reserved reviewer now submits its own result through
35
+ `flow_feature_complete`. The host authorizes new completions by agent identity,
36
+ while exact previously accepted requests remain read-only replays for active
37
+ Session v5 workflows.
38
+ - Pending-review recovery is source-aware: current assignments can be
39
+ redispatched after interruption, stale source routes to reset and full
40
+ revalidation, and fingerprint failures fail closed with repair guidance.
41
+ - Final reviewers receive the approved plan targets and validation intent,
42
+ completed feature IDs, and only assignment-linked evidence. Broad validation
43
+ is treated as a coverage claim; manager guidance warns against putting secrets
44
+ in durable commands, and raw output is reduced to completeness plus a digest.
45
+ - Closed-world permission tests and the pinned OpenCode host smoke cover every
46
+ registered Flow tool. Documentation distinguishes lifecycle mutation from
47
+ fail-closed quarantine maintenance.
48
+
49
+ Install or update:
50
+
51
+ ```bash
52
+ opencode plugin opencode-plugin-flow@6.3.0 --global --force
53
+ ```
54
+
9
55
  ## [6.2.0] - 2026-07-21
10
56
 
11
57
  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.1 --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.1` 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.1"]
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,23 @@ 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
+ }
2675
+ function exactFeatureCompleteReplay(session, request) {
2676
+ const priorOperation = session.operations.find((operation) => operation.id === request.operationId);
2677
+ if (priorOperation?.kind !== "feature-complete" || priorOperation.inputDigest !== operationInputDigest(request)) {
2678
+ return null;
2679
+ }
2680
+ const result = completeFeature(session, request);
2681
+ if (!result.replayed) {
2682
+ throw new Error("Expected an exact feature-completion replay.");
2683
+ }
2684
+ return { session: result.session, run: result.value };
2685
+ }
2606
2686
  async function loadExactArchivedClose(transaction, request) {
2607
2687
  const archived = await transaction.loadArchive(request.sessionId);
2608
2688
  const operation = archived?.operations.find((item) => item.id === request.operationId);
@@ -2660,6 +2740,33 @@ function createFlowService(repository, environment) {
2660
2740
  }
2661
2741
  });
2662
2742
  }
2743
+ if (request.view !== "reviewer" && activePendingReview(session)) {
2744
+ return await repository.transact(async (transaction) => {
2745
+ const current = await transaction.load();
2746
+ if (!current) {
2747
+ return ok("No active Flow session.", {
2748
+ projection: {
2749
+ view: request.view,
2750
+ status: "idle",
2751
+ revision: 0,
2752
+ nextAction: "flow_plan_save"
2753
+ }
2754
+ });
2755
+ }
2756
+ const pending = activePendingReview(current);
2757
+ let pendingReviewSourceStale = false;
2758
+ if (pending) {
2759
+ try {
2760
+ pendingReviewSourceStale = await transaction.computeSourceDigest() !== pending.sourceDigest;
2761
+ } catch (error) {
2762
+ return errorResponse(error, "Repair workspace fingerprinting before recovering this pending review. Do not redispatch the assignment until its source can be checked.");
2763
+ }
2764
+ }
2765
+ return ok("Flow status loaded.", {
2766
+ projection: project(current, request, pendingReviewSourceStale)
2767
+ });
2768
+ });
2769
+ }
2663
2770
  return ok("Flow status loaded.", {
2664
2771
  projection: project(session, request)
2665
2772
  });
@@ -2760,30 +2867,53 @@ function createFlowService(repository, environment) {
2760
2867
  async featureComplete(input) {
2761
2868
  try {
2762
2869
  const request = FeatureCompleteInputSchema.parse(input).request;
2870
+ const current = await repository.read();
2871
+ if (current) {
2872
+ const replay = exactFeatureCompleteReplay(current, request);
2873
+ if (replay) {
2874
+ return featureCompleteResponse(replay.session, request, replay.run, true);
2875
+ }
2876
+ }
2763
2877
  return await repository.transact(async (transaction) => {
2764
2878
  const session = await transaction.load();
2765
2879
  if (!session)
2766
2880
  throw new Error("No active Flow session exists.");
2881
+ const racedReplay = exactFeatureCompleteReplay(session, request);
2882
+ if (racedReplay) {
2883
+ return featureCompleteResponse(racedReplay.session, request, racedReplay.run, true);
2884
+ }
2767
2885
  const priorOperation = session.operations.find((operation) => operation.id === request.operationId);
2768
2886
  if (!priorOperation) {
2769
2887
  const assignment = session.runs.flatMap((run) => run.reviews).find((review) => review.id === request.assignmentId);
2770
2888
  if (!assignment)
2771
2889
  throw new Error("Unknown review assignment.");
2772
2890
  if (await transaction.computeSourceDigest() !== assignment.sourceDigest) {
2773
- throw new Error("Workspace content changed after review started; reset and rerun validation and review.");
2891
+ 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
2892
  }
2775
2893
  }
2776
2894
  const result = completeFeature(session, request);
2777
2895
  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
- });
2896
+ return featureCompleteResponse(result.session, request, result.value, result.replayed);
2782
2897
  });
2783
2898
  } catch (error) {
2784
2899
  return errorResponse(error);
2785
2900
  }
2786
2901
  },
2902
+ async featureCompleteReplay(input) {
2903
+ try {
2904
+ const request = FeatureCompleteInputSchema.parse(input).request;
2905
+ const session = await repository.read();
2906
+ if (!session)
2907
+ throw new Error("No active Flow session exists.");
2908
+ const replay = exactFeatureCompleteReplay(session, request);
2909
+ if (!replay) {
2910
+ throw new Error("Only the Flow reviewer may submit a new feature completion; other agents may replay only an exact previously accepted request.");
2911
+ }
2912
+ return featureCompleteResponse(replay.session, request, replay.run, true);
2913
+ } catch (error) {
2914
+ return errorResponse(error);
2915
+ }
2916
+ },
2787
2917
  async featureReset(input) {
2788
2918
  try {
2789
2919
  const request = FeatureResetInputSchema.parse(input).request;
@@ -2850,6 +2980,7 @@ var flowPlanApprove = (workspace, input) => service(workspace).planApprove(input
2850
2980
  var flowRunStart = (workspace, input) => service(workspace).runStart(input);
2851
2981
  var flowReviewStart = (workspace, input) => service(workspace).reviewStart(input);
2852
2982
  var flowFeatureComplete = (workspace, input) => service(workspace).featureComplete(input);
2983
+ var flowFeatureCompleteReplay = (workspace, input) => service(workspace).featureCompleteReplay(input);
2853
2984
  var flowFeatureReset = (workspace, input) => service(workspace).featureReset(input);
2854
2985
  var flowSessionClose = (workspace, input) => service(workspace).sessionClose(input);
2855
2986
 
@@ -2863,6 +2994,7 @@ function boundedHostText(label, options) {
2863
2994
  var text = boundedHostText("Text");
2864
2995
  var featureId = host.string().max(MAX_SESSION_ID_LENGTH).regex(/^[a-z0-9]+(?:-[a-z0-9]+)*$/);
2865
2996
  var operationId = host.string().min(1).max(128).regex(/^[a-zA-Z0-9][a-zA-Z0-9._:-]*$/);
2997
+ var reviewAssignmentId = host.string().min(1).max(256);
2866
2998
  var revision = host.number().int().safe().nonnegative();
2867
2999
  var guard = { operationId, expectedRevision: revision };
2868
3000
  var artifact = host.object({
@@ -2909,7 +3041,10 @@ var StatusArgs = {
2909
3041
  host.object({ view: host.literal("compact") }).strict(),
2910
3042
  host.object({ view: host.literal("detail") }).strict(),
2911
3043
  host.object({ view: host.literal("execution") }).strict(),
2912
- host.object({ view: host.literal("reviewer"), assignmentId: text.max(256) }).strict()
3044
+ host.object({
3045
+ view: host.literal("reviewer"),
3046
+ assignmentId: reviewAssignmentId
3047
+ }).strict()
2913
3048
  ])
2914
3049
  };
2915
3050
  var PlanSaveArgs = {
@@ -2942,7 +3077,7 @@ var FeatureCompleteArgs = {
2942
3077
  request: host.object({
2943
3078
  ...guard,
2944
3079
  featureId,
2945
- assignmentId: text.max(256),
3080
+ assignmentId: reviewAssignmentId,
2946
3081
  summary: text,
2947
3082
  result: reviewResult
2948
3083
  }).strict()
@@ -2984,6 +3119,12 @@ function executeMutation(context, validation, handler) {
2984
3119
  validation.cancel(context.sessionID);
2985
3120
  return execute(context, handler);
2986
3121
  }
3122
+ function executeReviewerMutation(context, handler, replayHandler) {
3123
+ if (context.agent !== "flow-reviewer") {
3124
+ return execute(context, replayHandler);
3125
+ }
3126
+ return execute(context, handler);
3127
+ }
2987
3128
  function createTools(_ctx, options) {
2988
3129
  return {
2989
3130
  flow_guidance: tool({
@@ -3039,9 +3180,9 @@ function createTools(_ctx, options) {
3039
3180
  execute: (args, context) => executeMutation(context, options.validation, (workspace) => flowReviewStart(workspace, args))
3040
3181
  }),
3041
3182
  flow_feature_complete: tool({
3042
- description: "Atomically record the pending review result and feature outcome.",
3183
+ description: "Submit a pending review result; only the reviewer may create a new completion, while exact accepted requests remain replayable for an active Session v5 workflow.",
3043
3184
  args: FeatureCompleteArgs,
3044
- execute: (args, context) => executeMutation(context, options.validation, (workspace) => flowFeatureComplete(workspace, args))
3185
+ execute: (args, context) => executeReviewerMutation(context, (workspace) => flowFeatureComplete(workspace, args), (workspace) => flowFeatureCompleteReplay(workspace, args))
3045
3186
  }),
3046
3187
  flow_feature_reset: tool({
3047
3188
  description: "Reset a feature and dependents for a fresh full retry.",
@@ -3307,4 +3448,4 @@ export {
3307
3448
  plugin_default as default
3308
3449
  };
3309
3450
 
3310
- //# debugId=48BDB7D8B1F2A60764756E2164756E21
3451
+ //# debugId=6D57AB8590A9BCD164756E2164756E21