@kontourai/flow-agents 2.0.0 → 2.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.
- package/.github/actions/trust-verify/action.yml +4 -2
- package/.github/workflows/ci.yml +12 -0
- package/.github/workflows/runtime-compat.yml +1 -1
- package/CHANGELOG.md +29 -0
- package/README.md +3 -3
- package/build/src/cli/workflow-sidecar.d.ts +16 -0
- package/build/src/cli/workflow-sidecar.js +72 -12
- package/build/src/lib/flow-resolver.d.ts +29 -0
- package/build/src/lib/flow-resolver.js +71 -0
- package/context/scripts/telemetry/lib/config.sh +15 -0
- package/context/scripts/telemetry/telemetry.conf +4 -0
- package/context/scripts/telemetry/telemetry.sh +23 -1
- package/docs/design/flowrun-eventsourcing-design.md +216 -0
- package/docs/design/workflowrun-observability-design.md +431 -0
- package/evals/ci/antigaming-suite.sh +2 -0
- package/evals/ci/run-baseline.sh +2 -0
- package/evals/integration/test_command_log_concurrency.sh +114 -0
- package/evals/integration/test_command_log_fork_classification.sh +134 -0
- package/evals/integration/test_kit_identity_trust.sh +393 -0
- package/evals/integration/test_usage_cost.sh +119 -0
- package/evals/integration/test_verify_cli.sh +23 -0
- package/evals/run.sh +2 -0
- package/integrations/strands/flow_agents_strands/hooks.py +126 -1
- package/integrations/strands/flow_agents_strands/telemetry.py +172 -0
- package/integrations/strands/tests/test_usage.py +129 -0
- package/integrations/strands-ts/src/hooks.ts +135 -1
- package/integrations/strands-ts/src/telemetry.ts +170 -0
- package/integrations/strands-ts/test/test-usage.ts +85 -0
- package/package.json +5 -5
- package/scripts/hooks/evidence-capture.js +75 -13
- package/scripts/hooks/stop-goal-fit.js +76 -23
- package/scripts/repair-command-log.js +115 -0
- package/scripts/telemetry/lib/config.sh +15 -0
- package/scripts/telemetry/lib/pricing.sh +42 -0
- package/scripts/telemetry/lib/usage.sh +108 -0
- package/scripts/telemetry/pricing.golden.json +15 -0
- package/scripts/telemetry/pricing.json +31 -0
- package/scripts/telemetry/telemetry.conf +4 -0
- package/scripts/telemetry/telemetry.sh +23 -1
- package/src/cli/workflow-sidecar.ts +73 -11
- package/src/lib/flow-resolver.ts +85 -0
|
@@ -113,7 +113,9 @@ runs:
|
|
|
113
113
|
BUNDLE_ARG=""
|
|
114
114
|
fi
|
|
115
115
|
|
|
116
|
-
|
|
116
|
+
# action_path is .github/actions/trust-verify/ — climb THREE levels to the
|
|
117
|
+
# repo root where scripts/ lives (trust-verify -> actions -> .github -> root).
|
|
118
|
+
node "${{ github.action_path }}/../../../scripts/ci/trust-reconcile.js" \
|
|
117
119
|
--commands "$VERIFY_COMMAND" \
|
|
118
120
|
--repo-root "${{ github.workspace }}" \
|
|
119
121
|
$BUNDLE_ARG || {
|
|
@@ -130,7 +132,7 @@ runs:
|
|
|
130
132
|
- name: Mint attestation
|
|
131
133
|
if: inputs.sign == 'true' && steps.trust-verify.outcome == 'success'
|
|
132
134
|
shell: bash
|
|
133
|
-
run: node "${{ github.action_path }}
|
|
135
|
+
run: node "${{ github.action_path }}/../../../scripts/ci/mint-attestation.js"
|
|
134
136
|
|
|
135
137
|
- name: Upload attestation
|
|
136
138
|
if: inputs.sign == 'true' && steps.trust-verify.outcome == 'success'
|
package/.github/workflows/ci.yml
CHANGED
|
@@ -14,6 +14,14 @@ concurrency:
|
|
|
14
14
|
cancel-in-progress: true
|
|
15
15
|
|
|
16
16
|
jobs:
|
|
17
|
+
# Suite-wide secret-scan gate, defined once in kontourai/.github (Hachure: one
|
|
18
|
+
# normative source). Scans git-tracked history; gitignored runtime/.env excluded.
|
|
19
|
+
secret-scan:
|
|
20
|
+
name: Secret Scan
|
|
21
|
+
uses: kontourai/.github/.github/workflows/secret-scan.yml@main
|
|
22
|
+
permissions:
|
|
23
|
+
contents: read
|
|
24
|
+
|
|
17
25
|
source-and-static:
|
|
18
26
|
name: Source and Static
|
|
19
27
|
runs-on: ubuntu-latest
|
|
@@ -242,6 +250,10 @@ jobs:
|
|
|
242
250
|
continue-on-error: true
|
|
243
251
|
run: bash evals/ci/run-baseline.sh --check telemetry-doctor-integration
|
|
244
252
|
|
|
253
|
+
- name: Usage and cost integration
|
|
254
|
+
continue-on-error: true
|
|
255
|
+
run: bash evals/ci/run-baseline.sh --check usage-and-cost-integration
|
|
256
|
+
|
|
245
257
|
- name: Utterance check integration
|
|
246
258
|
continue-on-error: true
|
|
247
259
|
run: bash evals/ci/run-baseline.sh --check utterance-check-integration
|
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,34 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## [2.1.0](https://github.com/kontourai/flow-agents/compare/v2.0.1...v2.1.0) (2026-06-29)
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
### Features
|
|
7
|
+
|
|
8
|
+
* **telemetry:** derive live pricing source from the console ([#242](https://github.com/kontourai/flow-agents/issues/242)) ([ddce44e](https://github.com/kontourai/flow-agents/commit/ddce44e813e9a3515953324f4878bf51c33252ba))
|
|
9
|
+
* **telemetry:** real token+cost capture with single-source versioned pricing ([#241](https://github.com/kontourai/flow-agents/issues/241)) ([b0bd4c3](https://github.com/kontourai/flow-agents/commit/b0bd4c347897ec77f60d84cae702e7f42b2871d7))
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
### Fixes
|
|
13
|
+
|
|
14
|
+
* **evidence-capture:** serialize command-log appends to prevent chain forks ([#232](https://github.com/kontourai/flow-agents/issues/232)) ([bb167e9](https://github.com/kontourai/flow-agents/commit/bb167e93e7f6cc19baa88da613e96fe88a681c10))
|
|
15
|
+
* **flow-agents:** stop corrupting sidecar JSONL event lines ([#244](https://github.com/kontourai/flow-agents/issues/244)) ([fb65d10](https://github.com/kontourai/flow-agents/commit/fb65d1017e5cb659ce2b48da7a548f0c1f360426))
|
|
16
|
+
* **trust-verify action:** correct cross-repo script path (../../ → ../../../) ([#240](https://github.com/kontourai/flow-agents/issues/240)) ([a75a6d2](https://github.com/kontourai/flow-agents/commit/a75a6d28baf68b4be527a2e8cdff8f007af88bd5))
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
### Documentation
|
|
20
|
+
|
|
21
|
+
* **design:** preserve WorkflowRun observability + FlowRun event-sourcing design notes ([#239](https://github.com/kontourai/flow-agents/issues/239)) ([c2dc116](https://github.com/kontourai/flow-agents/commit/c2dc11698cf63704f14087001c4494079195d197))
|
|
22
|
+
* **flow-agents:** advertise the real eval coverage, clearly scoped (ops[#23](https://github.com/kontourai/flow-agents/issues/23)) ([#248](https://github.com/kontourai/flow-agents/issues/248)) ([d208207](https://github.com/kontourai/flow-agents/commit/d20820749408d5fa63f2bf1470252000712de5d8))
|
|
23
|
+
|
|
24
|
+
## [2.0.1](https://github.com/kontourai/flow-agents/compare/v2.0.0...v2.0.1) (2026-06-27)
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
### Fixes
|
|
28
|
+
|
|
29
|
+
* carry KIT IDENTITY through the trust chain — stop flattening non-builder kits to "builder" ([#235](https://github.com/kontourai/flow-agents/issues/235)) ([02d2782](https://github.com/kontourai/flow-agents/commit/02d2782ca8d9158a018d0fc6c35adc6a34c827d5))
|
|
30
|
+
* **gate:** classify concurrent-fork vs tamper; never hard-block a benign race ([#233](https://github.com/kontourai/flow-agents/issues/233)) ([e24743b](https://github.com/kontourai/flow-agents/commit/e24743b7dbff05df64e198e420e47841ce534df3))
|
|
31
|
+
|
|
3
32
|
## [2.0.0](https://github.com/kontourai/flow-agents/compare/v1.4.0...v2.0.0) (2026-06-27)
|
|
4
33
|
|
|
5
34
|
|
package/README.md
CHANGED
|
@@ -29,7 +29,7 @@ Flow Agents addresses this with a process-discipline layer that sits between the
|
|
|
29
29
|
- **Four canonical policies** — workflow steering (phase reminders at each turn), quality gate (per-file checks after edits), stop-goal-fit (evidence check before the agent stops), and config protection (veto writes to linter/formatter configs). Each policy class has a canonical script under `scripts/hooks/` and compiles to the host's native hook format.
|
|
30
30
|
- **Evidence over confidence** — important work ends with tests, browser checks, CI results, review findings, governance reports, or an explicit `NOT_VERIFIED` gap. Optional [Veritas](docs/veritas-integration.md) integration attaches repo-governance evidence without making it mandatory.
|
|
31
31
|
- **Verifiable, un-gameable "done"** — the agent can't mark work complete that isn't: the gate re-derives the verdict from independent evidence, an external CI anchor re-runs the verification fresh and fails the merge on any divergence, and CI mints a Sigstore-signed record of what shipped. See [Verifiable Trust — why "done" actually means done](docs/verifiable-trust.md).
|
|
32
|
-
- **Evals that keep the bundle honest** —
|
|
32
|
+
- **Evals that keep the bundle honest** — 60 integration scenarios (1,829 assertions) and 7 static suites (110 assertions) validate the skills, contracts, fixtures, and hook influence as the bundle evolves.
|
|
33
33
|
|
|
34
34
|
## Flow Agents as a process-discipline layer
|
|
35
35
|
|
|
@@ -52,8 +52,8 @@ L2 means all four policy classes with blocking; L1 means steering and stop-goal-
|
|
|
52
52
|
|
|
53
53
|
| Runtime | Ships | Tested |
|
|
54
54
|
| --- | --- | --- |
|
|
55
|
-
| Claude Code | install + hooks + bundle |
|
|
56
|
-
| Codex | install + hooks + bundle |
|
|
55
|
+
| Claude Code | install + hooks + bundle | 60 integration scenarios + 7 static suites (1,939 assertions) — reference implementation |
|
|
56
|
+
| Codex | install + hooks + bundle | 60 integration scenarios + 7 static suites (1,939 assertions) — reference implementation |
|
|
57
57
|
| Kiro | install + hooks + bundle | included in bundle assertions |
|
|
58
58
|
|
|
59
59
|
**Partial support — L1 (steering + stop-goal-fit warning)**
|
|
@@ -167,6 +167,22 @@ export declare function sidecarBase(slug: string): AnyObj;
|
|
|
167
167
|
export declare function validateEvidenceRef(ref: AnyObj, label: string): AnyObj;
|
|
168
168
|
export declare function normalizeEvidenceRefs(raw: unknown, label: string): AnyObj[];
|
|
169
169
|
export declare function normalizeCheck(raw: AnyObj): AnyObj;
|
|
170
|
+
/**
|
|
171
|
+
* Derive kit identity from a parsed trust.bundle by structurally reading the
|
|
172
|
+
* DECLARED primary claim (kit-typed) rather than hardcoding "builder".
|
|
173
|
+
*
|
|
174
|
+
* Resolution order (no fallbacks to "builder"):
|
|
175
|
+
* 1. First non-workflow.* claim in bundle.claims[] → claimType drives kitId + subject.
|
|
176
|
+
* 2. No kit-typed claim: try current.json active_flow_id adjacent to the bundle file
|
|
177
|
+
* (bundle lives at <session-dir>/trust.bundle → flowAgentsDir = grandparent).
|
|
178
|
+
* 3. Genuinely unknown: mark as "unknown" — never hardcode a kit identity.
|
|
179
|
+
*/
|
|
180
|
+
export declare function kitIdentityFromBundle(raw: AnyObj, bundleFile: string): {
|
|
181
|
+
claimType: string;
|
|
182
|
+
kitId: string;
|
|
183
|
+
subject: string;
|
|
184
|
+
gateId: string;
|
|
185
|
+
};
|
|
170
186
|
export declare function writeState(dir: string, slug: string, status: string, phase: string, timestamp: string, summary: string, next?: string): void;
|
|
171
187
|
export declare function normalizeFinding(raw: AnyObj): AnyObj;
|
|
172
188
|
/**
|
|
@@ -6,7 +6,7 @@ import { createHash } from "node:crypto";
|
|
|
6
6
|
import { createRequire } from "node:module";
|
|
7
7
|
import { fileURLToPath } from "node:url";
|
|
8
8
|
// ADR 0016 Abstraction A: shared FlowDefinition resolver (P-a)
|
|
9
|
-
import { resolveActiveFlowStep, resolveFlowFilePath, resolvePhaseMap } from "../lib/flow-resolver.js";
|
|
9
|
+
import { resolveActiveFlowStep, resolveFlowFilePath, resolvePhaseMap, resolveRouteBackPolicy } from "../lib/flow-resolver.js";
|
|
10
10
|
export const statuses = new Set(["new", "planning", "planned", "in_progress", "blocked", "verifying", "verified", "needs_decision", "not_verified", "failed", "delivered", "accepted", "archived"]);
|
|
11
11
|
export const phases = ["idea", "backlog", "pickup", "planning", "execution", "verification", "goal_fit", "evidence", "release", "learning", "done"];
|
|
12
12
|
export const checkKinds = new Set(["build", "types", "lint", "test", "security", "diff", "browser", "runtime", "policy", "external"]);
|
|
@@ -15,11 +15,17 @@ export const verdicts = new Set(["pass", "partial", "fail", "not_verified"]);
|
|
|
15
15
|
function now() { return new Date().toISOString().replace(/\.\d{3}Z$/, "Z"); }
|
|
16
16
|
function read(file) { return fs.readFileSync(file, "utf8"); }
|
|
17
17
|
export function writeJson(file, payload) { fs.mkdirSync(path.dirname(file), { recursive: true }); fs.writeFileSync(file, `${JSON.stringify(payload, null, 2)}\n`); }
|
|
18
|
-
|
|
18
|
+
// Single-line but readable "key": "value" form. Built by collapsing the
|
|
19
|
+
// structural whitespace from an indented stringify — corruption-proof, unlike a
|
|
20
|
+
// regex that would also rewrite ":"/"," sequences inside string values.
|
|
21
|
+
function spacedLine(payload, replacer) {
|
|
22
|
+
return JSON.stringify(payload, replacer, 1).replace(/\n\s*/g, " ");
|
|
23
|
+
}
|
|
24
|
+
function printJson(payload) { console.log(spacedLine(payload)); }
|
|
19
25
|
export function loadJson(file, fallback = {}) { return fs.existsSync(file) ? JSON.parse(read(file)) : { ...fallback }; }
|
|
20
26
|
export function appendJsonl(file, payload) {
|
|
21
27
|
fs.mkdirSync(path.dirname(file), { recursive: true });
|
|
22
|
-
const line =
|
|
28
|
+
const line = spacedLine(payload, Object.keys(payload).sort());
|
|
23
29
|
fs.appendFileSync(file, `${line}\n`);
|
|
24
30
|
}
|
|
25
31
|
function die(message) { throw new Error(message); }
|
|
@@ -1071,15 +1077,63 @@ function deriveSurfaceStatus(ref) {
|
|
|
1071
1077
|
return "fail";
|
|
1072
1078
|
return "pass";
|
|
1073
1079
|
}
|
|
1080
|
+
/**
|
|
1081
|
+
* Derive kit identity from a parsed trust.bundle by structurally reading the
|
|
1082
|
+
* DECLARED primary claim (kit-typed) rather than hardcoding "builder".
|
|
1083
|
+
*
|
|
1084
|
+
* Resolution order (no fallbacks to "builder"):
|
|
1085
|
+
* 1. First non-workflow.* claim in bundle.claims[] → claimType drives kitId + subject.
|
|
1086
|
+
* 2. No kit-typed claim: try current.json active_flow_id adjacent to the bundle file
|
|
1087
|
+
* (bundle lives at <session-dir>/trust.bundle → flowAgentsDir = grandparent).
|
|
1088
|
+
* 3. Genuinely unknown: mark as "unknown" — never hardcode a kit identity.
|
|
1089
|
+
*/
|
|
1090
|
+
export function kitIdentityFromBundle(raw, bundleFile) {
|
|
1091
|
+
// 1. Structurally read the bundle's declared kit-typed claim.
|
|
1092
|
+
const claims = Array.isArray(raw.claims) ? raw.claims : [];
|
|
1093
|
+
for (const claim of claims) {
|
|
1094
|
+
const ct = typeof claim?.claimType === "string" ? claim.claimType : "";
|
|
1095
|
+
if (ct && !ct.startsWith("workflow.")) {
|
|
1096
|
+
const kitId = ct.split(".")[0] ?? "unknown";
|
|
1097
|
+
if (kitId && kitId !== "unknown") {
|
|
1098
|
+
return { claimType: ct, kitId, subject: `${kitId}-kit`, gateId: ct };
|
|
1099
|
+
}
|
|
1100
|
+
}
|
|
1101
|
+
}
|
|
1102
|
+
// 2. No kit-typed claim in bundle — try to derive kit from current.json active_flow_id.
|
|
1103
|
+
// The bundle lives at <session-dir>/trust.bundle, so:
|
|
1104
|
+
// sessionDir = path.dirname(bundleFile)
|
|
1105
|
+
// flowAgentsDir = path.dirname(sessionDir)
|
|
1106
|
+
try {
|
|
1107
|
+
const sessionDir = path.dirname(bundleFile);
|
|
1108
|
+
const flowAgentsDir = path.dirname(sessionDir);
|
|
1109
|
+
const currentFile = path.join(flowAgentsDir, "current.json");
|
|
1110
|
+
const current = JSON.parse(fs.readFileSync(currentFile, "utf8"));
|
|
1111
|
+
const flowId = typeof current["active_flow_id"] === "string" ? current["active_flow_id"] : null;
|
|
1112
|
+
if (flowId && flowId.includes(".")) {
|
|
1113
|
+
const kitId = flowId.split(".")[0];
|
|
1114
|
+
if (kitId) {
|
|
1115
|
+
const derivedClaimType = `${kitId}.trust.bundle`;
|
|
1116
|
+
return { claimType: derivedClaimType, kitId, subject: `${kitId}-kit`, gateId: derivedClaimType };
|
|
1117
|
+
}
|
|
1118
|
+
}
|
|
1119
|
+
}
|
|
1120
|
+
catch {
|
|
1121
|
+
// Ignore — fall through to unknown
|
|
1122
|
+
}
|
|
1123
|
+
// 3. Genuinely unknown — never fallback to "builder".
|
|
1124
|
+
return { claimType: "unknown.trust.bundle", kitId: "unknown", subject: "unknown-kit", gateId: "unknown.trust.bundle" };
|
|
1125
|
+
}
|
|
1074
1126
|
function surfaceCheckFromArtifact(file, index) {
|
|
1075
1127
|
const raw = JSON.parse(read(file));
|
|
1076
1128
|
const lower = JSON.stringify(raw).toLowerCase();
|
|
1129
|
+
// Structurally read kit identity from the bundle — never hardcode "builder".
|
|
1130
|
+
const { claimType: bundleClaimType, subject: bundleSubject, gateId: bundleGateId } = kitIdentityFromBundle(raw, file);
|
|
1077
1131
|
let ref;
|
|
1078
1132
|
if (lower.includes("provider") && lower.includes("absent")) {
|
|
1079
|
-
ref = { artifact_kind: "trust.bundle", artifact_ref: file, gate_id: "provider.unavailable", claim_type:
|
|
1133
|
+
ref = { artifact_kind: "trust.bundle", artifact_ref: file, gate_id: "provider.unavailable", claim_type: bundleClaimType, claim_status: "unknown", subject: bundleSubject, freshness: { status: "unknown", summary: "No trust provider is configured" }, authority: { producer: "unknown", summary: "No trust provider is configured" }, integrity: { status: "unknown", summary: "Unknown" }, status: "not_verified", summary: "No trust provider is configured" };
|
|
1080
1134
|
}
|
|
1081
1135
|
else if (lower.includes("artifact") && lower.includes("absent")) {
|
|
1082
|
-
ref = { artifact_kind: "trust.bundle", artifact_ref: file, gate_id: "artifact.unavailable", claim_type:
|
|
1136
|
+
ref = { artifact_kind: "trust.bundle", artifact_ref: file, gate_id: "artifact.unavailable", claim_type: bundleClaimType, claim_status: "unknown", subject: bundleSubject, freshness: { status: "unknown", summary: "Artifact not readable" }, authority: { producer: "unknown", summary: "Artifact not readable" }, integrity: { status: "unknown", summary: "Artifact not readable" }, status: "not_verified", summary: "artifact not readable" };
|
|
1083
1137
|
}
|
|
1084
1138
|
else {
|
|
1085
1139
|
const claimStatus = lower.includes("rejected") ? "rejected" : "accepted";
|
|
@@ -1087,7 +1141,7 @@ function surfaceCheckFromArtifact(file, index) {
|
|
|
1087
1141
|
const producer = lower.includes("missing-authority") ? "unknown" : "surface-local";
|
|
1088
1142
|
const integrity = lower.includes("mismatch") ? "mismatch" : "matched";
|
|
1089
1143
|
// Use trust.bundle as the canonical Hachure-aligned artifact_kind for all trust-backed evidence refs
|
|
1090
|
-
ref = { artifact_kind: "trust.bundle", artifact_ref: file, gate_id:
|
|
1144
|
+
ref = { artifact_kind: "trust.bundle", artifact_ref: file, gate_id: bundleGateId, claim_type: bundleClaimType, claim_status: claimStatus, subject: bundleSubject, freshness: { status: freshness, summary: freshness === "fresh" ? "fresh" : "not currently verifiable" }, authority: { producer, summary: producer === "unknown" ? "missing authority" : "Local Surface trust producer." }, integrity: { status: integrity, summary: integrity === "matched" ? "matched" : "integrity mismatch" } };
|
|
1091
1145
|
ref.status = deriveSurfaceStatus(ref);
|
|
1092
1146
|
ref.summary = ref.status === "pass" ? "accepted" : ref.status === "not_verified" ? "not currently verifiable" : (claimStatus === "rejected" ? "rejected" : producer === "unknown" ? "missing authority" : "integrity mismatch");
|
|
1093
1147
|
}
|
|
@@ -1342,16 +1396,22 @@ async function advanceState(p) {
|
|
|
1342
1396
|
if ((status === "archived" || status === "accepted") && prev.phase !== "learning")
|
|
1343
1397
|
diagnostic(dir, "terminal_jump_rejected", "Terminal workflow states require release and learning gates.");
|
|
1344
1398
|
const flow = opt(p, "flow-definition");
|
|
1345
|
-
|
|
1399
|
+
// Route-back guard: FlowDefinition-driven (not hardcoded to builder.build).
|
|
1400
|
+
// Fires when the active flow's gate for prev.phase declares a route_back_policy
|
|
1401
|
+
// AND the target phase maps to a step listed in on_route_back values.
|
|
1402
|
+
// builder.build verify-gate already carries this declaration — behavior preserved.
|
|
1403
|
+
const repoRoot = flow ? findRepoRootFromDir(dir) : "";
|
|
1404
|
+
const routeBack = flow ? resolveRouteBackPolicy(flow, prev.phase, phase, repoRoot) : null;
|
|
1405
|
+
if (routeBack) {
|
|
1346
1406
|
const reason = opt(p, "route-back-reason");
|
|
1347
1407
|
if (!reason)
|
|
1348
|
-
diagnostic(dir, "route_back_reason_required",
|
|
1408
|
+
diagnostic(dir, "route_back_reason_required", `Route-back from ${prev.phase} to ${phase} requires a --route-back-reason (e.g. implementation_defect).`);
|
|
1349
1409
|
const file = path.join(dir, "transition-attempts.json");
|
|
1350
1410
|
const attempts = loadJson(file);
|
|
1351
|
-
const key =
|
|
1411
|
+
const key = `${prev.phase}->${phase}:${reason}`;
|
|
1352
1412
|
const count = attempts[key]?.count ?? 0;
|
|
1353
|
-
if (count >=
|
|
1354
|
-
diagnostic(dir, "route_back_attempts_exceeded",
|
|
1413
|
+
if (count >= routeBack.maxAttempts)
|
|
1414
|
+
diagnostic(dir, "route_back_attempts_exceeded", `Route-back attempt limit (${routeBack.maxAttempts}) exceeded for ${prev.phase}→${phase}.`);
|
|
1355
1415
|
attempts[key] = { count: count + 1, reason, updated_at: opt(p, "timestamp", now()) };
|
|
1356
1416
|
writeJson(file, attempts);
|
|
1357
1417
|
}
|
|
@@ -1365,7 +1425,7 @@ async function advanceState(p) {
|
|
|
1365
1425
|
// --step-id individually. The repoRoot is derived by walking up from dir to find kits/.
|
|
1366
1426
|
if (flow) {
|
|
1367
1427
|
const root = path.resolve(opt(p, "artifact-root", path.dirname(dir)));
|
|
1368
|
-
|
|
1428
|
+
// repoRoot already computed above when flow is present
|
|
1369
1429
|
const phaseMap = resolvePhaseMap(flow, repoRoot);
|
|
1370
1430
|
const stepId = phaseMap?.[phase] ?? undefined;
|
|
1371
1431
|
if (stepId) {
|
|
@@ -80,3 +80,32 @@ export declare function resolvePhaseMap(flowId: string, repoRoot: string): Recor
|
|
|
80
80
|
* @returns ActiveFlowStep or null when fields are absent or resolution fails.
|
|
81
81
|
*/
|
|
82
82
|
export declare function resolveActiveFlowStep(flowAgentsDir: string): ActiveFlowStep | null;
|
|
83
|
+
/** The resolved route-back policy for a phase transition. */
|
|
84
|
+
export type RouteBackPolicy = {
|
|
85
|
+
/** Maximum allowed route-back attempts for this transition key. */
|
|
86
|
+
maxAttempts: number;
|
|
87
|
+
/** Action when attempts are exceeded (e.g. "block"). */
|
|
88
|
+
onExceeded: string;
|
|
89
|
+
/** The step id whose gate declared this policy (e.g. "verify"). */
|
|
90
|
+
fromStepId: string;
|
|
91
|
+
};
|
|
92
|
+
/**
|
|
93
|
+
* Resolve the route-back policy for a phase transition, if the active FlowDefinition
|
|
94
|
+
* declares one on the source phase's gate.
|
|
95
|
+
*
|
|
96
|
+
* A route-back is a transition where the source phase's gate declares both
|
|
97
|
+
* `route_back_policy` and `on_route_back`, and the target phase maps to a step
|
|
98
|
+
* listed as a route-back target in `on_route_back` values.
|
|
99
|
+
*
|
|
100
|
+
* This is the FlowDefinition-driven replacement for the hardcoded
|
|
101
|
+
* `flow === "builder.build" && prev.phase === "verification" && phase === "execution"`
|
|
102
|
+
* guard in advance-state. Any flow that declares `route_back_policy` on a gate
|
|
103
|
+
* automatically gets route-back enforcement without code changes.
|
|
104
|
+
*
|
|
105
|
+
* @param flowId e.g. "builder.build" — kitId is the prefix before the first ".".
|
|
106
|
+
* @param fromPhase Lifecycle phase leaving (e.g. "verification").
|
|
107
|
+
* @param toPhase Lifecycle phase entering (e.g. "execution").
|
|
108
|
+
* @param repoRoot Absolute path to the repository root (kits/ lives here).
|
|
109
|
+
* @returns RouteBackPolicy when the transition is a declared route-back, null otherwise.
|
|
110
|
+
*/
|
|
111
|
+
export declare function resolveRouteBackPolicy(flowId: string, fromPhase: string, toPhase: string, repoRoot: string): RouteBackPolicy | null;
|
|
@@ -235,3 +235,74 @@ export function resolveActiveFlowStep(flowAgentsDir) {
|
|
|
235
235
|
const repoRoot = findRepoRoot(path.dirname(flowAgentsDir));
|
|
236
236
|
return resolveFlowStep(flowId, stepId, repoRoot);
|
|
237
237
|
}
|
|
238
|
+
/**
|
|
239
|
+
* Resolve the route-back policy for a phase transition, if the active FlowDefinition
|
|
240
|
+
* declares one on the source phase's gate.
|
|
241
|
+
*
|
|
242
|
+
* A route-back is a transition where the source phase's gate declares both
|
|
243
|
+
* `route_back_policy` and `on_route_back`, and the target phase maps to a step
|
|
244
|
+
* listed as a route-back target in `on_route_back` values.
|
|
245
|
+
*
|
|
246
|
+
* This is the FlowDefinition-driven replacement for the hardcoded
|
|
247
|
+
* `flow === "builder.build" && prev.phase === "verification" && phase === "execution"`
|
|
248
|
+
* guard in advance-state. Any flow that declares `route_back_policy` on a gate
|
|
249
|
+
* automatically gets route-back enforcement without code changes.
|
|
250
|
+
*
|
|
251
|
+
* @param flowId e.g. "builder.build" — kitId is the prefix before the first ".".
|
|
252
|
+
* @param fromPhase Lifecycle phase leaving (e.g. "verification").
|
|
253
|
+
* @param toPhase Lifecycle phase entering (e.g. "execution").
|
|
254
|
+
* @param repoRoot Absolute path to the repository root (kits/ lives here).
|
|
255
|
+
* @returns RouteBackPolicy when the transition is a declared route-back, null otherwise.
|
|
256
|
+
*/
|
|
257
|
+
export function resolveRouteBackPolicy(flowId, fromPhase, toPhase, repoRoot) {
|
|
258
|
+
if (!flowId || !fromPhase || !toPhase)
|
|
259
|
+
return null;
|
|
260
|
+
const dotIdx = flowId.indexOf(".");
|
|
261
|
+
if (dotIdx < 1)
|
|
262
|
+
return null;
|
|
263
|
+
const kitId = flowId.slice(0, dotIdx);
|
|
264
|
+
const flowName = flowId.slice(dotIdx + 1);
|
|
265
|
+
if (!kitId || !flowName)
|
|
266
|
+
return null;
|
|
267
|
+
const flowFilePath = resolveFlowFilePath(kitId, flowName, flowId, repoRoot);
|
|
268
|
+
if (!flowFilePath)
|
|
269
|
+
return null;
|
|
270
|
+
let flowDef;
|
|
271
|
+
try {
|
|
272
|
+
const raw = fs.readFileSync(flowFilePath, "utf8");
|
|
273
|
+
flowDef = JSON.parse(raw);
|
|
274
|
+
}
|
|
275
|
+
catch {
|
|
276
|
+
return null; // ENOENT, permission error, or parse error — fail-open
|
|
277
|
+
}
|
|
278
|
+
if (!flowDef || typeof flowDef !== "object")
|
|
279
|
+
return null;
|
|
280
|
+
const phaseMap = flowDef.phase_map;
|
|
281
|
+
if (!phaseMap || typeof phaseMap !== "object" || Array.isArray(phaseMap))
|
|
282
|
+
return null;
|
|
283
|
+
const fromStep = phaseMap[fromPhase];
|
|
284
|
+
const toStep = phaseMap[toPhase];
|
|
285
|
+
if (!fromStep || !toStep)
|
|
286
|
+
return null; // phases not in this flow
|
|
287
|
+
if (!flowDef.gates)
|
|
288
|
+
return null;
|
|
289
|
+
for (const gate of Object.values(flowDef.gates)) {
|
|
290
|
+
if (!gate || gate.step !== fromStep)
|
|
291
|
+
continue;
|
|
292
|
+
if (!gate.route_back_policy || !gate.on_route_back)
|
|
293
|
+
return null;
|
|
294
|
+
// Check if toStep is a valid route-back target declared in on_route_back
|
|
295
|
+
const routeBackTargets = Object.values(gate.on_route_back);
|
|
296
|
+
if (!routeBackTargets.includes(toStep))
|
|
297
|
+
return null;
|
|
298
|
+
const maxAttempts = typeof gate.route_back_policy.max_attempts === "number"
|
|
299
|
+
? gate.route_back_policy.max_attempts
|
|
300
|
+
: 3;
|
|
301
|
+
return {
|
|
302
|
+
maxAttempts,
|
|
303
|
+
onExceeded: gate.route_back_policy.on_exceeded ?? "block",
|
|
304
|
+
fromStepId: fromStep,
|
|
305
|
+
};
|
|
306
|
+
}
|
|
307
|
+
return null;
|
|
308
|
+
}
|
|
@@ -38,6 +38,11 @@ CONSOLE_TELEMETRY_URL="${CONSOLE_TELEMETRY_URL:-${CONSOLE_URL:-}}"
|
|
|
38
38
|
CONSOLE_TELEMETRY_ENDPOINT_URL="${CONSOLE_TELEMETRY_ENDPOINT_URL:-}"
|
|
39
39
|
CONSOLE_TELEMETRY_TOKEN="${CONSOLE_TELEMETRY_TOKEN:-${CONSOLE_AUTH_TOKEN:-}}"
|
|
40
40
|
CONSOLE_TENANT_ID="${CONSOLE_TENANT_ID:-}"
|
|
41
|
+
# Pricing registry source (consumed by lib/pricing.sh). Explicit file/URL win;
|
|
42
|
+
# otherwise the URL is derived from the console below so all runtimes read one
|
|
43
|
+
# live pricing source. Falls back to the bundled pricing.json offline.
|
|
44
|
+
TELEMETRY_PRICING_FILE="${TELEMETRY_PRICING_FILE:-${FLOW_AGENTS_PRICING_FILE:-}}"
|
|
45
|
+
TELEMETRY_PRICING_URL="${TELEMETRY_PRICING_URL:-${FLOW_AGENTS_PRICING_URL:-}}"
|
|
41
46
|
|
|
42
47
|
# Load config file if it exists
|
|
43
48
|
if [[ -f "$TELEMETRY_CONFIG_FILE" ]]; then
|
|
@@ -78,6 +83,9 @@ if [[ -f "$TELEMETRY_CONFIG_FILE" ]]; then
|
|
|
78
83
|
console_telemetry_token) CONSOLE_TELEMETRY_TOKEN="$value" ;;
|
|
79
84
|
console_tenant_id) CONSOLE_TENANT_ID="$value" ;;
|
|
80
85
|
console_telemetry_redact) CONSOLE_TELEMETRY_REDACT="$value" ;;
|
|
86
|
+
console_pricing_url) TELEMETRY_PRICING_URL="$value" ;;
|
|
87
|
+
pricing_url) TELEMETRY_PRICING_URL="$value" ;;
|
|
88
|
+
pricing_file) TELEMETRY_PRICING_FILE="$value" ;;
|
|
81
89
|
esac
|
|
82
90
|
fi
|
|
83
91
|
done < "$TELEMETRY_CONFIG_FILE"
|
|
@@ -85,5 +93,12 @@ fi
|
|
|
85
93
|
|
|
86
94
|
CONSOLE_TELEMETRY_REDACT="${CONSOLE_TELEMETRY_REDACT:-${TELEMETRY_CHANNEL_ANALYTICS_REDACT}}"
|
|
87
95
|
|
|
96
|
+
# Derive the live pricing source from the console when not set explicitly, the
|
|
97
|
+
# same way the transport derives /api/telemetry/records. One live source for
|
|
98
|
+
# bash/Python/TS runtimes; lib/pricing.sh caches it and falls back to bundled.
|
|
99
|
+
if [[ -z "${TELEMETRY_PRICING_URL:-}" && -n "${CONSOLE_TELEMETRY_URL:-}" ]]; then
|
|
100
|
+
TELEMETRY_PRICING_URL="${CONSOLE_TELEMETRY_URL%/}/api/telemetry/pricing"
|
|
101
|
+
fi
|
|
102
|
+
|
|
88
103
|
# Ensure directories exist
|
|
89
104
|
mkdir -p "$TELEMETRY_DATA_DIR" "$TELEMETRY_SESSION_DIR" 2>/dev/null
|
|
@@ -8,6 +8,10 @@ channel.analytics.redact=tool.input,tool.output,turn.prompt_text,delegation.targ
|
|
|
8
8
|
# The transport derives /api/telemetry/records from console_telemetry_url.
|
|
9
9
|
# console_telemetry_token=
|
|
10
10
|
# console_tenant_id=
|
|
11
|
+
# Live pricing registry source. If unset, derived from console_telemetry_url as
|
|
12
|
+
# <console>/api/telemetry/pricing so bash/Python/TS runtimes read one live
|
|
13
|
+
# source; lib/pricing.sh caches it and falls back to bundled pricing.json.
|
|
14
|
+
# console_pricing_url=https://console.kontourai.io/api/telemetry/pricing
|
|
11
15
|
enrich_system=true
|
|
12
16
|
enrich_workspace=true
|
|
13
17
|
enrich_auth=true
|
|
@@ -309,13 +309,35 @@ add_stop_data_and_emit_usage() {
|
|
|
309
309
|
tool_count=$(usage_count_tool_calls "$session_id" "$full_log")
|
|
310
310
|
delegation_count=$(usage_count_delegations "$session_id" "$full_log")
|
|
311
311
|
|
|
312
|
+
# Ground-truth token + cost usage from the runtime transcript, when the
|
|
313
|
+
# runtime exposes one (Claude Code, Codex, etc. set hook.transcript_path).
|
|
314
|
+
# Tokens are source-of-truth; estimated_cost_usd is derived from pricing.json
|
|
315
|
+
# (recomputed authoritatively console-side, so pricing updates are retroactive).
|
|
316
|
+
local transcript_path transcript_usage
|
|
317
|
+
transcript_path=$(echo "$event" | jq -r '.hook.transcript_path // ""')
|
|
318
|
+
transcript_usage=$(usage_parse_transcript "$transcript_path")
|
|
319
|
+
[[ -z "$transcript_usage" ]] && transcript_usage='null'
|
|
320
|
+
|
|
312
321
|
local usage_event
|
|
313
322
|
usage_event=$(echo "$event" | jq -c \
|
|
314
323
|
--arg m "$model" \
|
|
315
324
|
--argjson tc "$tool_count" \
|
|
316
325
|
--argjson dc "$delegation_count" \
|
|
326
|
+
--argjson tu "$transcript_usage" \
|
|
317
327
|
'.event_type = "session.usage" | .event_id = (.event_id + "-usage") | . + {
|
|
318
|
-
usage: {
|
|
328
|
+
usage: ({
|
|
329
|
+
model: $m,
|
|
330
|
+
duration_s: .session.duration_s,
|
|
331
|
+
tool_invocations: $tc,
|
|
332
|
+
delegations: $dc,
|
|
333
|
+
input_tokens: ($tu.input_tokens // null),
|
|
334
|
+
output_tokens: ($tu.output_tokens // null),
|
|
335
|
+
cache_creation_input_tokens: ($tu.cache_creation_input_tokens // null),
|
|
336
|
+
cache_read_input_tokens: ($tu.cache_read_input_tokens // null),
|
|
337
|
+
estimated_cost_usd: ($tu.estimated_cost_usd // null),
|
|
338
|
+
pricing_version: ($tu.pricing_version // null),
|
|
339
|
+
by_model: ($tu.by_model // null)
|
|
340
|
+
})
|
|
319
341
|
}')
|
|
320
342
|
transport_emit "$usage_event"
|
|
321
343
|
fi
|