@kontourai/flow-agents 3.4.2 → 3.5.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.
Files changed (37) hide show
  1. package/CHANGELOG.md +14 -0
  2. package/agents/dev.json +0 -5
  3. package/build/src/builder-flow-runtime.js +18 -12
  4. package/build/src/cli/assignment-provider.d.ts +3 -0
  5. package/build/src/cli/assignment-provider.js +59 -10
  6. package/build/src/cli/workflow-sidecar.js +186 -26
  7. package/build/src/lib/flow-resolver.js +7 -2
  8. package/build/src/tools/build-universal-bundles.js +0 -2
  9. package/context/contracts/assignment-provider-contract.md +5 -2
  10. package/context/scripts/hooks/workflow-steering.js +2 -40
  11. package/docs/spec/builder-flow-runtime.md +19 -7
  12. package/docs/spec/runtime-hook-surface.md +4 -4
  13. package/evals/integration/test_assignment_provider_local_file.sh +54 -0
  14. package/evals/integration/test_builder_entry_enforcement.sh +369 -22
  15. package/evals/integration/test_builder_step_producers.sh +1 -1
  16. package/evals/integration/test_bundle_install.sh +46 -3
  17. package/evals/integration/test_current_json_per_actor.sh +3 -2
  18. package/evals/integration/test_dual_emit_flow_step.sh +43 -9
  19. package/evals/integration/test_flowdef_session_activation.sh +15 -6
  20. package/evals/integration/test_goal_fit_escape_hatch.sh +3 -3
  21. package/evals/integration/test_install_merge.sh +24 -0
  22. package/evals/integration/test_phase_map_and_gate_claim.sh +14 -10
  23. package/evals/integration/test_resolvefirststep_security.sh +1 -1
  24. package/evals/integration/test_sidecar_field_preservation.sh +4 -2
  25. package/evals/integration/test_takeover_protocol.sh +1 -1
  26. package/evals/integration/test_workflow_sidecar_writer.sh +18 -6
  27. package/evals/integration/test_workflow_steering_hook.sh +0 -72
  28. package/package.json +1 -1
  29. package/schemas/workflow-state.schema.json +1 -0
  30. package/scripts/hooks/workflow-steering.js +2 -40
  31. package/scripts/install-merge.js +2 -0
  32. package/src/builder-flow-runtime.ts +23 -12
  33. package/src/cli/assignment-provider.ts +55 -11
  34. package/src/cli/builder-flow-runtime.test.mjs +32 -0
  35. package/src/cli/workflow-sidecar.ts +183 -28
  36. package/src/lib/flow-resolver.ts +6 -2
  37. package/src/tools/build-universal-bundles.ts +0 -2
