@kontourai/flow-agents 1.0.1 → 1.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (46) hide show
  1. package/.github/workflows/ci.yml +110 -0
  2. package/CHANGELOG.md +16 -0
  3. package/build/src/cli/console-learning-projection.js +19 -2
  4. package/build/src/cli/effective-backlog-settings.js +18 -2
  5. package/build/src/cli/fixture-retirement-audit.js +19 -2
  6. package/build/src/cli/init.js +19 -2
  7. package/build/src/cli/promote-workflow-artifact.js +19 -2
  8. package/build/src/cli/publish-change-helper.js +19 -2
  9. package/build/src/cli/pull-work-provider.js +19 -2
  10. package/build/src/cli/runtime-adapter.js +20 -2
  11. package/build/src/cli/usage-feedback.js +19 -2
  12. package/build/src/cli/utterance-check.js +19 -2
  13. package/build/src/cli/validate-hook-influence.js +19 -2
  14. package/build/src/cli/veritas-governance.js +19 -2
  15. package/build/src/cli/workflow-artifact-cleanup-audit.js +19 -2
  16. package/build/src/runtime-adapters.js +55 -24
  17. package/build/src/tools/build-universal-bundles.js +19 -2
  18. package/build/src/tools/generate-context-map.js +19 -2
  19. package/build/src/tools/validate-package.js +19 -2
  20. package/build/src/tools/validate-source-tree.js +19 -2
  21. package/context/scripts/telemetry/console-presets.sh +1 -1
  22. package/docs/kit-authoring-guide.md +20 -3
  23. package/evals/ci/run-baseline.sh +55 -8
  24. package/evals/integration/test_runtime_adapter_activation.sh +138 -17
  25. package/evals/run.sh +2 -0
  26. package/evals/static/test_console_presets.sh +49 -0
  27. package/package.json +1 -1
  28. package/scripts/telemetry/console-presets.sh +1 -1
  29. package/src/cli/console-learning-projection.ts +7 -1
  30. package/src/cli/effective-backlog-settings.ts +6 -1
  31. package/src/cli/fixture-retirement-audit.ts +7 -1
  32. package/src/cli/init.ts +7 -1
  33. package/src/cli/promote-workflow-artifact.ts +7 -1
  34. package/src/cli/publish-change-helper.ts +7 -1
  35. package/src/cli/pull-work-provider.ts +7 -1
  36. package/src/cli/runtime-adapter.ts +8 -1
  37. package/src/cli/usage-feedback.ts +7 -1
  38. package/src/cli/utterance-check.ts +7 -1
  39. package/src/cli/validate-hook-influence.ts +7 -1
  40. package/src/cli/veritas-governance.ts +7 -1
  41. package/src/cli/workflow-artifact-cleanup-audit.ts +7 -1
  42. package/src/runtime-adapters.ts +54 -26
  43. package/src/tools/build-universal-bundles.ts +7 -1
  44. package/src/tools/generate-context-map.ts +7 -1
  45. package/src/tools/validate-package.ts +7 -1
  46. package/src/tools/validate-source-tree.ts +7 -1
@@ -1,5 +1,6 @@
1
1
  #!/usr/bin/env node
2
2
  import fs from "node:fs";
3
+ import { fileURLToPath } from "node:url";
3
4
  import path from "node:path";
4
5
  import { loadJson, readText, root, walkFiles, writeText } from "./common.js";
5
6
  const dist = process.env.FLOW_AGENTS_DIST_DIR ? path.resolve(process.env.FLOW_AGENTS_DIST_DIR) : path.join(root, "dist");
@@ -678,5 +679,21 @@ export function main() {
678
679
  }
679
680
  return 0;
680
681
  }
681
- if (import.meta.url === `file://${process.argv[1]}`)
682
- process.exit(main());
682
+ // Use process.exitCode (not process.exit) to allow stdout to be flushed before exit.
683
+ // Resolve real paths to handle symlinks (e.g. /tmp -> /private/tmp on macOS) so the
684
+ // entry-point guard fires correctly when the module is loaded directly as a script.
685
+ const _selfRealPath = (() => { try {
686
+ return fs.realpathSync(fileURLToPath(import.meta.url));
687
+ }
688
+ catch {
689
+ return fileURLToPath(import.meta.url);
690
+ } })();
691
+ const _argv1RealPath = (() => { try {
692
+ return fs.realpathSync(process.argv[1]);
693
+ }
694
+ catch {
695
+ return process.argv[1];
696
+ } })();
697
+ if (_selfRealPath === _argv1RealPath) {
698
+ process.exitCode = main();
699
+ }
@@ -1,5 +1,6 @@
1
1
  #!/usr/bin/env node
2
2
  import fs from "node:fs";
3
+ import { fileURLToPath } from "node:url";
3
4
  import path from "node:path";
4
5
  import { exists, loadJson, markdownTable, oneLine, readText, rel, root, writeText } from "./common.js";
5
6
  const defaultOutput = path.join(root, "docs/context-map.md");
@@ -194,5 +195,21 @@ export function main(argv = process.argv.slice(2)) {
194
195
  console.log(`Wrote ${rel(output)}`);
195
196
  return 0;
196
197
  }
