@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
@@ -27,3 +27,5 @@
27
27
  # FlowDefinition resolution + the trust producer/checkpoint/signing the gate depends on
28
28
  /src/lib/flow-resolver.ts @briananderson1222
29
29
  /src/cli/workflow-sidecar.ts @briananderson1222
30
+ # Modules decomposed out of workflow-sidecar.ts (ops#22) keep the same ownership.
31
+ /src/cli/sidecar-*.ts @briananderson1222
@@ -33,7 +33,7 @@ jobs:
33
33
 
34
34
  steps:
35
35
  - name: Checkout
36
- uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
36
+ uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
37
37
 
38
38
  - name: Set up Node.js
39
39
  uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0
@@ -99,7 +99,7 @@ jobs:
99
99
 
100
100
  steps:
101
101
  - name: Checkout
102
- uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
102
+ uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
103
103
 
104
104
  - name: Set up Node.js
105
105
  uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0
@@ -160,7 +160,7 @@ jobs:
160
160
 
161
161
  steps:
162
162
  - name: Checkout
163
- uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
163
+ uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
164
164
 
165
165
  - name: Set up Node.js
166
166
  uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0
@@ -292,7 +292,7 @@ jobs:
292
292
 
293
293
  steps:
294
294
  - name: Checkout
295
- uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
295
+ uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
296
296
 
297
297
  - name: Set up Node.js
298
298
  uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0
@@ -22,7 +22,7 @@ jobs:
22
22
  runs-on: ubuntu-latest
23
23
  steps:
24
24
  - name: Checkout
25
- uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
25
+ uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
26
26
 
27
27
  - name: Configure Pages
28
28
  uses: actions/configure-pages@45bfe0192ca1faeb007ade9deae92b16b8254a0d # v6.0.0
@@ -37,7 +37,7 @@ jobs:
37
37
 
38
38
  steps:
39
39
  - name: Checkout
40
- uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
40
+ uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
41
41
 
42
42
  - name: Set up Node.js
43
43
  uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0
@@ -103,7 +103,7 @@ jobs:
103
103
 
104
104
  steps:
105
105
  - name: Checkout
106
- uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
106
+ uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
107
107
 
108
108
  - name: Set up Node.js
109
109
  uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0
@@ -22,7 +22,7 @@ jobs:
22
22
  runs-on: ubuntu-latest
23
23
  steps:
24
24
  - name: Check out repository
25
- uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
25
+ uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
26
26
  with:
27
27
  fetch-depth: 0
28
28
 
@@ -58,7 +58,7 @@ jobs:
58
58
  id-token: write
59
59
  steps:
60
60
  - name: Check out repository
61
- uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
61
+ uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
62
62
  with:
63
63
  fetch-depth: 0
64
64
 
@@ -37,7 +37,7 @@ jobs:
37
37
  version: pi --version
38
38
  steps:
39
39
  - name: Checkout
40
- uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
40
+ uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
41
41
 
42
42
  - name: Set up Node.js
43
43
  uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0
@@ -67,7 +67,7 @@ jobs:
67
67
  timeout-minutes: 20
68
68
  steps:
69
69
  - name: Checkout
70
- uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
70
+ uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
71
71
 
72
72
  - name: Set up Node.js
73
73
  uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0
@@ -62,7 +62,7 @@ jobs:
62
62
 
63
63
  steps:
64
64
  - name: Checkout
65
- uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
65
+ uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
66
66
 
67
67
  - name: Set up Node.js
68
68
  uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0
package/CHANGELOG.md CHANGED
@@ -1,5 +1,34 @@
1
1
  # Changelog
2
2
 
