@kontourai/flow-agents 0.2.0 → 0.4.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/.github/workflows/release-please.yml +13 -1
- package/.github/workflows/runtime-compat.yml +1 -1
- package/AGENTS.md +8 -1
- package/CHANGELOG.md +41 -0
- package/README.md +38 -19
- package/build/src/cli/flow-kit.js +9 -4
- package/build/src/cli/runtime-adapter.js +9 -5
- package/build/src/cli/telemetry-doctor.js +4 -1
- package/build/src/runtime-adapters.js +34 -0
- package/build/src/tools/build-universal-bundles.js +18 -1
- package/console.telemetry.json +115 -20
- package/docs/_layouts/default.html +2 -0
- package/docs/index.md +8 -0
- package/docs/integrations/index.md +4 -0
- package/docs/integrations/knowledge-kit-live.md +211 -0
- package/docs/kit-authoring-guide.md +169 -0
- package/docs/spec/runtime-hook-surface.md +56 -3
- package/evals/acceptance/run.sh +10 -1
- package/evals/acceptance/test_knowledge_kit_live.sh +221 -0
- package/evals/acceptance/test_pi_harness.sh +15 -0
- package/evals/integration/test_runtime_adapter_activation.sh +113 -1
- package/evals/static/test_universal_bundles.sh +10 -0
- package/integrations/strands/examples/knowledge_kit_live.py +461 -0
- package/integrations/strands/flow_agents_strands/steering.py +54 -1
- package/integrations/strands/tests/test_hooks.py +88 -0
- package/integrations/strands-ts/src/hooks.ts +104 -0
- package/integrations/strands-ts/test/test-steering.ts +159 -0
- package/kits/catalog.json +6 -0
- package/kits/knowledge/adapters/default-store/index.js +902 -0
- package/kits/knowledge/adapters/flow-runner/index.js +1469 -0
- package/kits/knowledge/adapters/flow-runner/telemetry.js +174 -0
- package/kits/knowledge/adapters/similarity-vector/index.js +284 -0
- package/kits/knowledge/docs/README.md +328 -0
- package/kits/knowledge/docs/store-contract.md +650 -0
- package/kits/knowledge/evals/consolidation/suite.test.js +1234 -0
- package/kits/knowledge/evals/contract-suite/suite.test.js +675 -0
- package/kits/knowledge/evals/ingest-compile/suite.test.js +574 -0
- package/kits/knowledge/evals/retirement/suite.test.js +1173 -0
- package/kits/knowledge/evals/similarity-vector/suite.test.js +685 -0
- package/kits/knowledge/evals/synthesis/suite.test.js +916 -0
- package/kits/knowledge/flows/compile.flow.json +60 -0
- package/kits/knowledge/flows/consolidate.flow.json +77 -0
- package/kits/knowledge/flows/ingest.flow.json +60 -0
- package/kits/knowledge/flows/retire.flow.json +77 -0
- package/kits/knowledge/flows/store-contract.flow.json +48 -0
- package/kits/knowledge/flows/synthesize.flow.json +77 -0
- package/kits/knowledge/kit.json +98 -0
- package/package.json +1 -1
- package/src/cli/flow-kit.ts +10 -4
- package/src/cli/runtime-adapter.ts +10 -5
- package/src/cli/telemetry-doctor.ts +4 -1
- package/src/runtime-adapters.ts +35 -0
- package/src/tools/build-universal-bundles.ts +18 -1
|
@@ -0,0 +1,221 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# test_knowledge_kit_live.sh — Acceptance: Knowledge Kit S5 live example
|
|
3
|
+
#
|
|
4
|
+
# Gated on:
|
|
5
|
+
# 1. ollama binary at /run/current-system/sw/bin/ollama
|
|
6
|
+
# 2. qwen3:1.7b model pulled (checked via ollama list)
|
|
7
|
+
# 3. Python venv with strands-agents[ollama] at /tmp/strands-py-live/venv
|
|
8
|
+
#
|
|
9
|
+
# Skips cleanly if any gate is absent (matching other harness conventions).
|
|
10
|
+
# Starts ollama serve, runs the live example, asserts evidence, stops ollama.
|
|
11
|
+
#
|
|
12
|
+
# Assertions:
|
|
13
|
+
# A1. Script exits 0 (overall PASS printed)
|
|
14
|
+
# A2. <workspace>/.telemetry/full.jsonl exists and contains tool.invoke + tool.result
|
|
15
|
+
# A3. <workspace>/.flow-agents/.telemetry/full.jsonl exists and contains
|
|
16
|
+
# session.start, tool.invoke, tool.result (FlowAgentsHooks events)
|
|
17
|
+
# A4. No new .telemetry directory created in the workspace's parent directory
|
|
18
|
+
# by this script (pre-existing parent-dir .telemetry is not counted)
|
|
19
|
+
# A5. At least 1 compiled record in <workspace>/.knowledge-store/records/
|
|
20
|
+
# A6. Compiled record has provenance source_ids referencing raw records
|
|
21
|
+
#
|
|
22
|
+
set -uo pipefail
|
|
23
|
+
|
|
24
|
+
ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)"
|
|
25
|
+
|
|
26
|
+
OLLAMA_BIN="/run/current-system/sw/bin/ollama"
|
|
27
|
+
VENV_PYTHON="/tmp/strands-py-live/venv/bin/python3"
|
|
28
|
+
EXAMPLE_SCRIPT="$ROOT_DIR/integrations/strands/examples/knowledge_kit_live.py"
|
|
29
|
+
|
|
30
|
+
pass=0
|
|
31
|
+
fail=0
|
|
32
|
+
skip=0
|
|
33
|
+
OLLAMA_STARTED=0
|
|
34
|
+
|
|
35
|
+
_pass() { echo " ✓ $1"; pass=$((pass + 1)); }
|
|
36
|
+
_fail() { echo " ✗ $1"; fail=$((fail + 1)); }
|
|
37
|
+
_skip() { echo " ○ $1"; skip=$((skip + 1)); }
|
|
38
|
+
|
|
39
|
+
cleanup() {
|
|
40
|
+
if [[ "$OLLAMA_STARTED" -eq 1 ]]; then
|
|
41
|
+
pkill -f "ollama serve" 2>/dev/null || true
|
|
42
|
+
fi
|
|
43
|
+
}
|
|
44
|
+
trap cleanup EXIT
|
|
45
|
+
|
|
46
|
+
echo "=== Acceptance: Knowledge Kit S5 Live Example ==="
|
|
47
|
+
echo ""
|
|
48
|
+
|
|
49
|
+
# ── Gate checks ─────────────────────────────────────────────────────────────
|
|
50
|
+
if [[ ! -x "$OLLAMA_BIN" ]]; then
|
|
51
|
+
_skip "ollama binary not found at $OLLAMA_BIN"
|
|
52
|
+
echo ""
|
|
53
|
+
echo "Results: ${pass}/$((pass + fail)) passed, ${fail} failed, ${skip} skipped"
|
|
54
|
+
exit 0
|
|
55
|
+
fi
|
|
56
|
+
|
|
57
|
+
if [[ ! -x "$VENV_PYTHON" ]]; then
|
|
58
|
+
_skip "Python venv not found at $VENV_PYTHON — run: python3 -m venv /tmp/strands-py-live/venv && /tmp/strands-py-live/venv/bin/pip install 'strands-agents[ollama]'"
|
|
59
|
+
echo ""
|
|
60
|
+
echo "Results: ${pass}/$((pass + fail)) passed, ${fail} failed, ${skip} skipped"
|
|
61
|
+
exit 0
|
|
62
|
+
fi
|
|
63
|
+
|
|
64
|
+
_pass "Gate: ollama binary present"
|
|
65
|
+
_pass "Gate: Python venv with strands-agents present"
|
|
66
|
+
echo ""
|
|
67
|
+
|
|
68
|
+
# ── Start ollama serve ───────────────────────────────────────────────────────
|
|
69
|
+
echo "--- Starting ollama serve ---"
|
|
70
|
+
"$OLLAMA_BIN" serve > /tmp/ollama-knowledge-kit-live.log 2>&1 &
|
|
71
|
+
OLLAMA_STARTED=1
|
|
72
|
+
|
|
73
|
+
# Wait for server to be ready (up to 15 seconds)
|
|
74
|
+
for i in {1..15}; do
|
|
75
|
+
if curl -s localhost:11434/v1/models >/dev/null 2>&1; then
|
|
76
|
+
_pass "ollama serve ready (${i}s)"
|
|
77
|
+
break
|
|
78
|
+
fi
|
|
79
|
+
if [[ "$i" -eq 15 ]]; then
|
|
80
|
+
_fail "ollama serve did not start within 15 seconds"
|
|
81
|
+
echo ""
|
|
82
|
+
echo "Results: ${pass}/$((pass + fail)) passed, ${fail} failed, ${skip} skipped"
|
|
83
|
+
exit 1
|
|
84
|
+
fi
|
|
85
|
+
sleep 1
|
|
86
|
+
done
|
|
87
|
+
|
|
88
|
+
# Model gate AFTER server start: ollama list errors when no server is running,
|
|
89
|
+
# which previously misreported a pulled model as missing (skip-path bug).
|
|
90
|
+
if ! "$OLLAMA_BIN" list 2>/dev/null | grep -q "qwen3:1.7b"; then
|
|
91
|
+
_skip "qwen3:1.7b model not pulled — run: ollama pull qwen3:1.7b"
|
|
92
|
+
echo ""
|
|
93
|
+
echo "Results: ${pass}/$((pass + fail)) passed, ${fail} failed, ${skip} skipped"
|
|
94
|
+
exit 0
|
|
95
|
+
fi
|
|
96
|
+
_pass "Gate: qwen3:1.7b model pulled"
|
|
97
|
+
echo ""
|
|
98
|
+
|
|
99
|
+
# ── Run the example ──────────────────────────────────────────────────────────
|
|
100
|
+
echo "--- Running knowledge_kit_live.py ---"
|
|
101
|
+
EXAMPLE_OUTPUT="$(mktemp /tmp/knowledge-kit-live-output.XXXXXX)"
|
|
102
|
+
|
|
103
|
+
FLOW_AGENTS_ROOT="$ROOT_DIR" \
|
|
104
|
+
"$VENV_PYTHON" "$EXAMPLE_SCRIPT" 2>&1 | tee "$EXAMPLE_OUTPUT"
|
|
105
|
+
EXAMPLE_EXIT="${PIPESTATUS[0]}"
|
|
106
|
+
|
|
107
|
+
echo ""
|
|
108
|
+
|
|
109
|
+
# ── Assert A1: script exits 0 ─────────────────────────────────────────────
|
|
110
|
+
if [[ "$EXAMPLE_EXIT" -eq 0 ]]; then
|
|
111
|
+
_pass "A1: example script exits 0"
|
|
112
|
+
else
|
|
113
|
+
_fail "A1: example script exited $EXAMPLE_EXIT"
|
|
114
|
+
fi
|
|
115
|
+
|
|
116
|
+
# Extract workspace path from script output
|
|
117
|
+
WORKSPACE="$(grep "^Workspace: " "$EXAMPLE_OUTPUT" | head -1 | sed 's/^Workspace: //')"
|
|
118
|
+
if [[ -z "$WORKSPACE" ]]; then
|
|
119
|
+
_fail "Could not extract workspace path from script output"
|
|
120
|
+
echo ""
|
|
121
|
+
echo "Results: ${pass}/$((pass + fail)) passed, ${fail} failed, ${skip} skipped"
|
|
122
|
+
exit 1
|
|
123
|
+
fi
|
|
124
|
+
|
|
125
|
+
echo " Workspace: $WORKSPACE"
|
|
126
|
+
KIT_TELEMETRY="$WORKSPACE/.telemetry/full.jsonl"
|
|
127
|
+
SESSION_TELEMETRY="$WORKSPACE/.flow-agents/.telemetry/full.jsonl"
|
|
128
|
+
STORE_RECORDS="$WORKSPACE/.knowledge-store/records"
|
|
129
|
+
|
|
130
|
+
# ── Assert A2: kit telemetry contains tool.invoke + tool.result ───────────
|
|
131
|
+
if [[ -f "$KIT_TELEMETRY" ]] && \
|
|
132
|
+
node -e "
|
|
133
|
+
const fs = require('fs');
|
|
134
|
+
const lines = fs.readFileSync('$KIT_TELEMETRY', 'utf8').trim().split('\n').filter(Boolean);
|
|
135
|
+
const types = lines.map(l => { try { return JSON.parse(l).event_type; } catch(e) { return ''; } });
|
|
136
|
+
const required = ['tool.invoke', 'tool.result'];
|
|
137
|
+
const missing = required.filter(t => !types.includes(t));
|
|
138
|
+
if (missing.length > 0) { process.stderr.write('missing: ' + missing.join(', ') + '\n'); process.exit(1); }
|
|
139
|
+
" 2>/dev/null; then
|
|
140
|
+
_pass "A2: kit telemetry contains tool.invoke + tool.result gate events"
|
|
141
|
+
else
|
|
142
|
+
_fail "A2: kit telemetry missing or lacks required event types (tool.invoke, tool.result)"
|
|
143
|
+
fi
|
|
144
|
+
|
|
145
|
+
# ── Assert A3: session telemetry contains session.start, tool.invoke, tool.result ─
|
|
146
|
+
if [[ -f "$SESSION_TELEMETRY" ]] && \
|
|
147
|
+
node -e "
|
|
148
|
+
const fs = require('fs');
|
|
149
|
+
const lines = fs.readFileSync('$SESSION_TELEMETRY', 'utf8').trim().split('\n').filter(Boolean);
|
|
150
|
+
const types = lines.map(l => { try { return JSON.parse(l).event_type; } catch(e) { return ''; } });
|
|
151
|
+
const required = ['session.start', 'tool.invoke', 'tool.result'];
|
|
152
|
+
const missing = required.filter(t => !types.includes(t));
|
|
153
|
+
if (missing.length > 0) { process.stderr.write('missing: ' + missing.join(', ') + '\n'); process.exit(1); }
|
|
154
|
+
" 2>/dev/null; then
|
|
155
|
+
_pass "A3: session telemetry contains session.start, tool.invoke, tool.result"
|
|
156
|
+
else
|
|
157
|
+
_fail "A3: session telemetry missing or lacks required FlowAgentsHooks events"
|
|
158
|
+
fi
|
|
159
|
+
|
|
160
|
+
# ── Assert A4: workspace telemetry does not leak to parent ────────────────
|
|
161
|
+
# This assertion checks that telemetry written during this test run does not
|
|
162
|
+
# appear in the parent directory. We verify that the workspace telemetry is
|
|
163
|
+
# contained within WORKSPACE, not in its parent.
|
|
164
|
+
# (Pre-existing .telemetry in the system temp dir is not counted as a leak.)
|
|
165
|
+
PARENT_TELEMETRY="$(dirname "$WORKSPACE")/.telemetry"
|
|
166
|
+
if [[ -d "$PARENT_TELEMETRY" ]]; then
|
|
167
|
+
# Only fail if the directory was modified during our test (mtime within last 60s)
|
|
168
|
+
PARENT_MTIME="$(find "$PARENT_TELEMETRY" -newer "$EXAMPLE_OUTPUT" -name "*.jsonl" 2>/dev/null | wc -l | tr -d ' ')"
|
|
169
|
+
if [[ "$PARENT_MTIME" -gt 0 ]]; then
|
|
170
|
+
_fail "A4: telemetry leaked — new .jsonl files written to workspace parent directory during this test"
|
|
171
|
+
else
|
|
172
|
+
_pass "A4: workspace telemetry contained within workspace (pre-existing parent .telemetry not modified by this test)"
|
|
173
|
+
fi
|
|
174
|
+
else
|
|
175
|
+
_pass "A4: no .telemetry in workspace parent directory"
|
|
176
|
+
fi
|
|
177
|
+
|
|
178
|
+
# ── Assert A5: at least 1 compiled record exists ─────────────────────────
|
|
179
|
+
COMPILED_COUNT=0
|
|
180
|
+
if [[ -d "$STORE_RECORDS" ]]; then
|
|
181
|
+
COMPILED_COUNT=$(grep -rl "type: compiled" "$STORE_RECORDS"/*.md 2>/dev/null | wc -l | tr -d ' ')
|
|
182
|
+
fi
|
|
183
|
+
if [[ "$COMPILED_COUNT" -ge 1 ]]; then
|
|
184
|
+
_pass "A5: compiled record found in store ($COMPILED_COUNT)"
|
|
185
|
+
else
|
|
186
|
+
_fail "A5: no compiled records found in $STORE_RECORDS"
|
|
187
|
+
fi
|
|
188
|
+
|
|
189
|
+
# ── Assert A6: compiled record has provenance source_ids ─────────────────
|
|
190
|
+
PROVENANCE_OK=0
|
|
191
|
+
if [[ -d "$STORE_RECORDS" ]]; then
|
|
192
|
+
for compiled_md in "$STORE_RECORDS"/*.md; do
|
|
193
|
+
[[ -f "$compiled_md" ]] || continue
|
|
194
|
+
if grep -q "type: compiled" "$compiled_md" && grep -q "source_ids:" "$compiled_md"; then
|
|
195
|
+
# Verify at least 2 raw ids are referenced
|
|
196
|
+
SOURCE_COUNT=$(grep -c "^ - " "$compiled_md" 2>/dev/null || echo 0)
|
|
197
|
+
if [[ "$SOURCE_COUNT" -ge 2 ]]; then
|
|
198
|
+
PROVENANCE_OK=1
|
|
199
|
+
break
|
|
200
|
+
fi
|
|
201
|
+
fi
|
|
202
|
+
done
|
|
203
|
+
fi
|
|
204
|
+
if [[ "$PROVENANCE_OK" -eq 1 ]]; then
|
|
205
|
+
_pass "A6: compiled record has provenance source_ids with resolving raw refs"
|
|
206
|
+
else
|
|
207
|
+
_fail "A6: compiled record missing source_ids or insufficient provenance refs"
|
|
208
|
+
fi
|
|
209
|
+
|
|
210
|
+
# ── Cleanup temp files ───────────────────────────────────────────────────
|
|
211
|
+
rm -f "$EXAMPLE_OUTPUT"
|
|
212
|
+
if [[ -d "$WORKSPACE" ]]; then
|
|
213
|
+
rm -rf "$WORKSPACE"
|
|
214
|
+
fi
|
|
215
|
+
|
|
216
|
+
echo ""
|
|
217
|
+
echo "==========================="
|
|
218
|
+
total=$((pass + fail))
|
|
219
|
+
echo "Results: ${pass}/${total} passed, ${fail} failed, ${skip} skipped"
|
|
220
|
+
[[ "$fail" -gt 0 ]] && exit 1
|
|
221
|
+
exit 0
|
|
@@ -81,6 +81,21 @@ process.exit(0);
|
|
|
81
81
|
else
|
|
82
82
|
_fail "pi telemetry missing one or more required event types (session.start, tool.invoke, tool.result, session.end)"
|
|
83
83
|
fi
|
|
84
|
+
|
|
85
|
+
# Assert session.start appears exactly once (guards against before_agent_start double-emit).
|
|
86
|
+
if [[ -f "$telemetry_file" ]] && \
|
|
87
|
+
node -e "
|
|
88
|
+
const fs = require('fs');
|
|
89
|
+
const lines = fs.readFileSync('$telemetry_file', 'utf8').trim().split('\n');
|
|
90
|
+
const types = lines.map(l => { try { return JSON.parse(l).event_type; } catch(e) { return ''; } });
|
|
91
|
+
const count = types.filter(t => t === 'session.start').length;
|
|
92
|
+
if (count !== 1) { process.stderr.write('session.start count=' + count + ' (expected exactly 1)\n'); process.exit(1); }
|
|
93
|
+
process.exit(0);
|
|
94
|
+
" 2>/dev/null; then
|
|
95
|
+
_pass "pi telemetry: session.start appears exactly once (no double-emit)"
|
|
96
|
+
else
|
|
97
|
+
_fail "pi telemetry: session.start count is not 1 (double-emit or missing)"
|
|
98
|
+
fi
|
|
84
99
|
fi
|
|
85
100
|
|
|
86
101
|
PARENT_TELEMETRY="$(dirname "$TMP_WORK")/.telemetry"
|
|
@@ -86,16 +86,128 @@ elif node - "$UNKNOWN_OUT" <<'NODE'
|
|
|
86
86
|
const fs = require("node:fs");
|
|
87
87
|
const data = JSON.parse(fs.readFileSync(process.argv[2], "utf8"));
|
|
88
88
|
if (!data.available_adapters?.includes("codex-local")) throw new Error("available adapters missing codex-local");
|
|
89
|
+
if (!data.available_adapters?.includes("strands-local")) throw new Error("available adapters missing strands-local");
|
|
89
90
|
if (!data.errors?.length) throw new Error("unknown adapter did not report errors");
|
|
90
91
|
console.log("ok");
|
|
91
92
|
NODE
|
|
92
93
|
then
|
|
93
|
-
pass "unknown adapter reports available adapters"
|
|
94
|
+
pass "unknown adapter reports available adapters (codex-local and strands-local)"
|
|
94
95
|
else
|
|
95
96
|
fail "unknown adapter diagnostics missing"
|
|
96
97
|
sed -n '1,120p' "$UNKNOWN_OUT"
|
|
97
98
|
fi
|
|
98
99
|
|
|
100
|
+
# -------------------------------------------------------------------------
|
|
101
|
+
# strands-local adapter activation (Issue #32 AC1)
|
|
102
|
+
# -------------------------------------------------------------------------
|
|
103
|
+
|
|
104
|
+
echo ""
|
|
105
|
+
echo "=== strands-local Adapter Activation Checks (Issue #32 AC1) ==="
|
|
106
|
+
|
|
107
|
+
STRANDS_DEST="$TMP_DIR/strands-dest"
|
|
108
|
+
STRANDS_OUT="$TMP_DIR/strands-activation.json"
|
|
109
|
+
mkdir -p "$STRANDS_DEST"
|
|
110
|
+
|
|
111
|
+
# Use the builder kit (stable fixture) — activate for strands-local from the repo source root
|
|
112
|
+
if flow_agents_node "$CLI" activate --dest "$STRANDS_DEST" --source-root "$ROOT" --adapter strands-local --format json >"$STRANDS_OUT" 2>&1; then
|
|
113
|
+
pass "strands-local activation succeeds"
|
|
114
|
+
else
|
|
115
|
+
fail "strands-local activation failed"
|
|
116
|
+
sed -n '1,220p' "$STRANDS_OUT"
|
|
117
|
+
fi
|
|
118
|
+
|
|
119
|
+
if node - "$STRANDS_OUT" "$STRANDS_DEST" "$ROOT/kits/catalog.json" <<'NODE'
|
|
120
|
+
const fs = require("node:fs");
|
|
121
|
+
const path = require("node:path");
|
|
122
|
+
const data = JSON.parse(fs.readFileSync(process.argv[2], "utf8"));
|
|
123
|
+
const dest = process.argv[3];
|
|
124
|
+
const catalog = process.argv[4];
|
|
125
|
+
|
|
126
|
+
// Verify selected_adapter
|
|
127
|
+
if (data.selected_adapter !== "strands-local") throw new Error(`expected strands-local, got: ${data.selected_adapter}`);
|
|
128
|
+
if (JSON.stringify(data.supported_asset_classes) !== JSON.stringify(["flows"])) throw new Error(`unexpected supported_asset_classes: ${JSON.stringify(data.supported_asset_classes)}`);
|
|
129
|
+
|
|
130
|
+
// Verify builder kit flows are generated (builder kit is in catalog.json)
|
|
131
|
+
const ids = new Set(data.generated_runtime_files.map((item) => item.asset_id));
|
|
132
|
+
for (const expected of ["builder.shape", "builder.build", "strands-local.activation"]) {
|
|
133
|
+
if (!ids.has(expected)) throw new Error(`missing generated asset: ${expected}`);
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
// Verify generated runtime files actually exist on disk
|
|
137
|
+
for (const item of data.generated_runtime_files) {
|
|
138
|
+
if (item.asset_class === "activation-manifest") continue;
|
|
139
|
+
const generatedPath = path.join(dest, item.path);
|
|
140
|
+
if (!fs.existsSync(generatedPath)) throw new Error(`generated file missing: ${generatedPath}`);
|
|
141
|
+
// Verify runtime files are under .flow-agents/runtime/strands/flows/
|
|
142
|
+
if (!item.path.includes(".flow-agents/runtime/strands/flows/")) {
|
|
143
|
+
throw new Error(`generated path not under strands runtime dir: ${item.path}`);
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
// Verify activation.json written at strands runtime dir
|
|
148
|
+
const manifestPath = path.join(dest, ".flow-agents/runtime/strands/activation.json");
|
|
149
|
+
if (!fs.existsSync(manifestPath)) throw new Error("strands runtime activation.json missing");
|
|
150
|
+
const manifest = JSON.parse(fs.readFileSync(manifestPath, "utf8"));
|
|
151
|
+
if (manifest.adapter !== "strands-local") throw new Error(`activation.json adapter mismatch: ${manifest.adapter}`);
|
|
152
|
+
if (!Array.isArray(manifest.skipped_assets)) throw new Error("activation.json missing skipped_assets array");
|
|
153
|
+
|
|
154
|
+
// Verify skipped_assets have expected fields (parity with codex-local)
|
|
155
|
+
for (const item of manifest.skipped_assets) {
|
|
156
|
+
for (const key of ["asset_class", "path", "kit_id", "asset_id", "reason"]) {
|
|
157
|
+
if (!(key in item)) throw new Error(`skipped asset missing ${key}: ${JSON.stringify(item)}`);
|
|
158
|
+
}
|
|
159
|
+
if (!item.reason.includes("diagnostic-only")) throw new Error(`unexpected skip reason: ${item.reason}`);
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
// Non-flow asset classes should appear in skipped_assets
|
|
163
|
+
const skippedClasses = new Set(manifest.skipped_assets.map((item) => item.asset_class));
|
|
164
|
+
// builder kit has flows only; skipped_assets check requires a kit with non-flow assets,
|
|
165
|
+
// which the codex-local path already validates via mixed-runtime-kit above.
|
|
166
|
+
// Here we just confirm the field structure is present.
|
|
167
|
+
if (!Array.isArray(data.skipped_assets)) throw new Error("result skipped_assets is not an array");
|
|
168
|
+
|
|
169
|
+
// Catalog not mutated
|
|
170
|
+
if (path.resolve(catalog) === path.resolve(path.join(dest, ".flow-agents/runtime/strands/activation.json"))) {
|
|
171
|
+
throw new Error("activation generated over kits/catalog.json");
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
console.log("ok");
|
|
175
|
+
NODE
|
|
176
|
+
then
|
|
177
|
+
pass "strands-local: runtime flow files, activation.json, and skipped_assets present with correct structure"
|
|
178
|
+
else
|
|
179
|
+
fail "strands-local: activation diagnostics incomplete or incorrect"
|
|
180
|
+
sed -n '1,220p' "$STRANDS_OUT"
|
|
181
|
+
fi
|
|
182
|
+
|
|
183
|
+
# Verify codex-local activation is still intact (AC3 — existing tests still pass)
|
|
184
|
+
if flow_agents_node "$CLI" activate --dest "$STRANDS_DEST" --source-root "$ROOT" --format json >"$TMP_DIR/codex-after-strands.json" 2>&1; then
|
|
185
|
+
pass "codex-local still activates after strands-local has run"
|
|
186
|
+
else
|
|
187
|
+
fail "codex-local activation failed after strands-local activation"
|
|
188
|
+
sed -n '1,220p' "$TMP_DIR/codex-after-strands.json"
|
|
189
|
+
fi
|
|
190
|
+
|
|
191
|
+
if node - "$TMP_DIR/codex-after-strands.json" "$STRANDS_DEST" <<'NODE'
|
|
192
|
+
const fs = require("node:fs");
|
|
193
|
+
const path = require("node:path");
|
|
194
|
+
const data = JSON.parse(fs.readFileSync(process.argv[2], "utf8"));
|
|
195
|
+
const dest = process.argv[3];
|
|
196
|
+
if (data.selected_adapter !== "codex-local") throw new Error(`expected codex-local, got: ${data.selected_adapter}`);
|
|
197
|
+
const manifestPath = path.join(dest, ".flow-agents/runtime/codex/activation.json");
|
|
198
|
+
if (!fs.existsSync(manifestPath)) throw new Error("codex activation.json still not present");
|
|
199
|
+
// Strands runtime dir must also still exist
|
|
200
|
+
const strandsManifestPath = path.join(dest, ".flow-agents/runtime/strands/activation.json");
|
|
201
|
+
if (!fs.existsSync(strandsManifestPath)) throw new Error("strands activation.json was removed by codex-local run");
|
|
202
|
+
console.log("ok");
|
|
203
|
+
NODE
|
|
204
|
+
then
|
|
205
|
+
pass "codex-local and strands-local runtime dirs co-exist independently (AC3)"
|
|
206
|
+
else
|
|
207
|
+
fail "co-existence check failed"
|
|
208
|
+
sed -n '1,220p' "$TMP_DIR/codex-after-strands.json"
|
|
209
|
+
fi
|
|
210
|
+
|
|
99
211
|
echo ""
|
|
100
212
|
if [[ "$errors" -eq 0 ]]; then
|
|
101
213
|
echo "Runtime adapter activation checks passed."
|
|
@@ -286,6 +286,16 @@ else
|
|
|
286
286
|
_fail "opencode bundle missing opencode.json"
|
|
287
287
|
fi
|
|
288
288
|
|
|
289
|
+
# Root AGENTS.md carries a hand-maintained "Repository Conventions" section
|
|
290
|
+
# (commit/release rules for agents working in THIS repo). The rest of the
|
|
291
|
+
# file mirrors generated bundle output; this pin prevents a regeneration
|
|
292
|
+
# sync from silently dropping the repo-specific section.
|
|
293
|
+
if grep -q "## Repository Conventions (source repo only)" "$ROOT_DIR/AGENTS.md" 2>/dev/null && grep -q "release-please" "$ROOT_DIR/AGENTS.md" 2>/dev/null; then
|
|
294
|
+
_pass "root AGENTS.md retains the Repository Conventions section"
|
|
295
|
+
else
|
|
296
|
+
_fail "root AGENTS.md is missing the Repository Conventions section (regeneration clobbered it?)"
|
|
297
|
+
fi
|
|
298
|
+
|
|
289
299
|
# Generated hook artifacts must PARSE in their host language. The pi live
|
|
290
300
|
# smoke (2026-06-11) caught the generator emitting an unterminated string
|
|
291
301
|
# (template-literal escaping) that pi's loader rejected at startup.
|