opencode-plugin-flow 6.1.0 → 6.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +48 -0
- package/README.md +71 -157
- package/dist/index.js +288 -196
- package/dist/index.js.map +14 -13
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -7,10 +7,12 @@ description: Manage a Flow goal from planning through implementation, validation
|
|
|
7
7
|
# Flow
|
|
8
8
|
|
|
9
9
|
Flow is a small state ledger around ordinary coding work. The root manager owns
|
|
10
|
-
the session,
|
|
11
|
-
|
|
12
|
-
contribute disjoint work inside the active feature. The reserved
|
|
13
|
-
independently reviews
|
|
10
|
+
the session, integration, validation, review dispatch, reset, closure, and every
|
|
11
|
+
lifecycle mutation except review submission. Bounded \`flow-worker\`
|
|
12
|
+
instances may contribute disjoint work inside the active feature. The reserved
|
|
13
|
+
\`flow-reviewer\` independently reviews and submits its own result through
|
|
14
|
+
\`flow_feature_complete\`; it cannot edit the workspace or make any other
|
|
15
|
+
lifecycle mutation.
|
|
14
16
|
|
|
15
17
|
## Route from status
|
|
16
18
|
|
|
@@ -37,9 +39,10 @@ report that the plugin is not fully loaded instead of simulating state changes.
|
|
|
37
39
|
- Only one durable feature run is active at a time. Conversation-local worker
|
|
38
40
|
waves do not create additional runs or Flow state.
|
|
39
41
|
- Work stays inside the active feature and preserves unrelated user changes.
|
|
40
|
-
- A passing feature needs successful current-source validation and one
|
|
41
|
-
independent reviewer
|
|
42
|
-
a final review; it does not add a second review
|
|
42
|
+
- A passing feature needs successful current-source validation and one result
|
|
43
|
+
submitted directly by the assigned independent reviewer. The final feature
|
|
44
|
+
uses broad validation and a final review; it does not add a second review
|
|
45
|
+
pass.
|
|
43
46
|
- A failed review is recorded honestly. Any retry is a fresh run with full
|
|
44
47
|
validation and review.
|
|
45
48
|
- Use runtime revisions and operation ids for ordering and idempotency. Supply
|
|
@@ -53,7 +56,13 @@ report that the plugin is not fully loaded instead of simulating state changes.
|
|
|
53
56
|
|
|
54
57
|
On confusion or interruption, read compact status and follow its next action.
|
|
55
58
|
Use execution status for the active feature and reviewer status for a returned
|
|
56
|
-
assignment id.
|
|
59
|
+
assignment id. Redispatch a pending assignment after interruption or an
|
|
60
|
+
unconfirmed reviewer return; the manager never invents or submits a verdict. If
|
|
61
|
+
completion reports \`Workspace content changed after review started\`, call
|
|
62
|
+
\`flow_feature_reset\`; that source-stale assignment must not be redispatched.
|
|
63
|
+
Start a fresh run and repeat full validation and review. After the reviewer
|
|
64
|
+
returns, read compact status to learn the durable outcome. If status is closed
|
|
65
|
+
with \`archiveRetry\`, call
|
|
57
66
|
\`flow_session_close\` with that projected request byte-for-byte; do not create a
|
|
58
67
|
new operation id or revision. Never infer completion, retry count, or closure
|
|
59
68
|
from prose.
|
|
@@ -126,26 +135,30 @@ document in the repository unless the user explicitly requests one.
|
|
|
126
135
|
// skills/flow-review/SKILL.md
|
|
127
136
|
var SKILL_default3 = `---
|
|
128
137
|
name: flow-review
|
|
129
|
-
description: Independently review one runtime-owned Flow assignment. Reserved for the read-only flow-reviewer; managers dispatch assignments but do not perform this review themselves.
|
|
138
|
+
description: Independently review one runtime-owned Flow assignment. Reserved for the workspace-read-only flow-reviewer; managers dispatch assignments but do not perform this review themselves.
|
|
130
139
|
---
|
|
131
140
|
|
|
132
141
|
# Flow Review
|
|
133
142
|
|
|
134
143
|
You are the independent \`flow-reviewer\`. Review the assigned work; do not fix
|
|
135
144
|
it. You may read relevant files and supplied evidence, but must not edit files,
|
|
136
|
-
read outside the workspace, run commands, launch workers
|
|
137
|
-
|
|
145
|
+
read outside the workspace, run commands, or launch workers. You may call only
|
|
146
|
+
\`flow_status\` to read this assignment and \`flow_feature_complete\` to submit its
|
|
147
|
+
exact result. The latter is your sole lifecycle mutation.
|
|
138
148
|
|
|
139
149
|
## Recover the assignment
|
|
140
150
|
|
|
141
151
|
When given an assignment id, call only
|
|
142
152
|
\`flow_status { request: { view: "reviewer", assignmentId: "..." } }\`. Use its
|
|
143
|
-
bounded packet
|
|
144
|
-
|
|
153
|
+
bounded packet, assignment-linked validations, approved-plan context, and
|
|
154
|
+
completed feature IDs instead of reconstructing feature, source,
|
|
155
|
+
revision, validation, or lifecycle data from conversation memory.
|
|
145
156
|
|
|
146
|
-
If the
|
|
147
|
-
|
|
148
|
-
|
|
157
|
+
If the reviewer projection is available but evidence required to justify a
|
|
158
|
+
verdict is missing, submit a failed result with a blocking evidence-gap finding.
|
|
159
|
+
If the assignment itself is unavailable, report that failure without another
|
|
160
|
+
state change so the manager can inspect compact status. Never invent validation,
|
|
161
|
+
identity, revision, or time.
|
|
149
162
|
|
|
150
163
|
## Review
|
|
151
164
|
|
|
@@ -160,6 +173,12 @@ the manager's summary. Check that:
|
|
|
160
173
|
- persistence, concurrency, security, migration, package, UI, and recovery
|
|
161
174
|
risks were examined when relevant.
|
|
162
175
|
|
|
176
|
+
Validation scope is a claim. Treat \`broad\` as adequate only when the durable
|
|
177
|
+
command is the repository's canonical applicable gate or a justified equivalent
|
|
178
|
+
for the delivered state. Flow deliberately projects no raw command output; use
|
|
179
|
+
the durable command, exit code, completeness, digest, source binding, and your
|
|
180
|
+
workspace inspection. A weak or unclear coverage claim is an evidence gap.
|
|
181
|
+
|
|
163
182
|
For a final assignment, also trace every approved requirement and feature to
|
|
164
183
|
the delivered result, inspect broad validation, and confirm docs, commands,
|
|
165
184
|
package surfaces, and remaining gaps are consistent with completion. The final
|
|
@@ -170,25 +189,40 @@ approved outcome; otherwise use \`advisory\`. Every blocker needs a precise
|
|
|
170
189
|
summary. Every blocker must cite a changed artifact and location, or identify
|
|
171
190
|
the exact missing evidence or unmet approved requirement in \`evidence\`.
|
|
172
191
|
|
|
173
|
-
##
|
|
192
|
+
## Submit one result
|
|
174
193
|
|
|
175
|
-
|
|
194
|
+
Call \`flow_feature_complete\` directly with the assignment id, current reviewer
|
|
195
|
+
projection revision and feature id, a fresh operation id, a concise summary,
|
|
196
|
+
and exactly one assignment result:
|
|
176
197
|
|
|
177
198
|
\`\`\`json
|
|
178
199
|
{
|
|
179
|
-
"
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
200
|
+
"request": {
|
|
201
|
+
"operationId": "review-submit:fresh-id",
|
|
202
|
+
"expectedRevision": 12,
|
|
203
|
+
"featureId": "approved-feature-id",
|
|
204
|
+
"assignmentId": "review-assignment:runtime-id",
|
|
205
|
+
"summary": "Concise reviewed outcome.",
|
|
206
|
+
"result": {
|
|
207
|
+
"verdict": "passed",
|
|
208
|
+
"findings": [],
|
|
209
|
+
"terminalDisposition": "submitted"
|
|
210
|
+
}
|
|
211
|
+
}
|
|
183
212
|
}
|
|
184
213
|
\`\`\`
|
|
185
214
|
|
|
186
215
|
Each finding contains \`severity\`, \`summary\`, and optional \`evidence\`. Use
|
|
187
|
-
\`verdict: "failed"\` whenever any blocking finding remains.
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
216
|
+
\`verdict: "failed"\` whenever any blocking finding remains. Do not return or
|
|
217
|
+
invent run ids, source hashes, validation records, timestamps, review modes, or
|
|
218
|
+
attempt fields. Never ask the manager to copy or submit your verdict.
|
|
219
|
+
|
|
220
|
+
After the tool returns, report its durable outcome concisely. If submission
|
|
221
|
+
fails, report the exact failure. For \`Workspace content changed after review
|
|
222
|
+
started\`, tell the manager to reset the feature and do not recommend redispatch;
|
|
223
|
+
the source-stale assignment cannot complete. After an interruption with no
|
|
224
|
+
accepted result, the manager may recover the pending assignment. Never
|
|
225
|
+
downgrade, fabricate, or hand off a result for manager submission.
|
|
192
226
|
|
|
193
227
|
Approve only what you actually inspected. Missing coverage is a finding, never
|
|
194
228
|
a reason to lower the bar.
|
|
@@ -202,10 +236,11 @@ description: Implement, validate, independently review, and record one approved
|
|
|
202
236
|
|
|
203
237
|
# Flow Run
|
|
204
238
|
|
|
205
|
-
Work on exactly one approved feature. The root manager owns the session,
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
reserved \`flow-reviewer\` owns the
|
|
239
|
+
Work on exactly one approved feature. The root manager owns the session,
|
|
240
|
+
integration, validation, review dispatch, reset, closure, and every
|
|
241
|
+
lifecycle mutation except review submission. Bounded \`flow-worker\`
|
|
242
|
+
instances may contribute disjoint work; the reserved \`flow-reviewer\` owns the
|
|
243
|
+
independent review and submits its own result.
|
|
209
244
|
|
|
210
245
|
## Start and scope
|
|
211
246
|
|
|
@@ -235,26 +270,29 @@ that separate action.
|
|
|
235
270
|
|
|
236
271
|
Work serially by default. After manager orientation, fan out only when at least
|
|
237
272
|
two genuinely independent slices can be named. Run one cohort of two or three
|
|
238
|
-
\`flow-worker\` instances at a time.
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
273
|
+
\`flow-worker\` instances at a time. Issue every cohort Task call in the same
|
|
274
|
+
assistant tool-use turn before consuming any result. If the host or model
|
|
275
|
+
serializes those calls, treat and report that execution as serial instead of
|
|
276
|
+
claiming parallelism. Each prompt must name a stable slice id, the exact outcome
|
|
277
|
+
and read or write scope, expected coverage, recommended manager checks,
|
|
278
|
+
dependencies, and a stop condition. Edit scopes must be exact and
|
|
279
|
+
non-overlapping. Shared contracts, lockfiles, and generated outputs remain
|
|
280
|
+
manager-owned unless one worker receives the whole relevant scope.
|
|
245
281
|
|
|
246
282
|
Workers cannot call Flow tools or spawn children. Each returns one concise
|
|
247
283
|
handoff containing status, scope and coverage, evidence or changed paths,
|
|
248
|
-
checks, gaps and risks, and integration notes.
|
|
249
|
-
|
|
284
|
+
recommended manager checks, gaps and risks, and integration notes. Workers do
|
|
285
|
+
not run Bash; all executable checks remain manager-owned. Missing, partial, or
|
|
286
|
+
blocked output remains an explicit coverage gap.
|
|
250
287
|
|
|
251
|
-
After all workers stop,
|
|
288
|
+
After all workers stop, compare actual changed paths with every assigned scope,
|
|
289
|
+
then inspect the combined diff and evidence and reconcile unexpected paths or
|
|
252
290
|
conflicts before validation. At most one targeted follow-up wave may address a
|
|
253
|
-
failed slice, newly unlocked dependency, or material claim verification. Do
|
|
254
|
-
start an automatic third wave. Coordination stays in the conversation:
|
|
255
|
-
no manifest, sidecar, Session field, durable handoff, or recovery ledger.
|
|
256
|
-
an interruption, inspect Flow status and the worktree and treat partial
|
|
257
|
-
edits as untrusted.
|
|
291
|
+
failed slice, newly unlocked dependency, or material claim verification. Do
|
|
292
|
+
not start an automatic third wave. Coordination stays in the conversation:
|
|
293
|
+
create no manifest, sidecar, Session field, durable handoff, or recovery ledger.
|
|
294
|
+
After an interruption, inspect Flow status and the worktree and treat partial
|
|
295
|
+
worker edits as untrusted.
|
|
258
296
|
|
|
259
297
|
## Validate
|
|
260
298
|
|
|
@@ -268,17 +306,25 @@ checks from the changed behavior and risk:
|
|
|
268
306
|
sufficient alone only for genuinely mechanical or documentation-only work.
|
|
269
307
|
- UI claims need visual inspection when a runnable target is available.
|
|
270
308
|
- Cleanup claims need behavior-preservation evidence, not formatting alone.
|
|
309
|
+
- \`scope: "broad"\` is a claim about coverage, not a stronger label. Use it only
|
|
310
|
+
for the repository's canonical applicable gate or a justified equivalent
|
|
311
|
+
that covers the delivered repository state.
|
|
271
312
|
|
|
272
313
|
Immediately before each Bash command used as evidence, call
|
|
273
314
|
\`flow_validation_start\` with the current revision, feature id, the exact
|
|
274
315
|
command, and \`scope\` (\`focused\` or \`broad\`). Run that byte-for-byte command next
|
|
275
316
|
and inspect its complete outcome. Flow records the host-observed result directly
|
|
276
|
-
in the session; do not copy host-observed fields into a later request.
|
|
317
|
+
in the session; do not copy host-observed fields into a later request. The exact
|
|
318
|
+
command is durable, so never inline tokens, passwords, credentials, or other
|
|
319
|
+
secrets. Raw output is deliberately neither persisted nor projected: the
|
|
320
|
+
durable evidence is the command, exit code, output completeness, and output
|
|
321
|
+
digest, while the manager must inspect the live output.
|
|
277
322
|
|
|
278
323
|
Use focused validation for ordinary features. For the final feature, run the
|
|
279
324
|
repository's broad applicable gate after the last relevant edit. A source edit
|
|
280
325
|
invalidates earlier applicability. Failed or unavailable checks are blockers,
|
|
281
|
-
not passing evidence.
|
|
326
|
+
not passing evidence. If the canonical gate cannot run, explain why the chosen
|
|
327
|
+
equivalent is broad enough; otherwise record the narrower evidence as focused.
|
|
282
328
|
|
|
283
329
|
## Review and record
|
|
284
330
|
|
|
@@ -292,9 +338,19 @@ automatically and derives \`feature\` versus \`final\` review from plan progress
|
|
|
292
338
|
callers do not supply the review kind.
|
|
293
339
|
|
|
294
340
|
Dispatch the returned assignment only to the reserved \`flow-reviewer\`. Do not
|
|
295
|
-
perform the independent review in manager context
|
|
296
|
-
|
|
297
|
-
|
|
341
|
+
perform the independent review in manager context and never copy or submit its
|
|
342
|
+
verdict. The reviewer reads its assignment, inspects the workspace, and calls
|
|
343
|
+
\`flow_feature_complete\` directly; the runtime verifies the calling agent. The
|
|
344
|
+
reviewer remains workspace-read-only and may make only this exact result
|
|
345
|
+
submission as its sole lifecycle mutation.
|
|
346
|
+
|
|
347
|
+
After the reviewer returns, read compact status rather than treating prose as
|
|
348
|
+
the outcome. Redispatch the same pending assignment after interruption or an
|
|
349
|
+
unconfirmed reviewer return. If submission reports \`Workspace content changed
|
|
350
|
+
after review started\`, call \`flow_feature_reset\` and do not redispatch that
|
|
351
|
+
source-stale assignment; start a fresh run and repeat full validation and
|
|
352
|
+
review. Never fabricate a verdict. A submitted pass completes the feature; a
|
|
353
|
+
submitted blocking finding records a blocked outcome.
|
|
298
354
|
|
|
299
355
|
If repair is authorized after a failed review, reset the feature, fix it, and
|
|
300
356
|
repeat full validation and full review in a fresh run.
|
|
@@ -348,9 +404,11 @@ You may run concurrently with sibling workers. Do not enter their scopes, assume
|
|
|
348
404
|
- Use the manager assignment as your only source of Flow lifecycle context. Do not call any \`flow_*\` tool, including \`flow_status\`.
|
|
349
405
|
- Do not delegate, spawn subtasks, or load skills.
|
|
350
406
|
- Do not stage, commit, push, publish, or create a release.
|
|
407
|
+
- Do not run Bash commands. The manager owns every executable check.
|
|
408
|
+
- Never edit .flow or .git metadata paths; the host denies those paths.
|
|
351
409
|
- A read-only evidence slice must not edit files.
|
|
352
410
|
- An implementation slice may edit only the exact, non-overlapping write paths explicitly assigned by the manager. If required work would escape those paths, stop and return a partial or blocked handoff instead of expanding scope.
|
|
353
|
-
-
|
|
411
|
+
- Use only non-shell inspection relevant to the assigned slice. The manager owns integration, focused checks, and authoritative combined validation after all workers have stopped.
|
|
354
412
|
|
|
355
413
|
## Handoff
|
|
356
414
|
|
|
@@ -365,8 +423,8 @@ success | partial | blocked
|
|
|
365
423
|
## Findings / changed paths
|
|
366
424
|
- Evidence found or exact paths changed
|
|
367
425
|
|
|
368
|
-
##
|
|
369
|
-
-
|
|
426
|
+
## Recommended manager checks
|
|
427
|
+
- Exact checks the manager should run, or none
|
|
370
428
|
|
|
371
429
|
## Gaps & risks
|
|
372
430
|
- Missing coverage, blockers, conflicts, or none
|
|
@@ -410,7 +468,7 @@ function compileFlowPromptSurface(surface) {
|
|
|
410
468
|
return [
|
|
411
469
|
"# Flow review command",
|
|
412
470
|
"",
|
|
413
|
-
"Run this assignment only as the reserved `flow-reviewer`. The reviewer is independent and read-only; it
|
|
471
|
+
"Run this assignment only as the reserved `flow-reviewer`. The reviewer is independent and workspace-read-only; it may read reviewer status and submit only its own result through `flow_feature_complete`.",
|
|
414
472
|
"",
|
|
415
473
|
"Assignment: $ARGUMENTS"
|
|
416
474
|
].join(`
|
|
@@ -431,7 +489,7 @@ var FLOW_CORE_AGENTS = {
|
|
|
431
489
|
"flow-reviewer": {
|
|
432
490
|
mode: "subagent",
|
|
433
491
|
hidden: true,
|
|
434
|
-
description: "Independent read-only reviewer
|
|
492
|
+
description: "Independent workspace-read-only reviewer that submits one Flow result.",
|
|
435
493
|
prompt: compileFlowPromptSurface("flow-reviewer"),
|
|
436
494
|
permission: {
|
|
437
495
|
edit: "deny",
|
|
@@ -440,7 +498,8 @@ var FLOW_CORE_AGENTS = {
|
|
|
440
498
|
skill: "deny",
|
|
441
499
|
task: { "*": "deny" },
|
|
442
500
|
"flow_*": "deny",
|
|
443
|
-
flow_status: "allow"
|
|
501
|
+
flow_status: "allow",
|
|
502
|
+
flow_feature_complete: "allow"
|
|
444
503
|
}
|
|
445
504
|
},
|
|
446
505
|
"flow-worker": {
|
|
@@ -449,8 +508,14 @@ var FLOW_CORE_AGENTS = {
|
|
|
449
508
|
description: "Bounded worker for one read-only evidence or exact-scope implementation slice.",
|
|
450
509
|
prompt: compileFlowPromptSurface("flow-worker"),
|
|
451
510
|
permission: {
|
|
452
|
-
edit:
|
|
453
|
-
|
|
511
|
+
edit: {
|
|
512
|
+
"*": "allow",
|
|
513
|
+
".flow": "deny",
|
|
514
|
+
".flow/**": "deny",
|
|
515
|
+
".git": "deny",
|
|
516
|
+
".git/**": "deny"
|
|
517
|
+
},
|
|
518
|
+
bash: "deny",
|
|
454
519
|
external_directory: "deny",
|
|
455
520
|
skill: "deny",
|
|
456
521
|
task: { "*": "deny" },
|
|
@@ -475,7 +540,7 @@ var FLOW_CORE_COMMANDS = {
|
|
|
475
540
|
template: compileFlowPromptSurface("flow-run")
|
|
476
541
|
},
|
|
477
542
|
"flow-review": {
|
|
478
|
-
description: "Run one independent read-only Flow review",
|
|
543
|
+
description: "Run one independent workspace-read-only Flow review",
|
|
479
544
|
agent: "flow-reviewer",
|
|
480
545
|
subtask: true,
|
|
481
546
|
template: compileFlowPromptSurface("flow-review")
|
|
@@ -695,6 +760,39 @@ function planIssue(plan) {
|
|
|
695
760
|
return visited === plan.features.length ? null : "The plan dependency graph is cyclic.";
|
|
696
761
|
}
|
|
697
762
|
|
|
763
|
+
// src/domain/session.ts
|
|
764
|
+
function reviewResultSemanticIssues(result) {
|
|
765
|
+
const issues = [];
|
|
766
|
+
const blocking = result.findings.some((finding) => finding.severity === "blocking");
|
|
767
|
+
for (const [index, finding] of result.findings.entries()) {
|
|
768
|
+
if (finding.severity === "blocking" && !finding.evidence?.trim()) {
|
|
769
|
+
issues.push({
|
|
770
|
+
path: ["findings", index, "evidence"],
|
|
771
|
+
message: "A blocking finding requires concrete evidence."
|
|
772
|
+
});
|
|
773
|
+
}
|
|
774
|
+
}
|
|
775
|
+
if (result.verdict === "failed" && !blocking) {
|
|
776
|
+
issues.push({
|
|
777
|
+
path: ["findings"],
|
|
778
|
+
message: "A failed review requires a blocking finding."
|
|
779
|
+
});
|
|
780
|
+
}
|
|
781
|
+
if (result.verdict === "passed" && blocking) {
|
|
782
|
+
issues.push({
|
|
783
|
+
path: ["findings"],
|
|
784
|
+
message: "A passed review cannot contain blocking findings."
|
|
785
|
+
});
|
|
786
|
+
}
|
|
787
|
+
if (result.terminalDisposition === "observed_unsubmitted" && result.verdict !== "failed") {
|
|
788
|
+
issues.push({
|
|
789
|
+
path: ["terminalDisposition"],
|
|
790
|
+
message: "Observed-but-unsubmitted review work must fail closed."
|
|
791
|
+
});
|
|
792
|
+
}
|
|
793
|
+
return issues;
|
|
794
|
+
}
|
|
795
|
+
|
|
698
796
|
// src/domain/transitions.ts
|
|
699
797
|
class FlowTransitionError extends Error {
|
|
700
798
|
code = "FLOW_TRANSITION_REJECTED";
|
|
@@ -1030,19 +1128,9 @@ function assertReviewResult(result) {
|
|
|
1030
1128
|
if (result.findings.length > MAX_REVIEW_FINDINGS) {
|
|
1031
1129
|
fail(`A review may contain at most ${MAX_REVIEW_FINDINGS} findings.`);
|
|
1032
1130
|
}
|
|
1033
|
-
const
|
|
1034
|
-
|
|
1035
|
-
|
|
1036
|
-
fail("A blocking finding requires concrete evidence.");
|
|
1037
|
-
if (result.verdict === "failed" && !blocking) {
|
|
1038
|
-
fail("A failed review requires a blocking finding.");
|
|
1039
|
-
}
|
|
1040
|
-
if (result.verdict === "passed" && blocking) {
|
|
1041
|
-
fail("A passed review cannot contain a blocking finding.");
|
|
1042
|
-
}
|
|
1043
|
-
if (result.terminalDisposition === "observed_unsubmitted" && result.verdict !== "failed") {
|
|
1044
|
-
fail("Observed-but-unsubmitted review work must fail closed.");
|
|
1045
|
-
}
|
|
1131
|
+
const issue = reviewResultSemanticIssues(result)[0];
|
|
1132
|
+
if (issue)
|
|
1133
|
+
fail(issue.message);
|
|
1046
1134
|
}
|
|
1047
1135
|
function completeFeature(session, input) {
|
|
1048
1136
|
assertReviewResult(input.result);
|
|
@@ -1419,6 +1507,7 @@ function boundedText(label, options) {
|
|
|
1419
1507
|
}
|
|
1420
1508
|
var FeatureIdSchema = z.string().max(MAX_SESSION_ID_LENGTH).regex(FEATURE_ID_PATTERN, FEATURE_ID_MESSAGE);
|
|
1421
1509
|
var OperationIdSchema = z.string().min(1).max(128).regex(/^[a-zA-Z0-9][a-zA-Z0-9._:-]*$/);
|
|
1510
|
+
var ReviewAssignmentIdSchema = z.string().min(1).max(256);
|
|
1422
1511
|
var RevisionSchema = z.number().int().safe().nonnegative();
|
|
1423
1512
|
var SourceDigestSchema = z.custom((value) => typeof value === "string" && /^sha256:[a-f0-9]{64}$/.test(value), "Expected a sha256: content digest.");
|
|
1424
1513
|
var PlanFeatureSchema = z.object({
|
|
@@ -1453,36 +1542,8 @@ var PublicReviewResultSchema = z.object({
|
|
|
1453
1542
|
findings: z.array(ReviewFindingSchema).max(MAX_REVIEW_FINDINGS).default([]),
|
|
1454
1543
|
terminalDisposition: z.enum(["submitted", "observed_unsubmitted"])
|
|
1455
1544
|
}).strict().superRefine((result, context) => {
|
|
1456
|
-
const
|
|
1457
|
-
|
|
1458
|
-
if (finding.severity === "blocking" && !finding.evidence) {
|
|
1459
|
-
context.addIssue({
|
|
1460
|
-
code: "custom",
|
|
1461
|
-
path: ["findings", index, "evidence"],
|
|
1462
|
-
message: "A blocking finding requires concrete evidence."
|
|
1463
|
-
});
|
|
1464
|
-
}
|
|
1465
|
-
}
|
|
1466
|
-
if (result.verdict === "failed" && !blocking) {
|
|
1467
|
-
context.addIssue({
|
|
1468
|
-
code: "custom",
|
|
1469
|
-
path: ["findings"],
|
|
1470
|
-
message: "A failed review requires a blocking finding."
|
|
1471
|
-
});
|
|
1472
|
-
}
|
|
1473
|
-
if (result.verdict === "passed" && blocking) {
|
|
1474
|
-
context.addIssue({
|
|
1475
|
-
code: "custom",
|
|
1476
|
-
path: ["findings"],
|
|
1477
|
-
message: "A passed review cannot contain blocking findings."
|
|
1478
|
-
});
|
|
1479
|
-
}
|
|
1480
|
-
if (result.terminalDisposition === "observed_unsubmitted" && result.verdict !== "failed") {
|
|
1481
|
-
context.addIssue({
|
|
1482
|
-
code: "custom",
|
|
1483
|
-
path: ["terminalDisposition"],
|
|
1484
|
-
message: "Observed-but-unsubmitted review work must fail closed."
|
|
1485
|
-
});
|
|
1545
|
+
for (const issue of reviewResultSemanticIssues(result)) {
|
|
1546
|
+
context.addIssue({ code: "custom", ...issue });
|
|
1486
1547
|
}
|
|
1487
1548
|
});
|
|
1488
1549
|
var ValidationObservationSchema = z.object({
|
|
@@ -1502,7 +1563,7 @@ var ArtifactSchema = z.object({
|
|
|
1502
1563
|
path: boundedText("Artifact path", { maxBytes: MAX_PATH_BYTES }).refine(isArtifactPath, ARTIFACT_PATH_MESSAGE)
|
|
1503
1564
|
}).strict();
|
|
1504
1565
|
var ReviewAssignmentSchema = z.object({
|
|
1505
|
-
id:
|
|
1566
|
+
id: ReviewAssignmentIdSchema,
|
|
1506
1567
|
operationId: OperationIdSchema,
|
|
1507
1568
|
featureId: FeatureIdSchema,
|
|
1508
1569
|
runId: z.string().min(1).max(256),
|
|
@@ -1600,7 +1661,7 @@ var FeatureCompleteInputSchema = z.object({
|
|
|
1600
1661
|
request: z.object({
|
|
1601
1662
|
...guarded,
|
|
1602
1663
|
featureId: FeatureIdSchema,
|
|
1603
|
-
assignmentId:
|
|
1664
|
+
assignmentId: ReviewAssignmentIdSchema,
|
|
1604
1665
|
summary: boundedText("Feature result summary"),
|
|
1605
1666
|
result: PublicReviewResultSchema
|
|
1606
1667
|
}).strict()
|
|
@@ -1631,7 +1692,7 @@ var StatusInputSchema = z.object({
|
|
|
1631
1692
|
z.object({ view: z.literal("execution") }).strict(),
|
|
1632
1693
|
z.object({
|
|
1633
1694
|
view: z.literal("reviewer"),
|
|
1634
|
-
assignmentId:
|
|
1695
|
+
assignmentId: ReviewAssignmentIdSchema
|
|
1635
1696
|
}).strict()
|
|
1636
1697
|
])
|
|
1637
1698
|
}).strict();
|
|
@@ -2301,7 +2362,7 @@ function validIdentity(value) {
|
|
|
2301
2362
|
}
|
|
2302
2363
|
function canonicalProjectId(scopeId) {
|
|
2303
2364
|
if (!boundedText2(scopeId))
|
|
2304
|
-
throw new TypeError("Flow
|
|
2365
|
+
throw new TypeError("Flow runtime scope must be a non-empty path.");
|
|
2305
2366
|
const projectId = resolve2(scopeId);
|
|
2306
2367
|
try {
|
|
2307
2368
|
return realpathSync2(projectId);
|
|
@@ -2349,17 +2410,11 @@ function acquireRegistry() {
|
|
|
2349
2410
|
function snapshot(identity) {
|
|
2350
2411
|
return Object.freeze({ ...identity });
|
|
2351
2412
|
}
|
|
2352
|
-
function makeStatus(
|
|
2413
|
+
function makeStatus(reason) {
|
|
2353
2414
|
const operational = reason === "sole-instance";
|
|
2354
2415
|
return Object.freeze({
|
|
2355
|
-
instanceId: identity.instanceId,
|
|
2356
|
-
registered,
|
|
2357
2416
|
operational,
|
|
2358
|
-
role: operational ? "leader" : registered || reason === "incompatible-registry" ? "indeterminate" : "unregistered",
|
|
2359
2417
|
reason,
|
|
2360
|
-
registeredCount: reason === "incompatible-registry" && !registered ? null : registrations.length,
|
|
2361
|
-
diagnosticLeader: operational ? identity : null,
|
|
2362
|
-
registrations: Object.freeze([...registrations]),
|
|
2363
2418
|
message: operational ? "Flow is active for this project." : `Flow is not operational (${reason}).`
|
|
2364
2419
|
});
|
|
2365
2420
|
}
|
|
@@ -2370,7 +2425,7 @@ var createFlowPluginInstanceId = () => globalThis.crypto.randomUUID();
|
|
|
2370
2425
|
function registerFlowPluginInstance(scopeId, input) {
|
|
2371
2426
|
const projectId = canonicalProjectId(scopeId);
|
|
2372
2427
|
if (!validIdentity(input)) {
|
|
2373
|
-
throw new TypeError("Flow
|
|
2428
|
+
throw new TypeError("Flow runtime identity is invalid.");
|
|
2374
2429
|
}
|
|
2375
2430
|
const identity = snapshot(input);
|
|
2376
2431
|
const registry = acquireRegistry();
|
|
@@ -2383,7 +2438,7 @@ function registerFlowPluginInstance(scopeId, input) {
|
|
|
2383
2438
|
}
|
|
2384
2439
|
const existing = project.get(identity.instanceId);
|
|
2385
2440
|
if (existing && !sameIdentity(existing, identity)) {
|
|
2386
|
-
throw new Error(`Flow
|
|
2441
|
+
throw new Error(`Flow runtime instance ID '${identity.instanceId}' is already registered with different identity data.`);
|
|
2387
2442
|
}
|
|
2388
2443
|
record = existing ?? identity;
|
|
2389
2444
|
project.set(identity.instanceId, record);
|
|
@@ -2391,32 +2446,28 @@ function registerFlowPluginInstance(scopeId, input) {
|
|
|
2391
2446
|
let released = false;
|
|
2392
2447
|
const query = () => {
|
|
2393
2448
|
if (released)
|
|
2394
|
-
return makeStatus(
|
|
2449
|
+
return makeStatus("released");
|
|
2395
2450
|
const currentRegistry = readRegistry();
|
|
2396
2451
|
if (!record)
|
|
2397
|
-
return makeStatus(
|
|
2452
|
+
return makeStatus("incompatible-registry");
|
|
2398
2453
|
if (currentRegistry === undefined)
|
|
2399
|
-
return makeStatus(
|
|
2454
|
+
return makeStatus("not-registered");
|
|
2400
2455
|
if (!compatibleRegistry(currentRegistry))
|
|
2401
|
-
return makeStatus(
|
|
2456
|
+
return makeStatus("incompatible-registry");
|
|
2402
2457
|
const project = currentRegistry.projects.get(projectId);
|
|
2403
|
-
|
|
2404
|
-
|
|
2405
|
-
return makeStatus(identity, "not-registered", registrations);
|
|
2458
|
+
if (!project || project.get(identity.instanceId) !== record) {
|
|
2459
|
+
return makeStatus("not-registered");
|
|
2406
2460
|
}
|
|
2407
2461
|
if (identity.protocolVersion !== FLOW_LEADERSHIP_PROTOCOL_VERSION) {
|
|
2408
|
-
return makeStatus(
|
|
2462
|
+
return makeStatus("incompatible-registry");
|
|
2409
2463
|
}
|
|
2410
|
-
if (
|
|
2411
|
-
return makeStatus(
|
|
2464
|
+
if (project.size !== 1) {
|
|
2465
|
+
return makeStatus("duplicate-instances");
|
|
2412
2466
|
}
|
|
2413
|
-
return makeStatus(
|
|
2467
|
+
return makeStatus("sole-instance");
|
|
2414
2468
|
};
|
|
2415
2469
|
return Object.freeze({
|
|
2416
|
-
identity,
|
|
2417
|
-
scopeId: projectId,
|
|
2418
2470
|
query,
|
|
2419
|
-
isOperational: () => query().operational,
|
|
2420
2471
|
assertOperational(action) {
|
|
2421
2472
|
const current = query();
|
|
2422
2473
|
if (!current.operational) {
|
|
@@ -2476,7 +2527,10 @@ function featureProgress(session) {
|
|
|
2476
2527
|
const completed = session.plan?.features.filter((feature) => isFeatureComplete(session, feature.id)).length ?? 0;
|
|
2477
2528
|
return { completed, total, remaining: total - completed };
|
|
2478
2529
|
}
|
|
2479
|
-
function
|
|
2530
|
+
function activePendingReview(session) {
|
|
2531
|
+
return activeRun(session)?.reviews.find((review) => review.result === null) ?? null;
|
|
2532
|
+
}
|
|
2533
|
+
function nextAction(session, pendingReviewSourceStale = false) {
|
|
2480
2534
|
const status = sessionStatus(session);
|
|
2481
2535
|
if (status === "planning") {
|
|
2482
2536
|
return session.plan ? "flow_plan_approve" : "flow_plan_save";
|
|
@@ -2492,16 +2546,16 @@ function nextAction(session) {
|
|
|
2492
2546
|
const run = activeRun(session);
|
|
2493
2547
|
if (!run)
|
|
2494
2548
|
return "flow_status";
|
|
2495
|
-
|
|
2496
|
-
|
|
2497
|
-
|
|
2549
|
+
if (activePendingReview(session)) {
|
|
2550
|
+
return pendingReviewSourceStale ? "flow_feature_reset" : "dispatch-flow-reviewer";
|
|
2551
|
+
}
|
|
2498
2552
|
const finalRun = session.plan?.features.every((feature) => feature.id === run.featureId || isFeatureComplete(session, feature.id)) ?? false;
|
|
2499
2553
|
const hasPassingValidation = run.validations.some((validation) => validation.exitCode === 0 && validation.outputComplete && (!finalRun || validation.scope === "broad"));
|
|
2500
2554
|
if (!hasPassingValidation)
|
|
2501
2555
|
return "flow_validation_start";
|
|
2502
2556
|
return "flow_review_start";
|
|
2503
2557
|
}
|
|
2504
|
-
function compactProjection(session) {
|
|
2558
|
+
function compactProjection(session, pendingReviewSourceStale = false) {
|
|
2505
2559
|
const run = activeRun(session);
|
|
2506
2560
|
const retryRequest = closureRetryRequest(session);
|
|
2507
2561
|
if (session.closure && !retryRequest) {
|
|
@@ -2516,7 +2570,7 @@ function compactProjection(session) {
|
|
|
2516
2570
|
activeFeatureId: run?.featureId ?? null,
|
|
2517
2571
|
activeRunId: run?.id ?? null,
|
|
2518
2572
|
progress: featureProgress(session),
|
|
2519
|
-
nextAction: nextAction(session),
|
|
2573
|
+
nextAction: nextAction(session, pendingReviewSourceStale),
|
|
2520
2574
|
archiveRetry: retryRequest ? { request: retryRequest } : null
|
|
2521
2575
|
};
|
|
2522
2576
|
}
|
|
@@ -2528,11 +2582,11 @@ function archivedProjection(session) {
|
|
|
2528
2582
|
archived: true
|
|
2529
2583
|
};
|
|
2530
2584
|
}
|
|
2531
|
-
function executionProjection(session) {
|
|
2585
|
+
function executionProjection(session, pendingReviewSourceStale = false) {
|
|
2532
2586
|
const run = activeRun(session);
|
|
2533
2587
|
const feature = session.plan?.features.find((item) => item.id === run?.featureId);
|
|
2534
2588
|
return {
|
|
2535
|
-
...compactProjection(session),
|
|
2589
|
+
...compactProjection(session, pendingReviewSourceStale),
|
|
2536
2590
|
view: "execution",
|
|
2537
2591
|
goal: session.goal,
|
|
2538
2592
|
feature: feature ?? null,
|
|
@@ -2557,6 +2611,7 @@ function reviewerProjection(session, assignmentId) {
|
|
|
2557
2611
|
}
|
|
2558
2612
|
const feature = session.plan?.features.find((item) => item.id === assignment?.featureId);
|
|
2559
2613
|
const plan = session.plan;
|
|
2614
|
+
const assignedValidationIds = new Set(assignment.validationIds);
|
|
2560
2615
|
return {
|
|
2561
2616
|
view: "reviewer",
|
|
2562
2617
|
sessionId: session.id,
|
|
@@ -2571,26 +2626,30 @@ function reviewerProjection(session, assignmentId) {
|
|
|
2571
2626
|
id: candidate.id,
|
|
2572
2627
|
title: candidate.title,
|
|
2573
2628
|
summary: candidate.summary,
|
|
2629
|
+
targets: [...candidate.targets],
|
|
2630
|
+
validation: [...candidate.validation],
|
|
2574
2631
|
dependsOn: [...candidate.dependsOn]
|
|
2575
2632
|
}))
|
|
2576
2633
|
} : null,
|
|
2577
2634
|
feature: feature ?? null,
|
|
2578
2635
|
assignment,
|
|
2579
2636
|
artifactsChanged: run.artifactsChanged,
|
|
2580
|
-
validations: run.validations,
|
|
2637
|
+
validations: run.validations.filter((validation) => assignedValidationIds.has(validation.id)),
|
|
2581
2638
|
completedFeatureIds: plan?.features.filter((candidate) => isFeatureComplete(session, candidate.id)).map((candidate) => candidate.id) ?? []
|
|
2582
2639
|
};
|
|
2583
2640
|
}
|
|
2584
|
-
function project(session, request) {
|
|
2585
|
-
if (request.view === "compact")
|
|
2586
|
-
return compactProjection(session);
|
|
2587
|
-
|
|
2588
|
-
|
|
2641
|
+
function project(session, request, pendingReviewSourceStale = false) {
|
|
2642
|
+
if (request.view === "compact") {
|
|
2643
|
+
return compactProjection(session, pendingReviewSourceStale);
|
|
2644
|
+
}
|
|
2645
|
+
if (request.view === "execution") {
|
|
2646
|
+
return executionProjection(session, pendingReviewSourceStale);
|
|
2647
|
+
}
|
|
2589
2648
|
if (request.view === "reviewer") {
|
|
2590
2649
|
return reviewerProjection(session, request.assignmentId);
|
|
2591
2650
|
}
|
|
2592
2651
|
return {
|
|
2593
|
-
...compactProjection(session),
|
|
2652
|
+
...compactProjection(session, pendingReviewSourceStale),
|
|
2594
2653
|
view: "detail",
|
|
2595
2654
|
goal: session.goal,
|
|
2596
2655
|
plan: session.plan,
|
|
@@ -2607,6 +2666,12 @@ function operationResult(session, operationId, replayed, entity) {
|
|
|
2607
2666
|
...entity === undefined ? {} : { entity }
|
|
2608
2667
|
};
|
|
2609
2668
|
}
|
|
2669
|
+
function featureCompleteResponse(session, request, run, replayed) {
|
|
2670
|
+
return ok(request.result.verdict === "passed" ? "Feature completed." : "Feature blocked by review.", {
|
|
2671
|
+
operation: operationResult(session, request.operationId, replayed, run),
|
|
2672
|
+
projection: compactProjection(session)
|
|
2673
|
+
});
|
|
2674
|
+
}
|
|
2610
2675
|
async function loadExactArchivedClose(transaction, request) {
|
|
2611
2676
|
const archived = await transaction.loadArchive(request.sessionId);
|
|
2612
2677
|
const operation = archived?.operations.find((item) => item.id === request.operationId);
|
|
@@ -2664,6 +2729,33 @@ function createFlowService(repository, environment) {
|
|
|
2664
2729
|
}
|
|
2665
2730
|
});
|
|
2666
2731
|
}
|
|
2732
|
+
if (request.view !== "reviewer" && activePendingReview(session)) {
|
|
2733
|
+
return await repository.transact(async (transaction) => {
|
|
2734
|
+
const current = await transaction.load();
|
|
2735
|
+
if (!current) {
|
|
2736
|
+
return ok("No active Flow session.", {
|
|
2737
|
+
projection: {
|
|
2738
|
+
view: request.view,
|
|
2739
|
+
status: "idle",
|
|
2740
|
+
revision: 0,
|
|
2741
|
+
nextAction: "flow_plan_save"
|
|
2742
|
+
}
|
|
2743
|
+
});
|
|
2744
|
+
}
|
|
2745
|
+
const pending = activePendingReview(current);
|
|
2746
|
+
let pendingReviewSourceStale = false;
|
|
2747
|
+
if (pending) {
|
|
2748
|
+
try {
|
|
2749
|
+
pendingReviewSourceStale = await transaction.computeSourceDigest() !== pending.sourceDigest;
|
|
2750
|
+
} catch (error) {
|
|
2751
|
+
return errorResponse(error, "Repair workspace fingerprinting before recovering this pending review. Do not redispatch the assignment until its source can be checked.");
|
|
2752
|
+
}
|
|
2753
|
+
}
|
|
2754
|
+
return ok("Flow status loaded.", {
|
|
2755
|
+
projection: project(current, request, pendingReviewSourceStale)
|
|
2756
|
+
});
|
|
2757
|
+
});
|
|
2758
|
+
}
|
|
2667
2759
|
return ok("Flow status loaded.", {
|
|
2668
2760
|
projection: project(session, request)
|
|
2669
2761
|
});
|
|
@@ -2774,20 +2866,36 @@ function createFlowService(repository, environment) {
|
|
|
2774
2866
|
if (!assignment)
|
|
2775
2867
|
throw new Error("Unknown review assignment.");
|
|
2776
2868
|
if (await transaction.computeSourceDigest() !== assignment.sourceDigest) {
|
|
2777
|
-
|
|
2869
|
+
return errorResponse(new Error("Workspace content changed after review started."), "Call flow_feature_reset, start a fresh run, and repeat full validation and review. Do not redispatch this source-stale assignment.");
|
|
2778
2870
|
}
|
|
2779
2871
|
}
|
|
2780
2872
|
const result = completeFeature(session, request);
|
|
2781
2873
|
await transaction.save(result.session);
|
|
2782
|
-
return
|
|
2783
|
-
operation: operationResult(result.session, request.operationId, result.replayed, result.value),
|
|
2784
|
-
projection: compactProjection(result.session)
|
|
2785
|
-
});
|
|
2874
|
+
return featureCompleteResponse(result.session, request, result.value, result.replayed);
|
|
2786
2875
|
});
|
|
2787
2876
|
} catch (error) {
|
|
2788
2877
|
return errorResponse(error);
|
|
2789
2878
|
}
|
|
2790
2879
|
},
|
|
2880
|
+
async featureCompleteReplay(input) {
|
|
2881
|
+
try {
|
|
2882
|
+
const request = FeatureCompleteInputSchema.parse(input).request;
|
|
2883
|
+
const session = await repository.read();
|
|
2884
|
+
if (!session)
|
|
2885
|
+
throw new Error("No active Flow session exists.");
|
|
2886
|
+
const priorOperation = session.operations.find((operation) => operation.id === request.operationId);
|
|
2887
|
+
if (priorOperation?.kind !== "feature-complete" || priorOperation.inputDigest !== operationInputDigest(request)) {
|
|
2888
|
+
throw new Error("Only the Flow reviewer may submit a new feature completion; other agents may replay only an exact previously accepted request.");
|
|
2889
|
+
}
|
|
2890
|
+
const result = completeFeature(session, request);
|
|
2891
|
+
if (!result.replayed) {
|
|
2892
|
+
throw new Error("Expected an exact feature-completion replay.");
|
|
2893
|
+
}
|
|
2894
|
+
return featureCompleteResponse(result.session, request, result.value, true);
|
|
2895
|
+
} catch (error) {
|
|
2896
|
+
return errorResponse(error);
|
|
2897
|
+
}
|
|
2898
|
+
},
|
|
2791
2899
|
async featureReset(input) {
|
|
2792
2900
|
try {
|
|
2793
2901
|
const request = FeatureResetInputSchema.parse(input).request;
|
|
@@ -2854,6 +2962,7 @@ var flowPlanApprove = (workspace, input) => service(workspace).planApprove(input
|
|
|
2854
2962
|
var flowRunStart = (workspace, input) => service(workspace).runStart(input);
|
|
2855
2963
|
var flowReviewStart = (workspace, input) => service(workspace).reviewStart(input);
|
|
2856
2964
|
var flowFeatureComplete = (workspace, input) => service(workspace).featureComplete(input);
|
|
2965
|
+
var flowFeatureCompleteReplay = (workspace, input) => service(workspace).featureCompleteReplay(input);
|
|
2857
2966
|
var flowFeatureReset = (workspace, input) => service(workspace).featureReset(input);
|
|
2858
2967
|
var flowSessionClose = (workspace, input) => service(workspace).sessionClose(input);
|
|
2859
2968
|
|
|
@@ -2867,6 +2976,7 @@ function boundedHostText(label, options) {
|
|
|
2867
2976
|
var text = boundedHostText("Text");
|
|
2868
2977
|
var featureId = host.string().max(MAX_SESSION_ID_LENGTH).regex(/^[a-z0-9]+(?:-[a-z0-9]+)*$/);
|
|
2869
2978
|
var operationId = host.string().min(1).max(128).regex(/^[a-zA-Z0-9][a-zA-Z0-9._:-]*$/);
|
|
2979
|
+
var reviewAssignmentId = host.string().min(1).max(256);
|
|
2870
2980
|
var revision = host.number().int().safe().nonnegative();
|
|
2871
2981
|
var guard = { operationId, expectedRevision: revision };
|
|
2872
2982
|
var artifact = host.object({
|
|
@@ -2904,36 +3014,8 @@ var reviewResult = host.object({
|
|
|
2904
3014
|
findings: host.array(reviewFinding).max(MAX_REVIEW_FINDINGS).default([]),
|
|
2905
3015
|
terminalDisposition: host.enum(["submitted", "observed_unsubmitted"])
|
|
2906
3016
|
}).strict().superRefine((result, context) => {
|
|
2907
|
-
const
|
|
2908
|
-
|
|
2909
|
-
if (finding.severity === "blocking" && !finding.evidence) {
|
|
2910
|
-
context.addIssue({
|
|
2911
|
-
code: "custom",
|
|
2912
|
-
path: ["findings", index, "evidence"],
|
|
2913
|
-
message: "A blocking finding requires concrete evidence."
|
|
2914
|
-
});
|
|
2915
|
-
}
|
|
2916
|
-
}
|
|
2917
|
-
if (result.verdict === "failed" && !blocking) {
|
|
2918
|
-
context.addIssue({
|
|
2919
|
-
code: "custom",
|
|
2920
|
-
path: ["findings"],
|
|
2921
|
-
message: "A failed review requires a blocking finding."
|
|
2922
|
-
});
|
|
2923
|
-
}
|
|
2924
|
-
if (result.verdict === "passed" && blocking) {
|
|
2925
|
-
context.addIssue({
|
|
2926
|
-
code: "custom",
|
|
2927
|
-
path: ["findings"],
|
|
2928
|
-
message: "A passed review cannot contain blocking findings."
|
|
2929
|
-
});
|
|
2930
|
-
}
|
|
2931
|
-
if (result.terminalDisposition === "observed_unsubmitted" && result.verdict !== "failed") {
|
|
2932
|
-
context.addIssue({
|
|
2933
|
-
code: "custom",
|
|
2934
|
-
path: ["terminalDisposition"],
|
|
2935
|
-
message: "Observed-but-unsubmitted review work must fail closed."
|
|
2936
|
-
});
|
|
3017
|
+
for (const issue of reviewResultSemanticIssues(result)) {
|
|
3018
|
+
context.addIssue({ code: "custom", ...issue });
|
|
2937
3019
|
}
|
|
2938
3020
|
});
|
|
2939
3021
|
var StatusArgs = {
|
|
@@ -2941,7 +3023,10 @@ var StatusArgs = {
|
|
|
2941
3023
|
host.object({ view: host.literal("compact") }).strict(),
|
|
2942
3024
|
host.object({ view: host.literal("detail") }).strict(),
|
|
2943
3025
|
host.object({ view: host.literal("execution") }).strict(),
|
|
2944
|
-
host.object({
|
|
3026
|
+
host.object({
|
|
3027
|
+
view: host.literal("reviewer"),
|
|
3028
|
+
assignmentId: reviewAssignmentId
|
|
3029
|
+
}).strict()
|
|
2945
3030
|
])
|
|
2946
3031
|
};
|
|
2947
3032
|
var PlanSaveArgs = {
|
|
@@ -2974,7 +3059,7 @@ var FeatureCompleteArgs = {
|
|
|
2974
3059
|
request: host.object({
|
|
2975
3060
|
...guard,
|
|
2976
3061
|
featureId,
|
|
2977
|
-
assignmentId:
|
|
3062
|
+
assignmentId: reviewAssignmentId,
|
|
2978
3063
|
summary: text,
|
|
2979
3064
|
result: reviewResult
|
|
2980
3065
|
}).strict()
|
|
@@ -3016,6 +3101,12 @@ function executeMutation(context, validation, handler) {
|
|
|
3016
3101
|
validation.cancel(context.sessionID);
|
|
3017
3102
|
return execute(context, handler);
|
|
3018
3103
|
}
|
|
3104
|
+
function executeReviewerMutation(context, validation, handler, replayHandler) {
|
|
3105
|
+
if (context.agent !== "flow-reviewer") {
|
|
3106
|
+
return execute(context, replayHandler);
|
|
3107
|
+
}
|
|
3108
|
+
return executeMutation(context, validation, handler);
|
|
3109
|
+
}
|
|
3019
3110
|
function createTools(_ctx, options) {
|
|
3020
3111
|
return {
|
|
3021
3112
|
flow_guidance: tool({
|
|
@@ -3071,9 +3162,9 @@ function createTools(_ctx, options) {
|
|
|
3071
3162
|
execute: (args, context) => executeMutation(context, options.validation, (workspace) => flowReviewStart(workspace, args))
|
|
3072
3163
|
}),
|
|
3073
3164
|
flow_feature_complete: tool({
|
|
3074
|
-
description: "
|
|
3165
|
+
description: "Submit a pending review result; only the reviewer may create a new completion, while exact accepted requests remain replayable.",
|
|
3075
3166
|
args: FeatureCompleteArgs,
|
|
3076
|
-
execute: (args, context) =>
|
|
3167
|
+
execute: (args, context) => executeReviewerMutation(context, options.validation, (workspace) => flowFeatureComplete(workspace, args), (workspace) => flowFeatureCompleteReplay(workspace, args))
|
|
3077
3168
|
}),
|
|
3078
3169
|
flow_feature_reset: tool({
|
|
3079
3170
|
description: "Reset a feature and dependents for a fresh full retry.",
|
|
@@ -3134,7 +3225,7 @@ class ValidationCaptureCoordinator {
|
|
|
3134
3225
|
#prune() {
|
|
3135
3226
|
const cutoff = this.#now() - CAPTURE_TTL_MS;
|
|
3136
3227
|
for (const [sessionID, capture] of this.#pending) {
|
|
3137
|
-
if (capture.armedAt < cutoff) {
|
|
3228
|
+
if (capture.callID === null && capture.armedAt < cutoff) {
|
|
3138
3229
|
this.#pending.delete(sessionID);
|
|
3139
3230
|
}
|
|
3140
3231
|
}
|
|
@@ -3263,17 +3354,18 @@ function createCommandHook(assertOperational) {
|
|
|
3263
3354
|
}
|
|
3264
3355
|
};
|
|
3265
3356
|
}
|
|
3266
|
-
function guardTools(tools,
|
|
3357
|
+
function guardTools(tools, runtimeGuard) {
|
|
3267
3358
|
return Object.fromEntries(Object.entries(tools).map(([name, definition]) => [
|
|
3268
3359
|
name,
|
|
3269
3360
|
{
|
|
3270
3361
|
...definition,
|
|
3271
3362
|
execute: async (...args) => {
|
|
3272
|
-
|
|
3363
|
+
const status = runtimeGuard.query();
|
|
3364
|
+
if (!status.operational) {
|
|
3273
3365
|
return JSON.stringify({
|
|
3274
3366
|
status: "error",
|
|
3275
|
-
summary:
|
|
3276
|
-
workflowData: { runtimeGuard:
|
|
3367
|
+
summary: status.message,
|
|
3368
|
+
workflowData: { runtimeGuard: status }
|
|
3277
3369
|
});
|
|
3278
3370
|
}
|
|
3279
3371
|
return definition.execute(...args);
|
|
@@ -3284,13 +3376,13 @@ function guardTools(tools, leadership) {
|
|
|
3284
3376
|
var FlowPlugin = async (ctx) => {
|
|
3285
3377
|
const log = createFlowLog(ctx);
|
|
3286
3378
|
const version = resolveFlowPluginVersion();
|
|
3287
|
-
const
|
|
3379
|
+
const runtimeGuard = registerFlowPluginInstance(ctx.worktree ?? ctx.directory, {
|
|
3288
3380
|
packageName: "opencode-plugin-flow",
|
|
3289
3381
|
version,
|
|
3290
3382
|
protocolVersion: FLOW_LEADERSHIP_PROTOCOL_VERSION,
|
|
3291
3383
|
instanceId: createFlowPluginInstanceId()
|
|
3292
3384
|
});
|
|
3293
|
-
const initial =
|
|
3385
|
+
const initial = runtimeGuard.query();
|
|
3294
3386
|
log(initial.operational ? "info" : "error", `Flow ${version}: ${initial.message}`);
|
|
3295
3387
|
const validation = new ValidationCaptureCoordinator({
|
|
3296
3388
|
persistObservation: persistWorkspaceValidation
|
|
@@ -3301,10 +3393,10 @@ var FlowPlugin = async (ctx) => {
|
|
|
3301
3393
|
});
|
|
3302
3394
|
return {
|
|
3303
3395
|
config: createConfigHook(ctx, {
|
|
3304
|
-
assertOperational: (action) =>
|
|
3396
|
+
assertOperational: (action) => runtimeGuard.assertOperational(action)
|
|
3305
3397
|
}),
|
|
3306
|
-
tool: guardTools(tools,
|
|
3307
|
-
"command.execute.before": createCommandHook((action) =>
|
|
3398
|
+
tool: guardTools(tools, runtimeGuard),
|
|
3399
|
+
"command.execute.before": createCommandHook((action) => runtimeGuard.assertOperational(action)),
|
|
3308
3400
|
event: async (input) => {
|
|
3309
3401
|
const event = input.event;
|
|
3310
3402
|
if (event.type !== "session.idle" && event.type !== "session.compacted") {
|
|
@@ -3329,7 +3421,7 @@ var FlowPlugin = async (ctx) => {
|
|
|
3329
3421
|
}
|
|
3330
3422
|
},
|
|
3331
3423
|
dispose: async () => {
|
|
3332
|
-
|
|
3424
|
+
runtimeGuard.release();
|
|
3333
3425
|
}
|
|
3334
3426
|
};
|
|
3335
3427
|
};
|
|
@@ -3338,4 +3430,4 @@ export {
|
|
|
3338
3430
|
plugin_default as default
|
|
3339
3431
|
};
|
|
3340
3432
|
|
|
3341
|
-
//# debugId=
|
|
3433
|
+
//# debugId=E5E0649B3D35337264756E2164756E21
|