opencode-plugin-flow 7.3.1 → 8.0.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
@@ -187,19 +187,16 @@ Save one plan with:
187
187
  - \`overview\`: the implementation approach and important boundaries.
188
188
  - \`requirements\`: acceptance criteria, constraints, and non-goals.
189
189
  - \`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.
190
+ - \`evidence\`: one \`scope: "gate"\` entry for the canonical whole-repository
191
+ command, plus \`scope: "extra"\` entries for observations this host may lack.
192
+ Each entry names \`requirement\`, \`environment\`, \`command\`, \`platform\`
193
+ (\`win32\`, \`darwin\`, \`linux\`, or \`other\`), and \`assertions\` (empty when the
194
+ evidence is not a test result). Broad observations run the gate command
195
+ byte-for-byte. Extra entries may be omitted when the goal is fully observable
196
+ here. Final review and completed closure stay refused until every extra
197
+ entry is satisfied on its declared platform with named cases passing. The
198
+ gate is the command every broad observation must run; a failed or
199
+ claimed-broad gate still vetoes review.
203
200
  - \`features\`: ordered outcome slices, each with a stable \`id\`, \`title\`,
204
201
  \`summary\`, bounded \`targets\`, concrete \`validation\`, and \`dependsOn\` ids.
205
202
 
@@ -406,14 +403,14 @@ description: Implement, validate, independently review, and record one approved
406
403
 
407
404
  Work on exactly one approved feature.
408
405
 
409
- ## Start and scope
406
+ ## Start
410
407
 
411
408
  1. Call \`flow_status { request: { view: "compact" } }\` first. Treat
412
- \`nextAction\` as the durable default workflow direction, not as permission.
409
+ \`nextAction\` as the durable default, not as permission.
413
410
  2. If the top-level response status is \`error\`, report its exact summary and
414
411
  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\`.
412
+ below. This read made no lifecycle, Git, or release mutation. Stop. Do not
413
+ route its \`nextAction\`.
417
414
  3. If compact status contains \`archiveRetry\`, call \`flow_session_close\` once
418
415
  with the projected request byte-for-byte. Report delivery under the contract
419
416
  below. Refresh only if publication is unconfirmed. Stop after this cleanup
@@ -421,9 +418,9 @@ Work on exactly one approved feature.
421
418
  4. When the projection contains an active goal, align it with the current
422
419
  \`/flow-run\` request before another manager lifecycle mutation. Continue only
423
420
  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.
421
+ close completed work. Unless the next step applies, new or expanded work
422
+ makes no mutation: report that it has not started and offer continue, defer,
423
+ or abandon.
427
424
  5. If the aligned request explicitly chooses deferred or abandoned closure for
428
425
  a non-completed session, call \`flow_session_close\` with compact session id and
429
426
  revision, fresh operation id, that kind, and optional summary. Report delivery
@@ -432,227 +429,113 @@ Work on exactly one approved feature.
432
429
  explain that \`/flow-run\` requires an approved feature, and stop without
433
430
  mutation.
434
431
 
435
- Route every compact projection in this order:
432
+ Delivery handoff: report \`workflowData.delivery.report\` verbatim. Map IDs only
433
+ from delivery \`outcomeSummary\`/\`terminalFindings\`. Requirements are \`verified\`,
434
+ \`incomplete\`, or explicitly \`deferred\`, and \`abandoned\` remains the kind. If
435
+ delivery is absent, report exact recovery and no map. On revision conflict,
436
+ refresh compact; retry only for the same session and goal while status still
437
+ permits the selected closure kind; never close a replacement.
438
+
439
+ ## Route
440
+
441
+ Follow compact \`nextAction\` in this order:
436
442
 
437
443
  - \`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.
444
+ id/revision, fresh operation id, and \`kind: "completed"\`. Report delivery,
445
+ follow one exact \`archiveRetry\` if needed, and stop.
441
446
  - \`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.
447
+ \`flow_status { request: { view: "detail" } }\` exactly once, then apply
448
+ **Blocked review**.
454
449
  - 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.
450
+ the same feature as \`nextFeatureId\` when continuing it. Never redispatch that
451
+ assignment.
457
452
  - \`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.
453
+ its exact summary and recovery when present and stop. Otherwise route that
454
+ refreshed projection. Dispatch under **Review** only if \`nextAction\` is still
455
+ \`dispatch-flow-reviewer\`. If it is now running \`flow_feature_reset\`, follow
456
+ the source-stale reset route.
464
457
  - \`flow_run_start\`: start the ready feature, refresh compact status, and read
465
458
  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.
459
+ - \`flow_validation_start\`: read execution status and resume from the current
460
+ worktree.
461
+ - \`flow_review_start\`: read execution status and continue at **Review**.
462
+ - Any other action: report it and stop.
463
+
464
+ Use execution status for active scope and revision. Stay inside the feature.
465
+ Out-of-plan work stops. Use \`flow_feature_reset\` for a wrong design; do not
466
+ layer retries.
514
467
 
515
468
  ## Implement
516
469
 
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.
470
+ Make the smallest change that satisfies the approved outcome. Create no
471
+ lifecycle or handoff sidecars. Do not stage, commit, push, publish, or mutate
472
+ releases unless asked separately.
521
473
 
522
- Do not stage, commit, push, publish, or mutate releases unless asked separately.
474
+ Work serially. After a feature run is active, dispatch \`flow-worker\` only for
475
+ two or three genuinely independent slices with clear benefit. Workers call no
476
+ Flow tools, spawn no children, and run no Bash. Integrate and inspect the
477
+ combined diff before validation.
523
478
 
524
- ## Bounded worker waves
479
+ ## Validate
525
480
 
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.
481
+ Arm each evidence Bash command with \`flow_validation_start\` (current revision,
482
+ feature id, exact command, \`scope\`) immediately before running it byte-for-byte.
483
+ Flow records the host observation; copy no host-observed fields.
534
484
 
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.
485
+ \`scope: "broad"\` runs the plan's gate evidence command and nothing else.
538
486
 
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.
487
+ A failed, incomplete, or source-drifted observation of a plan-listed command or
488
+ of the declared gate command blocks review until that same command passes for
489
+ current source.
544
490
 
545
- ## Validate
491
+ Every host-observed validation advances revision. The \`[flow-validation]\`
492
+ marker reports \`passed\`, \`recordedRevision\`, and declared \`assertions\`. Use
493
+ \`recordedRevision\` for the next \`flow_validation_start\`, or for
494
+ \`flow_review_start\` only when \`passed: true\`. If the marker is absent, refresh
495
+ compact status before mutating.
546
496
 
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
497
+ For the final feature, run the plan's gate command at broad scope after the
498
+ last relevant edit.
499
+
500
+ An evidence command that cannot pass withholds completed closure. Reach the
501
+ passing command, or ask the user to choose deferred or abandoned closure.
502
+
503
+ ## Review
591
504
 
592
505
  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.
506
+ operation id, current revision, feature id, \`artifactsChanged\`, and a bounded
507
+ packet. Dispatch only reserved \`flow-reviewer\`. Never review or submit its
508
+ verdict in manager context.
611
509
 
612
510
  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.
511
+ summary/recovery and stop without further mutation. If status remains running,
512
+ apply the \`dispatch-flow-reviewer\` or running \`flow_feature_reset\` route. If
513
+ status is blocked, apply **Blocked review**. A recorded pass completes the
514
+ feature.
620
515
 
621
516
  ### Blocked review
622
517
 
623
518
  Follow \`nextAction\` with the one detail projection. The runtime already weighs
624
519
  \`failedReviewCount\` and \`blockedFeature.scopeBlocker\`.
625
520
 
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
521
+ - Ready \`await-user-direction\` has no blocked run left to reset. Identify the
522
+ planned feature whose latest relevant reviewed outcome remains failed and
523
+ checkpoint unless the current aligned request explicitly authorizes its
524
+ retry. When authorized, call \`flow_run_start\` with that exact \`featureId\`.
525
+ Never call \`flow_feature_reset\` from ready status.
526
+ - For blocked \`await-user-direction\`, checkpoint. Do not reset.
527
+ - For blocked \`flow_feature_reset\`, one automatic reset is allowed under
528
+ existing implementation authority. Pass the blocked \`featureId\` as
529
+ \`nextFeatureId\` so reset and run start are atomic. Fix only its blocking
630
530
  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
531
  - 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
655
- stops. Under \`/flow-auto\`, return to its lifecycle loop.
532
+ explicitly authorizes one additional attempt.
533
+ - If explicit direction selects another planned, dependency-independent
534
+ feature, pass that exact \`featureId\` as \`nextFeatureId\` on
535
+ \`flow_feature_reset\`.
536
+
537
+ Direct \`/flow-run\` reports this one feature's cumulative outcome and
538
+ \`nextAction\`, then stops. Under \`/flow-auto\`, return to its lifecycle loop.
656
539
  `;
