opencode-plugin-flow 7.3.2 → 8.1.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/dist/index.js CHANGED
@@ -109,7 +109,8 @@ If a Flow tool is absent, report an incomplete plugin load; never simulate state
109
109
  ## Recovery
110
110
 
111
111
  On interruption, read compact status; load \`flow-run\` for an active or blocked feature
112
- and use execution or reviewer status, never prose, for lifecycle truth.
112
+ and use execution or reviewer status, never prose, for lifecycle truth. At a
113
+ checkpoint or lease stop, print compact \`findingsDigest\` before any delivery map.
113
114
 
114
115
  Summaries keep plan/source IDs \`verified\` or \`incomplete\`. A prior finding is
115
116
  terminally \`fixed\` only when review passes with current evidence. A failed
@@ -187,21 +188,19 @@ Save one plan with:
187
188
  - \`overview\`: the implementation approach and important boundaries.
188
189
  - \`requirements\`: acceptance criteria, constraints, and non-goals.
189
190
  - \`decisions\`: assumptions and architecture or scope choices already made.
190
- - \`gate\`: the canonical whole-repository command, read from repository
191
- configuration. Broad evidence later runs this command byte-for-byte, so a check
192
- too weak to fail forecloses broad evidence for the session.
193
- - \`externalEvidence\`: each acceptance observation needing an operating system,
194
- architecture, service, credential, setting, or hardware this host may lack, as
195
- \`requirement\`, \`environment\`, the exact \`command\` whose passing is that observation,
196
- \`platform\`: \`win32\`, \`darwin\`, \`linux\`, or \`other\` for a non-OS environment, and
197
- \`assertions\`: the test case names whose passing is that observation, empty for
198
- evidence that is not a test result. Empty list when the goal is fully observable
199
- here. That command, on that platform, reporting those cases passing is what
200
- satisfies it: final review and completed closure stay refused until then. A skipped
201
- case exits zero and satisfies nothing, and recording the gap as a \`requirements\`
202
- non-goal declares nothing.
191
+ - \`evidence\`: one \`scope: "gate"\` entry for the canonical whole-repository
192
+ command, plus \`scope: "extra"\` entries for observations this host may lack.
193
+ Each entry names \`requirement\`, \`environment\`, \`command\`, \`platform\`
194
+ (\`win32\`, \`darwin\`, \`linux\`, or \`other\`), and \`assertions\` (empty when the
195
+ evidence is not a test result). Broad observations run the gate command
196
+ byte-for-byte. Extra entries may be omitted when the goal is fully observable
197
+ here. Final review and completed closure stay refused until every extra
198
+ entry is satisfied on its declared platform with named cases passing. The
199
+ gate is the command every broad observation must run; a failed or
200
+ claimed-broad gate still vetoes review.
203
201
  - \`features\`: ordered outcome slices, each with a stable \`id\`, \`title\`,
204
- \`summary\`, bounded \`targets\`, concrete \`validation\`, and \`dependsOn\` ids.
202
+ \`summary\`, bounded \`targets\`, concrete \`validation\`, \`dependsOn\` ids, and
203
+ optional \`kind\`.
205
204
 
206
205
  Each feature needs one observable outcome judgeable from bounded evidence and
207
206
  focused validation. Split only independent failures or true dependencies; file
@@ -216,6 +215,13 @@ outcome and its evidence.
216
215
  A \`validation\` entry naming a command is recorded byte-for-byte; prose there stays
217
216
  reviewer judgment, never a fabricated result.
218
217
 
218
+ If the request is inspect-only (review, audit, survey, no promised edit), invent
219
+ no repair features. Save at most a small set of inspect features with
220
+ \`kind: "inspect"\`, whose \`validation\` is reviewer inspection, whose \`targets\`
221
+ are existing paths, and whose \`decisions\` state that no source edit is
222
+ authorized. The gate may be the repo's existing check. Ask before turning an
223
+ inspect request into repairs.
224
+
219
225
  Before saving, confirm:
220
226
 
221
227
  - every requirement maps to a feature or an explicit non-goal;
@@ -280,17 +286,15 @@ the manager's summary. Check that:
280
286
  - changed behavior is correct at public and downstream call sites;
281
287
  - validation is strong enough for the behavior and main failure modes;
282
288
  - scope did not drift and unrelated user work was preserved;
283
- - relevant adjacent states, failure/cleanup ordering, repetition, retry,
284
- interruption, reentrancy, concurrency, and overlapping invariants still work;
285
289
  - the feature's actual base diff includes no unexplained deletion, rename, file
286
290
  type, generated artifact, or executable/file-mode change; and
287
291
  - persistence, concurrency, security, migration, compatibility, package, UI,
288
292
  and recovery risks were examined when relevant.
289
293
 
290
- Finish the supplied feature-specific risk checklist, represented by a bounded
291
- matrix for concurrency or state-machine work. Continue that matrix after finding
292
- one blocker so independently detectable interleavings arrive in the same review
293
- cohort.
294
+ Finish the supplied feature-specific risk checklist. Continue a concurrency or
295
+ state-machine matrix after one blocker only when \`assignment.packet.riskLenses\`
296
+ is non-empty or the packet summary includes a matrix. Otherwise inspect the
297
+ changed artifacts, the supplied validation, and live \`priorFindings\`.
294
298
 
295
299
  Scope plan/source IDs by assignment kind. An ordinary feature review records
296
300
  dispositions only for IDs mapped to the active feature or explicitly supplied
@@ -314,8 +318,8 @@ not a failure; a missing or conflicting baseline fact, or a material mode,
314
318
  platform, race, or failure-path claim without proof, is.
315
319
 
316
320
  Flow deliberately projects no raw command output; use the durable command, exit
317
- code, completeness, digest, source binding, and your workspace inspection. A weak or
318
- unclear coverage claim is an evidence gap.
321
+ code, completeness, digest, source binding, and your workspace inspection. A weak
322
+ coverage claim is an evidence gap.
319
323
 
320
324
  For a final assignment, also inspect broad validation and confirm docs,
321
325
  commands, package surfaces, and remaining gaps are consistent with completion.
@@ -329,8 +333,8 @@ IDs in summary or evidence.
329
333
 
330
334
  Report every problem you find. Severity is a routing decision the runtime acts
331
335
  on, not a filter on what to mention: \`blocking\` when the issue invalidates the
332
- approved outcome, \`advisory\` otherwise. When you are unsure, report it as
333
- \`advisory\` rather than omitting it.
336
+ approved outcome, \`advisory\` otherwise. If unsure, report it as
337
+ \`advisory\` rather than omitting it. Inspect kind completes with blockers.
334
338
 
335
339
  Set \`scopeBlocker: true\` on a blocking finding whose repair requires material
336
340
  work outside the approved plan, and identify the boundary in \`evidence\`. The
@@ -406,14 +410,14 @@ description: Implement, validate, independently review, and record one approved
406
410
 
407
411
  Work on exactly one approved feature.
408
412
 
409
- ## Start and scope
413
+ ## Start
410
414
 
411
415
  1. Call \`flow_status { request: { view: "compact" } }\` first. Treat
412
- \`nextAction\` as the durable default workflow direction, not as permission.
416
+ \`nextAction\` as the durable default, not as permission.
413
417
  2. If the top-level response status is \`error\`, report its exact summary and
414
418
  recovery when present and, if \`workflowData.delivery\` exists, the handoff
415
- below. State this initial read made no lifecycle, Git, or release mutation;
416
- stop and never route its \`nextAction\`.
419
+ below. This read made no lifecycle, Git, or release mutation. Stop. Do not
420
+ route its \`nextAction\`.
417
421
  3. If compact status contains \`archiveRetry\`, call \`flow_session_close\` once
418
422
  with the projected request byte-for-byte. Report delivery under the contract
419
423
  below. Refresh only if publication is unconfirmed. Stop after this cleanup
@@ -421,9 +425,9 @@ Work on exactly one approved feature.
421
425
  4. When the projection contains an active goal, align it with the current
422
426
  \`/flow-run\` request before another manager lifecycle mutation. Continue only
423
427
  for the same goal or a method/emphasis narrowing that preserves all outcomes;
424
- close completed work. Unless step 5 applies, new/expanded work makes no
425
- mutation: report that it has not started and offer continue, defer, or
426
- abandon.
428
+ close completed work. Unless the next step applies, new or expanded work
429
+ makes no mutation: report that it has not started and offer continue, defer,
430
+ or abandon.
427
431
  5. If the aligned request explicitly chooses deferred or abandoned closure for
428
432
  a non-completed session, call \`flow_session_close\` with compact session id and
429
433
  revision, fresh operation id, that kind, and optional summary. Report delivery
@@ -432,226 +436,113 @@ Work on exactly one approved feature.
432
436
  explain that \`/flow-run\` requires an approved feature, and stop without
433
437
  mutation.
434
438
 
435
- Route every compact projection in this order:
439
+ Delivery handoff: report \`workflowData.delivery.report\` verbatim. Map IDs only
440
+ from delivery \`outcomeSummary\`/\`terminalFindings\`. Requirements are \`verified\`,
441
+ \`incomplete\`, or explicitly \`deferred\`, and \`abandoned\` remains the kind. If
442
+ delivery is absent, report exact recovery and no map. On revision conflict,
443
+ refresh compact; retry only for the same session and goal while status still
444
+ permits the selected closure kind; never close a replacement.
445
+
446
+ ## Route
447
+
448
+ Follow compact \`nextAction\` in this order:
436
449
 
437
450
  - \`flow_session_close\`: close completed work with its projected session
438
- id/revision, fresh operation id, and \`kind: "completed"\`. Report delivery
439
- under the contract below, follow one exact \`archiveRetry\` if needed, and stop.
440
- New work may enter planning afterward; do not fabricate a run.
451
+ id/revision, fresh operation id, and \`kind: "completed"\`. Report delivery,
452
+ follow one exact \`archiveRetry\` if needed, and stop.
441
453
  - \`await-user-direction\` or blocked \`flow_feature_reset\`: call
442
- \`flow_status { request: { view: "detail" } }\` exactly once, then distinguish
443
- the projected status:
444
- - Ready \`await-user-direction\` has no blocked run left to reset. Identify the
445
- planned feature whose latest relevant reviewed outcome remains failed and
446
- checkpoint unless the current aligned request explicitly authorizes its
447
- retry. When authorized, call \`flow_run_start\` with that exact \`featureId\`;
448
- never call \`flow_feature_reset\` from ready status or rely on default
449
- selection.
450
- - For blocked status, apply **Blocked review** below. If it permits another
451
- feature run, pass that exact choice as \`nextFeatureId\` to
452
- \`flow_feature_reset\` so reset and run start are atomic, then route its
453
- returned projection; otherwise report the checkpoint and stop.
454
+ \`flow_status { request: { view: "detail" } }\` exactly once, then apply
455
+ **Blocked review**.
454
456
  - Running \`flow_feature_reset\`: the pending review is source-stale. Reset with
455
- the same feature as \`nextFeatureId\` when continuing it, then route the returned
456
- projection. Never redispatch that assignment.
457
+ the same feature as \`nextFeatureId\` when continuing it. Never redispatch that
458
+ assignment.
457
459
  - \`dispatch-flow-reviewer\`: read execution status. If that read errors, report
458
- its exact summary and recovery when present and stop without dispatching; do
459
- not infer a projection. Otherwise route that refreshed projection before
460
- acting. Dispatch the recovered pending assignment under **Review and record**
461
- only if \`nextAction\` is still \`dispatch-flow-reviewer\`. If it is now running
462
- \`flow_feature_reset\`, follow the source-stale reset route and never dispatch
463
- that assignment. Skip run start, implementation, and validation.
460
+ its exact summary and recovery when present and stop. Otherwise route that
461
+ refreshed projection. Dispatch under **Review** only if \`nextAction\` is still
462
+ \`dispatch-flow-reviewer\`. If it is now running \`flow_feature_reset\`, follow
463
+ the source-stale reset route.
464
464
  - \`flow_run_start\`: start the ready feature, refresh compact status, and read
465
465
  execution status.
466
- - \`flow_validation_start\`: read execution status and resume integration or
467
- validation from the current worktree.
468
- - \`flow_review_start\`: read execution status and continue at **Review and
469
- record** without fabricating another validation.
470
- - Any other action: report it and stop unless the runtime explicitly identifies
471
- an active execution path.
472
-
473
- Use execution status for active scope/revision guards. Before editing, read the
474
- feature summary, targets, validation, dependencies, requirements, and decisions.
475
- If a projected action fails an environment-sensitive guard, refresh compact and
476
- handle that rejection; never force it.
477
-
478
- Summaries keep plan/source IDs \`verified\` or \`incomplete\`.
479
- Delivery handoff: report \`workflowData.delivery.report\` verbatim. Map IDs only from
480
- delivery \`outcomeSummary\`/\`terminalFindings\`; requirements are \`verified\`,
481
- \`incomplete\`, or explicitly \`deferred\`, and \`abandoned\` remains the kind.
482
- If delivery is absent, report exact recovery and no map; never invent or read
483
- detail solely for closure. On revision conflict, refresh compact; retry only for
484
- the same session and goal while status still permits the selected closure kind;
485
- never close a replacement.
486
-
487
- Preserve unrelated work and stay inside the feature. Out-of-plan work stops;
488
- finish or obtain explicit deferred/abandoned closure before a new plan. Never
489
- replan in place. Use \`flow_feature_reset\` for a wrong design or assumption; do
490
- not layer retries.
491
-
492
- ## Evidence and risk preflight
493
-
494
- Before editing or dispatching a worker, perform one preflight from the approved
495
- feature and current worktree:
496
-
497
- - Preserve every named finding/requirement; map each to an observable acceptance
498
- outcome.
499
- - Inventory each \`externalEvidence\` command, its platform, and where it writes a
500
- JUnit report: arm it with \`resultsPath\` naming that file, or its declared cases go
501
- unobserved and the entry stays open on exit zero.
502
- - Reuse one conversational run baseline of unrelated work, deletions, renames,
503
- file types, and executable modes. Refresh changed facts; give each review only
504
- facts the feature changes or depends on, and give final review the full
505
- inventory.
506
- - Write one concise adversarial checklist covering failure and cleanup ordering,
507
- adjacent states, repetition, retry, interruption, concurrency, overlapping
508
- invariants, and relevant platform or persistence risks. For concurrency or
509
- state-machine work, express it as a compact matrix with \`state/interleaving\`,
510
- \`event\`, \`expected outcome\`, \`cleanup/invariant\`, and \`evidence\` columns.
511
-
512
- Carry the checklist/IDs through workers and review. Evidence needing user or
513
- external authority stops before implementation.
466
+ - \`flow_validation_start\`: read execution status and resume from the current
467
+ worktree.
468
+ - \`flow_review_start\`: read execution status and continue at **Review**.
469
+ - Any other action: report it and stop.
470
+
471
+ Use execution status for active scope and revision. Stay inside the feature.
472
+ Out-of-plan work stops. Use \`flow_feature_reset\` for a wrong design; do not
473
+ layer retries.
514
474
 
515
475
  ## Implement
516
476
 
517
- Make the smallest change satisfying the approved outcome and repository
518
- boundaries. Create no lifecycle, validation, audit, or handoff sidecars. A
519
- durable user-requested report is normally one stable Markdown artifact; JSON
520
- requires an explicit request.
477
+ Make the smallest change that satisfies the approved outcome. Create no
478
+ lifecycle or handoff sidecars. Do not stage, commit, push, publish, or mutate
479
+ releases unless asked separately.
521
480
 
522
- Do not stage, commit, push, publish, or mutate releases unless asked separately.
481
+ Work serially. After a feature run is active, dispatch \`flow-worker\` only for
482
+ two or three genuinely independent slices with clear benefit. Workers call no
483
+ Flow tools, spawn no children, and run no Bash. Integrate and inspect the
484
+ combined diff before validation.
523
485
 
524
- ## Bounded worker waves
486
+ ## Validate
525
487
 
526
- Work serially by default; existing authority covers a qualifying worker wave.
527
- After manager orientation, fan out only two or three genuinely independent,
528
- non-overlapping slices with clear benefit. Dispatch one cohort together if the
529
- host runs concurrent tasks, otherwise sequentially; report serial either way. Each
530
- assignment names a stable id, exact outcome/read-write scope, coverage, manager
531
- checks, dependencies, stop condition, and preflight risk checklist. The worker
532
- must receive the checklist before it codes. Shared contracts, lockfiles, and generated
533
- output stay manager-owned unless wholly assigned to one worker.
488
+ Arm each evidence Bash command with \`flow_validation_start\` (current revision,
489
+ feature id, exact command, \`scope\`) immediately before running it byte-for-byte.
490
+ Flow records the host observation; copy no host-observed fields.
534
491
 
535
- Workers call no Flow tools, spawn no children, and run no Bash. Each returns
536
- status, scope/coverage, evidence/changed paths, manager checks, gaps/risks, and
537
- integration notes; missing or blocked output is a coverage gap.
492
+ \`scope: "broad"\` runs the plan's gate evidence command and nothing else.
538
493
 
539
- After workers stop, reconcile paths/scopes and inspect combined diff/evidence
540
- before validation. At most one targeted follow-up wave may repair a slice,
541
- unlock a dependency, or verify a material claim; never a third. Create no
542
- coordination ledger/sidecar. After interruption, inspect status/worktree and
543
- treat partial worker edits as untrusted.
494
+ A failed, incomplete, or source-drifted observation of a plan-listed command or
495
+ of the declared gate command blocks review until that same command passes for
496
+ current source.
544
497
 
545
- ## Validate
498
+ Every host-observed validation advances revision. The \`[flow-validation]\`
499
+ marker reports \`passed\`, \`recordedRevision\`, and declared \`assertions\`. Use
500
+ \`recordedRevision\` for the next \`flow_validation_start\`, or for
501
+ \`flow_review_start\` only when \`passed: true\`. If the marker is absent, refresh
502
+ compact status before mutating.
546
503
 
547
- Only validate after every worker has stopped and integration is settled. Choose
548
- checks from the changed behavior and risk:
549
-
550
- - Prefer focused behavioral tests that would fail without the change.
551
- - Cover persistence, integration, API, browser, accessibility, package, or
552
- build paths when the feature touches them.
553
- - Typecheck, lint, build, and static inspection are indirect evidence; they are
554
- sufficient alone only for genuinely mechanical or documentation-only work.
555
- - UI claims need visual inspection when a runnable target is available.
556
- - Cleanup claims need behavior-preservation evidence, not formatting alone.
557
- - \`scope: "broad"\` runs the plan's declared \`gate\` and nothing else; the runtime
558
- refuses any other command at that scope.
559
-
560
- Immediately before each evidence Bash command, call \`flow_validation_start\`
561
- with current revision, feature id, exact command, and \`scope\` (\`focused\` or
562
- \`broad\`). Run it byte-for-byte next and inspect the complete outcome. Flow
563
- records the host observation; copy no host-observed fields into a later request.
564
- The command is durable, so include no secrets.
565
-
566
- A failed, incomplete, or source-drifted observation of a plan-listed command or of
567
- the declared \`gate\` blocks review until that same command passes for current
568
- source. No substitute discharges it; reverting the source revives nothing.
569
-
570
- A gate or \`externalEvidence\` command that cannot pass withholds completed closure,
571
- and reporting the blocker is half the move. Reach the passing command, or ask the
572
- user to choose deferred or abandoned closure. Ending the turn having done neither
573
- leaves the workflow with no next step, whatever the blocker was.
574
-
575
- Every host-observed validation advances the session revision, so the revision
576
- that armed a completed command is stale. The \`[flow-validation]\` marker reports
577
- \`passed\`, \`recordedRevision\`, and any declared \`assertions\` with what the report
578
- said about each. Use \`recordedRevision\` for the next
579
- \`flow_validation_start\`, or for \`flow_review_start\` only when \`passed: true\`. If
580
- the marker is absent or malformed, refresh compact status before mutating.
581
-
582
- Use focused validation for ordinary features. For the final feature, run the
583
- plan's declared \`gate\` at broad scope after the last relevant edit. A source edit
584
- invalidates earlier applicability. Failed or unavailable checks are blockers, not
585
- passing evidence.
586
-
587
- Immediately before review admission, reconcile the preflight inventory against the
588
- recorded current-source observations.
589
-
590
- ## Review and record
504
+ For the final feature, run the plan's gate command at broad scope after the
505
+ last relevant edit.
506
+
507
+ An evidence command that cannot pass withholds completed closure. Reach the
508
+ passing command, or ask the user to choose deferred or abandoned closure.
509
+
510
+ ## Review
591
511
 
592
512
  After successful applicable validation, call \`flow_review_start\` with a fresh
593
- operation id, current revision, feature id, \`artifactsChanged\` listing every
594
- changed workspace-relative artifact path, and a bounded packet summary plus risk
595
- lenses.
596
-
597
- Keep the packet bounded. Map IDs to current-source commands or scenarios,
598
- environment, and results. Put the feature-specific risk checklist under
599
- \`Risks/Matrix:\`, representing it as a transition matrix for concurrency or
600
- state-machine work. Include \`Baseline:\` facts only when this feature changes or
601
- depends on them, except that final review receives the full inventory.
602
- Ordinary-review plan/source IDs are limited to active-feature mappings or IDs
603
- explicitly supplied for its packet; final review includes every approved
604
- requirement/feature ID. Omit empty optional sections; state \`none\` only for a relevant
605
- inspected absence. Never hide a gap.
606
-
607
- Dispatch only to reserved \`flow-reviewer\`; never review, copy, or submit its
608
- verdict in manager context. It reads the assignment/workspace and calls
609
- \`flow_feature_complete\` directly; runtime verifies the caller. It stays
610
- workspace-read-only, with that result submission as its sole lifecycle mutation.
513
+ operation id, current revision, feature id, \`artifactsChanged\`, and a bounded
514
+ packet. Dispatch only reserved \`flow-reviewer\`. Never review or submit its
515
+ verdict in manager context.
611
516
 
612
517
  After dispatch, read compact status. On top-level error, report exact
613
- summary/recovery, say the latest lifecycle state could not be confirmed, and
614
- stop without further mutation. Do not claim this invocation made no lifecycle
615
- mutation: review may have started or recorded a result. Never invent or submit a
616
- verdict. If status remains running, apply the
617
- \`dispatch-flow-reviewer\` or running \`flow_feature_reset\` route above. If status
618
- is blocked, load detail through the single blocked route above. A recorded pass
619
- completes the feature.
518
+ summary/recovery and stop without further mutation. If status remains running,
519
+ apply the \`dispatch-flow-reviewer\` or running \`flow_feature_reset\` route. If
520
+ status is blocked, apply **Blocked review**. A recorded pass completes the
521
+ feature.
620
522
 
621
523
  ### Blocked review
622
524
 
623
- Follow \`nextAction\` with the one detail projection. The runtime already weighs
525
+ Follow \`nextAction\` with the one detail projection for routing, then print
526
+ compact \`findingsDigest\` as the user-facing list. The runtime already weighs
624
527
  \`failedReviewCount\` and \`blockedFeature.scopeBlocker\`.
625
528
 
626
- - \`await-user-direction\` means checkpoint. Do not reset.
627
- - \`flow_feature_reset\` permits one automatic reset under existing
628
- implementation authority, with the blocked \`featureId\` as \`nextFeatureId\`.
629
- That call atomically starts the fresh full retry. Fix only its blocking
529
+ - Ready \`await-user-direction\` has no blocked run left to reset. Identify the
530
+ planned feature whose latest relevant reviewed outcome remains failed and
531
+ checkpoint unless the current aligned request explicitly authorizes its
532
+ retry. When authorized, call \`flow_run_start\` with that exact \`featureId\`.
533
+ Never call \`flow_feature_reset\` from ready status.
534
+ - For blocked \`await-user-direction\`, checkpoint. Do not reset.
535
+ - For blocked \`flow_feature_reset\`, one automatic reset is allowed under
536
+ existing implementation authority. Pass the blocked \`featureId\` as
537
+ \`nextFeatureId\` so reset and run start are atomic. Fix only its blocking
630
538
  findings, then run full validation and full independent review.
631
- - A feature whose latest relevant reviewed outcome remains failed is never
632
- selected implicitly. \`/flow-auto\` may still continue an untouched,
633
- dependency-independent feature. When every runnable candidate requires a
634
- retry, compact status is \`ready\` with \`await-user-direction\`, handled by the
635
- ready route above.
636
539
  - When \`failedReviewCount >= 2\`, retry only when the current aligned request
637
- explicitly authorizes one additional attempt. Pass the blocked feature as
638
- \`nextFeatureId\` on \`flow_feature_reset\`; if that attempt fails, checkpoint
639
- again.
640
- - If explicit direction instead selects another planned,
641
- dependency-independent feature, pass that feature's exact \`featureId\` as
642
- \`nextFeatureId\` on \`flow_feature_reset\`. Reset supersedes the affected
643
- attempts and starts that exact run in one transaction. Do not reset first,
644
- call \`flow_run_start\` separately, or rely on default selection.
645
-
646
- When stopping blocked, label overall incomplete. Report the latest repair proved
647
- pending a passing review; recurring and new blockers; goal/progress; blocked
648
- feature, attempt, and failure count; completed/untouched features; latest
649
- validations and \`artifactsChanged\` as Flow-reported artifact evidence; Git/release
650
- mutation status; whether this request started and matched the goal; exact
651
- \`nextAction\`; and whether another attempt requires explicit authorization.
652
-
653
- Use that already-loaded compact status after every recorded outcome. Direct
654
- \`/flow-run\` reports this one feature's cumulative outcome and \`nextAction\`, then
540
+ explicitly authorizes one additional attempt.
541
+ - If explicit direction selects another planned, dependency-independent
542
+ feature, pass that exact \`featureId\` as \`nextFeatureId\` on
543
+ \`flow_feature_reset\`.
544
+
545
+ Direct \`/flow-run\` reports compact \`findingsDigest\` and \`nextAction\`, then
655
546
  stops. Under \`/flow-auto\`, return to its lifecycle loop.
656
547
  `;
