@kontourai/flow-agents 3.4.1 → 3.4.3
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/build/src/builder-flow-runtime.d.ts +1 -0
- package/build/src/builder-flow-runtime.js +192 -30
- package/build/src/cli/builder-run.js +13 -4
- package/build/src/cli/workflow-sidecar.js +90 -7
- package/docs/spec/builder-flow-runtime.md +35 -2
- package/docs/spec/runtime-hook-surface.md +2 -2
- package/docs/workflow-usage-guide.md +12 -0
- package/evals/integration/test_builder_entry_enforcement.sh +139 -5
- package/evals/integration/test_builder_step_producers.sh +1 -1
- package/evals/integration/test_bundle_install.sh +2 -1
- package/evals/integration/test_current_json_per_actor.sh +2 -2
- package/evals/integration/test_dual_emit_flow_step.sh +41 -9
- package/evals/integration/test_flowdef_session_activation.sh +11 -5
- package/evals/integration/test_install_merge.sh +24 -0
- package/evals/integration/test_phase_map_and_gate_claim.sh +10 -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 +17 -6
- package/kits/builder/skills/continue-work/SKILL.md +7 -0
- package/package.json +1 -1
- package/schemas/workflow-state.schema.json +8 -0
- package/scripts/install-merge.js +4 -1
- package/src/builder-flow-runtime.ts +231 -29
- package/src/cli/builder-flow-runtime.test.mjs +374 -0
- package/src/cli/builder-run.ts +13 -4
- package/src/cli/workflow-sidecar.ts +91 -10
|
@@ -17,6 +17,100 @@ WRITER="workflow-sidecar"
|
|
|
17
17
|
|
|
18
18
|
echo "=== Builder workflow entry enforcement ==="
|
|
19
19
|
|
|
20
|
+
NONCANONICAL_ROOT="$TMP/noncanonical-root"
|
|
21
|
+
if flow_agents_node "$WRITER" ensure-session \
|
|
22
|
+
--artifact-root "$NONCANONICAL_ROOT" \
|
|
23
|
+
--task-slug noncanonical \
|
|
24
|
+
--title "Noncanonical root" \
|
|
25
|
+
--summary "Builder state must use the product artifact root." \
|
|
26
|
+
--flow-id builder.build >"$TMP/noncanonical.out" 2>&1; then
|
|
27
|
+
fail "noncanonical Builder artifact root should be rejected"
|
|
28
|
+
elif [[ ! -e "$NONCANONICAL_ROOT" ]] \
|
|
29
|
+
&& grep -q 'requires --artifact-root <project>/.kontourai/flow-agents' "$TMP/noncanonical.out"; then
|
|
30
|
+
pass "noncanonical Builder artifact root is rejected before any sidecar write"
|
|
31
|
+
else
|
|
32
|
+
fail "noncanonical Builder root left partial state or the wrong diagnostic: $(cat "$TMP/noncanonical.out")"
|
|
33
|
+
fi
|
|
34
|
+
|
|
35
|
+
SYMLINK_PROJECT="$TMP/symlink-project"
|
|
36
|
+
SYMLINK_EXTERNAL="$TMP/symlink-external"
|
|
37
|
+
mkdir -p "$SYMLINK_PROJECT/.kontourai" "$SYMLINK_EXTERNAL"
|
|
38
|
+
ln -s "$SYMLINK_EXTERNAL" "$SYMLINK_PROJECT/.kontourai/flow-agents"
|
|
39
|
+
if flow_agents_node "$WRITER" ensure-session \
|
|
40
|
+
--artifact-root "$SYMLINK_PROJECT/.kontourai/flow-agents" \
|
|
41
|
+
--task-slug symlink-root \
|
|
42
|
+
--title "Symlink root" \
|
|
43
|
+
--summary "Builder state must not escape the project." \
|
|
44
|
+
--flow-id builder.build >"$TMP/symlink-root.out" 2>&1; then
|
|
45
|
+
fail "symlinked Builder artifact root should be rejected"
|
|
46
|
+
elif [[ -z "$(find "$SYMLINK_EXTERNAL" -mindepth 1 -print -quit)" ]] \
|
|
47
|
+
&& grep -q 'requires a non-symlink Flow Agents artifact root' "$TMP/symlink-root.out"; then
|
|
48
|
+
pass "symlinked Builder artifact root is rejected before any external write"
|
|
49
|
+
else
|
|
50
|
+
fail "symlinked Builder root wrote externally or returned the wrong diagnostic: $(cat "$TMP/symlink-root.out")"
|
|
51
|
+
fi
|
|
52
|
+
|
|
53
|
+
RACE_PROJECT="$TMP/race-project"
|
|
54
|
+
RACE_ROOT="$RACE_PROJECT/.kontourai/flow-agents"
|
|
55
|
+
RACE_MOVED="$RACE_PROJECT/.kontourai/flow-agents-acquired"
|
|
56
|
+
RACE_EXTERNAL="$TMP/race-external"
|
|
57
|
+
mkdir -p "$RACE_ROOT" "$RACE_EXTERNAL"
|
|
58
|
+
FLOW_AGENTS_WORKFLOW_SIDECAR_LOCK_DELAY=1 flow_agents_node "$WRITER" ensure-session \
|
|
59
|
+
--artifact-root "$RACE_ROOT" \
|
|
60
|
+
--task-slug lock-swap \
|
|
61
|
+
--title "Lock swap" \
|
|
62
|
+
--summary "Lock cleanup must remain on the acquired root." \
|
|
63
|
+
--flow-id builder.build >"$TMP/lock-swap.out" 2>&1 &
|
|
64
|
+
RACE_PID=$!
|
|
65
|
+
node - "$RACE_ROOT/.workflow-sidecar.lockdir" <<'NODE'
|
|
66
|
+
const fs = require('node:fs');
|
|
67
|
+
const lock = process.argv[2];
|
|
68
|
+
const deadline = Date.now() + 5000;
|
|
69
|
+
(function wait() {
|
|
70
|
+
if (fs.existsSync(lock)) process.exit(0);
|
|
71
|
+
if (Date.now() > deadline) process.exit(1);
|
|
72
|
+
setTimeout(wait, 10);
|
|
73
|
+
})();
|
|
74
|
+
NODE
|
|
75
|
+
mv "$RACE_ROOT" "$RACE_MOVED"
|
|
76
|
+
mkdir -p "$RACE_EXTERNAL/.workflow-sidecar.lockdir"
|
|
77
|
+
printf 'outside must survive\n' >"$RACE_EXTERNAL/.workflow-sidecar.lockdir/sentinel"
|
|
78
|
+
ln -s "$RACE_EXTERNAL" "$RACE_ROOT"
|
|
79
|
+
set +e
|
|
80
|
+
wait "$RACE_PID"
|
|
81
|
+
RACE_STATUS=$?
|
|
82
|
+
set -e
|
|
83
|
+
if [[ "$RACE_STATUS" -ne 0 ]] \
|
|
84
|
+
&& [[ -f "$RACE_EXTERNAL/.workflow-sidecar.lockdir/sentinel" ]] \
|
|
85
|
+
&& [[ -d "$RACE_MOVED/.workflow-sidecar.lockdir" ]] \
|
|
86
|
+
&& grep -q 'lock cleanup skipped because root or lock identity changed' "$TMP/lock-swap.out"; then
|
|
87
|
+
pass "lock cleanup refuses a swapped root and preserves external content"
|
|
88
|
+
else
|
|
89
|
+
fail "lock cleanup followed a swapped root or lost its identity diagnostic: $(cat "$TMP/lock-swap.out")"
|
|
90
|
+
fi
|
|
91
|
+
|
|
92
|
+
RELEASE_STATE="$TMP/release-3.4.2-state.json"
|
|
93
|
+
cat >"$RELEASE_STATE" <<'JSON'
|
|
94
|
+
{
|
|
95
|
+
"schema_version": "1.0",
|
|
96
|
+
"task_slug": "release-3-4-2",
|
|
97
|
+
"status": "new",
|
|
98
|
+
"phase": "pickup",
|
|
99
|
+
"updated_at": "2026-07-10T00:00:00Z",
|
|
100
|
+
"next_action": {
|
|
101
|
+
"status": "continue",
|
|
102
|
+
"summary": "Start the canonical Flow run.",
|
|
103
|
+
"command": "flow-agents builder-run start --session-dir .kontourai/flow-agents/release-3-4-2",
|
|
104
|
+
"enforcement": "before_tool_use"
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
JSON
|
|
108
|
+
if flow_agents_node "validate-workflow-artifacts" --skip-markdown-validation "$RELEASE_STATE" >/dev/null 2>&1; then
|
|
109
|
+
pass "3.4.2 sidecars remain schema-valid while deprecated enforcement is ignored"
|
|
110
|
+
else
|
|
111
|
+
fail "3.4.2 sidecar compatibility regressed"
|
|
112
|
+
fi
|
|
113
|
+
|
|
20
114
|
REFUSED_ROOT="$TMP/refused/.kontourai/flow-agents"
|
|
21
115
|
if flow_agents_node "$WRITER" ensure-session \
|
|
22
116
|
--artifact-root "$REFUSED_ROOT" \
|
|
@@ -63,15 +157,20 @@ const root = process.argv[2];
|
|
|
63
157
|
const current = JSON.parse(fs.readFileSync(path.join(root, 'current.json'), 'utf8'));
|
|
64
158
|
const state = JSON.parse(fs.readFileSync(path.join(root, 'local-request', 'state.json'), 'utf8'));
|
|
65
159
|
const workItem = JSON.parse(fs.readFileSync(path.join(root, 'local-request', 'work-item.json'), 'utf8'));
|
|
160
|
+
const flowState = JSON.parse(fs.readFileSync(path.join(path.dirname(root), 'flow', 'runs', 'local-request', 'state.json'), 'utf8'));
|
|
66
161
|
if (current.active_flow_id !== 'builder.build' || current.active_step_id !== 'pull-work') process.exit(1);
|
|
67
162
|
if (state.status !== 'new' || state.phase !== 'pickup') process.exit(1);
|
|
68
163
|
if (JSON.stringify(state.work_item_refs) !== JSON.stringify(['local:local-request'])) process.exit(1);
|
|
69
164
|
if (workItem.id !== 'local-request' || workItem.title !== 'Local request') process.exit(1);
|
|
70
165
|
if (workItem.source_provider?.kind !== 'local' || workItem.source_provider?.path !== 'work-item.json') process.exit(1);
|
|
71
|
-
if (
|
|
166
|
+
if (flowState.current_step !== 'pull-work' || flowState.subject !== 'local:local-request') process.exit(1);
|
|
167
|
+
if (state.flow_run?.current_step !== 'pull-work') process.exit(1);
|
|
168
|
+
if (JSON.stringify(state.next_action?.skills) !== JSON.stringify(['pull-work'])) process.exit(1);
|
|
169
|
+
if (!state.next_action?.command?.includes('builder-run sync')) process.exit(1);
|
|
170
|
+
if ('enforcement' in state.next_action) process.exit(1);
|
|
72
171
|
NODE
|
|
73
172
|
then
|
|
74
|
-
pass "providerless request creates a local Work Item and
|
|
173
|
+
pass "providerless request creates a local Work Item and canonical Flow run at pull-work"
|
|
75
174
|
else
|
|
76
175
|
fail "local Work Item or first-step state is invalid"
|
|
77
176
|
fi
|
|
@@ -80,7 +179,15 @@ else
|
|
|
80
179
|
fi
|
|
81
180
|
|
|
82
181
|
LOCAL_SESSION="$LOCAL_ROOT/local-request"
|
|
83
|
-
|
|
182
|
+
FLOW_DIGEST_BEFORE="$(find "$TMP/local/.kontourai/flow/runs/local-request" -type f -print0 | sort -z | xargs -0 shasum -a 256)"
|
|
183
|
+
if flow_agents_node "$WRITER" ensure-session \
|
|
184
|
+
--artifact-root "$LOCAL_ROOT" \
|
|
185
|
+
--task-slug local-request \
|
|
186
|
+
--title "Local request" \
|
|
187
|
+
--summary "Providerless work still needs an anchor." \
|
|
188
|
+
--flow-id builder.build \
|
|
189
|
+
--timestamp "2026-07-10T00:00:00Z" >"$TMP/builder-ensure-again.out" 2>&1 \
|
|
190
|
+
&& [[ "$FLOW_DIGEST_BEFORE" == "$(find "$TMP/local/.kontourai/flow/runs/local-request" -type f -print0 | sort -z | xargs -0 shasum -a 256)" ]] \
|
|
84
191
|
&& node - "$TMP/local" "$LOCAL_SESSION" <<'NODE'
|
|
85
192
|
const fs = require('node:fs');
|
|
86
193
|
const path = require('node:path');
|
|
@@ -94,9 +201,36 @@ if (JSON.stringify(sidecar.next_action?.skills) !== JSON.stringify(['pull-work']
|
|
|
94
201
|
if (!sidecar.next_action?.command?.includes('builder-run sync')) process.exit(1);
|
|
95
202
|
NODE
|
|
96
203
|
then
|
|
97
|
-
pass "
|
|
204
|
+
pass "repeated ensure-session loads the canonical Flow run without resetting its history"
|
|
205
|
+
else
|
|
206
|
+
fail "canonical Builder ensure was not idempotent: $(cat "$TMP/builder-ensure-again.out")"
|
|
207
|
+
fi
|
|
208
|
+
|
|
209
|
+
BROKEN_PROJECT="$TMP/broken-start"
|
|
210
|
+
BROKEN_ROOT="$BROKEN_PROJECT/.kontourai/flow-agents"
|
|
211
|
+
mkdir -p "$BROKEN_PROJECT/.kontourai"
|
|
212
|
+
printf 'not a run-store directory\n' > "$BROKEN_PROJECT/.kontourai/flow"
|
|
213
|
+
if flow_agents_node "$WRITER" ensure-session \
|
|
214
|
+
--artifact-root "$BROKEN_ROOT" \
|
|
215
|
+
--task-slug broken-start \
|
|
216
|
+
--title "Broken start" \
|
|
217
|
+
--summary "Flow startup must fail visibly." \
|
|
218
|
+
--flow-id builder.build >"$TMP/broken-start.out" 2>&1; then
|
|
219
|
+
fail "invalid canonical Flow startup should fail ensure-session"
|
|
220
|
+
elif [[ -f "$BROKEN_ROOT/broken-start/state.json" ]] \
|
|
221
|
+
&& [[ ! -e "$BROKEN_PROJECT/.kontourai/flow/runs/broken-start" ]] \
|
|
222
|
+
&& grep -q 'canonical Builder Flow run did not start' "$TMP/broken-start.out" \
|
|
223
|
+
&& grep -q 'flow-agents builder-run start --session-dir .kontourai/flow-agents/broken-start' "$TMP/broken-start.out" \
|
|
224
|
+
&& node - "$BROKEN_ROOT/broken-start/state.json" <<'NODE'
|
|
225
|
+
const fs = require('node:fs');
|
|
226
|
+
const state = JSON.parse(fs.readFileSync(process.argv[2], 'utf8'));
|
|
227
|
+
if (state.flow_run) process.exit(1);
|
|
228
|
+
if (!state.next_action?.command?.includes('builder-run start')) process.exit(1);
|
|
229
|
+
NODE
|
|
230
|
+
then
|
|
231
|
+
pass "failed canonical startup is visible and leaves only retryable sidecar guidance"
|
|
98
232
|
else
|
|
99
|
-
fail "canonical
|
|
233
|
+
fail "failed canonical startup forged state or lost recovery guidance: $(cat "$TMP/broken-start.out")"
|
|
100
234
|
fi
|
|
101
235
|
|
|
102
236
|
if flow_agents_node "$WRITER" record-gate-claim "$LOCAL_SESSION" \
|
|
@@ -139,7 +139,7 @@ test_produce_claim() {
|
|
|
139
139
|
|
|
140
140
|
local slug
|
|
141
141
|
slug="$(echo "prod-$step-$expectation" | tr '/' '-' | tr '.' '-')"
|
|
142
|
-
local aroot="$TMP/$slug"
|
|
142
|
+
local aroot="$TMP/$slug/.kontourai/flow-agents"
|
|
143
143
|
setup_session_for_produce "$aroot" "$slug" "$step"
|
|
144
144
|
|
|
145
145
|
if flow_agents_node "workflow-sidecar" record-gate-claim "$aroot/$slug" \
|
|
@@ -592,11 +592,12 @@ function hasWorkflowSteering(file, ...eventNames) {
|
|
|
592
592
|
}
|
|
593
593
|
for (const file of process.argv.slice(2)) {
|
|
594
594
|
if (!hasWorkflowSteering(file, "UserPromptSubmit", "userPromptSubmit")) throw new Error(`missing prompt-submit workflow steering: ${file}`);
|
|
595
|
+
if (hasWorkflowSteering(file, "PreToolUse", "preToolUse")) throw new Error(`workflow bootstrap must not be model-mediated at pre-tool time: ${file}`);
|
|
595
596
|
}
|
|
596
597
|
console.log("ok");
|
|
597
598
|
NODE
|
|
598
599
|
then
|
|
599
|
-
_pass "installed bundles
|
|
600
|
+
_pass "installed bundles keep workflow steering advisory and session bootstrap product-owned"
|
|
600
601
|
else
|
|
601
602
|
_fail "installed bundles do not wire prompt-submit workflow steering consistently"
|
|
602
603
|
fi
|
|
@@ -117,7 +117,7 @@ fi
|
|
|
117
117
|
# --- 2. actor A's own current resolution is unaffected by actor B's later call (AC7) ----------
|
|
118
118
|
echo "--- 2. actor A's current resolution survives actor B's later, unrelated ensure-session (AC7) ---"
|
|
119
119
|
|
|
120
|
-
AC7_ROOT="$TMPDIR_EVAL/ac7-
|
|
120
|
+
AC7_ROOT="$TMPDIR_EVAL/ac7-project/.kontourai/flow-agents"
|
|
121
121
|
|
|
122
122
|
flow_agents_node "workflow-sidecar" ensure-session \
|
|
123
123
|
--artifact-root "$AC7_ROOT" \
|
|
@@ -370,7 +370,7 @@ set -e
|
|
|
370
370
|
# --- 5. record-gate-claim resolves A's own per-actor flow/step, not B's legacy pointer (AC11) --
|
|
371
371
|
echo "--- 5. record-gate-claim resolves A's own per-actor flow/step, not B's legacy pointer (AC11) ---"
|
|
372
372
|
|
|
373
|
-
AC11_ROOT="$TMPDIR_EVAL/ac11-
|
|
373
|
+
AC11_ROOT="$TMPDIR_EVAL/ac11-project/.kontourai/flow-agents"
|
|
374
374
|
|
|
375
375
|
flow_agents_node "workflow-sidecar" ensure-session \
|
|
376
376
|
--artifact-root "$AC11_ROOT" \
|
|
@@ -34,7 +34,7 @@ cleanup() { rm -rf "$TMP"; }
|
|
|
34
34
|
trap cleanup EXIT
|
|
35
35
|
|
|
36
36
|
WRITER="workflow-sidecar"
|
|
37
|
-
SESSION_ROOT="$TMP/.flow-agents"
|
|
37
|
+
SESSION_ROOT="$TMP/.kontourai/flow-agents"
|
|
38
38
|
|
|
39
39
|
echo "── P-a resolver unit checks ──"
|
|
40
40
|
|
|
@@ -269,11 +269,25 @@ fi
|
|
|
269
269
|
|
|
270
270
|
DUAL_DIR="$SESSION_ROOT/dual-emit-test"
|
|
271
271
|
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
272
|
+
advance_builder_to_verify() {
|
|
273
|
+
local session_dir="$1"
|
|
274
|
+
flow_agents_node "$WRITER" record-gate-claim "$session_dir" --status pass --expectation selected-work \
|
|
275
|
+
--summary "Selected the fixture Work Item." --timestamp "2026-06-26T00:00:10Z" >/dev/null 2>&1 \
|
|
276
|
+
&& flow_agents_node "$WRITER" record-gate-claim "$session_dir" --status pass --expectation pickup-probe-readiness \
|
|
277
|
+
--summary "Pickup readiness is established." --timestamp "2026-06-26T00:00:20Z" >/dev/null 2>&1 \
|
|
278
|
+
&& flow_agents_node "$WRITER" record-gate-claim "$session_dir" --status pass --expectation probe-decisions-or-accepted-gaps \
|
|
279
|
+
--summary "Probe decisions are recorded." --timestamp "2026-06-26T00:00:30Z" >/dev/null 2>&1 \
|
|
280
|
+
&& flow_agents_node "$WRITER" record-gate-claim "$session_dir" --status pass --expectation implementation-plan \
|
|
281
|
+
--summary "Implementation plan is recorded." --timestamp "2026-06-26T00:00:40Z" >/dev/null 2>&1 \
|
|
282
|
+
&& flow_agents_node "$WRITER" record-gate-claim "$session_dir" --status pass --expectation implementation-scope \
|
|
283
|
+
--summary "Implementation scope is recorded." --timestamp "2026-06-26T00:00:50Z" >/dev/null 2>&1
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
if advance_builder_to_verify "$DUAL_DIR"; then
|
|
287
|
+
_pass "canonical Builder Flow advances through required gates to verify"
|
|
288
|
+
else
|
|
289
|
+
_fail "canonical Builder Flow did not reach verify"
|
|
290
|
+
fi
|
|
277
291
|
|
|
278
292
|
# Verify current.json carries the flow keys
|
|
279
293
|
if node -e "
|
|
@@ -328,8 +342,26 @@ fi
|
|
|
328
342
|
echo ""
|
|
329
343
|
echo "── P-d declared-only: policy-kind check maps to builder.verify.policy-compliance ──"
|
|
330
344
|
|
|
331
|
-
#
|
|
332
|
-
|
|
345
|
+
# Use a separate canonical run because the failing tests evidence above legitimately
|
|
346
|
+
# routes its run back to execute before this independent producer assertion.
|
|
347
|
+
if flow_agents_node "$WRITER" ensure-session \
|
|
348
|
+
--artifact-root "$SESSION_ROOT" \
|
|
349
|
+
--task-slug dual-emit-policy \
|
|
350
|
+
--flow-id builder.build \
|
|
351
|
+
--title "Declared Policy Test" \
|
|
352
|
+
--summary "Test declared-only policy emit for ADR 0016 P-d." \
|
|
353
|
+
--criterion "Policy passes" \
|
|
354
|
+
--timestamp "2026-06-26T00:01:30Z" >/dev/null 2>&1 \
|
|
355
|
+
&& advance_builder_to_verify "$SESSION_ROOT/dual-emit-policy"; then
|
|
356
|
+
_pass "policy fixture canonical Builder Flow reaches verify"
|
|
357
|
+
else
|
|
358
|
+
_fail "policy fixture canonical Builder Flow did not reach verify"
|
|
359
|
+
fi
|
|
360
|
+
|
|
361
|
+
POLICY_DIR="$SESSION_ROOT/dual-emit-policy"
|
|
362
|
+
POLICY_BUNDLE="$POLICY_DIR/trust.bundle"
|
|
363
|
+
|
|
364
|
+
if flow_agents_node "$WRITER" record-evidence "$POLICY_DIR" \
|
|
333
365
|
--verdict pass \
|
|
334
366
|
--check-json '{"id":"policy-check","kind":"policy","status":"pass","summary":"Policy compliance passed"}' \
|
|
335
367
|
--timestamp "2026-06-26T00:02:00Z" >"$TMP/policy-evidence.out" 2>"$TMP/policy-evidence.err"; then
|
|
@@ -340,7 +372,7 @@ fi
|
|
|
340
372
|
|
|
341
373
|
if node -e "
|
|
342
374
|
const fs = require('fs');
|
|
343
|
-
const bundle = JSON.parse(fs.readFileSync('${
|
|
375
|
+
const bundle = JSON.parse(fs.readFileSync('${POLICY_BUNDLE}', 'utf8'));
|
|
344
376
|
const claims = bundle.claims;
|
|
345
377
|
// Declared claim for policy kind should be builder.verify.policy-compliance
|
|
346
378
|
const policyDeclared = claims.find(c => c.claimType === 'builder.verify.policy-compliance');
|
|
@@ -43,29 +43,35 @@ 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
54
|
--title "Step 1 activation test" \
|
|
55
55
|
--summary "Test that --flow-id builder.build activates the FlowDefinition-driven path." \
|
|
56
56
|
--criterion "All gates produce declared claims" \
|
|
57
57
|
--flow-id builder.build \
|
|
58
|
-
--timestamp "2026-06-01T00:00:00Z" >/dev/null 2>&1
|
|
58
|
+
--timestamp "2026-06-01T00:00:00Z" >/dev/null 2>&1; then
|
|
59
|
+
_pass "ensure-session starts the canonical Builder Flow from a canonical artifact root"
|
|
60
|
+
else
|
|
61
|
+
_fail "ensure-session failed to start the canonical Builder Flow"
|
|
62
|
+
fi
|
|
59
63
|
|
|
60
64
|
node -e "
|
|
61
65
|
const fs = require('fs');
|
|
62
66
|
const c = JSON.parse(fs.readFileSync('$MAIN_AROOT/current.json', 'utf8'));
|
|
67
|
+
const flow = JSON.parse(fs.readFileSync('$TMP/main-project/.kontourai/flow/runs/$SLUG/state.json', 'utf8'));
|
|
63
68
|
if (c.active_flow_id !== 'builder.build') throw new Error('expected active_flow_id=builder.build, got ' + c.active_flow_id);
|
|
64
69
|
if (!c.active_step_id) throw new Error('expected active_step_id to be set (first step default), got ' + c.active_step_id);
|
|
70
|
+
if (flow.status !== 'active' || flow.current_step !== 'pull-work') throw new Error('canonical Flow did not start at pull-work: ' + JSON.stringify(flow));
|
|
65
71
|
console.log('current.json: active_flow_id=' + c.active_flow_id + ' active_step_id=' + c.active_step_id);
|
|
66
72
|
" 2>&1 \
|
|
67
|
-
&& _pass "ensure-session
|
|
68
|
-
|| _fail "ensure-session
|
|
73
|
+
&& _pass "ensure-session writes current projection and a canonical Flow run at pull-work" \
|
|
74
|
+
|| _fail "ensure-session did not create and project the canonical Flow run"
|
|
69
75
|
|
|
70
76
|
# ─── TEST 2: advance-state sets active_step_id via phase_map ─────────────────
|
|
71
77
|
echo ""
|
|
@@ -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 \
|
|
@@ -144,7 +145,7 @@ node -e "
|
|
|
144
145
|
echo ""
|
|
145
146
|
echo "=== 4. record-gate-claim produces builder.pull-work.selected claim ==="
|
|
146
147
|
|
|
147
|
-
CLAIM_ROOT="$TMP/gate-claim-
|
|
148
|
+
CLAIM_ROOT="$TMP/gate-claim-project/.kontourai/flow-agents"
|
|
148
149
|
mkdir -p "$CLAIM_ROOT"
|
|
149
150
|
|
|
150
151
|
flow_agents_node "workflow-sidecar" ensure-session \
|
|
@@ -192,7 +193,7 @@ node -e "
|
|
|
192
193
|
echo ""
|
|
193
194
|
echo "=== 4b. composed publish-learn gate claim emits builder.pr-open.pull-request ==="
|
|
194
195
|
|
|
195
|
-
COMPOSED_ROOT="$TMP/composed-gate-claim-
|
|
196
|
+
COMPOSED_ROOT="$TMP/composed-gate-claim-project/.kontourai/flow-agents"
|
|
196
197
|
mkdir -p "$COMPOSED_ROOT"
|
|
197
198
|
|
|
198
199
|
flow_agents_node "workflow-sidecar" ensure-session \
|
|
@@ -200,7 +201,6 @@ flow_agents_node "workflow-sidecar" ensure-session \
|
|
|
200
201
|
--task-slug composed-gate-claim \
|
|
201
202
|
--title "Composed Gate Claim Test" \
|
|
202
203
|
--summary "Test composed Builder publish/learn flow producer." \
|
|
203
|
-
--flow-id builder.build \
|
|
204
204
|
--timestamp "2026-06-26T00:00:00Z" >/dev/null 2>&1
|
|
205
205
|
|
|
206
206
|
flow_agents_node "workflow-sidecar" init-plan "$COMPOSED_ROOT/composed-gate-claim/composed-gate-claim--deliver.md" \
|
|
@@ -350,9 +350,9 @@ else
|
|
|
350
350
|
_fail "tamper warning does not name claimType: $tamper_out"
|
|
351
351
|
fi
|
|
352
352
|
|
|
353
|
-
# ─── Clean gate-claim:
|
|
353
|
+
# ─── Clean gate-claim: no false completion, active Flow still blocks ─────────
|
|
354
354
|
echo ""
|
|
355
|
-
echo "=== 6. CLEAN record-gate-claim
|
|
355
|
+
echo "=== 6. CLEAN record-gate-claim remains governed by active canonical Flow ==="
|
|
356
356
|
|
|
357
357
|
C_DIR="$TMP/clean-test"
|
|
358
358
|
mkdir -p "$C_DIR"
|
|
@@ -400,10 +400,10 @@ clean_out="$(FLOW_AGENTS_GOAL_FIT_MODE=block FLOW_AGENTS_GOAL_FIT_BACKSTOP=skip
|
|
|
400
400
|
clean_exit="$?"
|
|
401
401
|
set -e
|
|
402
402
|
|
|
403
|
-
if [ "$clean_exit" -
|
|
404
|
-
_pass "clean
|
|
403
|
+
if [ "$clean_exit" -eq 2 ] && echo "$clean_out" | grep -q 'release skipped for active Flow run "clean"'; then
|
|
404
|
+
_pass "clean claim advances to the next Flow step and active run blocks premature Stop"
|
|
405
405
|
else
|
|
406
|
-
_fail "clean
|
|
406
|
+
_fail "clean active Flow run returned the wrong Stop decision (exit $clean_exit): $clean_out"
|
|
407
407
|
fi
|
|
408
408
|
|
|
409
409
|
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"
|
|
@@ -4327,7 +4338,7 @@ fi
|
|
|
4327
4338
|
# label, exactly the real kontourai-flow-agents-270 wedge this closes) can NEVER be corrected:
|
|
4328
4339
|
# every attempt to re-record that exact id — the only way to supersede/fix it — is itself
|
|
4329
4340
|
# rejected by the guard, permanently wedging that id and blocking publish-preflight forever.
|
|
4330
|
-
CORRECTION_ROOT="$TMPDIR_EVAL/correction-
|
|
4341
|
+
CORRECTION_ROOT="$TMPDIR_EVAL/correction-project/.kontourai/flow-agents"
|
|
4331
4342
|
CORRECTION_SLUG="correction-270"
|
|
4332
4343
|
CORRECTION_DIR="$CORRECTION_ROOT/$CORRECTION_SLUG"
|
|
4333
4344
|
mkdir -p "$CORRECTION_ROOT"
|
|
@@ -4479,7 +4490,7 @@ fi
|
|
|
4479
4490
|
# check_kind so the replacement claim can be shaped to evade gateClaimShapeUnstampedId's detector
|
|
4480
4491
|
# (which requires check_kind==="external"). This eval reproduces that exact attack end-to-end
|
|
4481
4492
|
# against the CURRENT (narrowed) binary and asserts it now dies, with the stamp intact afterward.
|
|
4482
|
-
STAMPED_ROOT="$TMPDIR_EVAL/stamped-claim-
|
|
4493
|
+
STAMPED_ROOT="$TMPDIR_EVAL/stamped-claim-project/.kontourai/flow-agents"
|
|
4483
4494
|
STAMPED_SLUG="stamped-claim-270"
|
|
4484
4495
|
STAMPED_DIR="$STAMPED_ROOT/$STAMPED_SLUG"
|
|
4485
4496
|
mkdir -p "$STAMPED_ROOT"
|
|
@@ -27,6 +27,13 @@ continue-work ties these together for **one job**: take a multi-slice work item
|
|
|
27
27
|
- The request is **brand-new work** with nothing landed yet — that is selection from the backlog. Route to `pull-work`.
|
|
28
28
|
- The request is to **resume the *same* interrupted slice** after a restart (same in-flight slice, mid-execution, picking up new hooks/logic) — that is the resume surface (#153), which reconstructs `state.json` + `handoff.json` + plan + `trust.bundle` for the *same* increment. continue-work advances to the *next* increment; it does not re-enter an unfinished one.
|
|
29
29
|
|
|
30
|
+
For that same-slice case, when a canonical Builder Flow run already exists, restore
|
|
31
|
+
its sidecar/current projection with `flow-agents builder-run recover --session-dir
|
|
32
|
+
.kontourai/flow-agents/<slug>` before following the resume surface. The command
|
|
33
|
+
derives run identity from the session slug and only loads, validates, and projects;
|
|
34
|
+
never invent or supply a run id or step, and use `builder-run sync` separately only
|
|
35
|
+
when recorded evidence should be attached and evaluated.
|
|
36
|
+
|
|
30
37
|
If the boundary is ambiguous (is this the next slice or the same one?), stop and ask one question before routing. Do not silently assume.
|
|
31
38
|
|
|
32
39
|
## Boundary (ADR 0014)
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@kontourai/flow-agents",
|
|
3
|
-
"version": "3.4.
|
|
3
|
+
"version": "3.4.3",
|
|
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",
|
|
@@ -122,6 +122,14 @@
|
|
|
122
122
|
"command": {
|
|
123
123
|
"type": "string",
|
|
124
124
|
"minLength": 1
|
|
125
|
+
},
|
|
126
|
+
"enforcement": {
|
|
127
|
+
"description": "Deprecated compatibility field. Runtime steering ignores this value; session orchestration owns Builder Flow startup.",
|
|
128
|
+
"type": "string",
|
|
129
|
+
"enum": [
|
|
130
|
+
"advisory",
|
|
131
|
+
"before_tool_use"
|
|
132
|
+
]
|
|
125
133
|
}
|
|
126
134
|
}
|
|
127
135
|
}
|
package/scripts/install-merge.js
CHANGED
|
@@ -41,13 +41,16 @@ const path = require("node:path");
|
|
|
41
41
|
const os = require("node:os");
|
|
42
42
|
|
|
43
43
|
// ─── Marker strings ───────────────────────────────────────────────────────────
|
|
44
|
-
// These
|
|
44
|
+
// These statusMessage strings identify every flow-agents managed hook.
|
|
45
45
|
// They are the same strings used by COLLISION_MARKER in src/cli/init.ts and
|
|
46
46
|
// by exportClaudeSettings() / exportCodexHooks() in build-universal-bundles.ts.
|
|
47
47
|
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.
|
|
53
|
+
"Enforcing Flow Agents projected action",
|
|
51
54
|
];
|
|
52
55
|
|
|
53
56
|
const FA_OWNERSHIP_MARKERS = [
|