657
540
 
658
541
  // src/guidance/ids.ts
@@ -725,9 +608,8 @@ var FLOW_WORKER_PROMPT = [
725
608
  "- Never edit .flow or .git metadata paths; the host denies those paths.",
726
609
  "- A read-only evidence slice must not edit files.",
727
610
  [
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."
611
+ "- If the assignment includes a risk checklist or matrix, apply it before coding.",
612
+ "If required work would escape the assigned paths, stop and report the gap."
731
613
  ].join(" "),
732
614
  [
733
615
  "- An implementation slice may edit only the exact, non-overlapping write paths explicitly assigned by the manager.",
@@ -738,13 +620,7 @@ var FLOW_WORKER_PROMPT = [
738
620
  "- The manager owns integration, focused checks, and authoritative combined validation",
739
621
  "after all workers have stopped."
740
622
  ].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(" "),
623
+ "- Preserve every named finding, requirement, or prior review ID in your handoff.",
748
624
  "## Handoff",
749
625
  [
750
626
  "Return exactly one concise handoff with `Status` (success, partial, or blocked),",
@@ -1089,6 +965,51 @@ function isFeatureId(value) {
1089
965
  return FEATURE_ID_PATTERN.test(value);
1090
966
  }
1091
967
 
968
+ // src/domain/session.ts
969
+ function planEvidence(plan) {
970
+ return plan?.evidence ?? [];
971
+ }
972
+ function planGate(plan) {
973
+ return planEvidence(plan).find((entry) => entry.scope === "gate")?.command;
974
+ }
975
+ function reviewResultSemanticIssues(result) {
976
+ const issues = [];
977
+ const blocking = result.findings.some((finding) => finding.severity === "blocking");
978
+ for (const [index, finding] of result.findings.entries()) {
979
+ if (finding.severity === "blocking" && !finding.evidence?.trim()) {
980
+ issues.push({
981
+ path: ["findings", index, "evidence"],
982
+ message: "A blocking finding requires concrete evidence."
983
+ });
984
+ }
985
+ if (finding.scopeBlocker && finding.severity !== "blocking") {
986
+ issues.push({
987
+ path: ["findings", index, "scopeBlocker"],
988
+ message: "Only a blocking finding can be a scope blocker."
989
+ });
990
+ }
991
+ }
992
+ if (result.verdict === "failed" && !blocking) {
993
+ issues.push({
994
+ path: ["findings"],
995
+ message: "A failed review requires a blocking finding."
996
+ });
997
+ }
998
+ if (result.verdict === "passed" && blocking) {
999
+ issues.push({
1000
+ path: ["findings"],
1001
+ message: "A passed review cannot contain blocking findings."
1002
+ });
1003
+ }
1004
+ if (result.terminalDisposition === "observed_unsubmitted" && result.verdict !== "failed") {
1005
+ issues.push({
1006
+ path: ["terminalDisposition"],
1007
+ message: "Observed-but-unsubmitted review work must fail closed."
1008
+ });
1009
+ }
1010
+ return issues;
1011
+ }
1012
+
1092
1013
  // src/domain/test-results.ts
1093
1014
  var TESTCASE = /<testcase\b([^>]*?)(\/>|>([\s\S]*?)<\/testcase\s*>)/g;
1094
1015
  var ATTRIBUTE = /([\w:.-]+)\s*=\s*"([^"]*)"/g;
@@ -1202,7 +1123,7 @@ function narrowingArguments(command) {
1202
1123
  }
1203
1124
  function declaredAssertions(session, command) {
1204
1125
  return [
1205
- ...new Set((session.plan?.externalEvidence ?? []).filter((entry) => entry.command === command).flatMap((entry) => entry.assertions ?? []))
1126
+ ...new Set(planEvidence(session.plan).filter((entry) => entry.command === command).flatMap((entry) => entry.assertions ?? []))
1206
1127
  ];
1207
1128
  }
1208
1129
  function sameAssertions(left, right) {
@@ -1241,7 +1162,7 @@ function recordValidation(session, input) {
1241
1162
  if (narrowing.length > 0) {
1242
1163
  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
1164
  }
1244
- const gate = session.plan?.gate;
1165
+ const gate = planGate(session.plan);
1245
1166
  if (gate !== undefined && input.command !== gate) {
1246
1167
  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
1168
  }
@@ -1282,7 +1203,7 @@ function isObservedOnDeclaredPlatform(entry, observation) {
1282
1203
  return true;
1283
1204
  return observation.hostPlatform === entry.platform;
1284
1205
  }
1285
- function externalEvidenceRefusal(session, entry, sourceDigest) {
1206
+ function evidenceRefusal(session, entry, sourceDigest) {
1286
1207
  const eligible = session.runs.flatMap((run) => run.validations).filter((observation) => observation.command === entry.command && isValidationEligible(observation, sourceDigest));
1287
1208
  const wrongHosts = [
1288
1209
  ...new Set(eligible.filter((observation) => !isObservedOnDeclaredPlatform(entry, observation)).map((observation) => observation.hostPlatform ?? "an unrecorded host"))
@@ -1292,18 +1213,21 @@ function externalEvidenceRefusal(session, entry, sourceDigest) {
1292
1213
  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
1214
  return `${JSON.stringify(entry.command)} (${detail}, for ${entry.requirement})`;
1294
1215
  }
1295
- function unsatisfiedExternalEvidence(session, sourceDigest) {
1296
- const declared = session.plan?.externalEvidence ?? [];
1216
+ function unsatisfiedEvidence(session, sourceDigest) {
1217
+ const declared = planEvidence(session.plan);
1297
1218
  if (declared.length === 0)
1298
1219
  return [];
1299
1220
  const observed = session.runs.flatMap((run) => run.validations);
1300
1221
  return declared.filter((entry) => !observed.some((observation) => observation.command === entry.command && isObservedOnDeclaredPlatform(entry, observation) && assertionsSatisfied(entry.assertions ?? [], observation.observedAssertions) && isValidationEligible(observation, sourceDigest)));
1301
1222
  }
1223
+ function unsatisfiedExtraEvidence(session, sourceDigest) {
1224
+ return unsatisfiedEvidence(session, sourceDigest).filter((entry) => entry.scope === "extra");
1225
+ }
1302
1226
  function isValidationFresh(session, run, observation) {
1303
1227
  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
1228
  }
1305
1229
  function unresolvedVetoedCommands(session, run, sourceDigest) {
1306
- const gate = session.plan?.gate;
1230
+ const gate = planGate(session.plan);
1307
1231
  const planned = session.approval === "approved" ? [
1308
1232
  ...session.plan?.features.find((candidate) => candidate.id === run.featureId)?.validation ?? [],
1309
1233
  ...gate === undefined ? [] : [gate]
@@ -1321,8 +1245,9 @@ function gateIssue(gate) {
1321
1245
  return narrowing.length === 0 ? null : `The plan's canonical gate cannot select which tests it runs (${narrowing.join(", ")}).`;
1322
1246
  }
1323
1247
  function planIssue(plan) {
1324
- if (plan.gate !== undefined) {
1325
- const issue = gateIssue(plan.gate);
1248
+ const gate = planGate(plan);
1249
+ if (gate !== undefined) {
1250
+ const issue = gateIssue(gate);
1326
1251
  if (issue)
1327
1252
  return issue;
1328
1253
  }
@@ -1436,45 +1361,6 @@ function droppedFindingIds(session, featureId, findings) {
1436
1361
  return liveFindingIds(session, featureId).filter((id) => !submitted.has(id));
1437
1362
  }
1438
1363
 
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
1364
  // src/domain/transitions.ts
1479
1365
  function fail(message) {
1480
1366
  throw new FlowTransitionError(message);
@@ -1530,20 +1416,19 @@ function assertPlan(plan) {
1530
1416
  if (issue)
1531
1417
  fail(issue);
1532
1418
  }
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.");
1419
+ function assertDeclaredEvidence(plan) {
1420
+ if (plan.evidence === undefined) {
1421
+ 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
1422
  }
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.");
1423
+ const gates = planEvidence(plan).filter((entry) => entry.scope === "gate");
1424
+ if (gates.length !== 1) {
1425
+ 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
1426
  }
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.");
1427
+ if (plan.evidence.some((entry) => entry.platform === undefined)) {
1428
+ 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
1429
  }
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.");
1430
+ if (plan.evidence.some((entry) => entry.assertions === undefined)) {
1431
+ 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
1432
  }
1548
1433
  }
1549
1434
  function assertArtifacts(artifacts) {
@@ -1578,8 +1463,7 @@ function sessionStatus(session) {
1578
1463
  function savePlan(session, input, environment) {
1579
1464
  assertPlan(input.plan);
1580
1465
  if (!session) {
1581
- assertDeclaredGate(input.plan);
1582
- assertDeclaredExternalEvidence(input.plan);
1466
+ assertDeclaredEvidence(input.plan);
1583
1467
  if (input.expectedRevision !== 0) {
1584
1468
  fail("A new Flow session must start from expectedRevision 0.");
1585
1469
  }
@@ -1609,8 +1493,7 @@ function savePlan(session, input, environment) {
1609
1493
  const replay = existingOperation(session, "plan-save", input.operationId, input);
1610
1494
  if (replay)
1611
1495
  return { session, value: null, replayed: true };
1612
- assertDeclaredGate(input.plan);
1613
- assertDeclaredExternalEvidence(input.plan);
1496
+ assertDeclaredEvidence(input.plan);
1614
1497
  assertRevision(session, input.expectedRevision);
1615
1498
  assertMutable(session);
1616
1499
  if (session.approval === "approved")
@@ -1753,9 +1636,9 @@ function startReview(session, input, environment) {
1753
1636
  }
1754
1637
  const kind = isFinalFeatureRun(session, run) ? "final" : "feature";
1755
1638
  if (kind === "final") {
1756
- const unsatisfied = unsatisfiedExternalEvidence(session, input.sourceDigest);
1639
+ const unsatisfied = unsatisfiedExtraEvidence(session, input.sourceDigest);
1757
1640
  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.`);
1641
+ 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
1642
  }
1760
1643
  }
1761
1644
  const applicable = run.validations.filter((validation) => isValidationEligible(validation, input.sourceDigest) && isValidationFresh(session, run, validation));
@@ -1935,9 +1818,9 @@ function closeSession(session, input) {
1935
1818
  fail("A completed close requires every planned feature to pass review.");
1936
1819
  }
1937
1820
  if (input.kind === "completed") {
1938
- const unsatisfied = unsatisfiedExternalEvidence(session);
1821
+ const unsatisfied = unsatisfiedExtraEvidence(session);
1939
1822
  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.`);
1823
+ 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
1824
  }
1942
1825
  }
1943
1826
  let closure = null;
@@ -2169,12 +2052,13 @@ var PlanFeatureSchema = z.object({
2169
2052
  validation: z.array(boundedText("Feature validation")).max(MAX_PLAN_FEATURES).default([]),
2170
2053
  dependsOn: z.array(FeatureIdSchema).max(MAX_PLAN_FEATURES).default([])
2171
2054
  }).strict();
2172
- var ExternalEvidenceSchema = z.object({
2173
- requirement: boundedText("External evidence requirement"),
2174
- environment: boundedText("External evidence environment"),
2175
- command: boundedText("External evidence command"),
2055
+ var EvidenceEntrySchema = z.object({
2056
+ requirement: boundedText("Evidence requirement"),
2057
+ environment: boundedText("Evidence environment"),
2058
+ command: boundedText("Evidence command"),
2059
+ scope: z.enum(["gate", "extra"]),
2176
2060
  platform: z.enum(EVIDENCE_PLATFORMS).optional(),
2177
- assertions: z.array(boundedText("External evidence assertion")).max(MAX_DECLARED_ASSERTIONS).optional()
2061
+ assertions: z.array(boundedText("Evidence assertion")).max(MAX_DECLARED_ASSERTIONS).optional()
2178
2062
  }).strict();
2179
2063
  var ObservedAssertionSchema = z.object({
2180
2064
  name: boundedText("Observed assertion name"),
@@ -2186,8 +2070,7 @@ var PlanSchema = z.object({
2186
2070
  requirements: z.array(boundedText("Plan requirement")).max(MAX_PLAN_FEATURES).default([]),
2187
2071
  decisions: z.array(boundedText("Plan decision")).max(MAX_PLAN_FEATURES).default([]),
2188
2072
  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()
2073
+ evidence: z.array(EvidenceEntrySchema).max(MAX_PLAN_FEATURES).optional()
2191
2074
  }).strict().superRefine((plan, context) => {
2192
2075
  if (encoder.encode(JSON.stringify(plan)).byteLength > MAX_PLAN_BYTES) {
2193
2076
  context.addIssue({
@@ -2413,23 +2296,23 @@ function assuranceProjection(session) {
2413
2296
  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
2297
  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
2298
  ];
2416
- const gate = session.plan?.gate;
2299
+ const gate = planGate(session.plan);
2417
2300
  checks.push(gate === undefined ? {
2418
2301
  id: "canonical-gate",
2419
2302
  label: "Canonical gate",
2420
2303
  tier: "caller-declared",
2421
2304
  status: "not-applicable",
2422
- explanation: "This legacy plan declared no canonical gate."
2305
+ explanation: "This plan declared no canonical gate."
2423
2306
  } : 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;
2307
+ const declared = session.plan?.evidence;
2308
+ const missing = unsatisfiedEvidence(session).length;
2426
2309
  checks.push(declared === undefined ? {
2427
- id: "external-evidence",
2428
- label: "Declared external evidence",
2310
+ id: "declared-evidence",
2311
+ label: "Declared evidence",
2429
2312
  tier: "caller-declared",
2430
2313
  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.`));
2314
+ explanation: "This plan declared no evidence obligations."
2315
+ } : 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
2316
  return {
2434
2317
  conclusion: !complete ? "completion-not-claimed" : checks.some((item) => item.status === "unsatisfied") ? "completion-unsupported" : "completion-supported",
2435
2318
  checks,
@@ -2637,9 +2520,8 @@ function reviewerProjection(session, assignmentId) {
2637
2520
  validation: [...candidate.validation],
2638
2521
  dependsOn: [...candidate.dependsOn]
2639
2522
  })),
2640
- ...plan.gate === undefined ? {} : { gate: plan.gate },
2641
- ...plan.externalEvidence === undefined ? {} : {
2642
- externalEvidence: plan.externalEvidence.map((entry) => ({
2523
+ ...plan.evidence === undefined ? {} : {
2524
+ evidence: plan.evidence.map((entry) => ({
2643
2525
  ...entry
2644
2526
  }))
2645
2527
  }
@@ -4506,11 +4388,11 @@ var plan = host.object({
4506
4388
  requirements: host.array(text).max(MAX_PLAN_FEATURES).default([]),
4507
4389
  decisions: host.array(text).max(MAX_PLAN_FEATURES).default([]),
4508
4390
  features: host.array(planFeature).min(1).max(MAX_PLAN_FEATURES),
4509
- gate: text.optional(),
4510
- externalEvidence: host.array(host.object({
4391
+ evidence: host.array(host.object({
4511
4392
  requirement: text,
4512
4393
  environment: text,
4513
4394
  command: text,
4395
+ scope: host.enum(["gate", "extra"]),
4514
4396
  platform: host.enum(EVIDENCE_PLATFORMS).optional(),
4515
4397
  assertions: host.array(text).max(MAX_DECLARED_ASSERTIONS).optional()
4516
4398
  }).strict()).max(MAX_PLAN_FEATURES).optional()
@@ -4617,12 +4499,14 @@ function toolError(error) {
4617
4499
  }
4618
4500
  });
4619
4501
  }
4620
- function withAutoContext(response, options) {
4502
+ function withAutoContext(response, options, view) {
4621
4503
  let workflowData = response.workflowData;
4622
4504
  try {
4623
- const timing = options.autoTimingSnapshot?.();
4624
- if (timing)
4625
- workflowData = { ...workflowData, autoTiming: timing };
4505
+ if (view === "detail") {
4506
+ const timing = options.autoTimingSnapshot?.();
4507
+ if (timing)
4508
+ workflowData = { ...workflowData, autoTiming: timing };
4509
+ }
4626
4510
  } catch {}
4627
4511
  try {
4628
4512
  const support = options.autoContinuationSupport?.();
@@ -4669,7 +4553,7 @@ function createTools(_ctx, options) {
4669
4553
  flow_status: tool({
4670
4554
  description: "Read compact, execution, detail, or reviewer Flow state.",
4671
4555
  args: StatusArgs,
4672
- execute: (args, context) => execute(context, async (workspace) => withAutoContext(await flowStatus(workspace, args), options))
4556
+ execute: (args, context) => execute(context, async (workspace) => withAutoContext(await flowStatus(workspace, args), options, args.request.view))
4673
4557
  }),
4674
4558
  flow_plan_save: tool({
4675
4559
  description: "Create or replace the active draft plan.",
@@ -5139,4 +5023,4 @@ export {
5139
5023
  plugin_default as default
5140
5024
  };
5141
5025
 
5142
- //# debugId=719465FC0656BBA564756E2164756E21
5026
+ //# debugId=84B9887B14A5141164756E2164756E21