657
548
 
@@ -677,7 +568,9 @@ var FLOW_MANAGER_KERNEL = [
677
568
  ].join(" "),
678
569
  [
679
570
  "- Make one automatic fresh full retry only when the projected `nextAction`",
680
- "is `flow_feature_reset`; otherwise checkpoint."
571
+ "is `flow_feature_reset`; otherwise checkpoint.",
572
+ "On `await-user-direction` or a lease stop, print compact `findingsDigest`",
573
+ "without inventing ids."
681
574
  ].join(" "),
682
575
  [
683
576
  "- Before review, require current-source evidence appropriate to the changed outcome,",
@@ -725,9 +618,8 @@ var FLOW_WORKER_PROMPT = [
725
618
  "- Never edit .flow or .git metadata paths; the host denies those paths.",
726
619
  "- A read-only evidence slice must not edit files.",
727
620
  [
728
- "- The assignment must include an adversarial acceptance and risk checklist,",
729
- "represented as a transition matrix for concurrency or state-machine work, prepared before coding.",
730
- "If it is missing, stop without editing and report the gap."
621
+ "- If the assignment includes a risk checklist or matrix, apply it before coding.",
622
+ "If required work would escape the assigned paths, stop and report the gap."
731
623
  ].join(" "),
732
624
  [
733
625
  "- An implementation slice may edit only the exact, non-overlapping write paths explicitly assigned by the manager.",
@@ -738,13 +630,7 @@ var FLOW_WORKER_PROMPT = [
738
630
  "- The manager owns integration, focused checks, and authoritative combined validation",
739
631
  "after all workers have stopped."
740
632
  ].join(" "),
741
- [
742
- "Before editing, apply the supplied risk coverage through its matrix rows when present:",
743
- "primary behavior, failure and cleanup ordering, adjacent state transitions,",
744
- "repeated or interrupted operation, overlapping invariants, and relevant persistence,",
745
- "concurrency, security, compatibility, or file-metadata risks.",
746
- "Preserve every named finding, requirement, or prior review ID in your handoff."
747
- ].join(" "),
633
+ "- Preserve every named finding, requirement, or prior review ID in your handoff.",
748
634
  "## Handoff",
749
635
  [
750
636
  "Return exactly one concise handoff with `Status` (success, partial, or blocked),",
@@ -1089,6 +975,54 @@ function isFeatureId(value) {
1089
975
  return FEATURE_ID_PATTERN.test(value);
1090
976
  }
1091
977
 
978
+ // src/domain/session.ts
979
+ function planEvidence(plan) {
980
+ return plan?.evidence ?? [];
981
+ }
982
+ function planGate(plan) {
983
+ return planEvidence(plan).find((entry) => entry.scope === "gate")?.command;
984
+ }
985
+ function featureKind(feature) {
986
+ return feature?.kind === "inspect" ? "inspect" : "change";
987
+ }
988
+ function reviewResultSemanticIssues(result) {
989
+ const issues = [];
990
+ const blocking = result.findings.some((finding) => finding.severity === "blocking");
991
+ for (const [index, finding] of result.findings.entries()) {
992
+ if (finding.severity === "blocking" && !finding.evidence?.trim()) {
993
+ issues.push({
994
+ path: ["findings", index, "evidence"],
995
+ message: "A blocking finding requires concrete evidence."
996
+ });
997
+ }
998
+ if (finding.scopeBlocker && finding.severity !== "blocking") {
999
+ issues.push({
1000
+ path: ["findings", index, "scopeBlocker"],
1001
+ message: "Only a blocking finding can be a scope blocker."
1002
+ });
1003
+ }
1004
+ }
1005
+ if (result.verdict === "failed" && !blocking) {
1006
+ issues.push({
1007
+ path: ["findings"],
1008
+ message: "A failed review requires a blocking finding."
1009
+ });
1010
+ }
1011
+ if (result.verdict === "passed" && blocking) {
1012
+ issues.push({
1013
+ path: ["findings"],
1014
+ message: "A passed review cannot contain blocking findings."
1015
+ });
1016
+ }
1017
+ if (result.terminalDisposition === "observed_unsubmitted" && result.verdict !== "failed") {
1018
+ issues.push({
1019
+ path: ["terminalDisposition"],
1020
+ message: "Observed-but-unsubmitted review work must fail closed."
1021
+ });
1022
+ }
1023
+ return issues;
1024
+ }
1025
+
1092
1026
  // src/domain/test-results.ts
1093
1027
  var TESTCASE = /<testcase\b([^>]*?)(\/>|>([\s\S]*?)<\/testcase\s*>)/g;
1094
1028
  var ATTRIBUTE = /([\w:.-]+)\s*=\s*"([^"]*)"/g;
@@ -1202,7 +1136,7 @@ function narrowingArguments(command) {
1202
1136
  }
1203
1137
  function declaredAssertions(session, command) {
1204
1138
  return [
1205
- ...new Set((session.plan?.externalEvidence ?? []).filter((entry) => entry.command === command).flatMap((entry) => entry.assertions ?? []))
1139
+ ...new Set(planEvidence(session.plan).filter((entry) => entry.command === command).flatMap((entry) => entry.assertions ?? []))
1206
1140
  ];
1207
1141
  }
1208
1142
  function sameAssertions(left, right) {
@@ -1241,7 +1175,7 @@ function recordValidation(session, input) {
1241
1175
  if (narrowing.length > 0) {
1242
1176
  throw new FlowTransitionError(`A broad observation cannot select which tests it runs (${narrowing.join(", ")}). Arm the repository's canonical gate, or record this command as focused.`);
1243
1177
  }
1244
- const gate = session.plan?.gate;
1178
+ const gate = planGate(session.plan);
1245
1179
  if (gate !== undefined && input.command !== gate) {
1246
1180
  throw new FlowTransitionError(`A broad observation must run the plan-declared canonical gate (${gate}). Arm that exact command, or record this one as focused.`);
1247
1181
  }
@@ -1282,7 +1216,7 @@ function isObservedOnDeclaredPlatform(entry, observation) {
1282
1216
  return true;
1283
1217
  return observation.hostPlatform === entry.platform;
1284
1218
  }
1285
- function externalEvidenceRefusal(session, entry, sourceDigest) {
1219
+ function evidenceRefusal(session, entry, sourceDigest) {
1286
1220
  const eligible = session.runs.flatMap((run) => run.validations).filter((observation) => observation.command === entry.command && isValidationEligible(observation, sourceDigest));
1287
1221
  const wrongHosts = [
1288
1222
  ...new Set(eligible.filter((observation) => !isObservedOnDeclaredPlatform(entry, observation)).map((observation) => observation.hostPlatform ?? "an unrecorded host"))
@@ -1292,18 +1226,21 @@ function externalEvidenceRefusal(session, entry, sourceDigest) {
1292
1226
  const detail = wrongHosts.length > 0 ? `passed on ${wrongHosts.join(", ")} but this entry declares ${entry.platform}, so that run observed something else — a skipped case exits zero too` : unmet ? `passed on ${entry.platform ?? "the declared host"} but reported no passing result for ${unmet.join(", ")}; arm it again with \`resultsPath\` naming the report the command writes, and make those cases run` : `needs ${needs}`;
1293
1227
  return `${JSON.stringify(entry.command)} (${detail}, for ${entry.requirement})`;
1294
1228
  }
1295
- function unsatisfiedExternalEvidence(session, sourceDigest) {
1296
- const declared = session.plan?.externalEvidence ?? [];
1229
+ function unsatisfiedEvidence(session, sourceDigest) {
1230
+ const declared = planEvidence(session.plan);
1297
1231
  if (declared.length === 0)
1298
1232
  return [];
1299
1233
  const observed = session.runs.flatMap((run) => run.validations);
1300
1234
  return declared.filter((entry) => !observed.some((observation) => observation.command === entry.command && isObservedOnDeclaredPlatform(entry, observation) && assertionsSatisfied(entry.assertions ?? [], observation.observedAssertions) && isValidationEligible(observation, sourceDigest)));
1301
1235
  }
1236
+ function unsatisfiedExtraEvidence(session, sourceDigest) {
1237
+ return unsatisfiedEvidence(session, sourceDigest).filter((entry) => entry.scope === "extra");
1238
+ }
1302
1239
  function isValidationFresh(session, run, observation) {
1303
1240
  return session.runs.filter((candidate) => candidate.featureId === run.featureId).flatMap((candidate) => candidate.validations).every((candidate) => candidate.command !== observation.command || isValidationEligible(candidate) || candidate.recordedRevision < observation.recordedRevision);
1304
1241
  }
1305
1242
  function unresolvedVetoedCommands(session, run, sourceDigest) {
1306
- const gate = session.plan?.gate;
1243
+ const gate = planGate(session.plan);
1307
1244
  const planned = session.approval === "approved" ? [
1308
1245
  ...session.plan?.features.find((candidate) => candidate.id === run.featureId)?.validation ?? [],
1309
1246
  ...gate === undefined ? [] : [gate]
@@ -1321,8 +1258,9 @@ function gateIssue(gate) {
1321
1258
  return narrowing.length === 0 ? null : `The plan's canonical gate cannot select which tests it runs (${narrowing.join(", ")}).`;
1322
1259
  }
1323
1260
  function planIssue(plan) {
1324
- if (plan.gate !== undefined) {
1325
- const issue = gateIssue(plan.gate);
1261
+ const gate = planGate(plan);
1262
+ if (gate !== undefined) {
1263
+ const issue = gateIssue(gate);
1326
1264
  if (issue)
1327
1265
  return issue;
1328
1266
  }
@@ -1436,45 +1374,6 @@ function droppedFindingIds(session, featureId, findings) {
1436
1374
  return liveFindingIds(session, featureId).filter((id) => !submitted.has(id));
1437
1375
  }
1438
1376
 
1439
- // src/domain/session.ts
1440
- function reviewResultSemanticIssues(result) {
1441
- const issues = [];
1442
- const blocking = result.findings.some((finding) => finding.severity === "blocking");
1443
- for (const [index, finding] of result.findings.entries()) {
1444
- if (finding.severity === "blocking" && !finding.evidence?.trim()) {
1445
- issues.push({
1446
- path: ["findings", index, "evidence"],
1447
- message: "A blocking finding requires concrete evidence."
1448
- });
1449
- }
1450
- if (finding.scopeBlocker && finding.severity !== "blocking") {
1451
- issues.push({
1452
- path: ["findings", index, "scopeBlocker"],
1453
- message: "Only a blocking finding can be a scope blocker."
1454
- });
1455
- }
1456
- }
1457
- if (result.verdict === "failed" && !blocking) {
1458
- issues.push({
1459
- path: ["findings"],
1460
- message: "A failed review requires a blocking finding."
1461
- });
1462
- }
1463
- if (result.verdict === "passed" && blocking) {
1464
- issues.push({
1465
- path: ["findings"],
1466
- message: "A passed review cannot contain blocking findings."
1467
- });
1468
- }
1469
- if (result.terminalDisposition === "observed_unsubmitted" && result.verdict !== "failed") {
1470
- issues.push({
1471
- path: ["terminalDisposition"],
1472
- message: "Observed-but-unsubmitted review work must fail closed."
1473
- });
1474
- }
1475
- return issues;
1476
- }
1477
-
1478
1377
  // src/domain/transitions.ts
1479
1378
  function fail(message) {
1480
1379
  throw new FlowTransitionError(message);
@@ -1530,20 +1429,19 @@ function assertPlan(plan) {
1530
1429
  if (issue)
1531
1430
  fail(issue);
1532
1431
  }
1533
- function assertDeclaredGate(plan) {
1534
- if (plan.gate === undefined) {
1535
- fail("A saved plan must declare `gate`: the exact canonical command that validates the whole repository, which every broad observation then has to run.");
1432
+ function assertDeclaredEvidence(plan) {
1433
+ if (plan.evidence === undefined) {
1434
+ fail('A saved plan must declare `evidence`: one `scope: "gate"` command that validates the whole repository, plus every extra observation this host may be unable to produce. Extra entries may be an empty list when the goal is fully observable here.');
1536
1435
  }
1537
- }
1538
- function assertDeclaredExternalEvidence(plan) {
1539
- if (plan.externalEvidence === undefined) {
1540
- fail("A saved plan must declare `externalEvidence`: every acceptance observation needing an environment this host may not be, each with the exact command whose passing is that observation. Declare an empty list when the goal is fully observable here.");
1436
+ const gates = planEvidence(plan).filter((entry) => entry.scope === "gate");
1437
+ if (gates.length !== 1) {
1438
+ fail('A saved plan must declare exactly one `evidence` entry with `scope: "gate"`: the exact canonical command every broad observation then has to run.');
1541
1439
  }
1542
- if (plan.externalEvidence?.some((entry) => entry.platform === undefined)) {
1543
- fail("Every `externalEvidence` entry must declare `platform`: the operating system that can observe it (`win32`, `darwin`, or `linux`), or `other` when the missing environment is a service, credential, setting, or device rather than an OS. Flow compares an OS against the host the command actually ran on.");
1440
+ if (plan.evidence.some((entry) => entry.platform === undefined)) {
1441
+ fail("Every `evidence` entry must declare `platform`: the operating system that can observe it (`win32`, `darwin`, or `linux`), or `other` when the missing environment is a service, credential, setting, or device rather than an OS.");
1544
1442
  }
1545
- if (plan.externalEvidence?.some((entry) => entry.assertions === undefined)) {
1546
- fail("Every `externalEvidence` entry must declare `assertions`: the test case names whose passing is that observation, so a run cannot discharge it by exiting zero for a case that was skipped. Declare an empty list when the evidence is not a test result — a credential, a device, or a setting has no case names.");
1443
+ if (plan.evidence.some((entry) => entry.assertions === undefined)) {
1444
+ fail("Every `evidence` entry must declare `assertions`: the test case names whose passing is that observation. Declare an empty list when the evidence is not a test result.");
1547
1445
  }
1548
1446
  }
1549
1447
  function assertArtifacts(artifacts) {
@@ -1578,8 +1476,7 @@ function sessionStatus(session) {
1578
1476
  function savePlan(session, input, environment) {
1579
1477
  assertPlan(input.plan);
1580
1478
  if (!session) {
1581
- assertDeclaredGate(input.plan);
1582
- assertDeclaredExternalEvidence(input.plan);
1479
+ assertDeclaredEvidence(input.plan);
1583
1480
  if (input.expectedRevision !== 0) {
1584
1481
  fail("A new Flow session must start from expectedRevision 0.");
1585
1482
  }
@@ -1609,8 +1506,7 @@ function savePlan(session, input, environment) {
1609
1506
  const replay = existingOperation(session, "plan-save", input.operationId, input);
1610
1507
  if (replay)
1611
1508
  return { session, value: null, replayed: true };
1612
- assertDeclaredGate(input.plan);
1613
- assertDeclaredExternalEvidence(input.plan);
1509
+ assertDeclaredEvidence(input.plan);
1614
1510
  assertRevision(session, input.expectedRevision);
1615
1511
  assertMutable(session);
1616
1512
  if (session.approval === "approved")
@@ -1753,9 +1649,9 @@ function startReview(session, input, environment) {
1753
1649
  }
1754
1650
  const kind = isFinalFeatureRun(session, run) ? "final" : "feature";
1755
1651
  if (kind === "final") {
1756
- const unsatisfied = unsatisfiedExternalEvidence(session, input.sourceDigest);
1652
+ const unsatisfied = unsatisfiedExtraEvidence(session, input.sourceDigest);
1757
1653
  if (unsatisfied.length > 0) {
1758
- fail(`Final review requires the plan's declared external evidence to pass for the current workspace content: ${unsatisfied.map((entry) => externalEvidenceRefusal(session, entry, input.sourceDigest)).join(", ")}. A substitute observation cannot discharge it. If the environment is unavailable, ask the user to choose deferred or abandoned closure.`);
1654
+ fail(`Final review requires the plan's declared evidence to pass for the current workspace content: ${unsatisfied.map((entry) => evidenceRefusal(session, entry, input.sourceDigest)).join(", ")}. A substitute observation cannot discharge it. If the environment is unavailable, ask the user to choose deferred or abandoned closure.`);
1759
1655
  }
1760
1656
  }
1761
1657
  const applicable = run.validations.filter((validation) => isValidationEligible(validation, input.sourceDigest) && isValidationFresh(session, run, validation));
@@ -1830,6 +1726,7 @@ function completeFeature(session, input) {
1830
1726
  }
1831
1727
  }
1832
1728
  const findings = assignFindingIds(input.result.findings, findingIdPrefix(run.featureId, assignment.createdRevision));
1729
+ const inspect = featureKind(session.plan?.features.find((feature) => feature.id === run.featureId)) === "inspect";
1833
1730
  const next = commit(session, "feature-complete", input.operationId, input, (draft, revision) => ({
1834
1731
  ...draft,
1835
1732
  runs: draft.runs.map((item) => {
@@ -1837,7 +1734,7 @@ function completeFeature(session, input) {
1837
1734
  return item;
1838
1735
  return {
1839
1736
  ...item,
1840
- state: input.result.verdict === "passed" ? "completed" : "blocked",
1737
+ state: input.result.verdict === "passed" || inspect ? "completed" : "blocked",
1841
1738
  summary: input.summary,
1842
1739
  reviews: item.reviews.map((review) => review.id === assignment.id ? {
1843
1740
  ...review,
@@ -1932,12 +1829,12 @@ function closeSession(session, input) {
1932
1829
  fail("sessionId does not match active state.");
1933
1830
  assertMutable(session);
1934
1831
  if (input.kind === "completed" && sessionStatus(session) !== "completed") {
1935
- fail("A completed close requires every planned feature to pass review.");
1832
+ fail("A completed close requires every planned feature to be complete.");
1936
1833
  }
1937
1834
  if (input.kind === "completed") {
1938
- const unsatisfied = unsatisfiedExternalEvidence(session);
1835
+ const unsatisfied = unsatisfiedExtraEvidence(session);
1939
1836
  if (unsatisfied.length > 0) {
1940
- fail(`A completed close requires the plan's declared external evidence to have passed: ${unsatisfied.map((entry) => externalEvidenceRefusal(session, entry)).join(", ")}. Close deferred or abandoned instead.`);
1837
+ fail(`A completed close requires the plan's declared evidence to have passed: ${unsatisfied.map((entry) => evidenceRefusal(session, entry)).join(", ")}. Close deferred or abandoned instead.`);
1941
1838
  }
1942
1839
  }
1943
1840
  let closure = null;
@@ -1992,8 +1889,14 @@ class ArchiveCollisionError extends Error {
1992
1889
  import { z } from "zod";
1993
1890
 
1994
1891
  // src/domain/session-invariants.ts
1995
- function featurePassedBefore(session, featureId, revision) {
1996
- return session.runs.some((run) => run.featureId === featureId && run.reviews.some((review) => review.result?.verdict === "passed" && review.result.recordedRevision < revision));
1892
+ function featureSettledBefore(session, featureId, revision) {
1893
+ const inspect = featureKind(session.plan?.features.find((feature) => feature.id === featureId)) === "inspect";
1894
+ return session.runs.some((run) => run.featureId === featureId && run.reviews.some((review) => {
1895
+ const result = review.result;
1896
+ if (!result || result.recordedRevision >= revision)
1897
+ return false;
1898
+ return result.verdict === "passed" || inspect;
1899
+ }));
1997
1900
  }
1998
1901
  function sessionInvariantIssues(session) {
1999
1902
  const issues = [];
@@ -2110,7 +2013,7 @@ function sessionInvariantIssues(session) {
2110
2013
  if (referenced.some((validation) => validation.recordedRevision >= review.createdRevision)) {
2111
2014
  issues.push(`Review '${review.id}' references later validation.`);
2112
2015
  }
2113
- const expectedKind = session.plan.features.every((feature) => feature.id === run.featureId || featurePassedBefore(session, feature.id, review.createdRevision)) ? "final" : "feature";
2016
+ const expectedKind = session.plan.features.every((feature) => feature.id === run.featureId || featureSettledBefore(session, feature.id, review.createdRevision)) ? "final" : "feature";
2114
2017
  if (review.kind !== expectedKind) {
2115
2018
  issues.push(`Review '${review.id}' has the wrong derived kind.`);
2116
2019
  }
@@ -2128,7 +2031,10 @@ function sessionInvariantIssues(session) {
2128
2031
  issues.push(`Active run '${run.id}' contains a recorded outcome.`);
2129
2032
  }
2130
2033
  if (run.state === "completed" && last?.result?.verdict !== "passed") {
2131
- issues.push(`Completed run '${run.id}' lacks a passing review.`);
2034
+ const inspect = featureKind(session.plan?.features.find((feature) => feature.id === run.featureId)) === "inspect";
2035
+ if (!(inspect && last?.result?.verdict === "failed")) {
2036
+ issues.push(`Completed run '${run.id}' lacks a passing review.`);
2037
+ }
2132
2038
  }
2133
2039
  if (run.state === "blocked" && last?.result?.verdict !== "failed") {
2134
2040
  issues.push(`Blocked run '${run.id}' lacks a failed review.`);
@@ -2167,14 +2073,16 @@ var PlanFeatureSchema = z.object({
2167
2073
  summary: boundedText("Feature summary"),
2168
2074
  targets: z.array(boundedText("Feature target")).max(MAX_PLAN_FEATURES).default([]),
2169
2075
  validation: z.array(boundedText("Feature validation")).max(MAX_PLAN_FEATURES).default([]),
2170
- dependsOn: z.array(FeatureIdSchema).max(MAX_PLAN_FEATURES).default([])
2076
+ dependsOn: z.array(FeatureIdSchema).max(MAX_PLAN_FEATURES).default([]),
2077
+ kind: z.enum(["change", "inspect"]).optional()
2171
2078
  }).strict();
2172
- var ExternalEvidenceSchema = z.object({
2173
- requirement: boundedText("External evidence requirement"),
2174
- environment: boundedText("External evidence environment"),
2175
- command: boundedText("External evidence command"),
2079
+ var EvidenceEntrySchema = z.object({
2080
+ requirement: boundedText("Evidence requirement"),
2081
+ environment: boundedText("Evidence environment"),
2082
+ command: boundedText("Evidence command"),
2083
+ scope: z.enum(["gate", "extra"]),
2176
2084
  platform: z.enum(EVIDENCE_PLATFORMS).optional(),
2177
- assertions: z.array(boundedText("External evidence assertion")).max(MAX_DECLARED_ASSERTIONS).optional()
2085
+ assertions: z.array(boundedText("Evidence assertion")).max(MAX_DECLARED_ASSERTIONS).optional()
2178
2086
  }).strict();
2179
2087
  var ObservedAssertionSchema = z.object({
2180
2088
  name: boundedText("Observed assertion name"),
@@ -2186,8 +2094,7 @@ var PlanSchema = z.object({
2186
2094
  requirements: z.array(boundedText("Plan requirement")).max(MAX_PLAN_FEATURES).default([]),
2187
2095
  decisions: z.array(boundedText("Plan decision")).max(MAX_PLAN_FEATURES).default([]),
2188
2096
  features: z.array(PlanFeatureSchema).min(1).max(MAX_PLAN_FEATURES),
2189
- gate: boundedText("Plan gate").optional(),
2190
- externalEvidence: z.array(ExternalEvidenceSchema).max(MAX_PLAN_FEATURES).optional()
2097
+ evidence: z.array(EvidenceEntrySchema).max(MAX_PLAN_FEATURES).optional()
2191
2098
  }).strict().superRefine((plan, context) => {
2192
2099
  if (encoder.encode(JSON.stringify(plan)).byteLength > MAX_PLAN_BYTES) {
2193
2100
  context.addIssue({
@@ -2377,6 +2284,69 @@ var StatusInputSchema = z.object({
2377
2284
  ])
2378
2285
  }).strict();
2379
2286
 
2287
+ // src/application/findings-digest.ts
2288
+ function plannedFeatureIds(session) {
2289
+ if (session.plan)
2290
+ return session.plan.features.map((feature) => feature.id);
2291
+ const ids = [];
2292
+ for (const run of session.runs) {
2293
+ if (ids.includes(run.featureId))
2294
+ continue;
2295
+ ids.push(run.featureId);
2296
+ }
2297
+ return ids;
2298
+ }
2299
+ function findingsDigest(session) {
2300
+ const rows = [];
2301
+ const indexById = new Map;
2302
+ for (const featureId of plannedFeatureIds(session)) {
2303
+ for (const run of session.runs) {
2304
+ if (run.featureId !== featureId)
2305
+ continue;
2306
+ for (const review of run.reviews) {
2307
+ const result = review.result;
2308
+ if (!result)
2309
+ continue;
2310
+ for (const finding of result.findings) {
2311
+ if (!finding.findingId)
2312
+ continue;
2313
+ const row = {
2314
+ featureId,
2315
+ findingId: finding.findingId,
2316
+ severity: finding.severity,
2317
+ summary: finding.summary,
2318
+ attempt: run.attempt,
2319
+ verdict: result.verdict,
2320
+ live: false,
2321
+ ...finding.evidence === undefined ? {} : { evidence: finding.evidence }
2322
+ };
2323
+ const existing = indexById.get(finding.findingId);
2324
+ if (existing === undefined) {
2325
+ indexById.set(finding.findingId, rows.length);
2326
+ rows.push(row);
2327
+ } else {
2328
+ rows[existing] = row;
2329
+ }
2330
+ }
2331
+ }
2332
+ }
2333
+ }
2334
+ return rows.map((row) => ({
2335
+ ...row,
2336
+ live: liveFindingIds(session, row.featureId).includes(row.findingId)
2337
+ }));
2338
+ }
2339
+ function digestReportLines(digest) {
2340
+ if (digest.length === 0)
2341
+ return ["Findings digest: none"];
2342
+ const line = (row, kind) => `- ${kind} ${row.featureId} ${row.findingId} ${row.severity}: ${row.summary}`;
2343
+ return [
2344
+ "Findings digest:",
2345
+ ...digest.filter((row) => row.live).map((row) => line(row, "live")),
2346
+ ...digest.filter((row) => !row.live).map((row) => line(row, "historical"))
2347
+ ];
2348
+ }
2349
+
2380
2350
  // src/application/delivery.ts
2381
2351
  var LIMITATIONS = [
2382
2352
  "Artifact paths and the canonical gate are caller declarations; Flow validates binding, not completeness or fitness.",
@@ -2413,23 +2383,23 @@ function assuranceProjection(session) {
2413
2383
  check("recorded-completion", "Recorded completion", "ts-enforced", structural, `${completed}/${features.length} features and ${passing}/${features.length} independent reviews pass, including a final review with no terminal blocker.`),
2414
2384
  check("accepted-validation", "Accepted validation", "host-attested", runs.length === features.length && runs.every((run) => accepted.some((observation) => observation.runId === run.id)), `${runs.filter((run) => accepted.some((item) => item.runId === run.id)).length}/${features.length} terminal runs have eligible host evidence accepted by review.`)
2415
2385
  ];
2416
- const gate = session.plan?.gate;
2386
+ const gate = planGate(session.plan);
2417
2387
  checks.push(gate === undefined ? {
2418
2388
  id: "canonical-gate",
2419
2389
  label: "Canonical gate",
2420
2390
  tier: "caller-declared",
2421
2391
  status: "not-applicable",
2422
- explanation: "This legacy plan declared no canonical gate."
2392
+ explanation: "This plan declared no canonical gate."
2423
2393
  } : check("canonical-gate", "Canonical gate", "host-attested", accepted.some((observation) => observation.command === gate && observation.scope === "broad"), `${JSON.stringify(gate)} must have eligible broad evidence accepted by review.`));
2424
- const declared = session.plan?.externalEvidence;
2425
- const missing = unsatisfiedExternalEvidence(session).length;
2394
+ const declared = session.plan?.evidence;
2395
+ const missing = unsatisfiedEvidence(session).length;
2426
2396
  checks.push(declared === undefined ? {
2427
- id: "external-evidence",
2428
- label: "Declared external evidence",
2397
+ id: "declared-evidence",
2398
+ label: "Declared evidence",
2429
2399
  tier: "caller-declared",
2430
2400
  status: "not-applicable",
2431
- explanation: "This legacy plan declared no external-evidence obligations."
2432
- } : check("external-evidence", "Declared external evidence", declared.length === 0 ? "caller-declared" : "host-attested", missing === 0, `${declared.length - missing}/${declared.length} declared obligations have eligible evidence on their declared host with named cases passing.`));
2401
+ explanation: "This plan declared no evidence obligations."
2402
+ } : check("declared-evidence", "Declared evidence", declared.length === 0 ? "caller-declared" : "host-attested", missing === 0, `${declared.length - missing}/${declared.length} declared obligations have eligible evidence on their declared host with named cases passing.`));
2433
2403
  return {
2434
2404
  conclusion: !complete ? "completion-not-claimed" : checks.some((item) => item.status === "unsatisfied") ? "completion-unsupported" : "completion-supported",
2435
2405
  checks,
@@ -2457,6 +2427,7 @@ function formatReport(delivery) {
2457
2427
  `Progress: ${delivery.progress.completed} of ${delivery.progress.total} features complete`,
2458
2428
  "Features:",
2459
2429
  ...lines,
2430
+ ...digestReportLines(delivery.findingsDigest),
2460
2431
  `Assurance: ${delivery.assurance.conclusion.replaceAll("-", " ")}`,
2461
2432
  "Assurance checks:",
2462
2433
  ...delivery.assurance.checks.map((item) => `- ${item.status} [${TIER_LABELS[item.tier]}] ${item.label}: ${item.explanation}`),
@@ -2478,6 +2449,7 @@ function deliveryProjection(session) {
2478
2449
  const latest = grouped.flatMap(({ runs }) => runs.slice(-1));
2479
2450
  const latestArtifacts = new Set(latest.flatMap((run) => run.artifactsChanged.map((item) => item.path)));
2480
2451
  const allArtifacts = new Set(session.runs.flatMap((run) => run.artifactsChanged.map((item) => item.path)));
2452
+ const digest = findingsDigest(session);
2481
2453
  const delivery = {
2482
2454
  goal: session.goal,
2483
2455
  closure: { kind: session.closure.kind, summary: session.closure.summary },
@@ -2493,17 +2465,15 @@ function deliveryProjection(session) {
2493
2465
  attempts: runs.length,
2494
2466
  latestState: run?.state ?? "not-started",
2495
2467
  outcomeSummary: run?.summary ?? null,
2496
- terminalFindings: run?.reviews.at(-1)?.result?.findings.map(({ severity, summary }) => ({
2497
- severity,
2498
- summary
2499
- })) ?? []
2468
+ terminalFindings: digest.filter((row) => row.featureId === feature.id && row.live).map(({ severity, summary }) => ({ severity, summary }))
2500
2469
  };
2501
2470
  }),
2502
2471
  reportedArtifacts: {
2503
2472
  latestAttempts: [...latestArtifacts].sort(),
2504
2473
  supersededAttemptsOnly: [...allArtifacts].filter((path) => !latestArtifacts.has(path)).sort()
2505
2474
  },
2506
- assurance: assuranceProjection(session)
2475
+ assurance: assuranceProjection(session),
2476
+ findingsDigest: digest
2507
2477
  };
2508
2478
  return { ...delivery, report: formatReport(delivery) };
2509
2479
  }
@@ -2579,7 +2549,8 @@ function compactProjection(session, pendingReviewSourceStale = false) {
2579
2549
  blockedFeature,
2580
2550
  progress: featureProgress(session),
2581
2551
  nextAction: nextAction(session, pendingReviewSourceStale, blockedFeature),
2582
- archiveRetry: retryRequest ? { request: retryRequest } : null
2552
+ archiveRetry: retryRequest ? { request: retryRequest } : null,
2553
+ findingsDigest: findingsDigest(session)
2583
2554
  };
2584
2555
  }
2585
2556
  function archivedProjection(session) {
@@ -2637,9 +2608,8 @@ function reviewerProjection(session, assignmentId) {
2637
2608
  validation: [...candidate.validation],
2638
2609
  dependsOn: [...candidate.dependsOn]
2639
2610
  })),
2640
- ...plan.gate === undefined ? {} : { gate: plan.gate },
2641
- ...plan.externalEvidence === undefined ? {} : {
2642
- externalEvidence: plan.externalEvidence.map((entry) => ({
2611
+ ...plan.evidence === undefined ? {} : {
2612
+ evidence: plan.evidence.map((entry) => ({
2643
2613
  ...entry
2644
2614
  }))
2645
2615
  }
@@ -2668,7 +2638,8 @@ function idleProjection(view) {
2668
2638
  view,
2669
2639
  status: "idle",
2670
2640
  revision: 0,
2671
- nextAction: "flow_plan_save"
2641
+ nextAction: "flow_plan_save",
2642
+ findingsDigest: []
2672
2643
  };
2673
2644
  }
2674
2645
  function project(session, request, pendingReviewSourceStale = false) {
@@ -3919,6 +3890,10 @@ var CONTINUATION_ROUTE = [
3919
3890
  "for a fresh close use compact session id/revision plus a fresh operation id,",
3920
3891
  "and replay archiveRetry exactly from its projected request."
3921
3892
  ].join(" ");
3893
+ var HANDBACK_ROUTE = [
3894
+ "Call flow_status with the compact view first.",
3895
+ "Print findingsDigest as the user-facing list. Do not invent ids."
3896
+ ].join(" ");
3922
3897
  function inspectMessage(parts) {
3923
3898
  let token = null;
3924
3899
  let text = "";
@@ -3944,6 +3919,9 @@ function isCheckpoint(projection) {
3944
3919
  function isPendingReviewer(projection) {
3945
3920
  return projection.status === "running" && projection.nextAction === "dispatch-flow-reviewer";
3946
3921
  }
3922
+ function isHandback(projection) {
3923
+ return projection.status === "blocked" || projection.nextAction === "flow_feature_reset" || projection.nextAction === "dispatch-flow-reviewer";
3924
+ }
3947
3925
 
3948
3926
  class AutoDriveCoordinator {
3949
3927
  #lease = null;
@@ -3993,6 +3971,31 @@ class AutoDriveCoordinator {
3993
3971
  lease.lastPromptedRevision = null;
3994
3972
  this.#setTiming("waiting-for-user");
3995
3973
  }
3974
+ async#promptHandback(lease, projection) {
3975
+ if (!isHandback(projection))
3976
+ return;
3977
+ if (lease.handbackPromptedRevision === projection.revision)
3978
+ return;
3979
+ if (!lease.delivery)
3980
+ return;
3981
+ lease.handbackPromptedRevision = projection.revision;
3982
+ lease.messageId = null;
3983
+ this.#setTiming("active");
3984
+ lease.inFlight = "prompt";
3985
+ try {
3986
+ const handback = [
3987
+ `Flow is handing control back at compact revision ${projection.revision}.`,
3988
+ HANDBACK_ROUTE,
3989
+ `Then follow ${projection.nextAction} or stop at await-user-direction.`,
3990
+ "Do not expand the approved goal."
3991
+ ].join(" ");
3992
+ await this.#options.prompt(lease.hostSessionId, `${handback}
3993
+
3994
+ ${FLOW_MANAGER_KERNEL}`, lease.delivery, { [FLOW_AUTO_METADATA_KEY]: lease.token });
3995
+ } catch (error) {
3996
+ this.#stop(lease, `Flow auto prompt failed: ${String(error)}`);
3997
+ }
3998
+ }
3996
3999
  async#read(lease) {
3997
4000
  try {
3998
4001
  return await this.#options.readProjection();
@@ -4015,6 +4018,7 @@ class AutoDriveCoordinator {
4015
4018
  baseline: null,
4016
4019
  delivery: null,
4017
4020
  lastPromptedRevision: null,
4021
+ handbackPromptedRevision: null,
4018
4022
  checkpoint: null,
4019
4023
  pendingReply: false,
4020
4024
  inFlight: null,
@@ -4211,8 +4215,12 @@ ${FLOW_MANAGER_KERNEL}`;
4211
4215
  const mutationAdvanced = advance !== undefined && projection.revision === advance && isMechanical(projection);
4212
4216
  if (lease.pendingReply) {
4213
4217
  lease.pendingReply = false;
4214
- if (boundary && (!checkpoint || projection.revision > checkpoint.revision))
4218
+ if (boundary && (!checkpoint || projection.revision > checkpoint.revision)) {
4219
+ await this.#promptHandback(lease, projection);
4220
+ if (this.#lease !== lease)
4221
+ return;
4215
4222
  return void this.#waitAt(lease, projection.revision);
4223
+ }
4216
4224
  if (!checkpoint || !boundary && !mutationAdvanced)
4217
4225
  return void this.deactivate(hostSessionId);
4218
4226
  checkpoint.answered = true;
@@ -4222,15 +4230,27 @@ ${FLOW_MANAGER_KERNEL}`;
4222
4230
  if (boundary) {
4223
4231
  if (checkpoint && projection.revision < checkpoint.revision)
4224
4232
  return void this.deactivate(hostSessionId);
4233
+ await this.#promptHandback(lease, projection);
4234
+ if (this.#lease !== lease)
4235
+ return;
4225
4236
  return void this.#waitAt(lease, projection.revision);
4226
4237
  }
4238
+ if (!isMechanical(projection)) {
4239
+ const already = lease.handbackPromptedRevision === projection.revision;
4240
+ await this.#promptHandback(lease, projection);
4241
+ if (this.#lease !== lease)
4242
+ return;
4243
+ if (!already && lease.handbackPromptedRevision === projection.revision) {
4244
+ this.#setTiming("paused");
4245
+ return;
4246
+ }
4247
+ return void this.deactivate(hostSessionId);
4248
+ }
4227
4249
  if (checkpoint) {
4228
4250
  if (projection.revision <= checkpoint.revision || !mutationAdvanced)
4229
4251
  return void this.deactivate(hostSessionId);
4230
4252
  lease.checkpoint = null;
4231
4253
  }
4232
- if (!isMechanical(projection))
4233
- return void this.deactivate(hostSessionId);
4234
4254
  if (lease.lastPromptedRevision === projection.revision) {
4235
4255
  this.#setTiming("paused");
4236
4256
  return this.#warn(`Flow auto-drive paused after revision ${projection.revision} made no lifecycle progress.`);
@@ -4498,7 +4518,8 @@ var planFeature = host.object({
4498
4518
  summary: text,
4499
4519
  targets: host.array(text).max(MAX_PLAN_FEATURES).default([]),
4500
4520
  validation: host.array(text).max(MAX_PLAN_FEATURES).default([]),
4501
- dependsOn: host.array(featureId).max(MAX_PLAN_FEATURES).default([])
4521
+ dependsOn: host.array(featureId).max(MAX_PLAN_FEATURES).default([]),
4522
+ kind: host.enum(["change", "inspect"]).optional()
4502
4523
  }).strict();
4503
4524
  var plan = host.object({
4504
4525
  summary: text,
@@ -4506,11 +4527,11 @@ var plan = host.object({
4506
4527
  requirements: host.array(text).max(MAX_PLAN_FEATURES).default([]),
4507
4528
  decisions: host.array(text).max(MAX_PLAN_FEATURES).default([]),
4508
4529
  features: host.array(planFeature).min(1).max(MAX_PLAN_FEATURES),
4509
- gate: text.optional(),
4510
- externalEvidence: host.array(host.object({
4530
+ evidence: host.array(host.object({
4511
4531
  requirement: text,
4512
4532
  environment: text,
4513
4533
  command: text,
4534
+ scope: host.enum(["gate", "extra"]),
4514
4535
  platform: host.enum(EVIDENCE_PLATFORMS).optional(),
4515
4536
  assertions: host.array(text).max(MAX_DECLARED_ASSERTIONS).optional()
4516
4537
  }).strict()).max(MAX_PLAN_FEATURES).optional()
@@ -4617,12 +4638,14 @@ function toolError(error) {
4617
4638
  }
4618
4639
  });
4619
4640
  }
4620
- function withAutoContext(response, options) {
4641
+ function withAutoContext(response, options, view) {
4621
4642
  let workflowData = response.workflowData;
4622
4643
  try {
4623
- const timing = options.autoTimingSnapshot?.();
4624
- if (timing)
4625
- workflowData = { ...workflowData, autoTiming: timing };
4644
+ if (view === "detail") {
4645
+ const timing = options.autoTimingSnapshot?.();
4646
+ if (timing)
4647
+ workflowData = { ...workflowData, autoTiming: timing };
4648
+ }
4626
4649
  } catch {}
4627
4650
  try {
4628
4651
  const support = options.autoContinuationSupport?.();
@@ -4669,7 +4692,7 @@ function createTools(_ctx, options) {
4669
4692
  flow_status: tool({
4670
4693
  description: "Read compact, execution, detail, or reviewer Flow state.",
4671
4694
  args: StatusArgs,
4672
- execute: (args, context) => execute(context, async (workspace) => withAutoContext(await flowStatus(workspace, args), options))
4695
+ execute: (args, context) => execute(context, async (workspace) => withAutoContext(await flowStatus(workspace, args), options, args.request.view))
4673
4696
  }),
4674
4697
  flow_plan_save: tool({
4675
4698
  description: "Create or replace the active draft plan.",
@@ -5139,4 +5162,4 @@ export {
5139
5162
  plugin_default as default
5140
5163
  };
5141
5164
 
5142
- //# debugId=719465FC0656BBA564756E2164756E21
5165
+ //# debugId=4CB19287BD614D3B64756E2164756E21