@kontourai/flow-agents 2.1.0 → 2.2.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 (55) hide show
  1. package/.github/CODEOWNERS +2 -0
  2. package/.github/workflows/ci.yml +4 -4
  3. package/.github/workflows/docs-pages.yml +1 -1
  4. package/.github/workflows/kit-gates-demo.yml +2 -2
  5. package/.github/workflows/publish-npm.yml +2 -2
  6. package/.github/workflows/runtime-compat.yml +2 -2
  7. package/.github/workflows/trust-reconcile.yml +1 -1
  8. package/CHANGELOG.md +29 -0
  9. package/build/src/cli/sidecar-claim-explain.d.ts +45 -0
  10. package/build/src/cli/sidecar-claim-explain.js +87 -0
  11. package/build/src/cli/telemetry-doctor.js +2 -4
  12. package/build/src/cli/usage-feedback.js +16 -8
  13. package/build/src/cli/workflow-artifact-cleanup-audit.js +4 -2
  14. package/build/src/cli/workflow-sidecar.d.ts +2 -44
  15. package/build/src/cli/workflow-sidecar.js +18 -87
  16. package/build/src/index.d.ts +1 -0
  17. package/build/src/index.js +1 -0
  18. package/build/src/lib/local-artifact-root.d.ts +11 -0
  19. package/build/src/lib/local-artifact-root.js +30 -0
  20. package/build/src/tools/validate-source-tree.js +1 -0
  21. package/context/scripts/telemetry/lib/config.sh +3 -4
  22. package/docs/adr/0018-freeze-local-shell-heuristics.md +95 -0
  23. package/docs/repository-structure.md +7 -3
  24. package/evals/integration/test_bundle_lifecycle.sh +9 -9
  25. package/evals/integration/test_gate_lockdown.sh +25 -6
  26. package/evals/integration/test_migrate_local_artifacts.sh +102 -0
  27. package/evals/integration/test_workflow_sidecar_writer.sh +34 -0
  28. package/evals/run.sh +2 -0
  29. package/evals/static/test_unit_helpers.sh +26 -0
  30. package/integrations/strands-ts/package.json +3 -0
  31. package/integrations/strands-ts/src/telemetry.ts +31 -50
  32. package/kits/knowledge/adapters/flow-runner/index.js +1 -1
  33. package/kits/knowledge/adapters/flow-runner/telemetry.js +2 -2
  34. package/package.json +2 -1
  35. package/scripts/README.md +1 -0
  36. package/scripts/ci/trust-reconcile.js +7 -23
  37. package/scripts/hooks/config-protection.js +6 -0
  38. package/scripts/hooks/evidence-capture.js +26 -47
  39. package/scripts/hooks/lib/local-artifact-paths.js +32 -0
  40. package/scripts/hooks/stop-goal-fit.js +37 -58
  41. package/scripts/hooks/workflow-steering.js +5 -3
  42. package/scripts/lib/command-log-chain.js +78 -0
  43. package/scripts/migrate-local-artifacts.mjs +230 -0
  44. package/scripts/repair-command-log.js +8 -15
  45. package/scripts/telemetry/lib/config.sh +3 -4
  46. package/src/cli/public-api.test.mjs +21 -0
  47. package/src/cli/sidecar-claim-explain.ts +130 -0
  48. package/src/cli/sidecar-pure-helpers.test.mjs +168 -0
  49. package/src/cli/telemetry-doctor.ts +2 -4
  50. package/src/cli/usage-feedback.ts +15 -8
  51. package/src/cli/workflow-artifact-cleanup-audit.ts +4 -2
  52. package/src/cli/workflow-sidecar.ts +17 -131
  53. package/src/index.ts +14 -0
  54. package/src/lib/local-artifact-root.ts +39 -0
  55. package/src/tools/validate-source-tree.ts +1 -0
@@ -15,6 +15,7 @@
15
15
  */
16
16
  import * as path from "node:path";
17
17
  import { loadJson as _loadJson, writeJson as _writeJson } from "./cli/workflow-sidecar.js";