197
- if (import.meta.url === `file://${process.argv[1]}`)
198
- process.exit(main());
198
+ // Use process.exitCode (not process.exit) to allow stdout to be flushed before exit.
199
+ // Resolve real paths to handle symlinks (e.g. /tmp -> /private/tmp on macOS) so the
200
+ // entry-point guard fires correctly when the module is loaded directly as a script.
201
+ const _selfRealPath = (() => { try {
202
+ return fs.realpathSync(fileURLToPath(import.meta.url));
203
+ }
204
+ catch {
205
+ return fileURLToPath(import.meta.url);
206
+ } })();
207
+ const _argv1RealPath = (() => { try {
208
+ return fs.realpathSync(process.argv[1]);
209
+ }
210
+ catch {
211
+ return process.argv[1];
212
+ } })();
213
+ if (_selfRealPath === _argv1RealPath) {
214
+ process.exitCode = main();
215
+ }
@@ -1,5 +1,6 @@
1
1
  #!/usr/bin/env node
2
2
  import fs from "node:fs";
3
+ import { fileURLToPath } from "node:url";
3
4
  import path from "node:path";
4
5
  export function main(argv = process.argv.slice(2)) {
5
6
  const [prefixArg, localFlag] = argv;
@@ -60,5 +61,21 @@ export function main(argv = process.argv.slice(2)) {
60
61
  console.log(errors === 0 ? "Result: PASS" : `Result: FAIL (${errors} error(s))`);
61
62
  return errors === 0 ? 0 : 1;
62
63
  }
63
- if (import.meta.url === `file://${process.argv[1]}`)
64
- process.exit(main());
64
+ // Use process.exitCode (not process.exit) to allow stdout to be flushed before exit.
65
+ // Resolve real paths to handle symlinks (e.g. /tmp -> /private/tmp on macOS) so the
66
+ // entry-point guard fires correctly when the module is loaded directly as a script.
67
+ const _selfRealPath = (() => { try {
68
+ return fs.realpathSync(fileURLToPath(import.meta.url));
69
+ }
70
+ catch {
71
+ return fileURLToPath(import.meta.url);
72
+ } })();
73
+ const _argv1RealPath = (() => { try {
74
+ return fs.realpathSync(process.argv[1]);
75
+ }
76
+ catch {
77
+ return process.argv[1];
78
+ } })();
79
+ if (_selfRealPath === _argv1RealPath) {
80
+ process.exitCode = main();
81
+ }
@@ -1,5 +1,6 @@
1
1
  #!/usr/bin/env node
2
2
  import fs from "node:fs";
3
+ import { fileURLToPath } from "node:url";
3
4
  import path from "node:path";
4
5
  import { spawnSync } from "node:child_process";
5
6
  import { loadJson, readText, rel, root, walkFiles } from "./common.js";
@@ -624,5 +625,21 @@ export function main(argv = process.argv.slice(2)) {
624
625
  console.log("Source tree validation passed.");
625
626
  return 0;
626
627
  }
627
- if (import.meta.url === `file://${process.argv[1]}`)
628
- process.exit(main());
628
+ // Use process.exitCode (not process.exit) to allow stdout to be flushed before exit.
629
+ // Resolve real paths to handle symlinks (e.g. /tmp -> /private/tmp on macOS) so the
630
+ // entry-point guard fires correctly when the module is loaded directly as a script.
631
+ const _selfRealPath = (() => { try {
632
+ return fs.realpathSync(fileURLToPath(import.meta.url));
633
+ }
634
+ catch {
635
+ return fileURLToPath(import.meta.url);
636
+ } })();
637
+ const _argv1RealPath = (() => { try {
638
+ return fs.realpathSync(process.argv[1]);
639
+ }
640
+ catch {
641
+ return process.argv[1];
642
+ } })();
643
+ if (_selfRealPath === _argv1RealPath) {
644
+ process.exitCode = main();
645
+ }
@@ -6,7 +6,7 @@ flow_agents_local_kontour_console_url() {
6
6
  }
7
7
 
8
8
  flow_agents_kontour_cloud_console_url() {
9
- printf '%s\n' "${FLOW_AGENTS_KONTOUR_CLOUD_CONSOLE_URL:-https://console.kontourai.com}"
9
+ printf '%s\n' "${FLOW_AGENTS_KONTOUR_CLOUD_CONSOLE_URL:-https://console.kontourai.io}"
10
10
  }
11
11
 
12
12
  flow_agents_kontour_hosted_console_url() {
@@ -12,7 +12,7 @@ This guide walks you from an empty directory to a validated, locally installed k
12
12
 
13
13
  - **Kit** — a directory with a root `kit.json` manifest and the assets it declares. The manifest is the contract; Flow Agents validates it before anything is copied.
14
14
  - **Flow Definition** — a `.flow.json` file that declares steps, gates, and expected evidence. Validation of the Flow Definition semantics belongs to [Kontour Flow](https://kontourai.github.io/flow/); the kit contract delegates to it.
15
- - **Activation** — the step that reads the installed kit and writes runtime-local files into your workspace. Today the `codex-local` adapter is the only adapter, and it activates only Flow Definition assets.
15
+ - **Activation** — the step that reads the installed kit and writes runtime-local files into your workspace. Both `codex-local` and `strands-local` adapters activate Flow Definitions, skills, and docs. See the Activate section for the full asset-class table.
16
16
 
17
17
  ## Directory layout
18
18
 
@@ -54,7 +54,7 @@ Required fields:
54
54
  | `name` | Non-empty display name |
55
55
  | `flows` | Non-empty list; each entry must have `id` and `path` |
56
56
 
57
- Optional fields: `product_name`, `description`, `skills`, `docs`, `adapters`, `evals`, `assets`. Optional fields list relative asset paths or objects with `id`, `path`, and optional `description`. They are declared for provenance but only Flow Definition assets are activated today; others appear in diagnostics as `skipped_assets`.
57
+ Optional fields: `product_name`, `description`, `skills`, `docs`, `adapters`, `evals`, `assets`. Optional fields list relative asset paths or objects with `id`, `path`, and optional `description`. `skills` and `docs` assets are activated by both adapters alongside flows. `adapters`, `evals`, and `assets` appear in diagnostics as `skipped_assets` (see the Activate section for the full per-adapter table).
58
58
 
59
59
  ## Minimal flow file
60
60
 
@@ -140,7 +140,24 @@ After installing, run activate to write runtime-local files into the workspace:
140
140
  npx @kontourai/flow-agents flow-kit activate --dest /path/to/workspace --format json
141
141
  ```
142
142
 
143
- The `codex-local` adapter is selected automatically. It writes Flow Definition copies under `.flow-agents/runtime/codex/flows/<kit-id>/` and an `activation.json` manifest. Declared `skills`, `docs`, `adapters`, `evals`, and `assets` are recorded as `skipped_assets` — they are not an error, just not activated yet.
143
+ The `codex-local` adapter is selected automatically. To activate for Strands, pass `--adapter strands-local`.
144
+
145
+ ### What each adapter activates
146
+
147
+ Each adapter copies declared assets into `.flow-agents/runtime/<adapter>/` and produces an `activation.json` manifest. The table below shows which asset classes are activated today:
148
+
149
+ | Asset class | `codex-local` | `strands-local` | Notes |
150
+ |---|---|---|---|
151
+ | `flows` | Activated — `.flow-agents/runtime/codex/flows/<kit-id>/<asset-id>.flow.json` | Activated — `.flow-agents/runtime/strands/flows/<kit-id>/<asset-id>.flow.json` | Gate definitions read by each adapter's flow-routing layer. |
152
+ | `skills` | Activated — `.flow-agents/runtime/codex/skills/<kit-id>/<filename>` | Activated — `.flow-agents/runtime/strands/skills/<kit-id>/<filename>` | Agent guidance markdown. For codex-local, reference these paths from AGENTS.md. For strands-local, the Strands steering layer can glob for `*.md` under `skills/` during system-prompt injection. |
153
+ | `docs` | Activated — `.flow-agents/runtime/codex/docs/<kit-id>/<filename>` | Activated — `.flow-agents/runtime/strands/docs/<kit-id>/<filename>` | Documentation assets. Co-located with skill files for easy reference. |
154
+ | `adapters` | `skipped_assets` | `skipped_assets` | Framework or runtime adapter code — not copied by the activation layer. |
155
+ | `evals` | `skipped_assets` | `skipped_assets` | Evaluation suites — not run or copied during activation. |
156
+ | `assets` | `skipped_assets` | `skipped_assets` | General supporting assets — not copied during activation. |
157
+
158
+ Assets in `skipped_assets` are recorded in `activation.json` for diagnostics but are not an error. They are not activated because no activation path is defined for those classes in the current adapters.
159
+
160
+ Flows with a missing `id` field in `kit.json` are also placed in `skipped_assets` with an explicit reason.
144
161
 
145
162
  When installing through `npx @kontourai/flow-agents init` with the Codex runtime, pass `--activate-kits` to run activation as part of init:
146
163
 
@@ -26,6 +26,24 @@ CHECKS=(
26
26
  "Flow Kit repository integration|bash evals/integration/test_flow_kit_repository.sh"
27
27
  "Runtime adapter activation integration|bash evals/integration/test_runtime_adapter_activation.sh"
28
28
  "Bundle install integration|bash evals/integration/test_bundle_install.sh"
29
+ "Bundle lifecycle integration|bash evals/integration/test_bundle_lifecycle.sh"
30
+ "Activate npx context integration|bash evals/integration/test_activate_npx_context.sh"
31
+ "Kit conformance levels integration|bash evals/integration/test_kit_conformance_levels.sh"
32
+ "Local Flow Kit install integration|bash evals/integration/test_local_flow_kit_install.sh"
33
+ "Flow Kit install-git integration|bash evals/integration/test_flow_kit_install_git.sh"
34
+ "Console learning projection integration|bash evals/integration/test_console_learning_projection.sh"
35
+ "Context map integration|bash evals/integration/test_context_map.sh"
36
+ "Effective backlog settings integration|bash evals/integration/test_effective_backlog_settings.sh"
37
+ "Flow agents statusline integration|bash evals/integration/test_flow_agents_statusline.sh"
38
+ "Telemetry contract integration|bash evals/integration/test_telemetry.sh"
39
+ "Telemetry doctor integration|bash evals/integration/test_telemetry_doctor.sh"
40
+ "Utterance check integration|bash evals/integration/test_utterance_check.sh"
41
+ "Pull work provider integration|bash evals/integration/test_pull_work_provider.sh"
42
+ "Usage feedback import integration|bash evals/integration/test_usage_feedback_import.sh"
43
+ "Usage feedback outcomes integration|bash evals/integration/test_usage_feedback_outcomes.sh"
44
+ "Usage feedback report integration|bash evals/integration/test_usage_feedback_report.sh"
45
+ "Usage feedback dashboard integration|bash evals/integration/test_usage_feedback_dashboard.sh"
46
+ "Usage feedback global integration|bash evals/integration/test_usage_feedback_global.sh"
29
47
  )
30
48
 
31
49
  LANE_SOURCE_AND_STATIC=(
@@ -51,6 +69,27 @@ LANE_RUNTIME_AND_KIT=(
51
69
  "Flow Kit repository integration"
52
70
  "Runtime adapter activation integration"
53
71
  "Bundle install integration"
72
+ "Bundle lifecycle integration"
73
+ "Activate npx context integration"
74
+ "Kit conformance levels integration"
75
+ "Local Flow Kit install integration"
76
+ "Flow Kit install-git integration"
77
+ # QUARANTINED (#74): passes on macOS, fails on Linux CI — not gating until triaged
78
+ "Context map integration"
79
+ "Effective backlog settings integration"
80
+ "Flow agents statusline integration"
81
+ "Telemetry contract integration"
82
+ "Telemetry doctor integration"
83
+ "Utterance check integration"
84
+ "Pull work provider integration"
85
+ )
86
+
87
+ LANE_USAGE_FEEDBACK=(
88
+ "Usage feedback import integration"
89
+ "Usage feedback outcomes integration"
90
+ "Usage feedback report integration"
91
+ "Usage feedback dashboard integration"
92
+ "Usage feedback global integration"
54
93
  )
55
94
 
56
95
  slugify() {
@@ -78,6 +117,9 @@ lane_labels() {
78
117
  runtime-and-kit)
79
118
  printf '%s\n' "${LANE_RUNTIME_AND_KIT[@]}"
80
119
  ;;
120
+ usage-feedback)
121
+ printf '%s\n' "${LANE_USAGE_FEEDBACK[@]}"
122
+ ;;
81
123
  *)
82
124
  echo "Unknown CI baseline lane: $(active_lane)" >&2
83
125
  return 1
@@ -120,14 +162,19 @@ find_check() {
120
162
  }
121
163
 
122
164
  find_active_check() {
165
+ # Look up the check by id-or-label without streaming active_checks through a
166
+ # process substitution. Streaming and returning early from the consumer loop
167
+ # causes a SIGPIPE on the producer printf when pipefail is set, printing a
168
+ # spurious "write error: Broken pipe" on Linux even though every check passed.
169
+ # Instead: resolve via the pure-bash find_check, then confirm the label is
170
+ # present in the active lane.
123
171
  local requested="$1"
124
- local row id label command
125
- while IFS=$'\t' read -r id label command; do
126
- if [[ "$requested" == "$id" || "$requested" == "$label" ]]; then
127
- printf '%s\t%s\t%s\n' "$id" "$label" "$command"
128
- return 0
129
- fi
130
- done < <(active_checks)
172
+ local row id label line
173
+ row="$(find_check "$requested")" || return 1
174
+ IFS=$'\t' read -r id label _ <<<"$row"
175
+ while IFS= read -r line; do
176
+ [[ "$line" == "$label" ]] && { printf '%s\n' "$row"; return 0; }
177
+ done <<<"$(lane_labels)"
131
178
  return 1
132
179
  }
133
180
 
@@ -265,7 +312,7 @@ case "${1:-}" in
265
312
  ;;
266
313
  --lane)
267
314
  if [[ -z "${2:-}" ]]; then
268
- echo "Usage: $0 --lane <source-and-static|workflow-contracts|runtime-and-kit>" >&2
315
+ echo "Usage: $0 --lane <source-and-static|workflow-contracts|runtime-and-kit|usage-feedback>" >&2
269
316
  exit 2
270
317
  fi
271
318
  FLOW_AGENTS_CI_LANE="$2"
@@ -43,31 +43,68 @@ const data = JSON.parse(fs.readFileSync(process.argv[2], "utf8"));
43
43
  const dest = process.argv[3];
44
44
  const catalog = process.argv[4];
45
45
  if (data.selected_adapter !== "codex-local") throw new Error(`unexpected selected_adapter: ${data.selected_adapter}`);
46
- if (JSON.stringify(data.supported_asset_classes) !== JSON.stringify(["flows"])) throw new Error(`unexpected supported_asset_classes: ${data.supported_asset_classes}`);
46
+
47
+ // supported_asset_classes now includes skills and docs (Issue #58)
48
+ const supported = data.supported_asset_classes;
49
+ for (const expected of ["flows", "skills", "docs"]) {
50
+ if (!supported.includes(expected)) throw new Error(`supported_asset_classes missing ${expected}: ${JSON.stringify(supported)}`);
51
+ }
52
+
53
+ // generated_runtime_files: flows activated (builder, mixed), skill activated (mixed.skill), activation manifest
47
54
  const ids = new Set(data.generated_runtime_files.map((item) => item.asset_id));
48
55
  for (const expected of ["builder.shape", "builder.build", "mixed.runtime", "codex-local.activation"]) {
49
56
  if (!ids.has(expected)) throw new Error(`missing generated asset: ${expected}`);
50
57
  }
58
+ // mixed kit skill should now be in generated_runtime_files, not skipped
59
+ if (!ids.has("mixed.skill")) throw new Error("missing generated asset: mixed.skill (skills should be activated now)");
60
+ // mixed kit doc should now be in generated_runtime_files, not skipped
61
+ if (!ids.has("mixed.docs")) throw new Error("missing generated asset: mixed.docs (docs should be activated now)");
62
+
63
+ // All generated files must exist on disk
51
64
  for (const item of data.generated_runtime_files) {
52
65
  const generatedPath = path.join(dest, item.path);
53
66
  if (!fs.existsSync(generatedPath)) throw new Error(`generated file missing: ${generatedPath}`);
54
67
  if (path.resolve(catalog) === path.resolve(generatedPath)) throw new Error("activation generated over kits/catalog.json");
55
68
  }
56
- const classes = new Set(data.skipped_assets.map((item) => item.asset_class));
57
- for (const expected of ["skills", "docs", "adapters", "evals", "assets"]) {
58
- if (!classes.has(expected)) throw new Error(`missing skipped asset class: ${expected}`);
69
+
70
+ // Skills must be written to .flow-agents/runtime/codex/skills/<kit-id>/
71
+ const skillFiles = data.generated_runtime_files.filter((item) => item.asset_class === "skills");
72
+ if (!skillFiles.length) throw new Error("no skills in generated_runtime_files");
73
+ for (const item of skillFiles) {
74
+ if (!item.path.includes(".flow-agents/runtime/codex/skills/")) {
75
+ throw new Error(`skill not under codex skills dir: ${item.path}`);
76
+ }
77
+ if (!fs.existsSync(path.join(dest, item.path))) throw new Error(`skill file missing on disk: ${item.path}`);
78
+ }
79
+
80
+ // Docs must be written to .flow-agents/runtime/codex/docs/<kit-id>/
81
+ const docFiles = data.generated_runtime_files.filter((item) => item.asset_class === "docs");
82
+ if (!docFiles.length) throw new Error("no docs in generated_runtime_files");
83
+ for (const item of docFiles) {
84
+ if (!item.path.includes(".flow-agents/runtime/codex/docs/")) {
85
+ throw new Error(`doc not under codex docs dir: ${item.path}`);
86
+ }
87
+ }
88
+
89
+ // skipped_assets should NOT contain skills or docs any more
90
+ const skippedClasses = new Set(data.skipped_assets.map((item) => item.asset_class));
91
+ if (skippedClasses.has("skills")) throw new Error("skills should not be in skipped_assets after activation fix");
92
+ if (skippedClasses.has("docs")) throw new Error("docs should not be in skipped_assets after activation fix");
93
+
94
+ // adapters, evals, assets still skipped (not activated by codex-local)
95
+ for (const expected of ["adapters", "evals", "assets"]) {
96
+ if (!skippedClasses.has(expected)) throw new Error(`missing skipped asset class: ${expected}`);
59
97
  }
60
98
  for (const item of data.skipped_assets) {
61
99
  for (const key of ["asset_class", "path", "kit_id", "asset_id", "reason"]) {
62
100
  if (!(key in item)) throw new Error(`skipped asset missing ${key}: ${JSON.stringify(item)}`);
63
101
  }
64
- if (!item.reason.includes("diagnostic-only")) throw new Error(`unexpected skip reason: ${item.reason}`);
65
102
  }
66
103
  if (!fs.existsSync(path.join(dest, ".flow-agents/runtime/codex/activation.json"))) throw new Error("runtime activation manifest missing");
67
104
  console.log("ok");
68
105
  NODE
69
106
  then
70
- pass "diagnostics report default adapter, generated files, and skipped unsupported assets"
107
+ pass "diagnostics report default adapter, generated files (flows+skills+docs), and correct skipped_assets (adapters, evals, assets only)"
71
108
  else
72
109
  fail "activation diagnostics are incomplete"
73
110
  sed -n '1,220p' "$OUT"
@@ -108,6 +145,14 @@ STRANDS_DEST="$TMP_DIR/strands-dest"
108
145
  STRANDS_OUT="$TMP_DIR/strands-activation.json"
109
146
  mkdir -p "$STRANDS_DEST"
110
147
 
148
+ # Install the mixed kit into strands dest so we can assert skills land there too
149
+ if flow_agents_node "$CLI" install-local "$MIXED_SRC" --dest "$STRANDS_DEST" >"$TMP_DIR/strands-install.out" 2>&1; then
150
+ pass "mixed local kit installs into strands temp destination"
151
+ else
152
+ fail "mixed local kit install failed (strands dest)"
153
+ sed -n '1,160p' "$TMP_DIR/strands-install.out"
154
+ fi
155
+
111
156
  # Use the builder kit (stable fixture) — activate for strands-local from the repo source root
112
157
  if flow_agents_node "$CLI" activate --dest "$STRANDS_DEST" --source-root "$ROOT" --adapter strands-local --format json >"$STRANDS_OUT" 2>&1; then
113
158
  pass "strands-local activation succeeds"
@@ -125,25 +170,58 @@ const catalog = process.argv[4];
125
170
 
126
171
  // Verify selected_adapter
127
172
  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)}`);
173
+
174
+ // supported_asset_classes now includes skills and docs (Issue #58)
175
+ const supported = data.supported_asset_classes;
176
+ for (const expected of ["flows", "skills", "docs"]) {
177
+ if (!supported.includes(expected)) throw new Error(`supported_asset_classes missing ${expected}: ${JSON.stringify(supported)}`);
178
+ }
129
179
 
130
180
  // Verify builder kit flows are generated (builder kit is in catalog.json)
131
181
  const ids = new Set(data.generated_runtime_files.map((item) => item.asset_id));
132
182
  for (const expected of ["builder.shape", "builder.build", "strands-local.activation"]) {
133
183
  if (!ids.has(expected)) throw new Error(`missing generated asset: ${expected}`);
134
184
  }
185
+ // mixed kit skill should be in generated_runtime_files
186
+ if (!ids.has("mixed.skill")) throw new Error("missing generated asset: mixed.skill (skills should be activated by strands-local)");
187
+ // mixed kit doc should be in generated_runtime_files
188
+ if (!ids.has("mixed.docs")) throw new Error("missing generated asset: mixed.docs (docs should be activated by strands-local)");
135
189
 
136
190
  // Verify generated runtime files actually exist on disk
137
191
  for (const item of data.generated_runtime_files) {
138
192
  if (item.asset_class === "activation-manifest") continue;
139
193
  const generatedPath = path.join(dest, item.path);
140
194
  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}`);
195
+ // Verify flow files are under .flow-agents/runtime/strands/flows/
196
+ if (item.asset_class === "flows" && !item.path.includes(".flow-agents/runtime/strands/flows/")) {
197
+ throw new Error(`generated flow path not under strands runtime dir: ${item.path}`);
198
+ }
199
+ }
200
+
201
+ // Skills must be written to .flow-agents/runtime/strands/skills/<kit-id>/
202
+ const skillFiles = data.generated_runtime_files.filter((item) => item.asset_class === "skills");
203
+ if (!skillFiles.length) throw new Error("no skills in generated_runtime_files for strands-local");
204
+ for (const item of skillFiles) {
205
+ if (!item.path.includes(".flow-agents/runtime/strands/skills/")) {
206
+ throw new Error(`skill not under strands skills dir: ${item.path}`);
207
+ }
208
+ if (!fs.existsSync(path.join(dest, item.path))) throw new Error(`skill file missing on disk: ${item.path}`);
209
+ }
210
+
211
+ // Docs must be written to .flow-agents/runtime/strands/docs/<kit-id>/
212
+ const docFiles = data.generated_runtime_files.filter((item) => item.asset_class === "docs");
213
+ if (!docFiles.length) throw new Error("no docs in generated_runtime_files for strands-local");
214
+ for (const item of docFiles) {
215
+ if (!item.path.includes(".flow-agents/runtime/strands/docs/")) {
216
+ throw new Error(`doc not under strands docs dir: ${item.path}`);
144
217
  }
145
218
  }
146
219
 
220
+ // skipped_assets should NOT contain skills or docs
221
+ const skippedClasses = new Set(data.skipped_assets.map((item) => item.asset_class));
222
+ if (skippedClasses.has("skills")) throw new Error("skills should not be in skipped_assets for strands-local");
223
+ if (skippedClasses.has("docs")) throw new Error("docs should not be in skipped_assets for strands-local");
224
+
147
225
  // Verify activation.json written at strands runtime dir
148
226
  const manifestPath = path.join(dest, ".flow-agents/runtime/strands/activation.json");
149
227
  if (!fs.existsSync(manifestPath)) throw new Error("strands runtime activation.json missing");
@@ -156,14 +234,8 @@ for (const item of manifest.skipped_assets) {
156
234
  for (const key of ["asset_class", "path", "kit_id", "asset_id", "reason"]) {
157
235
  if (!(key in item)) throw new Error(`skipped asset missing ${key}: ${JSON.stringify(item)}`);
158
236
  }
159
- if (!item.reason.includes("diagnostic-only")) throw new Error(`unexpected skip reason: ${item.reason}`);
160
237
  }
161
238
 
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
239
  if (!Array.isArray(data.skipped_assets)) throw new Error("result skipped_assets is not an array");
168
240
 
169
241
  // Catalog not mutated
@@ -174,7 +246,7 @@ if (path.resolve(catalog) === path.resolve(path.join(dest, ".flow-agents/runtime
174
246
  console.log("ok");
175
247
  NODE
176
248
  then
177
- pass "strands-local: runtime flow files, activation.json, and skipped_assets present with correct structure"
249
+ pass "strands-local: runtime flow+skill+doc files, activation.json, and skipped_assets present with correct structure"
178
250
  else
179
251
  fail "strands-local: activation diagnostics incomplete or incorrect"
180
252
  sed -n '1,220p' "$STRANDS_OUT"
@@ -208,6 +280,55 @@ else
208
280
  sed -n '1,220p' "$TMP_DIR/codex-after-strands.json"
209
281
  fi
210
282
 
283
+ # -------------------------------------------------------------------------
284
+ # Skill activation with a kit that has NO skills (builder kit — flows only)
285
+ # -------------------------------------------------------------------------
286
+
287
+ echo ""
288
+ echo "=== Skills: kit-with-no-skills activates cleanly ==="
289
+
290
+ NO_SKILLS_DEST="$TMP_DIR/no-skills-dest"
291
+ NO_SKILLS_OUT="$TMP_DIR/no-skills-activation.json"
292
+ mkdir -p "$NO_SKILLS_DEST"
293
+
294
+ if flow_agents_node "$CLI" activate --dest "$NO_SKILLS_DEST" --source-root "$ROOT" --format json >"$NO_SKILLS_OUT" 2>&1; then
295
+ pass "activation succeeds for source-root with no skills (builder kit only)"
296
+ else
297
+ fail "activation failed for kit with no skills"
298
+ sed -n '1,220p' "$NO_SKILLS_OUT"
299
+ fi
300
+
301
+ if node - "$NO_SKILLS_OUT" "$NO_SKILLS_DEST" <<'NODE'
302
+ const fs = require("node:fs");
303
+ const path = require("node:path");
304
+ // Use builder-only source root (no installed local kits, built-in kits only)
305
+ const data = JSON.parse(fs.readFileSync(process.argv[2], "utf8"));
306
+ const dest = process.argv[3];
307
+ if (data.selected_adapter !== "codex-local") throw new Error(`expected codex-local, got: ${data.selected_adapter}`);
308
+ // builder kit has no skills or docs — skills dir should not exist (or be empty)
309
+ const skillsDir = path.join(dest, ".flow-agents/runtime/codex/skills");
310
+ // It's fine if the dir doesn't exist; builder kit has no skills
311
+ const docsDir = path.join(dest, ".flow-agents/runtime/codex/docs");
312
+ // builder kit has no docs either
313
+ // No skills or docs in skipped_assets (none declared)
314
+ const skippedClasses = new Set(data.skipped_assets.map((item) => item.asset_class));
315
+ // builder kit only has flows — no skills or docs — so neither should appear in skipped
316
+ if (skippedClasses.has("skills")) throw new Error("builder kit (no skills) should not have skills in skipped_assets");
317
+ if (skippedClasses.has("docs")) throw new Error("builder kit (no docs) should not have docs in skipped_assets");
318
+ // Flows must still be activated
319
+ const ids = new Set(data.generated_runtime_files.map((item) => item.asset_id));
320
+ if (!ids.has("builder.shape")) throw new Error("missing builder.shape flow");
321
+ if (!ids.has("builder.build")) throw new Error("missing builder.build flow");
322
+ if (!fs.existsSync(path.join(dest, ".flow-agents/runtime/codex/activation.json"))) throw new Error("activation.json missing");
323
+ console.log("ok");
324
+ NODE
325
+ then
326
+ pass "kit with no skills activates cleanly — flows activated, no skills or docs in skipped_assets"
327
+ else
328
+ fail "kit with no skills activation check failed"
329
+ sed -n '1,220p' "$NO_SKILLS_OUT"
330
+ fi
331
+
211
332
  echo ""
212
333
  if [[ "$errors" -eq 0 ]]; then
213
334
  echo "Runtime adapter activation checks passed."
package/evals/run.sh CHANGED
@@ -135,6 +135,8 @@ run_static() {
135
135
  echo ""
136
136
  bash "$EVAL_DIR/static/test_evidence_refs.sh" || result=1
137
137
  echo ""
138
+ bash "$EVAL_DIR/static/test_console_presets.sh" || result=1
139
+ echo ""
138
140
  bash "$EVAL_DIR/static/test_repo_hooks.sh" || result=1
139
141
  return $result
140
142
  }
@@ -0,0 +1,49 @@
1
+ #!/usr/bin/env bash
2
+ set -euo pipefail
3
+
4
+ ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)"
5
+
6
+ pass() {
7
+ echo "PASS: $1"
8
+ }
9
+
10
+ fail() {
11
+ echo "FAIL: $1" >&2
12
+ exit 1
13
+ }
14
+
15
+ assert_hosted_preset() {
16
+ local preset_file="$1"
17
+ local label="$2"
18
+ local default_url override_url
19
+
20
+ default_url="$(
21
+ unset FLOW_AGENTS_KONTOUR_CLOUD_CONSOLE_URL
22
+ # shellcheck source=/dev/null
23
+ source "$preset_file"
24
+ flow_agents_kontour_hosted_console_url
25
+ )"
26
+ [[ "$default_url" == "https://console.kontourai.io" ]] || fail "$label default hosted URL is $default_url"
27
+ pass "$label default hosted URL uses console.kontourai.io"
28
+
29
+ override_url="$(
30
+ export FLOW_AGENTS_KONTOUR_CLOUD_CONSOLE_URL="https://console.override.test"
31
+ # shellcheck source=/dev/null
32
+ source "$preset_file"
33
+ flow_agents_kontour_hosted_console_url
34
+ )"
35
+ [[ "$override_url" == "https://console.override.test" ]] || fail "$label hosted URL override is $override_url"
36
+ pass "$label hosted URL override is preserved"
37
+ }
38
+
39
+ echo "=== Console Preset Contract Checks ==="
40
+
41
+ assert_hosted_preset "$ROOT_DIR/scripts/telemetry/console-presets.sh" "source preset"
42
+ assert_hosted_preset "$ROOT_DIR/context/scripts/telemetry/console-presets.sh" "context preset"
43
+
44
+ if rg -F -q "https://console.kontourai.com" \
45
+ "$ROOT_DIR/scripts/telemetry/console-presets.sh" \
46
+ "$ROOT_DIR/context/scripts/telemetry/console-presets.sh"; then
47
+ fail "preset scripts still reference console.kontourai.com"
48
+ fi
49
+ pass "preset scripts do not reference console.kontourai.com"
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@kontourai/flow-agents",
3
- "version": "1.0.1",
3
+ "version": "1.1.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",
@@ -6,7 +6,7 @@ flow_agents_local_kontour_console_url() {
6
6
  }
7
7
 
8
8
  flow_agents_kontour_cloud_console_url() {
9
- printf '%s\n' "${FLOW_AGENTS_KONTOUR_CLOUD_CONSOLE_URL:-https://console.kontourai.com}"
9
+ printf '%s\n' "${FLOW_AGENTS_KONTOUR_CLOUD_CONSOLE_URL:-https://console.kontourai.io}"
10
10
  }
11
11
 
12
12
  flow_agents_kontour_hosted_console_url() {
@@ -1,4 +1,5 @@
1
1
  import * as fs from "node:fs";
2
+ import { fileURLToPath } from "node:url";
2
3
  import * as path from "node:path";
3
4
  import { flagBool, flagString, parseArgs } from "../lib/args.js";
4
5
  import { buildWorkflowLearningProjection, readWorkflowLearningSources } from "../lib/workflow-learning-projection.js";
@@ -137,4 +138,9 @@ export function main(argv = process.argv.slice(2)): number {
137
138
  }
138
139
  }
139
140
 
140
- if (import.meta.url === `file://${process.argv[1]}`) process.exit(main());
141
+ // Use process.exitCode (not process.exit) to allow stdout to be flushed before exit.
142
+ // Resolve real paths to handle symlinks (e.g. /tmp -> /private/tmp on macOS) so the
143
+ // entry-point guard fires correctly when the module is loaded directly as a script.
144
+ const _selfRealPath = (() => { try { return fs.realpathSync(fileURLToPath(import.meta.url)); } catch { return fileURLToPath(import.meta.url); } })();
145
+ const _argv1RealPath = (() => { try { return fs.realpathSync(process.argv[1]); } catch { return process.argv[1]; } })();
146
+ if (_selfRealPath === _argv1RealPath) { process.exitCode = main(); }
@@ -96,4 +96,9 @@ export function main(argv = process.argv.slice(2)): number {
96
96
  }
97
97
  }
98
98
 
99
- if (import.meta.url === `file://${process.argv[1]}`) process.exit(main());
99
+ // Use process.exitCode (not process.exit) to allow stdout to be flushed before exit.
100
+ // Resolve real paths to handle symlinks (e.g. /tmp -> /private/tmp on macOS) so the
101
+ // entry-point guard fires correctly when the module is loaded directly as a script.
102
+ const _selfRealPath = (() => { try { return fs.realpathSync(fileURLToPath(import.meta.url)); } catch { return fileURLToPath(import.meta.url); } })();
103
+ const _argv1RealPath = (() => { try { return fs.realpathSync(process.argv[1]); } catch { return process.argv[1]; } })();
104
+ if (_selfRealPath === _argv1RealPath) { process.exitCode = main(); }
@@ -1,5 +1,6 @@
1
1
  #!/usr/bin/env node
2
2
  import * as fs from "node:fs";
3
+ import { fileURLToPath } from "node:url";
3
4
  import * as path from "node:path";
4
5
 
5
6
  type FixtureAuditItem = {
@@ -151,4 +152,9 @@ export function main(argv = process.argv.slice(2)): number {
151
152
  }
152
153
  }
153
154
 
154
- if (import.meta.url === `file://${process.argv[1]}`) process.exit(main());
155
+ // Use process.exitCode (not process.exit) to allow stdout to be flushed before exit.
156
+ // Resolve real paths to handle symlinks (e.g. /tmp -> /private/tmp on macOS) so the
157
+ // entry-point guard fires correctly when the module is loaded directly as a script.
158
+ const _selfRealPath = (() => { try { return fs.realpathSync(fileURLToPath(import.meta.url)); } catch { return fileURLToPath(import.meta.url); } })();
159
+ const _argv1RealPath = (() => { try { return fs.realpathSync(process.argv[1]); } catch { return process.argv[1]; } })();
160
+ if (_selfRealPath === _argv1RealPath) { process.exitCode = main(); }