@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
|
@@ -43,29 +43,38 @@ WRITER="workflow-sidecar"
|
|
|
43
43
|
echo ""
|
|
44
44
|
echo "=== 1. ensure-session --flow-id builder.build activates FlowDefinition-driven path ==="
|
|
45
45
|
|
|
46
|
-
MAIN_AROOT="$TMP/main-
|
|
46
|
+
MAIN_AROOT="$TMP/main-project/.kontourai/flow-agents"
|
|
47
47
|
SLUG="activation-test"
|
|
48
48
|
SESSION_DIR="$MAIN_AROOT/$SLUG"
|
|
49
49
|
mkdir -p "$MAIN_AROOT"
|
|
50
50
|
|
|
51
|
-
flow_agents_node "$WRITER" ensure-session \
|
|
51
|
+
if flow_agents_node "$WRITER" ensure-session \
|
|
52
52
|
--artifact-root "$MAIN_AROOT" \
|
|
53
53
|
--task-slug "$SLUG" \
|
|
54
|
+
--actor activation-test-actor \
|
|
54
55
|
--title "Step 1 activation test" \
|
|
55
56
|
--summary "Test that --flow-id builder.build activates the FlowDefinition-driven path." \
|
|
56
57
|
--criterion "All gates produce declared claims" \
|
|
57
58
|
--flow-id builder.build \
|
|
58
|
-
--timestamp "2026-06-01T00:00:00Z" >/dev/null 2>&1
|
|
59
|
+
--timestamp "2026-06-01T00:00:00Z" >/dev/null 2>&1; then
|
|
60
|
+
_pass "ensure-session starts the canonical Builder Flow from a canonical artifact root"
|
|
61
|
+
else
|
|
62
|
+
_fail "ensure-session failed to start the canonical Builder Flow"
|
|
63
|
+
fi
|
|
59
64
|
|
|
60
65
|
node -e "
|
|
61
66
|
const fs = require('fs');
|
|
62
67
|
const c = JSON.parse(fs.readFileSync('$MAIN_AROOT/current.json', 'utf8'));
|
|
68
|
+
const flow = JSON.parse(fs.readFileSync('$TMP/main-project/.kontourai/flow/runs/$SLUG/state.json', 'utf8'));
|
|
69
|
+
const bundle = JSON.parse(fs.readFileSync('$SESSION_DIR/trust.bundle', 'utf8'));
|
|
63
70
|
if (c.active_flow_id !== 'builder.build') throw new Error('expected active_flow_id=builder.build, got ' + c.active_flow_id);
|
|
64
|
-
if (
|
|
71
|
+
if (c.active_step_id !== 'design-probe') throw new Error('expected active_step_id=design-probe, got ' + c.active_step_id);
|
|
72
|
+
if (flow.status !== 'active' || flow.current_step !== 'design-probe') throw new Error('canonical Flow did not advance through trusted selection: ' + JSON.stringify(flow));
|
|
73
|
+
if (!(bundle.claims || []).some((claim) => claim.claimType === 'builder.pull-work.selected' && claim.status === 'verified')) throw new Error('missing verified selected-work claim');
|
|
65
74
|
console.log('current.json: active_flow_id=' + c.active_flow_id + ' active_step_id=' + c.active_step_id);
|
|
66
75
|
" 2>&1 \
|
|
67
|
-
&& _pass "ensure-session
|
|
68
|
-
|| _fail "ensure-session
|
|
76
|
+
&& _pass "ensure-session records trusted selection and projects the canonical Flow run at design-probe" \
|
|
77
|
+
|| _fail "ensure-session did not create and project the canonical Flow run"
|
|
69
78
|
|
|
70
79
|
# ─── TEST 2: advance-state sets active_step_id via phase_map ─────────────────
|
|
71
80
|
echo ""
|
|
@@ -15,10 +15,10 @@ _pass() { echo " ✓ $1"; }
|
|
|
15
15
|
_fail() { echo " ✗ $1"; errors=$((errors + 1)); }
|
|
16
16
|
|
|
17
17
|
REPO="$TMPDIR_EVAL/repo"
|
|
18
|
-
mkdir -p "$REPO/.flow-agents/stuck"
|
|
18
|
+
mkdir -p "$REPO/.kontourai/flow-agents/stuck"
|
|
19
19
|
printf '# Test Repo\n' > "$REPO/AGENTS.md"
|
|
20
20
|
printf '# Stuck\n\nbranch: main\nstatus: executing\ntype: deliver\n\n## Plan\n\nTBD.\n' \
|
|
21
|
-
> "$REPO/.flow-agents/stuck/stuck--deliver.md"
|
|
21
|
+
> "$REPO/.kontourai/flow-agents/stuck/stuck--deliver.md"
|
|
22
22
|
|
|
23
23
|
PAYLOAD="{\"hook_event_name\":\"Stop\",\"cwd\":\"$REPO\"}"
|
|
24
24
|
|
|
@@ -54,7 +54,7 @@ c4=$(run_block "$TMPDIR_EVAL/b4.err")
|
|
|
54
54
|
printf '%s' "$PAYLOAD" | FLOW_AGENTS_GOAL_FIT_MODE=block FLOW_AGENTS_GOAL_FIT_MAX_BLOCKS=3 node "$ROOT/scripts/hooks/stop-goal-fit.js" >/dev/null 2>/dev/null
|
|
55
55
|
# mutate the artifact so the warning set differs
|
|
56
56
|
printf '# Stuck\n\nbranch: main\nstatus: verifying\ntype: deliver\n\n## Plan\n\nDifferent.\n' \
|
|
57
|
-
> "$REPO/.flow-agents/stuck/stuck--deliver.md"
|
|
57
|
+
> "$REPO/.kontourai/flow-agents/stuck/stuck--deliver.md"
|
|
58
58
|
cd=$(run_block "$TMPDIR_EVAL/bd.err")
|
|
59
59
|
[[ "$cd" -eq 2 ]] && rg -q 'Stop blocked .* \(block 1; after 3 identical blocks' "$TMPDIR_EVAL/bd.err" \
|
|
60
60
|
&& _pass "changed goal-fit gap resets the streak to 1/3" \
|
|
@@ -47,6 +47,30 @@ else
|
|
|
47
47
|
fi
|
|
48
48
|
echo ""
|
|
49
49
|
|
|
50
|
+
# The 3.4.2 bootstrap hook is intentionally absent from new bundles, but its
|
|
51
|
+
# marker remains an ownership tombstone so upgrades remove the retired denial.
|
|
52
|
+
if node - "$ROOT_DIR/scripts/install-merge.js" <<'NODE'
|
|
53
|
+
const { mergeSettings, isManagedHookGroup } = require(process.argv[2]);
|
|
54
|
+
const retired = { hooks: [{ type: "command", command: "old-flow-agents-entry", statusMessage: "Enforcing Flow Agents projected action" }] };
|
|
55
|
+
const user = { hooks: [{ type: "command", command: "user-pretool" }] };
|
|
56
|
+
const current = { hooks: [{ type: "command", command: "new-flow-agents", statusMessage: "Running Flow Agents hook policy" }] };
|
|
57
|
+
if (!isManagedHookGroup(retired)) throw new Error("retired hook marker is no longer recognized as managed");
|
|
58
|
+
const merged = mergeSettings(
|
|
59
|
+
{ hooks: { PreToolUse: [retired, user] } },
|
|
60
|
+
{ hooks: { UserPromptSubmit: [current] } },
|
|
61
|
+
);
|
|
62
|
+
const text = JSON.stringify(merged);
|
|
63
|
+
if (text.includes("old-flow-agents-entry")) throw new Error("retired bootstrap hook survived upgrade");
|
|
64
|
+
if (!text.includes("user-pretool")) throw new Error("user-owned PreToolUse hook was removed");
|
|
65
|
+
if (!text.includes("new-flow-agents")) throw new Error("current managed hook was not installed");
|
|
66
|
+
NODE
|
|
67
|
+
then
|
|
68
|
+
_pass "upgrade removes the retired 3.4.2 bootstrap hook while preserving user hooks"
|
|
69
|
+
else
|
|
70
|
+
_fail "upgrade did not cleanly replace the retired 3.4.2 bootstrap hook"
|
|
71
|
+
fi
|
|
72
|
+
echo ""
|
|
73
|
+
|
|
50
74
|
# ─── Scenario 1: Seeded user config ──────────────────────────────────────────
|
|
51
75
|
echo "--- Scenario 1: Seeded user config (user keys + non-FA hook survive) ---"
|
|
52
76
|
|
|
@@ -8,7 +8,8 @@
|
|
|
8
8
|
# 4. record-gate-claim at pull-work step produces builder.pull-work.selected claim (status=verified).
|
|
9
9
|
# 5. A TAMPERED bundle (stored verified, evidence fail) at pull-work step BLOCKS (exit 2)
|
|
10
10
|
# with the tamper warning naming the declared claimType.
|
|
11
|
-
# 6. A CLEAN record-gate-claim bundle
|
|
11
|
+
# 6. A CLEAN record-gate-claim bundle is not reported as false completion;
|
|
12
|
+
# its still-active canonical Flow run remains blocked from stopping.
|
|
12
13
|
#
|
|
13
14
|
# Deterministic, no model spend, self-cleaning.
|
|
14
15
|
# Usage: bash evals/integration/test_phase_map_and_gate_claim.sh
|
|
@@ -119,7 +120,7 @@ test_advance_state "learning" "learn"
|
|
|
119
120
|
echo ""
|
|
120
121
|
echo "=== 3. ensure-session --flow-id builder.build defaults to pull-work ==="
|
|
121
122
|
|
|
122
|
-
ENSURE_ROOT="$TMP/ensure-
|
|
123
|
+
ENSURE_ROOT="$TMP/ensure-project/.kontourai/flow-agents"
|
|
123
124
|
mkdir -p "$ENSURE_ROOT"
|
|
124
125
|
|
|
125
126
|
flow_agents_node "workflow-sidecar" ensure-session \
|
|
@@ -128,6 +129,7 @@ flow_agents_node "workflow-sidecar" ensure-session \
|
|
|
128
129
|
--title "Ensure Default Step" \
|
|
129
130
|
--summary "Test ensure-session default step." \
|
|
130
131
|
--flow-id builder.build \
|
|
132
|
+
--skip-ownership-guard \
|
|
131
133
|
--timestamp "2026-06-26T00:00:00Z" >/dev/null 2>&1
|
|
132
134
|
|
|
133
135
|
node -e "
|
|
@@ -144,7 +146,7 @@ node -e "
|
|
|
144
146
|
echo ""
|
|
145
147
|
echo "=== 4. record-gate-claim produces builder.pull-work.selected claim ==="
|
|
146
148
|
|
|
147
|
-
CLAIM_ROOT="$TMP/gate-claim-
|
|
149
|
+
CLAIM_ROOT="$TMP/gate-claim-project/.kontourai/flow-agents"
|
|
148
150
|
mkdir -p "$CLAIM_ROOT"
|
|
149
151
|
|
|
150
152
|
flow_agents_node "workflow-sidecar" ensure-session \
|
|
@@ -153,6 +155,7 @@ flow_agents_node "workflow-sidecar" ensure-session \
|
|
|
153
155
|
--title "Gate Claim Test" \
|
|
154
156
|
--summary "Test gate claim producer." \
|
|
155
157
|
--flow-id builder.build \
|
|
158
|
+
--skip-ownership-guard \
|
|
156
159
|
--timestamp "2026-06-26T00:00:00Z" >/dev/null 2>&1
|
|
157
160
|
|
|
158
161
|
flow_agents_node "workflow-sidecar" init-plan "$CLAIM_ROOT/gate-claim/gate-claim--deliver.md" \
|
|
@@ -192,7 +195,7 @@ node -e "
|
|
|
192
195
|
echo ""
|
|
193
196
|
echo "=== 4b. composed publish-learn gate claim emits builder.pr-open.pull-request ==="
|
|
194
197
|
|
|
195
|
-
COMPOSED_ROOT="$TMP/composed-gate-claim-
|
|
198
|
+
COMPOSED_ROOT="$TMP/composed-gate-claim-project/.kontourai/flow-agents"
|
|
196
199
|
mkdir -p "$COMPOSED_ROOT"
|
|
197
200
|
|
|
198
201
|
flow_agents_node "workflow-sidecar" ensure-session \
|
|
@@ -200,7 +203,6 @@ flow_agents_node "workflow-sidecar" ensure-session \
|
|
|
200
203
|
--task-slug composed-gate-claim \
|
|
201
204
|
--title "Composed Gate Claim Test" \
|
|
202
205
|
--summary "Test composed Builder publish/learn flow producer." \
|
|
203
|
-
--flow-id builder.build \
|
|
204
206
|
--timestamp "2026-06-26T00:00:00Z" >/dev/null 2>&1
|
|
205
207
|
|
|
206
208
|
flow_agents_node "workflow-sidecar" init-plan "$COMPOSED_ROOT/composed-gate-claim/composed-gate-claim--deliver.md" \
|
|
@@ -261,6 +263,7 @@ flow_agents_node "workflow-sidecar" ensure-session \
|
|
|
261
263
|
--title "Tamper Test" \
|
|
262
264
|
--summary "Testing tamper detection." \
|
|
263
265
|
--flow-id builder.build \
|
|
266
|
+
--skip-ownership-guard \
|
|
264
267
|
--timestamp "2026-06-26T00:00:00Z" >/dev/null 2>&1
|
|
265
268
|
|
|
266
269
|
flow_agents_node "workflow-sidecar" init-plan "$T_DIR/.kontourai/flow-agents/tamper/tamper--deliver.md" \
|
|
@@ -350,9 +353,9 @@ else
|
|
|
350
353
|
_fail "tamper warning does not name claimType: $tamper_out"
|
|
351
354
|
fi
|
|
352
355
|
|
|
353
|
-
# ─── Clean gate-claim:
|
|
356
|
+
# ─── Clean gate-claim: no false completion, active Flow still blocks ─────────
|
|
354
357
|
echo ""
|
|
355
|
-
echo "=== 6. CLEAN record-gate-claim
|
|
358
|
+
echo "=== 6. CLEAN record-gate-claim remains governed by active canonical Flow ==="
|
|
356
359
|
|
|
357
360
|
C_DIR="$TMP/clean-test"
|
|
358
361
|
mkdir -p "$C_DIR"
|
|
@@ -364,6 +367,7 @@ flow_agents_node "workflow-sidecar" ensure-session \
|
|
|
364
367
|
--title "Clean Test" \
|
|
365
368
|
--summary "Testing clean gate claim." \
|
|
366
369
|
--flow-id builder.build \
|
|
370
|
+
--skip-ownership-guard \
|
|
367
371
|
--timestamp "2026-06-26T00:00:00Z" >/dev/null 2>&1
|
|
368
372
|
|
|
369
373
|
flow_agents_node "workflow-sidecar" init-plan "$C_DIR/.kontourai/flow-agents/clean/clean--deliver.md" \
|
|
@@ -400,10 +404,10 @@ clean_out="$(FLOW_AGENTS_GOAL_FIT_MODE=block FLOW_AGENTS_GOAL_FIT_BACKSTOP=skip
|
|
|
400
404
|
clean_exit="$?"
|
|
401
405
|
set -e
|
|
402
406
|
|
|
403
|
-
if [ "$clean_exit" -
|
|
404
|
-
_pass "clean
|
|
407
|
+
if [ "$clean_exit" -eq 2 ] && echo "$clean_out" | grep -q 'release skipped for active Flow run "clean"'; then
|
|
408
|
+
_pass "clean claim advances to the next Flow step and active run blocks premature Stop"
|
|
405
409
|
else
|
|
406
|
-
_fail "clean
|
|
410
|
+
_fail "clean active Flow run returned the wrong Stop decision (exit $clean_exit): $clean_out"
|
|
407
411
|
fi
|
|
408
412
|
|
|
409
413
|
if echo "$clean_out" | grep -q "caught false-completion"; then
|
|
@@ -85,13 +85,15 @@ _assert_preserved() {
|
|
|
85
85
|
[[ "$ok" -eq 1 ]] && _pass "persistent identity fields (${FIELDS[*]}) survive $label"
|
|
86
86
|
}
|
|
87
87
|
|
|
88
|
-
# ─── Seed:
|
|
88
|
+
# ─── Seed: standalone sidecar session with an injected actor and a KNOWN timestamp ──────────
|
|
89
|
+
# This sweep tests sidecar writer preservation. Canonical Flow startup and transitions are
|
|
90
|
+
# covered separately; advance-state below activates Builder FlowDefinition claim typing without
|
|
91
|
+
# creating a run that would legitimately project over this fixture's synthetic phase sequence.
|
|
89
92
|
SEED_TS="2026-06-20T08:00:00Z"
|
|
90
93
|
if ! flow_agents_node "$WRITER" ensure-session \
|
|
91
94
|
--artifact-root "$SESSION_ROOT" \
|
|
92
95
|
--task-slug "$SLUG" \
|
|
93
96
|
--actor sweep-actor \
|
|
94
|
-
--flow-id builder.build \
|
|
95
97
|
--title "Sidecar Field Preservation Sweep" \
|
|
96
98
|
--source-request "Seed a fully-populated session for the field-preservation invariant sweep." \
|
|
97
99
|
--summary "Seed session for the class-level field-preservation sweep." \
|
|
@@ -3437,7 +3437,7 @@ fi
|
|
|
3437
3437
|
# ─── #270/#298 compose layer: gate-claim accumulation, gate-claim typing survives rebuild, ──
|
|
3438
3438
|
# compose-two/three/four-writer round-trip, waiver + artifact_refs/standard_refs round-trip,
|
|
3439
3439
|
# runnability rejection at record time (AC1-AC6, AC8, AC10) ──────────────────────────────────
|
|
3440
|
-
COMPOSE_ROOT="$TMPDIR_EVAL/compose-
|
|
3440
|
+
COMPOSE_ROOT="$TMPDIR_EVAL/compose-project/.kontourai/flow-agents"
|
|
3441
3441
|
COMPOSE_SLUG="compose-270"
|
|
3442
3442
|
COMPOSE_DIR="$COMPOSE_ROOT/$COMPOSE_SLUG"
|
|
3443
3443
|
mkdir -p "$COMPOSE_ROOT"
|
|
@@ -3446,13 +3446,24 @@ flow_agents_node "$WRITER" ensure-session \
|
|
|
3446
3446
|
--artifact-root "$COMPOSE_ROOT" \
|
|
3447
3447
|
--task-slug "$COMPOSE_SLUG" \
|
|
3448
3448
|
--actor compose-actor \
|
|
3449
|
-
--flow-id builder.build \
|
|
3450
3449
|
--title "Compose layer session" \
|
|
3451
3450
|
--source-request "Compose-safe writer layer smoke session." \
|
|
3452
3451
|
--summary "Seed session for compose-layer round-trip assertions." \
|
|
3453
3452
|
--criterion "Compose layer round-trips losslessly" \
|
|
3454
3453
|
--timestamp "2026-07-05T09:00:00Z" >"$TMPDIR_EVAL/compose-seed.out" 2>"$TMPDIR_EVAL/compose-seed.err"
|
|
3455
3454
|
|
|
3455
|
+
# This fixture exercises sidecar writer composition, not canonical run
|
|
3456
|
+
# transitions. Activate FlowDefinition claim typing through the standalone
|
|
3457
|
+
# transition primitive so preliminary generic evidence cannot advance Flow.
|
|
3458
|
+
flow_agents_node "$WRITER" advance-state "$COMPOSE_DIR" \
|
|
3459
|
+
--actor compose-actor \
|
|
3460
|
+
--status in_progress \
|
|
3461
|
+
--phase pickup \
|
|
3462
|
+
--summary "Compose writer fixture at pull-work." \
|
|
3463
|
+
--next-action "Record composed claims." \
|
|
3464
|
+
--flow-definition builder.build \
|
|
3465
|
+
--timestamp "2026-07-05T09:00:30Z" >"$TMPDIR_EVAL/compose-activate.out" 2>"$TMPDIR_EVAL/compose-activate.err"
|
|
3466
|
+
|
|
3456
3467
|
_compose_claims_json() {
|
|
3457
3468
|
cat "$COMPOSE_DIR/trust.bundle"
|
|
3458
3469
|
}
|
|
@@ -4157,7 +4168,7 @@ fi
|
|
|
4157
4168
|
# misclassified as an unstamped pre-cluster-270 gate claim on the NEXT rebuild — is now rejected
|
|
4158
4169
|
# AT RECORD TIME instead, in a FRESH session (isolated from COMPOSE_DIR) so no rebuild landmine
|
|
4159
4170
|
# is ever created.
|
|
4160
|
-
COLLISION_ROOT="$TMPDIR_EVAL/collision-
|
|
4171
|
+
COLLISION_ROOT="$TMPDIR_EVAL/collision-project/.kontourai/flow-agents"
|
|
4161
4172
|
COLLISION_SLUG="collision-270"
|
|
4162
4173
|
COLLISION_DIR="$COLLISION_ROOT/$COLLISION_SLUG"
|
|
4163
4174
|
mkdir -p "$COLLISION_ROOT"
|
|
@@ -4207,7 +4218,7 @@ fi
|
|
|
4207
4218
|
# (real flow, real record-gate-claim), then break resolution by pointing active_flow_id at a
|
|
4208
4219
|
# bogus flow id that cannot resolve under kits/ (mirrors the re-reviewer's approach of pointing
|
|
4209
4220
|
# at an unresolvable kits/ path), in an ISOLATED fixture copied from a fresh session.
|
|
4210
|
-
UNRESOLVABLE_ROOT="$TMPDIR_EVAL/unresolvable-
|
|
4221
|
+
UNRESOLVABLE_ROOT="$TMPDIR_EVAL/unresolvable-project/.kontourai/flow-agents"
|
|
4211
4222
|
UNRESOLVABLE_SLUG="unresolvable-270"
|
|
4212
4223
|
UNRESOLVABLE_DIR="$UNRESOLVABLE_ROOT/$UNRESOLVABLE_SLUG"
|
|
4213
4224
|
mkdir -p "$UNRESOLVABLE_ROOT"
|
|
@@ -4217,6 +4228,7 @@ flow_agents_node "$WRITER" ensure-session \
|
|
|
4217
4228
|
--task-slug "$UNRESOLVABLE_SLUG" \
|
|
4218
4229
|
--actor unresolvable-actor \
|
|
4219
4230
|
--flow-id builder.build \
|
|
4231
|
+
--skip-ownership-guard \
|
|
4220
4232
|
--title "Unresolvable FlowDefinition regression" \
|
|
4221
4233
|
--source-request "Regression: an unresolvable FlowDefinition must die with the dedicated cannot-be-loaded message." \
|
|
4222
4234
|
--summary "Seed session with a REAL flow so a real stamped gate claim can be recorded." \
|
|
@@ -4327,7 +4339,7 @@ fi
|
|
|
4327
4339
|
# label, exactly the real kontourai-flow-agents-270 wedge this closes) can NEVER be corrected:
|
|
4328
4340
|
# every attempt to re-record that exact id — the only way to supersede/fix it — is itself
|
|
4329
4341
|
# rejected by the guard, permanently wedging that id and blocking publish-preflight forever.
|
|
4330
|
-
CORRECTION_ROOT="$TMPDIR_EVAL/correction-
|
|
4342
|
+
CORRECTION_ROOT="$TMPDIR_EVAL/correction-project/.kontourai/flow-agents"
|
|
4331
4343
|
CORRECTION_SLUG="correction-270"
|
|
4332
4344
|
CORRECTION_DIR="$CORRECTION_ROOT/$CORRECTION_SLUG"
|
|
4333
4345
|
mkdir -p "$CORRECTION_ROOT"
|
|
@@ -4479,7 +4491,7 @@ fi
|
|
|
4479
4491
|
# check_kind so the replacement claim can be shaped to evade gateClaimShapeUnstampedId's detector
|
|
4480
4492
|
# (which requires check_kind==="external"). This eval reproduces that exact attack end-to-end
|
|
4481
4493
|
# against the CURRENT (narrowed) binary and asserts it now dies, with the stamp intact afterward.
|
|
4482
|
-
STAMPED_ROOT="$TMPDIR_EVAL/stamped-claim-
|
|
4494
|
+
STAMPED_ROOT="$TMPDIR_EVAL/stamped-claim-project/.kontourai/flow-agents"
|
|
4483
4495
|
STAMPED_SLUG="stamped-claim-270"
|
|
4484
4496
|
STAMPED_DIR="$STAMPED_ROOT/$STAMPED_SLUG"
|
|
4485
4497
|
mkdir -p "$STAMPED_ROOT"
|
|
@@ -21,78 +21,6 @@ mkdir -p "$REPO/docs"
|
|
|
21
21
|
printf '# Test Repo\n' > "$REPO/AGENTS.md"
|
|
22
22
|
printf '# Context Map\n' > "$REPO/docs/context-map.md"
|
|
23
23
|
|
|
24
|
-
ENTRY_REPO="$TMPDIR_EVAL/entry-repo"
|
|
25
|
-
ENTRY_COMMAND='flow-agents builder-run start --session-dir .kontourai/flow-agents/entry-demo'
|
|
26
|
-
mkdir -p "$ENTRY_REPO/.kontourai/flow-agents/entry-demo"
|
|
27
|
-
printf '# Entry Repo\n' > "$ENTRY_REPO/AGENTS.md"
|
|
28
|
-
cat > "$ENTRY_REPO/.kontourai/flow-agents/entry-demo/state.json" <<JSON
|
|
29
|
-
{
|
|
30
|
-
"schema_version": "1.0",
|
|
31
|
-
"task_slug": "entry-demo",
|
|
32
|
-
"status": "new",
|
|
33
|
-
"phase": "pickup",
|
|
34
|
-
"updated_at": "2026-07-10T00:00:00Z",
|
|
35
|
-
"next_action": {
|
|
36
|
-
"status": "continue",
|
|
37
|
-
"summary": "Start the canonical Flow run.",
|
|
38
|
-
"skills": ["pull-work"],
|
|
39
|
-
"command": "$ENTRY_COMMAND",
|
|
40
|
-
"enforcement": "before_tool_use"
|
|
41
|
-
}
|
|
42
|
-
}
|
|
43
|
-
JSON
|
|
44
|
-
|
|
45
|
-
if node "$ROOT/scripts/hooks/workflow-steering.js" >"$TMPDIR_EVAL/entry-direct.out" 2>"$TMPDIR_EVAL/entry-direct.err" <<JSON
|
|
46
|
-
{"hook_event_name":"PreToolUse","cwd":"$ENTRY_REPO","tool_name":"Bash","tool_input":{"command":"git status --short"}}
|
|
47
|
-
JSON
|
|
48
|
-
then
|
|
49
|
-
_fail "workflow entry enforcement should block unrelated tools"
|
|
50
|
-
else
|
|
51
|
-
status=$?
|
|
52
|
-
if [[ "$status" -eq 2 ]] && rg -F -q "Run exactly: $ENTRY_COMMAND" "$TMPDIR_EVAL/entry-direct.err"; then
|
|
53
|
-
_pass "workflow entry enforcement blocks unrelated tools with the projected action"
|
|
54
|
-
else
|
|
55
|
-
_fail "workflow entry enforcement returned the wrong direct-hook decision: rc=$status $(cat "$TMPDIR_EVAL/entry-direct.err")"
|
|
56
|
-
fi
|
|
57
|
-
fi
|
|
58
|
-
|
|
59
|
-
if node "$ROOT/scripts/hooks/workflow-steering.js" >"$TMPDIR_EVAL/entry-allow.out" 2>"$TMPDIR_EVAL/entry-allow.err" <<JSON
|
|
60
|
-
{"hook_event_name":"PreToolUse","cwd":"$ENTRY_REPO","tool_name":"Bash","tool_input":{"command":"$ENTRY_COMMAND"}}
|
|
61
|
-
JSON
|
|
62
|
-
then
|
|
63
|
-
_pass "workflow entry enforcement allows the exact projected action"
|
|
64
|
-
else
|
|
65
|
-
_fail "workflow entry enforcement blocked the projected action: $(cat "$TMPDIR_EVAL/entry-allow.err")"
|
|
66
|
-
fi
|
|
67
|
-
|
|
68
|
-
for adapter in codex claude; do
|
|
69
|
-
if [[ "$adapter" == "codex" ]]; then
|
|
70
|
-
adapter_command=(node "$ROOT/scripts/hooks/codex-hook-adapter.js" pre:workflow-entry workflow-steering.js standard,strict)
|
|
71
|
-
else
|
|
72
|
-
adapter_command=(node "$ROOT/scripts/hooks/claude-hook-adapter.js" PreToolUse pre:workflow-entry workflow-steering.js standard,strict)
|
|
73
|
-
fi
|
|
74
|
-
if "${adapter_command[@]}" >"$TMPDIR_EVAL/entry-$adapter.out" 2>"$TMPDIR_EVAL/entry-$adapter.err" <<JSON
|
|
75
|
-
{"hook_event_name":"PreToolUse","cwd":"$ENTRY_REPO","tool_name":"Bash","tool_input":{"command":"git status --short"}}
|
|
76
|
-
JSON
|
|
77
|
-
then
|
|
78
|
-
if node - "$TMPDIR_EVAL/entry-$adapter.out" "$ENTRY_COMMAND" <<'NODE'
|
|
79
|
-
const fs = require('node:fs');
|
|
80
|
-
const payload = JSON.parse(fs.readFileSync(process.argv[2], 'utf8'));
|
|
81
|
-
const expected = process.argv[3];
|
|
82
|
-
if (payload.hookSpecificOutput?.hookEventName !== 'PreToolUse') process.exit(1);
|
|
83
|
-
if (payload.hookSpecificOutput?.permissionDecision !== 'deny') process.exit(1);
|
|
84
|
-
if (!String(payload.hookSpecificOutput?.permissionDecisionReason || '').includes(expected)) process.exit(1);
|
|
85
|
-
NODE
|
|
86
|
-
then
|
|
87
|
-
_pass "$adapter adapter translates projected-action enforcement into a PreToolUse denial"
|
|
88
|
-
else
|
|
89
|
-
_fail "$adapter adapter returned the wrong projected-action denial: $(cat "$TMPDIR_EVAL/entry-$adapter.out")"
|
|
90
|
-
fi
|
|
91
|
-
else
|
|
92
|
-
_fail "$adapter adapter should translate a policy block without failing: $(cat "$TMPDIR_EVAL/entry-$adapter.err")"
|
|
93
|
-
fi
|
|
94
|
-
done
|
|
95
|
-
|
|
96
24
|
cat > "$REPO/.kontourai/flow-agents/steering-demo/state.json" <<'JSON'
|
|
97
25
|
{
|
|
98
26
|
"schema_version": "1.0",
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@kontourai/flow-agents",
|
|
3
|
-
"version": "3.
|
|
3
|
+
"version": "3.5.0",
|
|
4
4
|
"description": "Flow Agents — a Kontour product that applies Flow and Veritas discipline as a portable process layer inside the agent tools you already use: Claude Code, Codex, Kiro, opencode, pi, and GitHub Actions — with framework adapters (AWS Strands preview) on the same policy-engine contract.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"agents",
|
|
@@ -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
|
};
|
package/scripts/install-merge.js
CHANGED
|
@@ -48,6 +48,8 @@ const FA_MARKERS = [
|
|
|
48
48
|
"Recording Flow Agents telemetry",
|
|
49
49
|
"Running Flow Agents hook policy",
|
|
50
50
|
"Capturing Flow Agents command evidence",
|
|
51
|
+
// Cleanup tombstone: 3.4.2 installed this retired PreToolUse hook. Keep the
|
|
52
|
+
// marker recognizable so upgrades remove it even though new bundles do not.
|
|
51
53
|
"Enforcing Flow Agents projected action",
|
|
52
54
|
];
|
|
53
55
|
|
|
@@ -83,16 +83,20 @@ export async function startBuilderFlowSession(input: BuilderFlowSessionInput): P
|
|
|
83
83
|
},
|
|
84
84
|
});
|
|
85
85
|
}
|
|
86
|
-
|
|
86
|
+
assertRunSubjectBinding(run, subject);
|
|
87
|
+
return syncAndProject(context, run, sidecarSnapshot);
|
|
87
88
|
}
|
|
88
89
|
|
|
89
90
|
export async function syncBuilderFlowSession(input: BuilderFlowSessionInput): Promise<BuilderFlowSessionResult> {
|
|
90
91
|
const context = resolveSessionContext(input.sessionDir);
|
|
92
|
+
const sidecarSnapshot = readSidecarSnapshot(context);
|
|
93
|
+
const subject = workflowSubject(sidecarSnapshot.state);
|
|
91
94
|
const run = await loadBuilderBuildRun({
|
|
92
95
|
cwd: context.projectRoot,
|
|
93
96
|
runId: context.slug,
|
|
94
97
|
});
|
|
95
|
-
|
|
98
|
+
assertRunSubjectBinding(run, subject);
|
|
99
|
+
return syncAndProject(context, run, sidecarSnapshot);
|
|
96
100
|
}
|
|
97
101
|
|
|
98
102
|
export async function recoverBuilderFlowSession(input: BuilderFlowSessionInput): Promise<BuilderFlowSessionResult> {
|
|
@@ -103,14 +107,7 @@ export async function recoverBuilderFlowSession(input: BuilderFlowSessionInput):
|
|
|
103
107
|
cwd: context.projectRoot,
|
|
104
108
|
runId: context.slug,
|
|
105
109
|
});
|
|
106
|
-
|
|
107
|
-
throw new BuilderBuildRunInputError("flow_run.state.subject", "must match the selected Work Item");
|
|
108
|
-
}
|
|
109
|
-
if (isRecord(run.state.params)
|
|
110
|
-
&& Object.prototype.hasOwnProperty.call(run.state.params, "subject")
|
|
111
|
-
&& run.state.params.subject !== subject) {
|
|
112
|
-
throw new BuilderBuildRunInputError("flow_run.state.params.subject", "must match the selected Work Item");
|
|
113
|
-
}
|
|
110
|
+
assertRunSubjectBinding(run, subject);
|
|
114
111
|
const projection = projectFlowRun(context, run, sidecarSnapshot.state);
|
|
115
112
|
writeProjection(context, projection, sidecarSnapshot.raw, "recovery");
|
|
116
113
|
return {
|
|
@@ -134,7 +131,11 @@ export async function syncBuilderFlowSessionIfPresent(sessionDir: string): Promi
|
|
|
134
131
|
return syncBuilderFlowSession({ sessionDir });
|
|
135
132
|
}
|
|
136
133
|
|
|
137
|
-
async function syncAndProject(
|
|
134
|
+
async function syncAndProject(
|
|
135
|
+
context: SessionContext,
|
|
136
|
+
initial: BuilderBuildRunResult,
|
|
137
|
+
sidecarSnapshot: SidecarSnapshot,
|
|
138
|
+
): Promise<BuilderFlowSessionResult> {
|
|
138
139
|
let run = initial;
|
|
139
140
|
let attached = false;
|
|
140
141
|
const gates = openGatesForResult(run);
|
|
@@ -164,7 +165,6 @@ async function syncAndProject(context: SessionContext, initial: BuilderBuildRunR
|
|
|
164
165
|
}
|
|
165
166
|
}
|
|
166
167
|
}
|
|
167
|
-
const sidecarSnapshot = readSidecarSnapshot(context);
|
|
168
168
|
const projection = projectFlowRun(context, run, sidecarSnapshot.state);
|
|
169
169
|
writeProjection(context, projection, sidecarSnapshot.raw, "projection");
|
|
170
170
|
return {
|
|
@@ -176,6 +176,17 @@ async function syncAndProject(context: SessionContext, initial: BuilderBuildRunR
|
|
|
176
176
|
};
|
|
177
177
|
}
|
|
178
178
|
|
|
179
|
+
function assertRunSubjectBinding(run: BuilderBuildRunResult, subject: string): void {
|
|
180
|
+
if (run.state.subject !== subject) {
|
|
181
|
+
throw new BuilderBuildRunInputError("flow_run.state.subject", "must match the selected Work Item");
|
|
182
|
+
}
|
|
183
|
+
if (isRecord(run.state.params)
|
|
184
|
+
&& Object.prototype.hasOwnProperty.call(run.state.params, "subject")
|
|
185
|
+
&& run.state.params.subject !== subject) {
|
|
186
|
+
throw new BuilderBuildRunInputError("flow_run.state.params.subject", "must match the selected Work Item");
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
|
|
179
190
|
function resolveSessionContext(sessionDirInput: string): SessionContext {
|
|
180
191
|
const sessionDir = path.resolve(sessionDirInput);
|
|
181
192
|
const artifactRoot = path.dirname(sessionDir);
|