@@ -363,7 +363,6 @@ function exportClaudeSettings() {
363
363
  hooks.UserPromptSubmit.push({ hooks: [shellHook(claudePolicy("UserPromptSubmit", "workflow-steering.js"), 30, "Running Flow Agents hook policy")] });
364
364
  hooks.PostToolUse.push({ hooks: [shellHook(claudePolicy("PostToolUse", "quality-gate.js"), 30, "Running Flow Agents hook policy")] });
365
365
  hooks.PostToolUse.push({ hooks: [shellHook(claudePolicy("PostToolUse", "evidence-capture.js"), 30, "Capturing Flow Agents command evidence")] });
366
- hooks.PreToolUse.push({ hooks: [shellHook(claudePolicy("PreToolUse", "workflow-steering.js"), 30, "Enforcing Flow Agents projected action")] });
367
366
  hooks.PreToolUse.push({ hooks: [shellHook(claudePolicy("PreToolUse", "config-protection.js"), 30, "Running Flow Agents hook policy")] });
368
367
  return `${JSON.stringify({
369
368
  statusLine: { type: "command", command: 'bash -lc \'root="${CLAUDE_PROJECT_DIR:-$(pwd)}"; node "$root/scripts/statusline/flow-agents-statusline.js"\'' },
@@ -381,7 +380,6 @@ function exportCodexHooks() {
381
380
  hooks.SessionStart.push({ hooks: [shellHook(codexPolicy("workflow-steering.js"), 30, "Running Flow Agents hook policy")] });
382
381
  hooks.UserPromptSubmit.push({ hooks: [shellHook(codexPolicy("workflow-steering.js"), 30, "Running Flow Agents hook policy")] });
383
382
  hooks.PostToolUse.push({ hooks: [shellHook(codexPolicy("evidence-capture.js"), 30, "Capturing Flow Agents command evidence")] });
384
- hooks.PreToolUse.push({ hooks: [shellHook(codexPolicy("workflow-steering.js"), 30, "Enforcing Flow Agents projected action")] });
385
383
  return `${JSON.stringify({ hooks }, null, 2)}\n`;
386
384
  }
387
385
  function copySharedContent(targetRoot, targetName, token) {
@@ -138,8 +138,10 @@ detect an incompatible future shape before parsing fields it does not understand
138
138
  {
139
139
  "schema_version": "1.0",
140
140
  "role": "AssignmentClaimRecord",
141
- "subject_id": "kontourai/flow-agents#290",
141
+ "subject_id": "kontourai-flow-agents-290",
142
142
  "actor": { "runtime": "claude-code", "session_id": "...", "host": "...", "human": null },
143
+ "actor_key": "claude-code:...:host",
144
+ "work_item_ref": "kontourai/flow-agents#290",
143
145
  "claimed_at": "2026-07-02T00:00:00Z",
144
146
  "ttl_seconds": 1800,
145
147
  "branch": "agent/<actor>/<slug>",
@@ -152,9 +154,10 @@ detect an incompatible future shape before parsing fields it does not understand
152
154
  | --- | --- | --- |
153
155
  | `schema_version` | yes | Version of this record shape, `"1.0"`. |
154
156
  | `role` | yes | Constant `"AssignmentClaimRecord"`, for readers scanning mixed content (e.g. a GitHub comment thread) for this record type. |
155
- | `subject_id` | yes | The claimed work item, in `owner/repo#id` form the same string `workItemSlug` derives the deterministic session slug from. |
157
+ | `subject_id` | yes | The provider subject key. The local-file provider uses the deterministic session slug; provider-backed records may use their native subject identifier. |
156
158
  | `actor` | yes | `{ runtime, session_id, host, human? }` — the exact struct `actor-identity.js` defines. `human` is set (non-null) only for a human assignee; its presence, not a username heuristic, gates the human-held join state. |
157
159
  | `actor_key` | no (additive, #291) | The canonical `resolveActor(env).actor` string for the claiming actor — the same flat/bare token `liveness whoami`, `liveness claim --actor`, per-actor `current.json`, and pull-work's `--self-actor` all use. When present, `computeEffectiveState` compares against THIS (not a re-serialization of `actor`) for both self-recognition and the liveness join, because `serializeActor(actor)` and `resolveActor(env).actor` diverge for an explicit-override actor (a bare token vs. a `explicit-override:<value>:<host>` triple) while agreeing for a derived actor. Absent on any pre-#291 record or fixture — `computeEffectiveState` falls back to `serializeActor(actor)` in that case, reproducing pre-#291 behavior exactly. |
160
+ | `work_item_ref` | no (additive, #541) | Exact Work Item reference acquired by Builder `ensure-session`. This preserves identity beyond the local provider's lossy filesystem slug and allows an interrupted acquisition to retry safely. Older records and ordinary assignment-provider claims omit it and cannot automatically satisfy `builder.pull-work.selected`. |
158
161
  | `claimed_at` | yes | ISO-8601 timestamp the claim was recorded. Mirrors the liveness stream's own claim-event field so a reader compares freshness with one mental model across both layers, even though the two are stored in different media. |
159
162
  | `ttl_seconds` | yes | Same field name/semantics as the liveness stream's `ttlSeconds` (default `1800`). |
160
163
  | `branch` | yes | The branch this actor is working on, per the `agent/<actor>/<slug>` convention. |
@@ -9,8 +9,7 @@
9
9
  * survive context loss instead of relying on the model voluntarily re-reading
10
10
  * the sidecar.
11
11
  *
12
- * Advisory by default. A structured next action may explicitly require its
13
- * projected command before unrelated tool use; only that PreToolUse case blocks.
12
+ * Non-blocking always exits 0.
14
13
  */
15
14
 
16
15
  'use strict';
@@ -283,32 +282,6 @@ function stateSteering(root) {
283
282
  return parts.join(' ');
284
283
  }
285
284
 
286
- function normalizedCommand(value) {
287
- return typeof value === 'string' ? value.replace(/\s+/g, ' ').trim() : '';
288
- }
289
-
290
- function toolCommands(input) {
291
- const toolInput = input && input.tool_input && typeof input.tool_input === 'object' ? input.tool_input : {};
292
- return [
293
- toolInput.command,
294
- toolInput.content && toolInput.content.command,
295
- toolInput.args && toolInput.args.command,
296
- ].map(normalizedCommand).filter(Boolean);
297
- }
298
-
299
- function beforeToolUseEnforcement(input, current) {
300
- if (!input || input.hook_event_name !== 'PreToolUse' || !current) return null;
301
- const next = current.payload && current.payload.next_action;
302
- if (!next || next.status !== 'continue' || next.enforcement !== 'before_tool_use') return null;
303
- const expected = normalizedCommand(next.command);
304
- if (!expected) return null;
305
- if (toolCommands(input).some(command => command === expected)) return null;
306
- return {
307
- exitCode: 2,
308
- stderr: `[workflow-entry] Run the required projected action before using other tools. Run exactly: ${safeStateText(expected, 500)}`,
309
- };
310
- }
311
-
312
285
  function contextMapSteering(root) {
313
286
  const mapPath = path.join(root, 'docs', 'context-map.md');
314
287
  if (!fs.existsSync(mapPath)) return '';
@@ -599,8 +572,6 @@ function run(rawInput) {
599
572
  const toolInput = input.tool_input || {};
600
573
  const root = findRepoRoot(input.cwd || process.cwd());
601
574
  const current = latestWorkflowState(root);
602
- const enforcement = beforeToolUseEnforcement(input, current);
603
- if (enforcement) return enforcement;
604
575
  const hints = [];
605
576
  let shouldAppendWorkflowContext = false;
606
577
 
@@ -677,14 +648,7 @@ if (require.main === module) {
677
648
  data += chunk;
678
649
  });
679
650
  process.stdin.on('end', () => {
680
- const result = run(data);
681
- if (result && typeof result === 'object') {
682
- if (result.stderr) process.stderr.write(String(result.stderr) + (String(result.stderr).endsWith('\n') ? '' : '\n'));
683
- if (Object.prototype.hasOwnProperty.call(result, 'stdout')) process.stdout.write(String(result.stdout || ''));
684
- process.exitCode = Number.isInteger(result.exitCode) ? result.exitCode : 0;
685
- } else {
686
- process.stdout.write(String(result));
687
- }
651
+ process.stdout.write(String(run(data)));
688
652
  });
689
653
  }
690
654
 
@@ -703,6 +667,4 @@ module.exports = {
703
667
  promptText,
704
668
  looksLikeImplementationWork,
705
669
  kitWorkflowSteering,
706
- beforeToolUseEnforcement,
707
- toolCommands,
708
670
  };
@@ -28,18 +28,30 @@ expectations, and Builder Kit's structured action map.
28
28
 
29
29
  ## Entry And Synchronization
30
30
 
31
- A Builder session must first be created at the Flow Definition's entry step by the
32
- workflow sidecar. Start the canonical run with:
31
+ A Builder session is created at the Flow Definition's entry step by the workflow
32
+ sidecar. If automatic startup reports a failure, retry the canonical operation with:
33
33
 
34
34
  ```bash
35
35
  flow-agents builder-run start --session-dir .kontourai/flow-agents/<slug>
36
36
  ```
37
37
 
38
- The initial sidecar projects this as `next_action.command` with
39
- `enforcement: "before_tool_use"`. Runtime adapters deny unrelated tool calls until
40
- the projected command runs. Starting the canonical run replaces that bootstrap
41
- action with the ordinary Flow-step projection; later actions remain advisory while
42
- the agent performs their declared skills and operations.
38
+ `ensure-session --flow-id builder.build` starts or loads this canonical run before
39
+ returning. When the local assignment provider durably assigns the exact Work Item
40
+ to the resolved workflow actor, `ensure-session` rereads that record and produces
41
+ the declared `builder.pull-work.selected` claim through the normal Surface trust
42
+ bundle path. Flow evaluates that subject-bound evidence and advances to
43
+ `design-probe`; Flow Agents does not write a transition or gate outcome directly.
44
+ Skipped ownership, unresolved actors, precomputed state, and non-local providers do
45
+ not produce selection evidence and remain at `pull-work`.
46
+
47
+ The command remains the explicit recovery path if Flow startup fails after sidecar
48
+ creation; the failure is returned to the caller and no substitute run state is
49
+ invented. Runtime hooks keep projected actions advisory while the agent performs
50
+ their declared skills and operations.
51
+
52
+ Sidecars written by 3.4.2 may still contain `next_action.enforcement`. The 1.0
53
+ schema accepts that deprecated field for artifact compatibility, but current
54
+ runtime steering ignores it and does not install a PreToolUse bootstrap hook.
43
55
 
44
56
  `start` requires exactly one `state.work_item_refs` entry and uses that stable Work
45
57
  Item reference as the Flow run subject. It is idempotent for an existing canonical
@@ -123,16 +123,16 @@ Flow Agents currently ships five canonical policy classes. Each policy class has
123
123
 
124
124
  **Canonical script**: `scripts/hooks/workflow-steering.js`
125
125
 
126
- **Canonical trigger event**: `userPromptSubmit` and `agentSpawn`/`SessionStart` (active-goal re-grounding), `postToolUse` (after `InvokeSubagents` tool calls), and `preToolUse` when the projected `next_action` explicitly declares `enforcement: "before_tool_use"`
126
+ **Canonical trigger event**: `userPromptSubmit` and `agentSpawn`/`SessionStart` (active-goal re-grounding), `postToolUse` (after `InvokeSubagents` tool calls)
127
127
 
128
128
  **Inputs consumed**:
129
129
  - `.kontourai/flow-agents/<slug>/state.json` — current workflow phase and status
130
130
  - `.kontourai/flow-agents/<slug>/critique.json` — open critique findings
131
131
  - `docs/context-map.md` — structure hint for repo navigation
132
132
 
133
- **Decision contract**: Advisory by default. It appends steering text to the agent's context via `additionalContext` and re-grounds the active workflow goal (status, phase, recorded next step) at the start of every user turn — not only for flagged/blocked states — and on `SessionStart`, which fires after context compaction and on resume. A structured `next_action` may explicitly declare a non-empty `command` with `enforcement: "before_tool_use"`; at `PreToolUse`, the canonical script then exits 2 for every unrelated call and includes the exact projected command in the denial reason. The exact projected command is allowed. This narrow bootstrap policy keeps workflow authority in the projected state instead of relying on the model voluntarily following prompt text; ordinary Flow-step projections remain advisory.
133
+ **Decision contract**: Non-blocking. Always exits 0. Appends steering text to the agent's context via `additionalContext`. It re-grounds the active workflow goal (status, phase, recorded next step) at the start of every user turn — not only for flagged/blocked states — and on `SessionStart`, which fires after context compaction and on resume. Canonical Builder run creation is part of session orchestration rather than a model-mediated hook action.
134
134
 
135
- **Degradation when host lacks trigger**: If the host has no `userPromptSubmit`-equivalent hook, workflow steering is silent and the agent receives no ambient phase reminders at turn start. If the host has no block-capable `preToolUse` equivalent, explicit projected-action enforcement degrades to the same advisory command guidance. These are capability losses, not reasons to invent a second lifecycle authority. Record each missing capability in the adapter's conformance declaration.
135
+ **Degradation when host lacks trigger**: If the host has no `userPromptSubmit`-equivalent hook, workflow steering is silent. The agent receives no ambient phase reminders at turn start. This is a capability loss, not a blocking failure. Log the gap in the adapter's conformance declaration.
136
136
 
137
137
  **Codex live hook influence caveat**: Codex hook influence on live sessions is limited — the agent may not honor all injected context. The hook influence behavioral cases in `evals/fixtures/hook-influence/cases.json` document which behaviors are expected (`agent_must_do`) versus which may only be soft guidance. Adapters on similar runtimes should apply the same classification.
138
138
 
@@ -550,7 +550,7 @@ For structured `run()` responses (native import form), the return value is:
550
550
  | config-protection | Fail-closed (exit 2 on protected file) | Yes — hook runtime errors exit 0 | Yes (preToolUse) |
551
551
  | quality-gate | Fail-open (exit 0 always) | Yes | No |
552
552
  | stop-goal-fit | Engine default warn (fail-open); blocks in `FLOW_AGENTS_GOAL_FIT_MODE=block` (shipped L2 default) | Yes — hook runtime errors exit 0 | Yes (stop, block mode) |
553
- | workflow-steering | Advisory; exit 2 only for an explicit `before_tool_use` projected command | Yes — hook runtime and malformed-state errors exit 0 | Yes (preToolUse, explicit projection only) |
553
+ | workflow-steering | Fail-open (exit 0 always) | Yes | No |
554
554
  | evidence-capture | Fail-open (exit 0 always) | Yes — capture errors never block or corrupt the log | No |
555
555
 
556
556
  **Telemetry**: Always fail-open. Hook runtime errors in telemetry scripts must never block agent work.
@@ -69,6 +69,60 @@ node "$CLI" assignment-provider status \
69
69
  [[ "$(json_query "$TMPDIR_EVAL/status-a.json" "assignment.assignee")" == "claude-code:eval-actor-a-session:eval-host" ]] && pass "status reports actor A as assignee" || fail "status reports actor A as assignee"
70
70
  [[ "$(json_query "$TMPDIR_EVAL/status-a.json" "assignment.record.claimed_at")" == "$CLAIMED_AT" ]] && pass "status claimed_at matches claim's claimed_at" || fail "status claimed_at matches claim's claimed_at"
71
71
 
72
+ # A generic same-actor refresh must preserve exact Builder provenance that an interrupted
73
+ # ensure-session recorded, even though the assignment-provider CLI does not accept that field.
74
+ PROVENANCE_SUBJECT="builder-provenance"
75
+ node "$CLI" assignment-provider claim \
76
+ --provider local-file --artifact-root "$ARTIFACT_ROOT" \
77
+ --subject-id "$PROVENANCE_SUBJECT" \
78
+ --actor-json "$ACTOR_A" \
79
+ --branch "$BRANCH_A" \
80
+ --artifact-dir "$ARTIFACT_DIR_A" \
81
+ --ttl-seconds 1800 \
82
+ > "$TMPDIR_EVAL/provenance-claim.json"
83
+ PROVENANCE_FILE="$(node - "$ARTIFACT_ROOT/assignment" "$PROVENANCE_SUBJECT" <<'NODE'
84
+ const fs = require('node:fs');
85
+ const path = require('node:path');
86
+ const [dir, subject] = process.argv.slice(2);
87
+ for (const name of fs.readdirSync(dir)) {
88
+ if (!name.endsWith('.json')) continue;
89
+ const file = path.join(dir, name);
90
+ if (JSON.parse(fs.readFileSync(file, 'utf8')).subject_id === subject) {
91
+ process.stdout.write(file);
92
+ process.exit(0);
93
+ }
94
+ }
95
+ process.exit(1);
96
+ NODE
97
+ )"
98
+ node - "$PROVENANCE_FILE" <<'NODE'
99
+ const fs = require('node:fs');
100
+ const file = process.argv[2];
101
+ const record = JSON.parse(fs.readFileSync(file, 'utf8'));
102
+ record.work_item_ref = 'kontourai/flow-agents#9001';
103
+ fs.writeFileSync(file, `${JSON.stringify(record, null, 2)}\n`);
104
+ NODE
105
+ node "$CLI" assignment-provider claim \
106
+ --provider local-file --artifact-root "$ARTIFACT_ROOT" \
107
+ --subject-id "$PROVENANCE_SUBJECT" \
108
+ --actor-json "$ACTOR_A" \
109
+ --branch "$BRANCH_A" \
110
+ --artifact-dir "$ARTIFACT_DIR_A" \
111
+ --ttl-seconds 1800 \
112
+ > "$TMPDIR_EVAL/provenance-refresh.json"
113
+ [[ "$(json_query "$TMPDIR_EVAL/provenance-refresh.json" "record.work_item_ref")" == "$SUBJECT_ID" ]] && pass "same-actor refresh preserves exact Work Item provenance" || fail "same-actor refresh preserves exact Work Item provenance"
114
+ node "$CLI" assignment-provider supersede \
115
+ --provider local-file --artifact-root "$ARTIFACT_ROOT" \
116
+ --subject-id "$PROVENANCE_SUBJECT" \
117
+ --from-actor-json "$ACTOR_A" --to-actor-json "$ACTOR_B" \
118
+ --reason "provenance handoff" >"$TMPDIR_EVAL/provenance-supersede.json"
119
+ [[ "$(json_query "$TMPDIR_EVAL/provenance-supersede.json" "record.work_item_ref")" == "$SUBJECT_ID" ]] && pass "generic supersede preserves exact Work Item provenance" || fail "generic supersede preserves exact Work Item provenance"
120
+ node "$CLI" assignment-provider release \
121
+ --provider local-file --artifact-root "$ARTIFACT_ROOT" \
122
+ --subject-id "$PROVENANCE_SUBJECT" \
123
+ --actor-json "$ACTOR_B" \
124
+ --reason "provenance fixture cleanup" >/dev/null
125
+
72
126
  # 3. supersede A -> B updates the actor and records an audit trail entry.
73
127
  node "$CLI" assignment-provider supersede \
74
128
  --provider local-file --artifact-root "$ARTIFACT_ROOT" --subject-id "$SUBJECT_ID" \