3
+ ## [2.2.0](https://github.com/kontourai/flow-agents/compare/v2.1.1...v2.2.0) (2026-06-30)
4
+
5
+
6
+ ### Features
7
+
8
+ * **strands-ts:** consume @kontourai/console-telemetry for pricing/cost ([#243](https://github.com/kontourai/flow-agents/issues/243)) ([eba7ff3](https://github.com/kontourai/flow-agents/commit/eba7ff39433f602e7e17b17164a84fa187aa3dc8))
9
+
10
+
11
+ ### Fixes
12
+
13
+ * **flow-agents:** expose kontourai artifact roots ([#258](https://github.com/kontourai/flow-agents/issues/258)) ([82db131](https://github.com/kontourai/flow-agents/commit/82db1319e9115820c606c71c40087c05b1278fce))
14
+
15
+
16
+ ### Documentation
17
+
18
+ * **flow-agents:** ADR 0018 — freeze local shell-parsing heuristics (ops[#21](https://github.com/kontourai/flow-agents/issues/21)) ([#255](https://github.com/kontourai/flow-agents/issues/255)) ([777fc50](https://github.com/kontourai/flow-agents/commit/777fc5029409f85bb3097da7f3141e3b4783a789))
19
+
20
+
21
+ ### Refactoring
22
+
23
+ * **flow-agents:** extract a pure seam from workflow-sidecar + add TS unit layer (ops[#22](https://github.com/kontourai/flow-agents/issues/22)) ([#257](https://github.com/kontourai/flow-agents/issues/257)) ([7af3f44](https://github.com/kontourai/flow-agents/commit/7af3f44fb1d4c05e155da9e4f6090f1a99b534ef))
24
+
25
+ ## [2.1.1](https://github.com/kontourai/flow-agents/compare/v2.1.0...v2.1.1) (2026-06-29)
26
+
27
+
28
+ ### Refactoring
29
+
30
+ * **flow-agents:** one shared module for command-log chain helpers (ops[#20](https://github.com/kontourai/flow-agents/issues/20)) ([#249](https://github.com/kontourai/flow-agents/issues/249)) ([67af85f](https://github.com/kontourai/flow-agents/commit/67af85f5010dace3f33b36b86245e0c7aad95f77))
31
+
3
32
  ## [2.1.0](https://github.com/kontourai/flow-agents/compare/v2.0.1...v2.1.0) (2026-06-29)
4
33
 
5
34
 
@@ -0,0 +1,45 @@
1
+ type AnyObj = Record<string, any>;
2
+ export interface ClaimEvidenceItem {
3
+ evidenceType: string;
4
+ label: string;
5
+ execution: {
6
+ runner: string;
7
+ label: string;
8
+ isError: boolean;
9
+ exitCode: number | null;
10
+ } | null;
11
+ passing: boolean;
12
+ summary: string;
13
+ }
14
+ export interface ClaimExplanation {
15
+ found: boolean;
16
+ status: string;
17
+ value: string;
18
+ claimType: string;
19
+ evidence: ClaimEvidenceItem[];
20
+ policy: {
21
+ id: string;
22
+ requiredEvidence: string[];
23
+ requiredMethods?: string[];
24
+ acceptanceCriteria: string[];
25
+ reviewAuthority: string;
26
+ } | null;
27
+ why: {
28
+ directInputs: AnyObj[];
29
+ leafClaims: AnyObj[];
30
+ diagnostics: AnyObj[];
31
+ transparencyGaps: AnyObj[];
32
+ changeRecords: AnyObj[];
33
+ };
34
+ }
35
+ /**
36
+ * Build a structured explanation for a specific claim.
37
+ * PURE: report + bundle + id in, structured explanation out.
38
+ * No fs, no CLI, no .flow-agents paths. Promotable to Surface #171.
39
+ *
40
+ * @param report TrustReport from buildTrustReport(bundle) — required for derived status
41
+ * @param bundle Raw parsed trust.bundle (BundleFile shape)
42
+ * @param claimId The claim id to explain
43
+ */
44
+ export declare function buildClaimExplanation(report: Record<string, unknown>, bundle: Record<string, unknown>, claimId: string): ClaimExplanation;
45
+ export {};
@@ -0,0 +1,87 @@
1
+ // Claim explanation — a PURE projection extracted from workflow-sidecar.ts (ops#22).
2
+ //
3
+ // buildClaimExplanation is a pure function: report + bundle + id in, structured
4
+ // explanation out. No fs, no CLI, no .flow-agents paths, no shared module state —
5
+ // zero flow-agents specifics inside it, so it is unit-testable in isolation and can
6
+ // be lifted to Surface unchanged (issue #171). The IO command handler `claimLookup`
7
+ // that consumes it stays in workflow-sidecar.ts.
8
+ /**
9
+ * Build a structured explanation for a specific claim.
10
+ * PURE: report + bundle + id in, structured explanation out.
11
+ * No fs, no CLI, no .flow-agents paths. Promotable to Surface #171.
12
+ *
13
+ * @param report TrustReport from buildTrustReport(bundle) — required for derived status
14
+ * @param bundle Raw parsed trust.bundle (BundleFile shape)
15
+ * @param claimId The claim id to explain
16
+ */
17
+ export function buildClaimExplanation(report, bundle, claimId) {
18
+ const reportClaims = Array.isArray(report.claims) ? report.claims : [];
19
+ const reportClaim = reportClaims.find((c) => c.id === claimId);
20
+ if (!reportClaim) {
21
+ return {
22
+ found: false,
23
+ status: "unknown",
24
+ value: "",
25
+ claimType: "",
26
+ evidence: [],
27
+ policy: null,
28
+ why: { directInputs: [], leafClaims: [], diagnostics: [], transparencyGaps: [], changeRecords: [] },
29
+ };
30
+ }
31
+ const bundleClaims = Array.isArray(bundle.claims) ? bundle.claims : [];
32
+ const bundleClaim = bundleClaims.find((c) => c.id === claimId) ?? reportClaim;
33
+ const bundlePolicies = Array.isArray(bundle.policies) ? bundle.policies : [];
34
+ const bundleEvidence = Array.isArray(bundle.evidence) ? bundle.evidence : [];
35
+ // Governing policy — follow verificationPolicyId into bundle.policies[]
36
+ const verificationPolicyId = typeof bundleClaim.verificationPolicyId === "string" ? bundleClaim.verificationPolicyId : undefined;
37
+ const rawPolicy = verificationPolicyId ? bundlePolicies.find((p) => p.id === verificationPolicyId) : undefined;
38
+ const policy = rawPolicy
39
+ ? {
40
+ id: String(rawPolicy.id ?? ""),
41
+ requiredEvidence: Array.isArray(rawPolicy.requiredEvidence) ? rawPolicy.requiredEvidence : [],
42
+ requiredMethods: Array.isArray(rawPolicy.requiredMethods) ? rawPolicy.requiredMethods : undefined,
43
+ acceptanceCriteria: Array.isArray(rawPolicy.acceptanceCriteria) ? rawPolicy.acceptanceCriteria : [],
44
+ reviewAuthority: String(rawPolicy.reviewAuthority ?? ""),
45
+ }
46
+ : null;
47
+ // Evidence enhancement: pull evidence items for this claim, surface the execution block
48
+ const claimEvidenceItems = bundleEvidence.filter((ev) => ev && ev.claimId === claimId);
49
+ const evidence = claimEvidenceItems.map((ev) => {
50
+ const exec = ev.execution && typeof ev.execution === "object" ? ev.execution : null;
51
+ const execution = exec
52
+ ? {
53
+ runner: String(exec.runner ?? exec.label ?? ""),
54
+ label: String(exec.label ?? exec.runner ?? ""),
55
+ isError: Boolean(exec.isError ?? (typeof exec.exitCode === "number" && exec.exitCode !== 0)),
56
+ exitCode: typeof exec.exitCode === "number" ? exec.exitCode : null,
57
+ }
58
+ : null;
59
+ return {
60
+ evidenceType: String(ev.evidenceType ?? ev.type ?? "unknown"),
61
+ label: String(ev.label ?? ev.excerptOrSummary ?? ev.sourceRef ?? ev.id ?? ""),
62
+ execution,
63
+ passing: execution ? !execution.isError : String(ev.status ?? "") !== "disputed",
64
+ summary: String(ev.excerptOrSummary ?? ev.summary ?? ev.label ?? ""),
65
+ };
66
+ });
67
+ // Drilldown: extract from report structure (report.transparencyGaps, report.changeRecords)
68
+ const allGaps = Array.isArray(report.transparencyGaps) ? report.transparencyGaps : [];
69
+ const allChanges = Array.isArray(report.changeRecords) ? report.changeRecords : [];
70
+ const transparencyGaps = allGaps.filter((g) => g && g.claimId === claimId);
71
+ const changeRecords = allChanges.filter((c) => c && c.claimId === claimId);
72
+ return {
73
+ found: true,
74
+ status: String(reportClaim.status ?? "unknown"),
75
+ value: String(bundleClaim.value ?? reportClaim.value ?? ""),
76
+ claimType: String(bundleClaim.claimType ?? reportClaim.claimType ?? ""),
77
+ evidence,
78
+ policy,
79
+ why: {
80
+ directInputs: [], // populated by buildDerivationDrilldown if non-leaf
81
+ leafClaims: [],
82
+ diagnostics: [],
83
+ transparencyGaps,
84
+ changeRecords,
85
+ },
86
+ };
87
+ }
@@ -3,6 +3,7 @@ import * as http from "node:http";
3
3
  import * as https from "node:https";
4
4
  import * as path from "node:path";
5
5
  import { parseArgs, flagBool, flagString } from "../lib/args.js";
6
+ import { telemetryDataDir as defaultTelemetryDataDir } from "../lib/local-artifact-root.js";
6
7
  const defaultChannels = "full";
7
8
  const sensitiveQueryKeys = new Set(["token", "api_key", "apikey", "key", "secret", "password", "auth", "authorization", "access_token"]);
8
9
  function usage() {
@@ -46,10 +47,7 @@ function channelConfigValue(config, channel, key, fallback = "") {
46
47
  }
47
48
  function telemetryDataDir(dest) {
48
49
  const configured = process.env.TELEMETRY_DATA_DIR;
49
- // Must mirror scripts/telemetry/lib/config.sh: the sink lives INSIDE the
50
- // workspace at <dest>/.telemetry. The previous "../.telemetry" duplicated
51
- // the parent-escape bug fixed in config.sh on 2026-06-11.
52
- return configured ? path.resolve(dest, configured) : path.resolve(dest, ".telemetry");
50
+ return configured ? path.resolve(dest, configured) : defaultTelemetryDataDir(dest);
53
51
  }
54
52
  function deriveConsoleEndpoint(consoleUrl, explicitEndpoint) {
55
53
  if (explicitEndpoint)
@@ -3,9 +3,11 @@ import { fileURLToPath } from "node:url";
3
3
  import * as os from "node:os";
4
4
  import * as path from "node:path";
5
5
  import { parseArgs, flagBool, flagList, flagString } from "../lib/args.js";
6
+ import { defaultArtifactRootForRead, defaultTelemetryDirForRead, defaultTelemetryDirsForRead, telemetryDataDir, flowAgentsArtifactRoot, legacyFlowAgentsArtifactRoot } from "../lib/local-artifact-root.js";
6
7
  const VALID_RESULTS = new Set(["success", "partial", "failure", "not_verified"]);
7
8
  function telemetryDir(flags) {
8
- return path.resolve(flagString(flags, "telemetry-dir", process.env.TELEMETRY_DATA_DIR ?? ".telemetry") ?? ".telemetry");
9
+ const explicit = flagString(flags, "telemetry-dir") ?? process.env.TELEMETRY_DATA_DIR;
10
+ return explicit ? path.resolve(explicit) : telemetryDataDir();
9
11
  }
10
12
  function ensureSafeDir(dir) {
11
13
  let current = path.resolve(dir);
@@ -112,7 +114,8 @@ function normalize(input, runtime, flags, fallbackSource = "flow-agents") {
112
114
  function importTelemetry(argv, defaultRuntime) {
113
115
  const { flags } = parseArgs(argv);
114
116
  const runtime = defaultRuntime ?? flagString(flags, "runtime", "codex") ?? "codex";
115
- const input = flagString(flags, "input-full-jsonl") ?? path.join(flagString(flags, "input-telemetry-dir") ?? "", "full.jsonl");
117
+ const explicitInputDir = flagString(flags, "input-telemetry-dir");
118
+ const input = flagString(flags, "input-full-jsonl") ?? path.join(explicitInputDir ? path.resolve(explicitInputDir) : defaultTelemetryDirForRead(), "full.jsonl");
116
119
  if (!input || !fs.existsSync(input))
117
120
  throw new Error(`input telemetry file does not exist: ${input}`);
118
121
  const dir = telemetryDir(flags);
@@ -149,7 +152,7 @@ function syncArtifacts(argv) {
149
152
  const dir = telemetryDir(flags);
150
153
  ensureSafeDir(dir);
151
154
  const artifacts = flagList(flags, "artifact-dir");
152
- const records = (artifacts.length ? artifacts : [".flow-agents"]).flatMap((item) => fs.existsSync(item) ? artifactOutcomes(item, flags) : []);
155
+ const records = (artifacts.length ? artifacts : [flowAgentsArtifactRoot(), legacyFlowAgentsArtifactRoot()]).flatMap((item) => fs.existsSync(item) ? artifactOutcomes(item, flags) : []);
153
156
  writeJsonlUpsert(path.join(dir, "outcomes.jsonl"), records, "outcome_id");
154
157
  if (!flagBool(flags, "quiet"))
155
158
  console.log(`synced ${records.length} artifact outcome(s) to ${path.join(dir, "outcomes.jsonl")}`);
@@ -296,7 +299,10 @@ function markdownReport(data, groupBy) {
296
299
  }
297
300
  function report(argv) {
298
301
  const { flags } = parseArgs(argv);
299
- const dirs = flagList(flags, "telemetry-dir").map((dir) => path.resolve(dir));
302
+ const requestedDirs = flagList(flags, "telemetry-dir");
303
+ const dirs = (requestedDirs.length ? requestedDirs.map((dir) => path.resolve(dir)) : defaultTelemetryDirsForRead()).filter(Boolean);
304
+ if (dirs.length === 0)
305
+ dirs.push(telemetryDataDir());
300
306
  dirs.forEach(ensureSafeDir);
301
307
  const data = reportData(dirs, flagString(flags, "group-by"));
302
308
  const format = flagString(flags, "format", "markdown");
@@ -331,7 +337,7 @@ function registerProject(argv) {
331
337
  ensureSafeDir(globalDir);
332
338
  const repoRoot = path.resolve(flagString(flags, "repo-root", ".") ?? ".");
333
339
  const name = flagString(flags, "name", path.basename(repoRoot)) ?? path.basename(repoRoot);
334
- const record = { name, repo_root: repoRoot, artifact_dir: path.join(repoRoot, ".flow-agents"), input_telemetry_dir: path.join(repoRoot, ".telemetry"), runtime: flagString(flags, "runtime", "codex"), repo: flagString(flags, "repo", name), agent: flagString(flags, "agent"), profile_id: flagString(flags, "profile-id"), prompt_id: flagString(flags, "prompt-id"), prompt_variant: flagString(flags, "prompt-variant"), skill_ids: flagList(flags, "skill-id"), skill_variant: flagString(flags, "skill-variant") };
340
+ const record = { name, repo_root: repoRoot, artifact_dir: defaultArtifactRootForRead(repoRoot), input_telemetry_dir: defaultTelemetryDirForRead(repoRoot), runtime: flagString(flags, "runtime", "codex"), repo: flagString(flags, "repo", name), agent: flagString(flags, "agent"), profile_id: flagString(flags, "profile-id"), prompt_id: flagString(flags, "prompt-id"), prompt_variant: flagString(flags, "prompt-variant"), skill_ids: flagList(flags, "skill-id"), skill_variant: flagString(flags, "skill-variant") };
335
341
  const registryFile = path.join(globalDir, "projects.json");
336
342
  const existing = fs.existsSync(registryFile) ? JSON.parse(fs.readFileSync(registryFile, "utf8")) : { projects: [] };
337
343
  const projects = Array.isArray(existing) ? existing : Array.isArray(existing.projects) ? existing.projects : [];
@@ -355,7 +361,9 @@ function syncProject(project, globalDir) {
355
361
  const name = String(project.name ?? project.repo ?? "project").replace(/[^a-zA-Z0-9_.-]+/g, "-") || "project";
356
362
  const store = path.join(globalDir, "projects", name);
357
363
  ensureSafeDir(store);
358
- const artifactDir = String(project.artifact_dir ?? path.join(String(project.repo_root), ".flow-agents"));
364
+ const repoRoot = String(project.repo_root ?? process.cwd());
365
+ const configuredArtifactDir = typeof project.artifact_dir === "string" ? project.artifact_dir : "";
366
+ const artifactDir = configuredArtifactDir && fs.existsSync(configuredArtifactDir) ? configuredArtifactDir : defaultArtifactRootForRead(repoRoot);
359
367
  const flags = {
360
368
  "repo": String(project.repo ?? name),
361
369
  "runtime": String(project.runtime ?? "codex"),
@@ -374,9 +382,9 @@ function discoverProjects(root) {
374
382
  if (!fs.existsSync(root))
375
383
  return [];
376
384
  const candidates = [root, ...fs.readdirSync(root).map((name) => path.join(root, name))];
377
- return candidates.filter((candidate) => fs.existsSync(path.join(candidate, ".flow-agents"))).map((repoRoot) => {
385
+ return candidates.filter((candidate) => fs.existsSync(flowAgentsArtifactRoot(candidate)) || fs.existsSync(legacyFlowAgentsArtifactRoot(candidate))).map((repoRoot) => {
378
386
  const name = path.basename(repoRoot);
379
- return { name, repo: name, repo_root: repoRoot, artifact_dir: path.join(repoRoot, ".flow-agents"), input_telemetry_dir: path.join(repoRoot, ".telemetry"), runtime: "codex", skill_ids: [] };
387
+ return { name, repo: name, repo_root: repoRoot, artifact_dir: defaultArtifactRootForRead(repoRoot), input_telemetry_dir: defaultTelemetryDirForRead(repoRoot), runtime: "codex", skill_ids: [] };
380
388
  });
381
389
  }
382
390
  function syncProjects(argv) {
@@ -2,6 +2,7 @@ import * as fs from "node:fs";
2
2
  import { fileURLToPath } from "node:url";
3
3
  import * as path from "node:path";
4
4
  import { flagBool, flagString, parseArgs } from "../lib/args.js";
5
+ import { defaultArtifactRootForRead } from "../lib/local-artifact-root.js";
5
6
  const ACTIVE_STATUSES = new Set([
6
7
  "planning",
7
8
  "planned",
@@ -23,7 +24,7 @@ function printHelp() {
23
24
  console.log("Read-only dry-run audit for local workflow artifact directories.");
24
25
  console.log("");
25
26
  console.log("Options:");
26
- console.log(" --artifact-root <path> Local artifact root to scan (default: .flow-agents)");
27
+ console.log(" --artifact-root <path> Local artifact root to scan (default: .kontourai/flow-agents, with .flow-agents read fallback)");
27
28
  console.log(" --json Print stable JSON buckets instead of text");
28
29
  console.log(" --help Show this help");
29
30
  console.log("");
@@ -257,7 +258,8 @@ export function main(argv = process.argv.slice(2)) {
257
258
  }
258
259
  let result;
259
260
  try {
260
- result = audit(flagString(args.flags, "artifact-root", ".flow-agents") ?? ".flow-agents");
261
+ const root = flagString(args.flags, "artifact-root") ? path.resolve(flagString(args.flags, "artifact-root")) : defaultArtifactRootForRead();
262
+ result = audit(root);
261
263
  }
262
264
  catch (error) {
263
265
  console.error(`workflow-artifact-cleanup-audit: ${error instanceof Error ? error.message : String(error)}`);
@@ -294,47 +294,5 @@ export declare function gateAdvisoryFix(calibration: GateCalibration, claimId: s
294
294
  * @param now Optional timestamp override for deterministic tests
295
295
  */
296
296
  export declare function buildGateInquiryRecords(bundle: BundleFile, blockSignal: GateBlockSignal, slug: string, expectedCriterionIds: string[], surface: SurfaceModule, now?: Date): AnyObj[];
297
- export interface ClaimEvidenceItem {
298
- evidenceType: string;
299
- label: string;
300
- execution: {
301
- runner: string;
302
- label: string;
303
- isError: boolean;
304
- exitCode: number | null;
305
- } | null;
306
- passing: boolean;
307
- summary: string;
308
- }
309
- export interface ClaimExplanation {
310
- found: boolean;
311
- status: string;
312
- value: string;
313
- claimType: string;
314
- evidence: ClaimEvidenceItem[];
315
- policy: {
316
- id: string;
317
- requiredEvidence: string[];
318
- requiredMethods?: string[];
319
- acceptanceCriteria: string[];
320
- reviewAuthority: string;
321
- } | null;
322
- why: {
323
- directInputs: AnyObj[];
324
- leafClaims: AnyObj[];
325
- diagnostics: AnyObj[];
326
- transparencyGaps: AnyObj[];
327
- changeRecords: AnyObj[];
328
- };
329
- }
330
- /**
331
- * Build a structured explanation for a specific claim.
332
- * PURE: report + bundle + id in, structured explanation out.
333
- * No fs, no CLI, no .flow-agents paths. Promotable to Surface #171.
334
- *
335
- * @param report TrustReport from buildTrustReport(bundle) — required for derived status
336
- * @param bundle Raw parsed trust.bundle (BundleFile shape)
337
- * @param claimId The claim id to explain
338
- */
339
- export declare function buildClaimExplanation(report: Record<string, unknown>, bundle: Record<string, unknown>, claimId: string): ClaimExplanation;
340
- export {};
297
+ export { buildClaimExplanation } from "./sidecar-claim-explain.js";
298
+ export type { ClaimEvidenceItem, ClaimExplanation } from "./sidecar-claim-explain.js";
@@ -7,6 +7,7 @@ import { createRequire } from "node:module";
7
7
  import { fileURLToPath } from "node:url";
8
8
  // ADR 0016 Abstraction A: shared FlowDefinition resolver (P-a)
9
9
  import { resolveActiveFlowStep, resolveFlowFilePath, resolvePhaseMap, resolveRouteBackPolicy } from "../lib/flow-resolver.js";
10
+ import { defaultArtifactRootForRead, flowAgentsArtifactRoot } from "../lib/local-artifact-root.js";
10
11
  export const statuses = new Set(["new", "planning", "planned", "in_progress", "blocked", "verifying", "verified", "needs_decision", "not_verified", "failed", "delivered", "accepted", "archived"]);
11
12
  export const phases = ["idea", "backlog", "pickup", "planning", "execution", "verification", "goal_fit", "evidence", "release", "learning", "done"];
12
13
  export const checkKinds = new Set(["build", "types", "lint", "test", "security", "diff", "browser", "runtime", "policy", "external"]);
@@ -618,7 +619,7 @@ function explicitArtifactRoot(p) {
618
619
  const explicit = opt(p, "artifact-dir");
619
620
  const configuredRoot = opt(p, "artifact-root");
620
621
  if (!explicit)
621
- return path.resolve(configuredRoot || ".flow-agents");
622
+ return configuredRoot ? path.resolve(configuredRoot) : flowAgentsArtifactRoot();
622
623
  const dir = path.resolve(explicit);
623
624
  if (!fs.existsSync(dir))
624
625
  die(`artifact directory does not exist: ${dir}`);
@@ -880,7 +881,7 @@ function initSidecars(dir, slug, sourceRequest, summary, nextAction, timestamp,
880
881
  });
881
882
  }
882
883
  function ensureSession(p) {
883
- const root = path.resolve(opt(p, "artifact-root", ".flow-agents"));
884
+ const root = opt(p, "artifact-root") ? path.resolve(opt(p, "artifact-root")) : flowAgentsArtifactRoot();
884
885
  const slug = opt(p, "task-slug") || (opt(p, "work-item") ? workItemSlug(opt(p, "work-item")) : die("--task-slug is required (or pass --work-item to derive it)"));
885
886
  const dir = sessionDirFor(root, slug);
886
887
  fs.mkdirSync(dir, { recursive: true });
@@ -913,7 +914,7 @@ function ensureSession(p) {
913
914
  return 0;
914
915
  }
915
916
  function current(p) {
916
- const root = path.resolve(opt(p, "artifact-root", ".flow-agents"));
917
+ const root = opt(p, "artifact-root") ? path.resolve(opt(p, "artifact-root")) : defaultArtifactRootForRead();
917
918
  const dir = currentDir(root);
918
919
  if (!dir)
919
920
  die("no current workflow session is recorded");
@@ -1908,7 +1909,7 @@ function assertExistingLearningValid(dir) {
1908
1909
  }
1909
1910
  }
1910
1911
  async function dogfoodPass(p) {
1911
- const root = path.resolve(opt(p, "artifact-root", ".flow-agents"));
1912
+ const root = opt(p, "artifact-root") ? path.resolve(opt(p, "artifact-root")) : defaultArtifactRootForRead();
1912
1913
  const dir = path.resolve(opt(p, "artifact-dir") || currentDir(root) || "");
1913
1914
  requireArtifactDirUnderRoot(dir, root);
1914
1915
  assertExistingLearningValid(dir);
@@ -2283,7 +2284,7 @@ function loadSurfacePanelJs() {
2283
2284
  return "";
2284
2285
  }
2285
2286
  async function renderTrustPanel(p) {
2286
- const root = path.resolve(opt(p, "artifact-root", ".flow-agents"));
2287
+ const root = opt(p, "artifact-root") ? path.resolve(opt(p, "artifact-root")) : defaultArtifactRootForRead();
2287
2288
  const dir = p.positional[0] ? artifactDirFrom(p.positional[0]) : currentDir(root);
2288
2289
  if (!dir)
2289
2290
  die("render-trust-panel requires a workflow dir or a recorded current session");
@@ -2482,7 +2483,7 @@ function livenessLifecycle(taskDir, slug, kind, timestamp) {
2482
2483
  catch { /* best-effort; liveness is advisory and must never break the workflow */ }
2483
2484
  }
2484
2485
  async function liveness(p) {
2485
- const root = path.resolve(opt(p, "artifact-root", ".flow-agents"));
2486
+ const root = opt(p, "artifact-root") ? path.resolve(opt(p, "artifact-root")) : flowAgentsArtifactRoot();
2486
2487
  const action = p.positional[0] || "";
2487
2488
  const subjectId = p.positional[1] || "";
2488
2489
  const actor = opt(p, "actor", process.env.FLOW_AGENTS_ACTOR || "unknown");
@@ -2545,86 +2546,14 @@ async function liveness(p) {
2545
2546
  die("liveness action must be one of: claim | heartbeat | release | status");
2546
2547
  return 1;
2547
2548
  }
2548
- /**
2549
- * Build a structured explanation for a specific claim.
2550
- * PURE: report + bundle + id in, structured explanation out.
2551
- * No fs, no CLI, no .flow-agents paths. Promotable to Surface #171.
2552
- *
2553
- * @param report TrustReport from buildTrustReport(bundle) required for derived status
2554
- * @param bundle Raw parsed trust.bundle (BundleFile shape)
2555
- * @param claimId The claim id to explain
2556
- */
2557
- export function buildClaimExplanation(report, bundle, claimId) {
2558
- const reportClaims = Array.isArray(report.claims) ? report.claims : [];
2559
- const reportClaim = reportClaims.find((c) => c.id === claimId);
2560
- if (!reportClaim) {
2561
- return {
2562
- found: false,
2563
- status: "unknown",
2564
- value: "",
2565
- claimType: "",
2566
- evidence: [],
2567
- policy: null,
2568
- why: { directInputs: [], leafClaims: [], diagnostics: [], transparencyGaps: [], changeRecords: [] },
2569
- };
2570
- }
2571
- const bundleClaims = Array.isArray(bundle.claims) ? bundle.claims : [];
2572
- const bundleClaim = bundleClaims.find((c) => c.id === claimId) ?? reportClaim;
2573
- const bundlePolicies = Array.isArray(bundle.policies) ? bundle.policies : [];
2574
- const bundleEvidence = Array.isArray(bundle.evidence) ? bundle.evidence : [];
2575
- // Governing policy — follow verificationPolicyId into bundle.policies[]
2576
- const verificationPolicyId = typeof bundleClaim.verificationPolicyId === "string" ? bundleClaim.verificationPolicyId : undefined;
2577
- const rawPolicy = verificationPolicyId ? bundlePolicies.find((p) => p.id === verificationPolicyId) : undefined;
2578
- const policy = rawPolicy
2579
- ? {
2580
- id: String(rawPolicy.id ?? ""),
2581
- requiredEvidence: Array.isArray(rawPolicy.requiredEvidence) ? rawPolicy.requiredEvidence : [],
2582
- requiredMethods: Array.isArray(rawPolicy.requiredMethods) ? rawPolicy.requiredMethods : undefined,
2583
- acceptanceCriteria: Array.isArray(rawPolicy.acceptanceCriteria) ? rawPolicy.acceptanceCriteria : [],
2584
- reviewAuthority: String(rawPolicy.reviewAuthority ?? ""),
2585
- }
2586
- : null;
2587
- // Evidence enhancement: pull evidence items for this claim, surface the execution block
2588
- const claimEvidenceItems = bundleEvidence.filter((ev) => ev && ev.claimId === claimId);
2589
- const evidence = claimEvidenceItems.map((ev) => {
2590
- const exec = ev.execution && typeof ev.execution === "object" ? ev.execution : null;
2591
- const execution = exec
2592
- ? {
2593
- runner: String(exec.runner ?? exec.label ?? ""),
2594
- label: String(exec.label ?? exec.runner ?? ""),
2595
- isError: Boolean(exec.isError ?? (typeof exec.exitCode === "number" && exec.exitCode !== 0)),
2596
- exitCode: typeof exec.exitCode === "number" ? exec.exitCode : null,
2597
- }
2598
- : null;
2599
- return {
2600
- evidenceType: String(ev.evidenceType ?? ev.type ?? "unknown"),
2601
- label: String(ev.label ?? ev.excerptOrSummary ?? ev.sourceRef ?? ev.id ?? ""),
2602
- execution,
2603
- passing: execution ? !execution.isError : String(ev.status ?? "") !== "disputed",
2604
- summary: String(ev.excerptOrSummary ?? ev.summary ?? ev.label ?? ""),
2605
- };
2606
- });
2607
- // Drilldown: extract from report structure (report.transparencyGaps, report.changeRecords)
2608
- const allGaps = Array.isArray(report.transparencyGaps) ? report.transparencyGaps : [];
2609
- const allChanges = Array.isArray(report.changeRecords) ? report.changeRecords : [];
2610
- const transparencyGaps = allGaps.filter((g) => g && g.claimId === claimId);
2611
- const changeRecords = allChanges.filter((c) => c && c.claimId === claimId);
2612
- return {
2613
- found: true,
2614
- status: String(reportClaim.status ?? "unknown"),
2615
- value: String(bundleClaim.value ?? reportClaim.value ?? ""),
2616
- claimType: String(bundleClaim.claimType ?? reportClaim.claimType ?? ""),
2617
- evidence,
2618
- policy,
2619
- why: {
2620
- directInputs: [], // populated by buildDerivationDrilldown if non-leaf
2621
- leafClaims: [],
2622
- diagnostics: [],
2623
- transparencyGaps,
2624
- changeRecords,
2625
- },
2626
- };
2627
- }
2549
+ // ─────────────────────────────────────────────────────────────────────────────
2550
+ // ─── Claim Lookup pure helper (promotable to Surface #171) ─────────────────
2551
+ // buildClaimExplanation + its types are extracted to ./sidecar-claim-explain.ts
2552
+ // (ops#22): a PURE projection (report + bundle + id in, structured explanation out)
2553
+ // with no fs/CLI/shared state, unit-tested in isolation. Re-exported here so the
2554
+ // library facade (src/index.ts) and the IO `claimLookup` handler below are unchanged.
2555
+ export { buildClaimExplanation } from "./sidecar-claim-explain.js";
2556
+ import { buildClaimExplanation } from "./sidecar-claim-explain.js";
2628
2557
  /**
2629
2558
  * claim <id> <dir>
2630
2559
  *
@@ -2778,7 +2707,9 @@ async function main() {
2778
2707
  const p = parseArgs(process.argv.slice(2));
2779
2708
  if (!p.command)
2780
2709
  die("workflow-sidecar command is required");
2781
- const lockRoot = ["ensure-session", "current", "dogfood-pass", "liveness"].includes(p.command) ? path.resolve(opt(p, "artifact-root", ".flow-agents")) : p.command === "record-agent-event" ? explicitArtifactRoot(p) : p.command === "claim" ? (p.positional[1] ? path.resolve(p.positional[1]) : "") : p.positional[0] ? artifactDirFrom(p.positional[0]) : "";
2710
+ const lockRoot = ["ensure-session", "current", "dogfood-pass", "liveness"].includes(p.command)
2711
+ ? (opt(p, "artifact-root") ? path.resolve(opt(p, "artifact-root")) : (p.command === "ensure-session" ? flowAgentsArtifactRoot() : defaultArtifactRootForRead()))
2712
+ : p.command === "record-agent-event" ? explicitArtifactRoot(p) : p.command === "claim" ? (p.positional[1] ? path.resolve(p.positional[1]) : "") : p.positional[0] ? artifactDirFrom(p.positional[0]) : "";
2782
2713
  return withLock(lockRoot, ["ensure-session", "record-agent-event", "dogfood-pass"].includes(p.command), p.command, () => {
2783
2714
  switch (p.command) {
2784
2715
  case "ensure-session": return ensureSession(p);
@@ -1,3 +1,4 @@
1
+ export { defaultArtifactRootForRead, defaultTelemetryDirForRead, defaultTelemetryDirsForRead, firstExistingPath, flowAgentsArtifactRoot, KONTOURAI_DIR, legacyFlowAgentsArtifactRoot, LEGACY_FLOW_AGENTS_DIR, legacyTelemetryDataDir, LEGACY_TELEMETRY_DIR, telemetryDataDir, } from "./lib/local-artifact-root.js";
1
2
  export { validateTrustBundle, normalizeCheck, normalizeFinding, normalizeLearning, normalizeEvidenceRefs, validateEvidenceRef, validateLearningCorrection, loadJson, writeJson, appendJsonl, sidecarBase, writeState, statuses, phases, checkKinds, checkStatuses, verdicts, } from "./cli/workflow-sidecar.js";
2
3
  /** Read a sidecar JSON file from a workflow artifact directory; returns `{}` if absent. */
3
4
  export declare function readSidecar(dir: string, name: string): Record<string, any>;