opencode-plugin-flow 6.1.0 → 6.2.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
@@ -235,26 +235,29 @@ that separate action.
235
235
 
236
236
  Work serially by default. After manager orientation, fan out only when at least
237
237
  two genuinely independent slices can be named. Run one cohort of two or three
238
- \`flow-worker\` instances at a time. Launch the cohort concurrently through
239
- OpenCode's native task/subagent facility; do not wait for one slice before
240
- launching the next. Each prompt must name a stable slice id, the exact outcome
241
- and read or write scope, expected coverage and checks, dependencies, and a stop
242
- condition. Edit scopes must be exact and non-overlapping. Shared contracts,
243
- lockfiles, and generated outputs remain manager-owned unless one worker
244
- receives the whole relevant scope.
238
+ \`flow-worker\` instances at a time. Issue every cohort Task call in the same
239
+ assistant tool-use turn before consuming any result. If the host or model
240
+ serializes those calls, treat and report that execution as serial instead of
241
+ claiming parallelism. Each prompt must name a stable slice id, the exact outcome
242
+ and read or write scope, expected coverage, recommended manager checks,
243
+ dependencies, and a stop condition. Edit scopes must be exact and
244
+ non-overlapping. Shared contracts, lockfiles, and generated outputs remain
245
+ manager-owned unless one worker receives the whole relevant scope.
245
246
 
246
247
  Workers cannot call Flow tools or spawn children. Each returns one concise
247
248
  handoff containing status, scope and coverage, evidence or changed paths,
248
- checks, gaps and risks, and integration notes. Missing, partial, or blocked
249
- output remains an explicit coverage gap; worker checks are advisory.
249
+ recommended manager checks, gaps and risks, and integration notes. Workers do
250
+ not run Bash; all executable checks remain manager-owned. Missing, partial, or
251
+ blocked output remains an explicit coverage gap.
250
252
 
251
- After all workers stop, inspect the combined diff and evidence and reconcile
253
+ After all workers stop, compare actual changed paths with every assigned scope,
254
+ then inspect the combined diff and evidence and reconcile unexpected paths or
252
255
  conflicts before validation. At most one targeted follow-up wave may address a
253
- failed slice, newly unlocked dependency, or material claim verification. Do not
254
- start an automatic third wave. Coordination stays in the conversation: create
255
- no manifest, sidecar, Session field, durable handoff, or recovery ledger. After
256
- an interruption, inspect Flow status and the worktree and treat partial worker
257
- edits as untrusted.
256
+ failed slice, newly unlocked dependency, or material claim verification. Do
257
+ not start an automatic third wave. Coordination stays in the conversation:
258
+ create no manifest, sidecar, Session field, durable handoff, or recovery ledger.
259
+ After an interruption, inspect Flow status and the worktree and treat partial
260
+ worker edits as untrusted.
258
261
 
259
262
  ## Validate
260
263
 
@@ -348,9 +351,11 @@ You may run concurrently with sibling workers. Do not enter their scopes, assume
348
351
  - Use the manager assignment as your only source of Flow lifecycle context. Do not call any \`flow_*\` tool, including \`flow_status\`.
349
352
  - Do not delegate, spawn subtasks, or load skills.
350
353
  - Do not stage, commit, push, publish, or create a release.
354
+ - Do not run Bash commands. The manager owns every executable check.
355
+ - Never edit .flow or .git metadata paths; the host denies those paths.
351
356
  - A read-only evidence slice must not edit files.
352
357
  - 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
- - Run only checks relevant to the assigned slice. These checks are advisory: the manager owns integration and performs authoritative combined validation after all workers have stopped.
358
+ - 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
359
 
355
360
  ## Handoff
356
361
 
@@ -365,8 +370,8 @@ success | partial | blocked
365
370
  ## Findings / changed paths
366
371
  - Evidence found or exact paths changed
367
372
 
368
- ## Checks
369
- - Commands run and outcomes, or not run with reason
373
+ ## Recommended manager checks
374
+ - Exact checks the manager should run, or none
370
375
 
371
376
  ## Gaps & risks
372
377
  - Missing coverage, blockers, conflicts, or none
@@ -449,8 +454,14 @@ var FLOW_CORE_AGENTS = {
449
454
  description: "Bounded worker for one read-only evidence or exact-scope implementation slice.",
450
455
  prompt: compileFlowPromptSurface("flow-worker"),
451
456
  permission: {
452
- edit: "ask",
453
- bash: "ask",
457
+ edit: {
458
+ "*": "allow",
459
+ ".flow": "deny",
460
+ ".flow/**": "deny",
461
+ ".git": "deny",
462
+ ".git/**": "deny"
463
+ },
464
+ bash: "deny",
454
465
  external_directory: "deny",
455
466
  skill: "deny",
456
467
  task: { "*": "deny" },
@@ -695,6 +706,39 @@ function planIssue(plan) {
695
706
  return visited === plan.features.length ? null : "The plan dependency graph is cyclic.";
696
707
  }
697
708
 
709
+ // src/domain/session.ts
710
+ function reviewResultSemanticIssues(result) {
711
+ const issues = [];
712
+ const blocking = result.findings.some((finding) => finding.severity === "blocking");
713
+ for (const [index, finding] of result.findings.entries()) {
714
+ if (finding.severity === "blocking" && !finding.evidence?.trim()) {
715
+ issues.push({
716
+ path: ["findings", index, "evidence"],
717
+ message: "A blocking finding requires concrete evidence."
718
+ });
719
+ }
720
+ }
721
+ if (result.verdict === "failed" && !blocking) {
722
+ issues.push({
723
+ path: ["findings"],
724
+ message: "A failed review requires a blocking finding."
725
+ });
726
+ }
727
+ if (result.verdict === "passed" && blocking) {
728
+ issues.push({
729
+ path: ["findings"],
730
+ message: "A passed review cannot contain blocking findings."
731
+ });
732
+ }
733
+ if (result.terminalDisposition === "observed_unsubmitted" && result.verdict !== "failed") {
734
+ issues.push({
735
+ path: ["terminalDisposition"],
736
+ message: "Observed-but-unsubmitted review work must fail closed."
737
+ });
738
+ }
739
+ return issues;
740
+ }
741
+
698
742
  // src/domain/transitions.ts
699
743
  class FlowTransitionError extends Error {
700
744
  code = "FLOW_TRANSITION_REJECTED";
@@ -1030,19 +1074,9 @@ function assertReviewResult(result) {
1030
1074
  if (result.findings.length > MAX_REVIEW_FINDINGS) {
1031
1075
  fail(`A review may contain at most ${MAX_REVIEW_FINDINGS} findings.`);
1032
1076
  }
1033
- const blocking = result.findings.some((finding) => finding.severity === "blocking");
1034
- const unsupported = result.findings.some((finding) => finding.severity === "blocking" && !finding.evidence?.trim());
1035
- if (unsupported)
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
- }
1077
+ const issue = reviewResultSemanticIssues(result)[0];
1078
+ if (issue)
1079
+ fail(issue.message);
1046
1080
  }
1047
1081
  function completeFeature(session, input) {
1048
1082
  assertReviewResult(input.result);
@@ -1453,36 +1487,8 @@ var PublicReviewResultSchema = z.object({
1453
1487
  findings: z.array(ReviewFindingSchema).max(MAX_REVIEW_FINDINGS).default([]),
1454
1488
  terminalDisposition: z.enum(["submitted", "observed_unsubmitted"])
1455
1489
  }).strict().superRefine((result, context) => {
1456
- const blocking = result.findings.some((finding) => finding.severity === "blocking");
1457
- for (const [index, finding] of result.findings.entries()) {
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
- });
1490
+ for (const issue of reviewResultSemanticIssues(result)) {
1491
+ context.addIssue({ code: "custom", ...issue });
1486
1492
  }
1487
1493
  });
1488
1494
  var ValidationObservationSchema = z.object({
@@ -2301,7 +2307,7 @@ function validIdentity(value) {
2301
2307
  }
2302
2308
  function canonicalProjectId(scopeId) {
2303
2309
  if (!boundedText2(scopeId))
2304
- throw new TypeError("Flow leadership scope ID must be a non-empty path.");
2310
+ throw new TypeError("Flow runtime scope must be a non-empty path.");
2305
2311
  const projectId = resolve2(scopeId);
2306
2312
  try {
2307
2313
  return realpathSync2(projectId);
@@ -2349,17 +2355,11 @@ function acquireRegistry() {
2349
2355
  function snapshot(identity) {
2350
2356
  return Object.freeze({ ...identity });
2351
2357
  }
2352
- function makeStatus(identity, reason, registrations = [], registered = false) {
2358
+ function makeStatus(reason) {
2353
2359
  const operational = reason === "sole-instance";
2354
2360
  return Object.freeze({
2355
- instanceId: identity.instanceId,
2356
- registered,
2357
2361
  operational,
2358
- role: operational ? "leader" : registered || reason === "incompatible-registry" ? "indeterminate" : "unregistered",
2359
2362
  reason,
2360
- registeredCount: reason === "incompatible-registry" && !registered ? null : registrations.length,
2361
- diagnosticLeader: operational ? identity : null,
2362
- registrations: Object.freeze([...registrations]),
2363
2363
  message: operational ? "Flow is active for this project." : `Flow is not operational (${reason}).`
2364
2364
  });
2365
2365
  }
@@ -2370,7 +2370,7 @@ var createFlowPluginInstanceId = () => globalThis.crypto.randomUUID();
2370
2370
  function registerFlowPluginInstance(scopeId, input) {
2371
2371
  const projectId = canonicalProjectId(scopeId);
2372
2372
  if (!validIdentity(input)) {
2373
- throw new TypeError("Flow leadership identity is invalid.");
2373
+ throw new TypeError("Flow runtime identity is invalid.");
2374
2374
  }
2375
2375
  const identity = snapshot(input);
2376
2376
  const registry = acquireRegistry();
@@ -2383,7 +2383,7 @@ function registerFlowPluginInstance(scopeId, input) {
2383
2383
  }
2384
2384
  const existing = project.get(identity.instanceId);
2385
2385
  if (existing && !sameIdentity(existing, identity)) {
2386
- throw new Error(`Flow leadership instance ID '${identity.instanceId}' is already registered with different identity data.`);
2386
+ throw new Error(`Flow runtime instance ID '${identity.instanceId}' is already registered with different identity data.`);
2387
2387
  }
2388
2388
  record = existing ?? identity;
2389
2389
  project.set(identity.instanceId, record);
@@ -2391,32 +2391,28 @@ function registerFlowPluginInstance(scopeId, input) {
2391
2391
  let released = false;
2392
2392
  const query = () => {
2393
2393
  if (released)
2394
- return makeStatus(identity, "released");
2394
+ return makeStatus("released");
2395
2395
  const currentRegistry = readRegistry();
2396
2396
  if (!record)
2397
- return makeStatus(identity, "incompatible-registry");
2397
+ return makeStatus("incompatible-registry");
2398
2398
  if (currentRegistry === undefined)
2399
- return makeStatus(identity, "not-registered");
2399
+ return makeStatus("not-registered");
2400
2400
  if (!compatibleRegistry(currentRegistry))
2401
- return makeStatus(identity, "incompatible-registry");
2401
+ return makeStatus("incompatible-registry");
2402
2402
  const project = currentRegistry.projects.get(projectId);
2403
- const registrations = project ? [...project.values()].map(snapshot) : [];
2404
- if (project?.get(identity.instanceId) !== record) {
2405
- return makeStatus(identity, "not-registered", registrations);
2403
+ if (!project || project.get(identity.instanceId) !== record) {
2404
+ return makeStatus("not-registered");
2406
2405
  }
2407
2406
  if (identity.protocolVersion !== FLOW_LEADERSHIP_PROTOCOL_VERSION) {
2408
- return makeStatus(identity, "incompatible-registry", registrations, true);
2407
+ return makeStatus("incompatible-registry");
2409
2408
  }
2410
- if (registrations.length !== 1) {
2411
- return makeStatus(identity, "duplicate-instances", registrations, true);
2409
+ if (project.size !== 1) {
2410
+ return makeStatus("duplicate-instances");
2412
2411
  }
2413
- return makeStatus(identity, "sole-instance", registrations, true);
2412
+ return makeStatus("sole-instance");
2414
2413
  };
2415
2414
  return Object.freeze({
2416
- identity,
2417
- scopeId: projectId,
2418
2415
  query,
2419
- isOperational: () => query().operational,
2420
2416
  assertOperational(action) {
2421
2417
  const current = query();
2422
2418
  if (!current.operational) {
@@ -2904,36 +2900,8 @@ var reviewResult = host.object({
2904
2900
  findings: host.array(reviewFinding).max(MAX_REVIEW_FINDINGS).default([]),
2905
2901
  terminalDisposition: host.enum(["submitted", "observed_unsubmitted"])
2906
2902
  }).strict().superRefine((result, context) => {
2907
- const blocking = result.findings.some((finding) => finding.severity === "blocking");
2908
- for (const [index, finding] of result.findings.entries()) {
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
- });
2903
+ for (const issue of reviewResultSemanticIssues(result)) {
2904
+ context.addIssue({ code: "custom", ...issue });
2937
2905
  }
2938
2906
  });
2939
2907
  var StatusArgs = {
@@ -3134,7 +3102,7 @@ class ValidationCaptureCoordinator {
3134
3102
  #prune() {
3135
3103
  const cutoff = this.#now() - CAPTURE_TTL_MS;
3136
3104
  for (const [sessionID, capture] of this.#pending) {
3137
- if (capture.armedAt < cutoff) {
3105
+ if (capture.callID === null && capture.armedAt < cutoff) {
3138
3106
  this.#pending.delete(sessionID);
3139
3107
  }
3140
3108
  }
@@ -3263,17 +3231,18 @@ function createCommandHook(assertOperational) {
3263
3231
  }
3264
3232
  };
3265
3233
  }
3266
- function guardTools(tools, leadership) {
3234
+ function guardTools(tools, runtimeGuard) {
3267
3235
  return Object.fromEntries(Object.entries(tools).map(([name, definition]) => [
3268
3236
  name,
3269
3237
  {
3270
3238
  ...definition,
3271
3239
  execute: async (...args) => {
3272
- if (!leadership.isOperational()) {
3240
+ const status = runtimeGuard.query();
3241
+ if (!status.operational) {
3273
3242
  return JSON.stringify({
3274
3243
  status: "error",
3275
- summary: "Flow is disabled because more than one runtime is registered for this project.",
3276
- workflowData: { runtimeGuard: leadership.query() }
3244
+ summary: status.message,
3245
+ workflowData: { runtimeGuard: status }
3277
3246
  });
3278
3247
  }
3279
3248
  return definition.execute(...args);
@@ -3284,13 +3253,13 @@ function guardTools(tools, leadership) {
3284
3253
  var FlowPlugin = async (ctx) => {
3285
3254
  const log = createFlowLog(ctx);
3286
3255
  const version = resolveFlowPluginVersion();
3287
- const leadership = registerFlowPluginInstance(ctx.worktree ?? ctx.directory, {
3256
+ const runtimeGuard = registerFlowPluginInstance(ctx.worktree ?? ctx.directory, {
3288
3257
  packageName: "opencode-plugin-flow",
3289
3258
  version,
3290
3259
  protocolVersion: FLOW_LEADERSHIP_PROTOCOL_VERSION,
3291
3260
  instanceId: createFlowPluginInstanceId()
3292
3261
  });
3293
- const initial = leadership.query();
3262
+ const initial = runtimeGuard.query();
3294
3263
  log(initial.operational ? "info" : "error", `Flow ${version}: ${initial.message}`);
3295
3264
  const validation = new ValidationCaptureCoordinator({
3296
3265
  persistObservation: persistWorkspaceValidation
@@ -3301,10 +3270,10 @@ var FlowPlugin = async (ctx) => {
3301
3270
  });
3302
3271
  return {
3303
3272
  config: createConfigHook(ctx, {
3304
- assertOperational: (action) => leadership.assertOperational(action)
3273
+ assertOperational: (action) => runtimeGuard.assertOperational(action)
3305
3274
  }),
3306
- tool: guardTools(tools, leadership),
3307
- "command.execute.before": createCommandHook((action) => leadership.assertOperational(action)),
3275
+ tool: guardTools(tools, runtimeGuard),
3276
+ "command.execute.before": createCommandHook((action) => runtimeGuard.assertOperational(action)),
3308
3277
  event: async (input) => {
3309
3278
  const event = input.event;
3310
3279
  if (event.type !== "session.idle" && event.type !== "session.compacted") {
@@ -3329,7 +3298,7 @@ var FlowPlugin = async (ctx) => {
3329
3298
  }
3330
3299
  },
3331
3300
  dispose: async () => {
3332
- leadership.release();
3301
+ runtimeGuard.release();
3333
3302
  }
3334
3303
  };
3335
3304
  };
@@ -3338,4 +3307,4 @@ export {
3338
3307
  plugin_default as default
3339
3308
  };
3340
3309
 
3341
- //# debugId=D206226F320FFC7564756E2164756E21
3310
+ //# debugId=48BDB7D8B1F2A60764756E2164756E21