@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.
- package/CHANGELOG.md +14 -0
- package/agents/dev.json +0 -5
- package/build/src/builder-flow-runtime.js +18 -12
- package/build/src/cli/assignment-provider.d.ts +3 -0
- package/build/src/cli/assignment-provider.js +59 -10
- package/build/src/cli/workflow-sidecar.js +186 -26
- package/build/src/lib/flow-resolver.js +7 -2
- package/build/src/tools/build-universal-bundles.js +0 -2
- package/context/contracts/assignment-provider-contract.md +5 -2
- package/context/scripts/hooks/workflow-steering.js +2 -40
- package/docs/spec/builder-flow-runtime.md +19 -7
- package/docs/spec/runtime-hook-surface.md +4 -4
- package/evals/integration/test_assignment_provider_local_file.sh +54 -0
- package/evals/integration/test_builder_entry_enforcement.sh +369 -22
- package/evals/integration/test_builder_step_producers.sh +1 -1
- package/evals/integration/test_bundle_install.sh +46 -3
- package/evals/integration/test_current_json_per_actor.sh +3 -2
- package/evals/integration/test_dual_emit_flow_step.sh +43 -9
- package/evals/integration/test_flowdef_session_activation.sh +15 -6
- package/evals/integration/test_goal_fit_escape_hatch.sh +3 -3
- package/evals/integration/test_install_merge.sh +24 -0
- package/evals/integration/test_phase_map_and_gate_claim.sh +14 -10
- package/evals/integration/test_resolvefirststep_security.sh +1 -1
- package/evals/integration/test_sidecar_field_preservation.sh +4 -2
- package/evals/integration/test_takeover_protocol.sh +1 -1
- package/evals/integration/test_workflow_sidecar_writer.sh +18 -6
- package/evals/integration/test_workflow_steering_hook.sh +0 -72
- package/package.json +1 -1
- package/schemas/workflow-state.schema.json +1 -0
- package/scripts/hooks/workflow-steering.js +2 -40
- package/scripts/install-merge.js +2 -0
- package/src/builder-flow-runtime.ts +23 -12
- package/src/cli/assignment-provider.ts +55 -11
- package/src/cli/builder-flow-runtime.test.mjs +32 -0
- package/src/cli/workflow-sidecar.ts +183 -28
- package/src/lib/flow-resolver.ts +6 -2
- 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
|
|
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
|
|
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
|
-
*
|
|
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
|
-
|
|
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
|
|
32
|
-
|
|
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
|
-
|
|
39
|
-
|
|
40
|
-
the
|
|
41
|
-
|
|
42
|
-
|
|
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)
|
|
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**:
|
|
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
|
|
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 |
|
|
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" \
|