18
+ export { defaultArtifactRootForRead, defaultTelemetryDirForRead, defaultTelemetryDirsForRead, firstExistingPath, flowAgentsArtifactRoot, KONTOURAI_DIR, legacyFlowAgentsArtifactRoot, LEGACY_FLOW_AGENTS_DIR, legacyTelemetryDataDir, LEGACY_TELEMETRY_DIR, telemetryDataDir, } from "./lib/local-artifact-root.js";
18
19
  export {
19
20
  // Trust-bundle (Hachure) validation — the same validator the writer uses.
20
21
  validateTrustBundle,
@@ -0,0 +1,11 @@
1
+ export declare const KONTOURAI_DIR = ".kontourai";
2
+ export declare const LEGACY_FLOW_AGENTS_DIR = ".flow-agents";
3
+ export declare const LEGACY_TELEMETRY_DIR = ".telemetry";
4
+ export declare function flowAgentsArtifactRoot(cwd?: string): string;
5
+ export declare function legacyFlowAgentsArtifactRoot(cwd?: string): string;
6
+ export declare function telemetryDataDir(cwd?: string): string;
7
+ export declare function legacyTelemetryDataDir(cwd?: string): string;
8
+ export declare function firstExistingPath(candidates: string[]): string;
9
+ export declare function defaultArtifactRootForRead(cwd?: string): string;
10
+ export declare function defaultTelemetryDirForRead(cwd?: string): string;
11
+ export declare function defaultTelemetryDirsForRead(cwd?: string): string[];
@@ -0,0 +1,30 @@
1
+ import * as fs from "node:fs";
2
+ import * as path from "node:path";
3
+ export const KONTOURAI_DIR = ".kontourai";
4
+ export const LEGACY_FLOW_AGENTS_DIR = ".flow-agents";
5
+ export const LEGACY_TELEMETRY_DIR = ".telemetry";
6
+ export function flowAgentsArtifactRoot(cwd = process.cwd()) {
7
+ return path.resolve(cwd, KONTOURAI_DIR, "flow-agents");
8
+ }
9
+ export function legacyFlowAgentsArtifactRoot(cwd = process.cwd()) {
10
+ return path.resolve(cwd, LEGACY_FLOW_AGENTS_DIR);
11
+ }
12
+ export function telemetryDataDir(cwd = process.cwd()) {
13
+ return path.resolve(cwd, KONTOURAI_DIR, "telemetry");
14
+ }
15
+ export function legacyTelemetryDataDir(cwd = process.cwd()) {
16
+ return path.resolve(cwd, LEGACY_TELEMETRY_DIR);
17
+ }
18
+ export function firstExistingPath(candidates) {
19
+ return candidates.find((candidate) => fs.existsSync(candidate)) ?? candidates[0];
20
+ }
21
+ export function defaultArtifactRootForRead(cwd = process.cwd()) {
22
+ return firstExistingPath([flowAgentsArtifactRoot(cwd), legacyFlowAgentsArtifactRoot(cwd)]);
23
+ }
24
+ export function defaultTelemetryDirForRead(cwd = process.cwd()) {
25
+ return firstExistingPath([telemetryDataDir(cwd), legacyTelemetryDataDir(cwd)]);
26
+ }
27
+ export function defaultTelemetryDirsForRead(cwd = process.cwd()) {
28
+ const dirs = [telemetryDataDir(cwd), legacyTelemetryDataDir(cwd)];
29
+ return dirs.filter((dir, index) => dirs.indexOf(dir) === index && fs.existsSync(dir));
30
+ }
@@ -74,6 +74,7 @@ const hookFilePolicies = new Map([
74
74
  ["scripts/hooks/lib/audit-transport.sh", { category: "shared hook library", requiredNeedles: ["audit_emit"] }],
75
75
  ["scripts/hooks/lib/hook-flags.js", { category: "shared hook library", requiredNeedles: ["isHookEnabled"] }],
76
76
  ["scripts/hooks/lib/liveness-read.js", { category: "shared hook library", requiredNeedles: ["freshHolders", "readLivenessEvents"] }],
77
+ ["scripts/hooks/lib/local-artifact-paths.js", { category: "shared hook library", requiredNeedles: ["flowAgentsArtifactRoot", "defaultArtifactRootForRead"] }],
77
78
  ["scripts/hooks/lib/patterns.sh", { category: "shared hook library", requiredNeedles: ["_detect_secrets"] }],
78
79
  ["scripts/hooks/lib/resolve-formatter.js", { category: "shared hook library", requiredNeedles: ["resolveFormatter"] }],
79
80
  ]);
@@ -7,10 +7,9 @@ TELEMETRY_CONFIG_FILE="${TELEMETRY_CONFIG_FILE:-${TELEMETRY_DIR}/telemetry.conf}
7
7
  # Defaults
8
8
  TELEMETRY_ENABLED="${TELEMETRY_ENABLED:-true}"
9
9
  # TELEMETRY_DIR is <workspace>/scripts/telemetry, so the workspace root is
10
- # two levels up. Three levels escaped into the workspace's PARENT directory
11
- # (caught by live acceptance smoke 2026-06-11: events landed in /tmp/.telemetry
12
- # instead of the installed workspace).
13
- TELEMETRY_DATA_DIR="${TELEMETRY_DATA_DIR:-$(cd "${TELEMETRY_DIR}/../.." && pwd)/.telemetry}"
10
+ # two levels up. Local runtime telemetry defaults under .kontourai/telemetry;
11
+ # explicit TELEMETRY_DATA_DIR still wins.
12
+ TELEMETRY_DATA_DIR="${TELEMETRY_DATA_DIR:-$(cd "${TELEMETRY_DIR}/../.." && pwd)/.kontourai/telemetry}"
14
13
  TELEMETRY_SESSION_DIR="${TELEMETRY_SESSION_DIR:-${TELEMETRY_DATA_DIR}/sessions}"
15
14
  TELEMETRY_ENRICH_SYSTEM="${TELEMETRY_ENRICH_SYSTEM:-true}"
16
15
  TELEMETRY_ENRICH_WORKSPACE="${TELEMETRY_ENRICH_WORKSPACE:-true}"
@@ -0,0 +1,95 @@
1
+ ---
2
+ title: "ADR 0018: Freeze the Local Shell-Parsing Heuristics; Route New Enforcement to the CI Anchor"
3
+ ---
4
+
5
+ # ADR 0018: Freeze the Local Shell-Parsing Heuristics; Route New Enforcement to the CI Anchor
6
+
7
+ Status: Accepted
8
+
9
+ ## Context
10
+
11
+ ADR 0017 records the anti-gaming trust model: a **layered** defense whose *authoritative*
12
+ boundary is the external CI anchor (`trust-reconcile.js` + the required anti-gaming suite),
13
+ with the local Stop gate and `config-protection.js` as **Layer 1 bar-raisers** — explicitly
14
+ "a bar-raiser, not a wall." That model was earned by a 7-round adversarial loop (PRs
15
+ #213–#228), and ADR 0017 states the loop is "run to convergence."
16
+
17
+ The local shell-parsing heuristics have nonetheless kept accreting per-round string-matching
18
+ rules:
19
+
20
+ - `config-protection.js` — redirect detection (`>`, `>>`, `tee`), interpreter-write
21
+ detection (`node -e`, `py -c`, `sed -i`, `perl -e`), and `cp`/`mv`/`install` target
22
+ matching, grown across R5a / R6 / R7.
23
+ - `hasLaunderingOperator` (now in `scripts/lib/command-log-chain.js`) — the exit-code-mask
24
+ screen, extended in R6a / R7.
25
+
26
+ Their **own headers already admit the ceiling**: *"An agent with shell access can still
27
+ evade. The real anchor is external."* Each new round adds another regex for another evasion
28
+ shape (runtime-constructed paths, base64, exotic tools, here-docs, …) while the set remains,
29
+ by construction, non-exhaustive. This is **low-ROI churn** that also manufactures *false
30
+ confidence* in a control ADR 0017 already classifies as best-effort.
31
+
32
+ Worse, the pattern-chasing has a real cost beyond effort: the blanket `||` rule in
33
+ `hasLaunderingOperator` flags **any** `||` in a claimed verification command, which
34
+ **false-positives legitimate commands** that use `||` for control flow rather than exit-code
35
+ laundering — e.g. `test -d node_modules || npm ci`, `command -v shellcheck || npm i -g shellcheck`.
36
+ It errs toward blocking, which is acceptable for a bar-raiser but not something to keep
37
+ extending.
38
+
39
+ ## Decision
40
+
41
+ **Freeze the local shell-parsing heuristics as feature-complete bar-raisers.** Specifically:
42
+
43
+ 1. **No new evasion-pattern rules** are added to `config-protection.js`'s redirect/
44
+ interpreter/copy detection or to `hasLaunderingOperator`. The current rule sets are the
45
+ frozen surface. They stand as documented, intentionally-incomplete local bar-raisers.
46
+
47
+ 2. **New enforcement concerns route to the external anchor**, not to new local regex:
48
+ - a new way to launder a verification result → strengthen the CI **`trust-reconcile`**
49
+ reconciliation and add a case to the required **anti-gaming suite**
50
+ (`evals/ci/antigaming-suite.sh`);
51
+ - a new self-tamper / kill-switch vector → it is already defended structurally by Layer 2
52
+ (CI does not trust the agent's files or environment) and Layer 4 (CODEOWNERS + required
53
+ checks). Encode the *regression test* there, not another local path matcher.
54
+
55
+ 3. **Still allowed** under the freeze: correctness/bug fixes that do **not** expand the
56
+ pattern surface (e.g. removing a clear false-positive, consolidating duplicated copies as
57
+ in ops#20), and changes required to keep the existing rules working. The freeze targets
58
+ *scope growth*, not maintenance.
59
+
60
+ The decisive line — already the consistent finding of ADR 0017 — is that a local control
61
+ running inside the agent's environment cannot be made airtight by adding rules; effort spent
62
+ making the bar-raiser taller has sharply diminishing returns once the external anchor exists.
63
+
64
+ ## Accepted limitations (documented, not closed)
65
+
66
+ These are now **accepted properties** of the frozen Layer 1, with the CI anchor as the
67
+ authoritative backstop:
68
+
69
+ - **`config-protection.js` — incomplete coverage.** Runtime-constructed protected paths
70
+ (`homedir() + '/.bashrc'`), base64/multi-step path assembly, interpreters outside the
71
+ listed set (ruby, php, …), `rsync`/`dd`/process-substitution writes, and here-docs are
72
+ **not** caught. *Backstop:* Layer 2 does not trust those files; Layer 4 + human review.
73
+ - **`hasLaunderingOperator` — blanket `||` over-blocks.** A legitimate verification command
74
+ that uses `||` for control flow is flagged as laundering. *Backstop / workaround:* split
75
+ the conditional out of the single claimed-verify command; CI reconciles the real result
76
+ regardless. We keep the blanket rule (fail-toward-blocking) rather than enumerate "safe"
77
+ `||` forms — enumeration is exactly the churn this ADR freezes.
78
+
79
+ ## Consequences
80
+
81
+ - The adversarial-hardening loop is declared **converged at the local layer**: ongoing trust
82
+ investment goes to the external anchor (re-run freshness, reconciliation breadth, the
83
+ required suite, activation of `Trust Reconcile` as a no-bypass check — ADR 0017 §Activation,
84
+ issue #225), not to more local string parsing.
85
+ - Reviewers can decline new "Round N" local-heuristic PRs by pointing here: the bar-raiser is
86
+ frozen by decision, and the gap belongs in the CI anchor.
87
+ - No behavior change ships with this ADR. The heuristics keep doing exactly what they do; we
88
+ stop growing them and we write down why.
89
+
90
+ ## Related
91
+
92
+ ADR 0017 (anti-gaming trust security model — the layered defense this freezes the local
93
+ layer of), ADR 0016 (three-hard-boundary model), ADR 0010 (workflow trust state as a hachure
94
+ bundle). Implements ops#21. Consolidation of the frozen helpers: ops#20
95
+ (`scripts/lib/command-log-chain.js`).
@@ -10,9 +10,10 @@ This is the canonical developer-facing map for the Flow Agents repository. Use i
10
10
 
11
11
  - Edit canonical source in the repo root areas listed below, then regenerate derived output with the documented commands.
12
12
  - Do not edit `dist/`, `build/`, or `_site/` by hand. They are generated from tracked source.
13
- - Do not commit local runtime state from `.flow-agents/<slug>/`, `.codex/`, `.claude/`, `.omx/`, `.promptfoo/`, `.telemetry/`, `.surface/`, `.veritas/`, or tool caches.
13
+ - Do not commit local runtime state from `.flow-agents/<slug>/`, `.kontourai/`, `.codex/`, `.claude/`, `.omx/`, `.promptfoo/`, `.telemetry/`, `.surface/`, generated `.veritas/` output, or tool caches; intentionally tracked `.veritas/` governance/config remains durable source.
14
14
  - Runtime workflow artifacts stay local and ignored; promote reviewable or durable outcomes to docs, source, schemas, or provider records before merging to `main`.
15
15
  - Treat generated exports and installed runtime config as products of `packaging/manifest.json`, `src/tools/build-universal-bundles.ts`, `scripts/install-*.sh`, and the source directories they copy.
16
+ - Use `.kontourai/` for non-durable local or generated Kontour workspace state. Keep durable tracked files in intuitive product-owned paths such as `.veritas/`, `.flow/`, `.agents/`, `.claude/commands`, and `docs/`; existing transition ignores remain during migration.
16
17
 
17
18
  ## Target Layout
18
19
 
@@ -32,7 +33,7 @@ This is the canonical developer-facing map for the Flow Agents repository. Use i
32
33
  docs/ # durable docs and GitHub Pages source
33
34
  integrations/ # optional external integration config
34
35
  dist/ build/ _site/ # generated output; ignored
35
- .flow-agents/ .codex/ .claude/ ... # local runtime state; ignored by default
36
+ .flow-agents/ .kontourai/ .codex/ .claude/ ... # local runtime state; ignored by default
36
37
  ```
37
38
 
38
39
  ## Top-Level Inventory
@@ -40,11 +41,12 @@ This is the canonical developer-facing map for the Flow Agents repository. Use i
40
41
  | Path | Classification | Source of truth | Generated or runtime policy | Safe cleanup rule |
41
42
  | --- | --- | --- | --- | --- |
42
43
  | `.flow-agents/` | runtime state | Workflow tools write local session artifacts. | Ignored. | Do not commit task runtime roots; promote durable decisions to docs, source, schemas, or providers before merge. |
44
+ | `.kontourai/` | local/generated workspace state | Local Kontour tools and developer workflows. | Ignored. | Safe local cleanup when no active workflow needs it; durable source and decisions stay in product-owned tracked paths. |
43
45
  | `.claude/` | installed runtime config | Generated bundle or local runtime install. | Ignored. | Reinstall from `dist/claude-code/` instead of editing as source. |
44
46
  | `.codex/` | installed runtime config | Generated bundle or local runtime install. | Ignored. | Reinstall from `dist/codex/` or `scripts/install-codex-home.sh`; do not treat local hooks as canonical. |
45
47
  | `.githooks/` | canonical repo tooling | Tracked repository hook scripts. | Source, not runtime agent hooks. | Keep compatible with `npm run setup:repo-hooks` and `npm run validate:repo-hooks --`. |
46
48
  | `.github/` | canonical CI config | GitHub workflow files. | Source. | Preserve workflow command names and artifact expectations. |
47
- | `.ai/`, `.omx/`, `.promptfoo/`, `.surface/`, `.telemetry/`, `.veritas/` | runtime, cache, or integration output | Local tools and optional integrations. | Ignored runtime state. | Clean locally when not needed; promote only stable integration config under `integrations/` or durable docs. |
49
+ | `.ai/`, `.omx/`, `.promptfoo/`, `.surface/`, `.telemetry/`, `.veritas/` | runtime, cache, or integration output | Local tools, optional integrations, and generated Veritas output. | Ignored runtime state, except intentionally tracked Veritas governance/config. | Clean locally when not needed; promote only stable integration config under `integrations/` or durable docs. |
48
50
  | `.venv/`, `node_modules/`, `test-results/`, `__pycache__/` | dependency/cache output | Package managers and test tools. | Ignored. | Safe local cleanup; recreate with normal install or test commands. |
49
51
  | `_site/` | generated docs output | Built from `docs/`. | Ignored. | Recreate with docs preview/build tooling. |
50
52
  | `agent-cards/` | canonical source | Discovery metadata for routable agents. | Exported into runtime bundles. | Do not delete without checking bundle manifests and evals. |
@@ -115,6 +117,8 @@ The package requires Node `>=22`, and GitHub Actions runs CI on Node 22. Keep `@
115
117
 
116
118
  `.flow-agents/<slug>/` is workflow working memory. Keep plans, sidecars, evidence, and handoffs there while work is active. Promote stable outcomes into `docs/`, schemas, source, or provider records before final acceptance.
117
119
 
120
+ Across Kontour repos, `.kontourai/` is the simple ignored home for non-durable local/generated workspace state. It is not a durable source root. Keep durable governance, Flow config, agent source, command source, and documentation in the product-owned tracked paths where developers expect them, including `.veritas/`, `.flow/`, `.agents/`, `.claude/commands`, and `docs/`. Existing transition ignores such as `.flow-agents/`, `.kontour/`, `.surface/`, `.veritas/evidence/`, `.flow/runs/`, and `.telemetry/` remain in place during migration.
121
+
118
122
  `evals/fixtures/` ownership is tracked in [Fixture Ownership](fixture-ownership.md).
119
123
  Do not delete or add fixture directories without updating that inventory and the
120
124
  owning eval evidence.
@@ -630,14 +630,14 @@ fi
630
630
  echo ""
631
631
  echo "--- opencode Plugin Hook Chain (end-to-end telemetry persistence) ---"
632
632
  # Execute the REAL generated plugin module under node, invoke its handlers,
633
- # and assert telemetry events persist inside the workspace .telemetry/ —
633
+ # and assert telemetry events persist inside the workspace .kontourai/telemetry/ —
634
634
  # not the workspace PARENT. Pins three live-smoke findings (2026-06-11):
635
635
  # 1. spawning process.execPath fails under non-node hosts (NODE_BIN guard)
636
636
  # 2. empty stdin makes the telemetry pipeline silently skip the emit
637
637
  # 3. TELEMETRY_DATA_DIR escaping to the workspace parent (../../.. depth bug)
638
638
  CHAIN_WS="$TMPDIR_EVAL/plugin-chain-opencode"
639
639
  (cd "$ROOT_DIR/dist/opencode" && bash install.sh "$CHAIN_WS" >/dev/null 2>&1) || true
640
- rm -rf "$CHAIN_WS/.telemetry" "$TMPDIR_EVAL/.telemetry"
640
+ rm -rf "$CHAIN_WS/.kontourai/telemetry" "$CHAIN_WS/.telemetry" "$TMPDIR_EVAL/.kontourai" "$TMPDIR_EVAL/.telemetry"
641
641
 
642
642
  if (cd "$CHAIN_WS" && node --input-type=module -e "
643
643
  const mod = await import('./.opencode/plugins/flow-agents.js');
@@ -653,21 +653,21 @@ fi
653
653
  # The telemetry emit is detached (disowned) and can take a few seconds to
654
654
  # land; poll rather than fixed-sleep.
655
655
  for _i in 1 2 3 4 5 6 7 8 9 10; do
656
- [[ -s "$CHAIN_WS/.telemetry/full.jsonl" ]] && break
656
+ [[ -s "$CHAIN_WS/.kontourai/telemetry/full.jsonl" ]] && break
657
657
  sleep 1
658
658
  done
659
- if [[ -s "$CHAIN_WS/.telemetry/full.jsonl" ]] && node -e "
660
- require('fs').readFileSync('$CHAIN_WS/.telemetry/full.jsonl','utf8').trim().split('\n').map(JSON.parse);
659
+ if [[ -s "$CHAIN_WS/.kontourai/telemetry/full.jsonl" ]] && node -e "
660
+ require('fs').readFileSync('$CHAIN_WS/.kontourai/telemetry/full.jsonl','utf8').trim().split('\n').map(JSON.parse);
661
661
  " 2>/dev/null; then
662
- _pass "opencode plugin: handlers persisted telemetry events in workspace .telemetry/"
662
+ _pass "opencode plugin: handlers persisted telemetry events in workspace .kontourai/telemetry/"
663
663
  else
664
- _fail "opencode plugin: no telemetry events persisted in workspace .telemetry/"
664
+ _fail "opencode plugin: no telemetry events persisted in workspace .kontourai/telemetry/"
665
665
  fi
666
666
 
667
- if [[ ! -e "$TMPDIR_EVAL/.telemetry" ]]; then
667
+ if [[ ! -e "$TMPDIR_EVAL/.kontourai/telemetry" && ! -e "$TMPDIR_EVAL/.telemetry" ]]; then
668
668
  _pass "opencode plugin: telemetry did not leak into the workspace parent directory"
669
669
  else
670
- _fail "opencode plugin: telemetry leaked into workspace parent (.telemetry escape)"
670
+ _fail "opencode plugin: telemetry leaked into workspace parent"
671
671
  fi
672
672
 
673
673
  echo ""
@@ -790,10 +790,21 @@ echo "=== AC3.1 — Surface unavailable fail-closed ==="
790
790
  echo ""
791
791
  echo "--- AC3.1a: Isolated (no @kontourai/surface) with high-impact claim → BLOCKS ---"
792
792
 
793
+ # The gate imports the shared scripts/lib/command-log-chain.js helpers. A real
794
+ # install rsyncs the whole tree, so the lib always sits beside the hooks. Mirror that:
795
+ # the isolated gates live at "$TMP/surface-iso*/stop-goal-fit.js", so "../lib" resolves
796
+ # to "$TMP/lib" for both. This keeps the test exercising surface-unavailable fail-closed
797
+ # (not a spurious module-not-found crash).
798
+ ISO_LIBDIR="$TMP/lib"
799
+ mkdir -p "$ISO_LIBDIR"
800
+ cp "$ROOT/scripts/lib/command-log-chain.js" "$ISO_LIBDIR/"
801
+
793
802
  # Create isolated node context that can't find @kontourai/surface
794
803
  ISO_DIR="$TMP/surface-iso"
795
804
  mkdir -p "$ISO_DIR/repo/.flow-agents/surftest"
805
+ mkdir -p "$ISO_DIR/lib"
796
806
  cp "$GATE" "$ISO_DIR/stop-goal-fit.js"
807
+ cp "$ROOT/scripts/hooks/lib/local-artifact-paths.js" "$ISO_DIR/lib/"
797
808
  printf '# Repo\n' > "$ISO_DIR/repo/AGENTS.md"
798
809
  # Non-terminal session (execution phase, in_progress status)
799
810
  printf '%s' '{"schema_version":"1.0","task_slug":"surftest","status":"in_progress","phase":"execution","updated_at":"2026-06-27T00:00:00Z","next_action":{"status":"in_progress","summary":"running"}}' \
@@ -837,7 +848,9 @@ echo "--- AC3.1b: Low-impact-only bundle with unavailable surface → NOT blocke
837
848
 
838
849
  ISO2_DIR="$TMP/surface-iso2"
839
850
  mkdir -p "$ISO2_DIR/repo/.flow-agents/lowtest"
851
+ mkdir -p "$ISO2_DIR/lib"
840
852
  cp "$GATE" "$ISO2_DIR/stop-goal-fit.js"
853
+ cp "$ROOT/scripts/hooks/lib/local-artifact-paths.js" "$ISO2_DIR/lib/"
841
854
  printf '# Repo\n' > "$ISO2_DIR/repo/AGENTS.md"
842
855
  printf '%s' '{"schema_version":"1.0","task_slug":"lowtest","status":"in_progress","phase":"execution","updated_at":"2026-06-27T00:00:00Z","next_action":{"status":"in_progress","summary":"running"}}' \
843
856
  > "$ISO2_DIR/repo/.flow-agents/lowtest/state.json"
@@ -1014,13 +1027,19 @@ else
1014
1027
  fi
1015
1028
 
1016
1029
  echo ""
1017
- echo "--- AC3.3c: Both files use the SAME genesis constant value ---"
1018
- genesis_ec=$(grep "const CHAIN_GENESIS = " "$ROOT/scripts/hooks/evidence-capture.js" | sed "s/.*= '//;s/'.*//")
1019
- genesis_sg=$(grep "const CHAIN_GENESIS_VERIFY = " "$ROOT/scripts/hooks/stop-goal-fit.js" | sed "s/.*= '//;s/'.*//")
1020
- if [ "$genesis_ec" = "$genesis_sg" ] && [ -n "$genesis_ec" ]; then
1021
- _pass "AC3.3: Both files use the same genesis constant ($genesis_ec)"
1030
+ echo "--- AC3.3c: genesis is single-sourced and imported by writer + verifier (cannot diverge) ---"
1031
+ # Stronger than the old "two literals match" check: the genesis literal now lives in
1032
+ # exactly ONE module, and both the writer and verifier import it — so divergence is
1033
+ # structurally impossible rather than merely currently-equal.
1034
+ genesis_lib=$(grep "const CHAIN_GENESIS = " "$ROOT/scripts/lib/command-log-chain.js" | sed "s/.*= '//;s/'.*//")
1035
+ if [ -n "$genesis_lib" ] \
1036
+ && grep -q "require.*command-log-chain" "$ROOT/scripts/hooks/evidence-capture.js" \
1037
+ && grep -q "require.*command-log-chain" "$ROOT/scripts/hooks/stop-goal-fit.js" \
1038
+ && ! grep -qE "const CHAIN_GENESIS = '" "$ROOT/scripts/hooks/evidence-capture.js" \
1039
+ && ! grep -qE "const CHAIN_GENESIS_VERIFY = '" "$ROOT/scripts/hooks/stop-goal-fit.js"; then
1040
+ _pass "AC3.3: genesis single-sourced in scripts/lib/command-log-chain.js ($genesis_lib); writer + verifier import it, no divergent literal"
1022
1041
  else
1023
- _fail "AC3.3: Genesis constant mismatch: evidence-capture=$genesis_ec stop-goal-fit=$genesis_sg"
1042
+ _fail "AC3.3: genesis not single-sourced (lib='$genesis_lib') or a divergent literal remains in a consumer"
1024
1043
  fi
1025
1044
 
1026
1045
 
@@ -0,0 +1,102 @@
1
+ #!/usr/bin/env bash
2
+ set -uo pipefail
3
+
4
+ ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)"
5
+ TMPDIR_EVAL="$(mktemp -d)"
6
+ errors=0
7
+
8
+ cleanup() { rm -rf "$TMPDIR_EVAL"; }
9
+ trap cleanup EXIT
10
+
11
+ _pass() { echo " ✓ $1"; }
12
+ _fail() { echo " ✗ $1"; errors=$((errors + 1)); }
13
+
14
+ REPO="$TMPDIR_EVAL/repo"
15
+ mkdir -p \
16
+ "$REPO/.telemetry" \
17
+ "$REPO/.flow/runs/run-1" \
18
+ "$REPO/.surface/runs/run-2" \
19
+ "$REPO/.veritas/evidence" \
20
+ "$REPO/.veritas/repo-standards" \
21
+ "$REPO/.flow/definitions" \
22
+ "$REPO/.flow-agents/task"
23
+
24
+ printf '%s\n' '{"event":1}' > "$REPO/.telemetry/full.jsonl"
25
+ printf '%s\n' '{"run":1}' > "$REPO/.flow/runs/run-1/state.json"
26
+ printf '%s\n' '{"surface":1}' > "$REPO/.surface/runs/run-2/latest.json"
27
+ printf '%s\n' '{"evidence":1}' > "$REPO/.veritas/evidence/e.json"
28
+ printf '%s\n' '{"durable":1}' > "$REPO/.veritas/repo-map.json"
29
+ printf '%s\n' '{"standard":1}' > "$REPO/.veritas/repo-standards/std.json"
30
+ printf '%s\n' '{"definition":1}' > "$REPO/.flow/definitions/main.json"
31
+ printf '%s\n' '{"state":1}' > "$REPO/.flow-agents/task/state.json"
32
+ ln -s "$REPO/.telemetry/full.jsonl" "$REPO/.telemetry/link.jsonl"
33
+
34
+ if node "$ROOT/scripts/migrate-local-artifacts.mjs" --repo "$REPO" --json > "$TMPDIR_EVAL/dry.json"; then
35
+ if [[ ! -e "$REPO/.kontourai/telemetry/full.jsonl" ]] \
36
+ && node - "$TMPDIR_EVAL/dry.json" <<'NODE'
37
+ const fs = require("node:fs");
38
+ const data = JSON.parse(fs.readFileSync(process.argv[2], "utf8"));
39
+ if (!data.summary.dry_run || data.summary.applied) throw new Error("expected dry-run");
40
+ const text = JSON.stringify(data);
41
+ for (const needle of [".telemetry/full.jsonl", ".flow/runs/run-1/state.json", ".surface/runs/run-2/latest.json", ".veritas/evidence/e.json"]) {
42
+ if (!text.includes(needle)) throw new Error(`missing copy plan ${needle}`);
43
+ }
44
+ for (const needle of [".veritas/repo-map.json", ".veritas/repo-standards", ".flow/definitions", ".flow-agents"]) {
45
+ if (!text.includes(needle)) throw new Error(`missing durable skip ${needle}`);
46
+ }
47
+ if (!text.includes("source-is-symlink")) throw new Error("missing symlink skip");
48
+ NODE
49
+ then
50
+ _pass "migration script defaults to dry-run and reports generated copies plus durable skips"
51
+ else
52
+ _fail "migration dry-run output was wrong"
53
+ fi
54
+ else
55
+ _fail "migration dry-run command failed"
56
+ fi
57
+
58
+ if node "$ROOT/scripts/migrate-local-artifacts.mjs" --repo "$REPO" --apply --include-flow-agents > "$TMPDIR_EVAL/apply.out"; then
59
+ if [[ -f "$REPO/.kontourai/telemetry/full.jsonl" ]] \
60
+ && [[ -f "$REPO/.kontourai/flow/runs/run-1/state.json" ]] \
61
+ && [[ -f "$REPO/.kontourai/surface/runs/run-2/latest.json" ]] \
62
+ && [[ -f "$REPO/.kontourai/veritas/evidence/e.json" ]] \
63
+ && [[ -f "$REPO/.kontourai/flow-agents/task/state.json" ]] \
64
+ && [[ -f "$REPO/.telemetry/full.jsonl" ]] \
65
+ && [[ -f "$REPO/.flow-agents/task/state.json" ]] \
66
+ && [[ ! -e "$REPO/.kontourai/veritas/repo-map.json" ]] \
67
+ && [[ ! -e "$REPO/.kontourai/flow/definitions/main.json" ]]; then
68
+ _pass "migration apply copies only generated allowlisted paths without deleting old files"
69
+ else
70
+ _fail "migration apply copied wrong files or deleted old files"
71
+ fi
72
+ else
73
+ _fail "migration apply command failed"
74
+ fi
75
+
76
+ TARGET_SYMLINK_WORKSPACE="$TMPDIR_EVAL/target-symlink-workspace"
77
+ TARGET_SYMLINK_REPO="$TARGET_SYMLINK_WORKSPACE/repo"
78
+ OUTSIDE="$TMPDIR_EVAL/outside.txt"
79
+ mkdir -p "$TARGET_SYMLINK_REPO/.telemetry" "$TARGET_SYMLINK_REPO/.kontourai/telemetry"
80
+ printf '%s\n' '{"safe":true}' > "$TARGET_SYMLINK_REPO/.telemetry/full.jsonl"
81
+ printf '%s\n' 'outside-original' > "$OUTSIDE"
82
+ ln -s "$OUTSIDE" "$TARGET_SYMLINK_REPO/.kontourai/telemetry/full.jsonl"
83
+
84
+ if node "$ROOT/scripts/migrate-local-artifacts.mjs" --repo "$TARGET_SYMLINK_REPO" --apply --force --json > "$TMPDIR_EVAL/target-symlink.json"; then
85
+ if [[ "$(cat "$OUTSIDE")" == "outside-original" ]] \
86
+ && node - "$TMPDIR_EVAL/target-symlink.json" <<'NODE'
87
+ const fs = require("node:fs");
88
+ const data = JSON.parse(fs.readFileSync(process.argv[2], "utf8"));
89
+ const text = JSON.stringify(data);
90
+ if (!text.includes("target-is-symlink")) throw new Error("missing target symlink skip");
91
+ if (data.summary.copies !== 0) throw new Error(`expected no copies, got ${data.summary.copies}`);
92
+ NODE
93
+ then
94
+ _pass "migration apply --force refuses target symlinks"
95
+ else
96
+ _fail "migration apply --force followed or failed to report target symlink"
97
+ fi
98
+ else
99
+ _fail "migration apply --force target symlink command failed"
100
+ fi
101
+
102
+ exit "$errors"
@@ -37,6 +37,40 @@ VALIDATOR="validate-workflow-artifacts"
37
37
  ARTIFACT_DIR="$TMPDIR_EVAL/repo/.flow-agents/auto-sidecars"
38
38
  mkdir -p "$ARTIFACT_DIR"
39
39
 
40
+ DEFAULT_ROOT_REPO="$TMPDIR_EVAL/default-root-repo"
41
+ mkdir -p "$DEFAULT_ROOT_REPO"
42
+ if (cd "$DEFAULT_ROOT_REPO" && flow_agents_node "$WRITER" ensure-session \
43
+ --task-slug default-root \
44
+ --title "Default Root" \
45
+ --summary "Default root should use the Kontour runtime artifact home." \
46
+ --criterion "Default root exists" \
47
+ --timestamp "2026-05-09T00:00:00Z" >"$TMPDIR_EVAL/default-root.out" 2>"$TMPDIR_EVAL/default-root.err"); then
48
+ if [[ -f "$DEFAULT_ROOT_REPO/.kontourai/flow-agents/default-root/state.json" ]] \
49
+ && [[ -f "$DEFAULT_ROOT_REPO/.kontourai/flow-agents/current.json" ]] \
50
+ && [[ ! -e "$DEFAULT_ROOT_REPO/.flow-agents/default-root/state.json" ]]; then
51
+ _pass "sidecar writer defaults new sessions to .kontourai/flow-agents"
52
+ else
53
+ _fail "sidecar writer default root did not use .kontourai/flow-agents"
54
+ fi
55
+ else
56
+ _fail "sidecar writer default-root ensure-session failed: $(cat "$TMPDIR_EVAL/default-root.out" "$TMPDIR_EVAL/default-root.err")"
57
+ fi
58
+
59
+ OLD_ROOT_REPO="$TMPDIR_EVAL/old-root-repo"
60
+ mkdir -p "$OLD_ROOT_REPO/.flow-agents/old-session"
61
+ cat > "$OLD_ROOT_REPO/.flow-agents/current.json" <<'JSON'
62
+ {"schema_version":"1.0","active_slug":"old-session","artifact_dir":"old-session"}
63
+ JSON
64
+ cat > "$OLD_ROOT_REPO/.flow-agents/old-session/state.json" <<'JSON'
65
+ {"schema_version":"1.0","task_slug":"old-session","status":"planned","phase":"planning","next_action":{"status":"continue","summary":"continue"}}
66
+ JSON
67
+ if (cd "$OLD_ROOT_REPO" && flow_agents_node "$WRITER" current --format slug >"$TMPDIR_EVAL/old-current.out" 2>"$TMPDIR_EVAL/old-current.err") \
68
+ && [[ "$(cat "$TMPDIR_EVAL/old-current.out")" == "old-session" ]]; then
69
+ _pass "sidecar writer reads legacy .flow-agents current session when no new root exists"
70
+ else
71
+ _fail "sidecar writer did not read legacy current session: $(cat "$TMPDIR_EVAL/old-current.out" "$TMPDIR_EVAL/old-current.err")"
72
+ fi
73
+
40
74
  SESSION_ROOT="$TMPDIR_EVAL/repo/.flow-agents"
41
75
  if flow_agents_node "$WRITER" ensure-session \
42
76
  --artifact-root "$SESSION_ROOT" \
package/evals/run.sh CHANGED
@@ -140,6 +140,8 @@ run_static() {
140
140
  bash "$EVAL_DIR/static/test_console_presets.sh" || result=1
141
141
  echo ""
142
142
  bash "$EVAL_DIR/static/test_repo_hooks.sh" || result=1
143
+ echo ""
144
+ bash "$EVAL_DIR/static/test_unit_helpers.sh" || result=1
143
145
  return $result
144
146
  }
145
147
 
@@ -0,0 +1,26 @@
1
+ #!/usr/bin/env bash
2
+ # test_unit_helpers.sh — Layer 1: TypeScript-layer unit tests for the PURE
3
+ # workflow-sidecar helpers (ops#22).
4
+ #
5
+ # Historically every assertion covering workflow-sidecar was black-box bash driving
6
+ # the CLI. These node:test units exercise the pure projection/validation helpers
7
+ # directly against the built JS — fast, deterministic, isolated — and complement
8
+ # (do not replace) the bash evals.
9
+ set -uo pipefail
10
+ ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)"
11
+ cd "$ROOT_DIR"
12
+
13
+ echo "── TS pure-helper unit tests (node --test) ──"
14
+
15
+ # The units import the build output. CI builds before evals; build here too when run
16
+ # standalone so the test is self-sufficient.
17
+ if [[ ! -f build/src/cli/workflow-sidecar.js ]]; then
18
+ npm run build --silent
19
+ fi
20
+
21
+ if node --test src/cli/*.test.mjs; then
22
+ echo " PASS: workflow-sidecar pure-helper unit tests"
23
+ else
24
+ echo " FAIL: workflow-sidecar pure-helper unit tests"
25
+ exit 1
26
+ fi
@@ -41,6 +41,9 @@
41
41
  "conformance:self": "node ../../packaging/conformance/run-conformance.js --self",
42
42
  "conformance": "node ../../packaging/conformance/run-conformance.js --adapter-cmd \"node bin/conformance-shim.mjs\" --level L2"
43
43
  },
44
+ "dependencies": {
45
+ "@kontourai/console-telemetry": "^0.1.0"
46
+ },
44
47
  "peerDependencies": {
45
48
  "@strands-agents/sdk": ">=1.0.0"
46
49
  },