@kontourai/flow-agents 3.3.0 → 3.4.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.github/workflows/add-to-project.yml +15 -0
- package/.github/workflows/ci.yml +161 -0
- package/CHANGELOG.md +48 -0
- package/CONTEXT.md +5 -1
- package/README.md +19 -8
- package/build/src/builder-flow-run-adapter.d.ts +80 -0
- package/build/src/builder-flow-run-adapter.js +241 -0
- package/build/src/builder-flow-runtime.d.ts +16 -0
- package/build/src/builder-flow-runtime.js +290 -0
- package/build/src/cli/builder-run.d.ts +1 -0
- package/build/src/cli/builder-run.js +27 -0
- package/build/src/cli/effective-backlog-settings.js +70 -2
- package/build/src/cli/init.d.ts +34 -0
- package/build/src/cli/init.js +341 -61
- package/build/src/cli/kit.js +55 -12
- package/build/src/cli/pull-work-provider.js +346 -5
- package/build/src/cli/skill-drift-check.d.ts +1 -0
- package/build/src/cli/skill-drift-check.js +165 -0
- package/build/src/cli/telemetry-doctor.d.ts +37 -0
- package/build/src/cli/telemetry-doctor.js +53 -6
- package/build/src/cli/validate-hook-influence.js +37 -7
- package/build/src/cli/workflow-sidecar.d.ts +93 -8
- package/build/src/cli/workflow-sidecar.js +1175 -158
- package/build/src/cli.js +5 -0
- package/build/src/flow-kit/validate.d.ts +54 -34
- package/build/src/flow-kit/validate.js +237 -26
- package/build/src/index.d.ts +2 -0
- package/build/src/index.js +1 -0
- package/build/src/lib/console-connect-options.d.ts +97 -0
- package/build/src/lib/console-connect-options.js +199 -0
- package/build/src/lib/console-telemetry-validate.d.ts +49 -0
- package/build/src/lib/console-telemetry-validate.js +91 -0
- package/build/src/lib/flow-resolver.d.ts +56 -3
- package/build/src/lib/flow-resolver.js +151 -11
- package/build/src/lib/fs.d.ts +17 -0
- package/build/src/lib/fs.js +172 -0
- package/build/src/lib/local-artifact-root.d.ts +44 -1
- package/build/src/lib/local-artifact-root.js +131 -3
- package/build/src/runtime-adapters.d.ts +39 -3
- package/build/src/runtime-adapters.js +77 -31
- package/build/src/tools/build-universal-bundles.js +40 -2
- package/build/src/tools/codex-agent-routing.d.ts +2 -0
- package/build/src/tools/codex-agent-routing.js +49 -0
- package/build/src/tools/generate-context-map.js +1 -0
- package/build/src/tools/validate-source-tree.js +27 -1
- package/context/scripts/hooks/lib/kit-catalog.js +235 -0
- package/context/scripts/hooks/lib/runnable-command.js +177 -0
- package/context/scripts/hooks/stop-goal-fit.js +278 -48
- package/context/scripts/hooks/workflow-steering.js +121 -21
- package/context/scripts/package.json +3 -0
- package/context/scripts/telemetry/install-console-config.sh +25 -4
- package/context/scripts/telemetry/lib/config.sh +102 -12
- package/context/scripts/telemetry/lib/pricing.sh +50 -0
- package/context/scripts/telemetry/lib/session.sh +3 -0
- package/context/scripts/telemetry/lib/transport.sh +87 -0
- package/context/scripts/telemetry/lib/usage.sh +205 -4
- package/context/scripts/telemetry/telemetry.conf +6 -0
- package/context/scripts/telemetry/telemetry.sh +48 -0
- package/context/settings/workspace-backlog-provider-settings.example.json +48 -0
- package/docs/agent-usage-feedback-loop.md +35 -0
- package/docs/architecture-engine-and-kits.md +110 -0
- package/docs/context-map.md +2 -0
- package/docs/decisions/embeddable-engine.md +152 -0
- package/docs/decisions/index.md +3 -1
- package/docs/decisions/trust-ledger-retention.md +88 -0
- package/docs/decisions/workflow-enforcement.md +31 -9
- package/docs/fixture-ownership.md +3 -0
- package/docs/implementing-trust-reconciliation.md +129 -0
- package/docs/index.md +19 -9
- package/docs/integrations/flow-agents-console.md +167 -0
- package/docs/kit-authoring-guide.md +52 -21
- package/docs/spec/builder-flow-runtime.md +80 -0
- package/docs/spec/runtime-hook-surface.md +45 -1
- package/docs/specs/economics-record-contract.md +270 -0
- package/docs/specs/harness-capability-matrix.md +74 -0
- package/docs/specs/learning-review-proposals-contract.md +340 -0
- package/docs/specs/routing-efficiency-review.md +59 -0
- package/docs/verifiable-trust.md +74 -25
- package/docs/workflow-usage-guide.md +10 -0
- package/evals/acceptance/prove-capture-teeth.sh +132 -0
- package/evals/ci/antigaming-suite.sh +1 -0
- package/evals/ci/run-baseline.sh +72 -4
- package/evals/fixtures/economics/acceptance.json +12 -0
- package/evals/fixtures/economics/agents/tool-worker-1/events.jsonl +2 -0
- package/evals/fixtures/economics/agents/tool-worker-2/events.jsonl +2 -0
- package/evals/fixtures/economics/agents/tool-worker-3/events.jsonl +2 -0
- package/evals/fixtures/economics/agents/tool-worker-4/events.jsonl +1 -0
- package/evals/fixtures/economics/agents/tool-worker-5/events.jsonl +2 -0
- package/evals/fixtures/economics/critique.json +22 -0
- package/evals/fixtures/economics/expected-record.json +71 -0
- package/evals/fixtures/economics/session-usage-event.json +1 -0
- package/evals/fixtures/economics/state.json +11 -0
- package/evals/fixtures/economics/transcript.jsonl +3 -0
- package/evals/fixtures/hook-influence/cases.json +7 -7
- package/evals/fixtures/learning-review-proposals/balanced/economics.jsonl +6 -0
- package/evals/fixtures/learning-review-proposals/effect-follow-up/economics.jsonl +5 -0
- package/evals/fixtures/learning-review-proposals/effect-follow-up/sessions/task-lr-ef-1/trust.bundle +21 -0
- package/evals/fixtures/learning-review-proposals/effect-follow-up/sessions/task-lr-ef-2/trust.bundle +21 -0
- package/evals/fixtures/learning-review-proposals/effect-follow-up/sessions/task-lr-ef-3/trust.bundle +21 -0
- package/evals/fixtures/learning-review-proposals/effect-follow-up/sessions/task-lr-ef-4/trust.bundle +21 -0
- package/evals/fixtures/learning-review-proposals/effect-follow-up/sessions/task-lr-ef-5/trust.bundle +21 -0
- package/evals/fixtures/learning-review-proposals/pattern-present/economics.jsonl +6 -0
- package/evals/fixtures/learning-review-proposals/pattern-present/expected-aggregates.json +30 -0
- package/evals/fixtures/learning-review-proposals/pattern-present/expected-aggregates.md +66 -0
- package/evals/fixtures/learning-review-proposals/pattern-present/sessions/task-lr-pp-1/gate-review.inquiries.json +26 -0
- package/evals/fixtures/learning-review-proposals/pattern-present/sessions/task-lr-pp-1/trust.bundle +21 -0
- package/evals/fixtures/learning-review-proposals/pattern-present/sessions/task-lr-pp-2/gate-review.inquiries.json +26 -0
- package/evals/fixtures/learning-review-proposals/pattern-present/sessions/task-lr-pp-2/trust.bundle +21 -0
- package/evals/fixtures/learning-review-proposals/pattern-present/sessions/task-lr-pp-3/gate-review.inquiries.json +26 -0
- package/evals/fixtures/learning-review-proposals/pattern-present/sessions/task-lr-pp-3/trust.bundle +21 -0
- package/evals/fixtures/learning-review-proposals/pattern-present/sessions/task-lr-pp-4/gate-review.inquiries.json +26 -0
- package/evals/fixtures/learning-review-proposals/pattern-present/sessions/task-lr-pp-4/trust.bundle +21 -0
- package/evals/fixtures/learning-review-proposals/pattern-present/sessions/task-lr-pp-5/trust.bundle +21 -0
- package/evals/fixtures/learning-review-proposals/pattern-present/sessions/task-lr-pp-6/trust.bundle +21 -0
- package/evals/fixtures/learning-review-proposals/repeat-window/economics.jsonl +6 -0
- package/evals/fixtures/learning-review-proposals/under-threshold/economics.jsonl +3 -0
- package/evals/fixtures/telemetry/usage-transcript-sample.jsonl +4 -0
- package/evals/fixtures/trust-reconcile-exploits/mcp-degrade.json +42 -0
- package/evals/integration/test_builder_entry_enforcement.sh +241 -0
- package/evals/integration/test_builder_step_producers.sh +18 -10
- package/evals/integration/test_bundle_install.sh +172 -0
- package/evals/integration/test_console_tenant_isolation.sh +167 -0
- package/evals/integration/test_critique_supersession_roundtrip.sh +4 -1
- package/evals/integration/test_dual_emit_flow_step.sh +10 -4
- package/evals/integration/test_economics_record.sh +674 -0
- package/evals/integration/test_effective_backlog_settings.sh +1 -1
- package/evals/integration/test_evidence_capture_hook.sh +17 -2
- package/evals/integration/test_exemption_usage_review.sh +198 -0
- package/evals/integration/test_fixture_retirement_audit.sh +2 -2
- package/evals/integration/test_flow_kit_install_git.sh +83 -0
- package/evals/integration/test_flowdef_session_activation.sh +0 -1
- package/evals/integration/test_flowdef_session_history_preservation.sh +13 -3
- package/evals/integration/test_gate_lockdown.sh +7 -0
- package/evals/integration/test_gate_review_inquiry_records.sh +9 -1
- package/evals/integration/test_goal_fit_hook.sh +2031 -0
- package/evals/integration/test_hook_category_behaviors.sh +8 -1
- package/evals/integration/test_hook_influence_cases.sh +25 -1
- package/evals/integration/test_install_merge.sh +227 -2
- package/evals/integration/test_kit_conformance_levels.sh +6 -6
- package/evals/integration/test_learning_review_proposals.sh +329 -0
- package/evals/integration/test_liveness_conflict_injection.sh +26 -22
- package/evals/integration/test_liveness_console_relay.sh +166 -0
- package/evals/integration/test_liveness_heartbeat.sh +17 -17
- package/evals/integration/test_liveness_worktree_root.sh +575 -0
- package/evals/integration/test_phase_map_and_gate_claim.sh +6 -1
- package/evals/integration/test_publish_delivery.sh +331 -1
- package/evals/integration/test_pull_work_board.sh +200 -0
- package/evals/integration/test_pull_work_provider.sh +1 -1
- package/evals/integration/test_record_check.sh +378 -0
- package/evals/integration/test_routing_efficiency.sh +71 -0
- package/evals/integration/test_runtime_adapter_activation.sh +28 -0
- package/evals/integration/test_session_resume_roundtrip.sh +16 -19
- package/evals/integration/test_skill_drift_check.sh +870 -0
- package/evals/integration/test_telemetry.sh +445 -0
- package/evals/integration/test_telemetry_doctor.sh +66 -0
- package/evals/integration/test_telemetry_usage_pipeline.sh +228 -0
- package/evals/integration/test_trust_reconcile_negatives.sh +30 -13
- package/evals/integration/test_trust_reconcile_trailer_diagnostic.sh +247 -0
- package/evals/integration/test_usage_cost.sh +61 -0
- package/evals/integration/test_workflow_sidecar_writer.sh +1395 -0
- package/evals/integration/test_workflow_steering_hook.sh +157 -16
- package/evals/integration/test_workspace_settings.sh +176 -0
- package/evals/lib/env.sh +26 -0
- package/evals/lib/node.sh +8 -0
- package/evals/run.sh +29 -0
- package/evals/static/test_ci_integration_coverage.sh +115 -0
- package/evals/static/test_declared_scope_forms_documented.sh +114 -0
- package/evals/static/test_universal_bundles.sh +34 -0
- package/evals/static/test_validate_source_kit_asset_scope.sh +259 -0
- package/evals/static/test_workflow_skills.sh +1 -1
- package/kits/builder/flows/build.flow.json +9 -18
- package/kits/builder/flows/publish-learn.flow.json +5 -1
- package/kits/builder/kit.json +120 -0
- package/kits/builder/skills/deliver/SKILL.md +42 -0
- package/kits/builder/skills/evidence-gate/SKILL.md +12 -0
- package/kits/builder/skills/execute-plan/SKILL.md +9 -0
- package/kits/builder/skills/learning-review/SKILL.md +51 -0
- package/kits/builder/skills/plan-work/SKILL.md +17 -20
- package/kits/builder/skills/pull-work/SKILL.md +21 -0
- package/kits/builder/skills/release-readiness/SKILL.md +12 -0
- package/kits/knowledge/kit.json +9 -0
- package/kits/veritas-governance/docs/README.md +35 -7
- package/kits/veritas-governance/fixtures/exemption-review/mixed-fresh-stale.DECLARED.json +14 -0
- package/kits/veritas-governance/kit.json +14 -0
- package/kits/veritas-governance/skills/exemption-usage-review/SKILL.md +128 -0
- package/kits/veritas-governance/skills/exemption-usage-review/review-exemptions.mjs +231 -0
- package/package.json +2 -2
- package/packaging/manifest.json +29 -0
- package/schemas/backlog-provider-settings.schema.json +13 -0
- package/schemas/workflow-state.schema.json +44 -0
- package/scripts/README.md +4 -0
- package/scripts/check-content-boundary.cjs +8 -1
- package/scripts/ci/trust-reconcile.js +136 -0
- package/scripts/hooks/codex-hook-adapter.js +77 -2
- package/scripts/hooks/evidence-capture.js +38 -5
- package/scripts/hooks/lib/codex-exit-code.js +316 -0
- package/scripts/hooks/lib/kit-catalog.js +235 -0
- package/scripts/hooks/lib/liveness-write.js +28 -1
- package/scripts/hooks/lib/local-artifact-paths.js +97 -1
- package/scripts/hooks/lib/runnable-command.js +177 -0
- package/scripts/hooks/lib/skill-drift.js +350 -0
- package/scripts/hooks/stop-goal-fit.js +278 -48
- package/scripts/hooks/workflow-steering.js +121 -21
- package/scripts/install-codex-home.sh +97 -47
- package/scripts/install-merge.js +72 -14
- package/scripts/install-owned-files.js +178 -0
- package/scripts/liveness/relay.sh +84 -0
- package/scripts/telemetry/economics-record.schema.json +145 -0
- package/scripts/telemetry/economics-record.sh +331 -0
- package/scripts/telemetry/install-console-config.sh +25 -4
- package/scripts/telemetry/learning-review-decide.sh +124 -0
- package/scripts/telemetry/learning-review-proposals.schema.json +161 -0
- package/scripts/telemetry/learning-review-proposals.sh +484 -0
- package/scripts/telemetry/lib/config.sh +102 -12
- package/scripts/telemetry/lib/pricing.sh +14 -6
- package/scripts/telemetry/lib/session.sh +3 -0
- package/scripts/telemetry/lib/transport.sh +133 -15
- package/scripts/telemetry/lib/usage.sh +121 -28
- package/scripts/telemetry/routing-efficiency.sh +0 -0
- package/scripts/telemetry/telemetry.conf +6 -0
- package/scripts/telemetry/telemetry.sh +48 -0
- package/src/builder-flow-run-adapter.ts +357 -0
- package/src/builder-flow-runtime.ts +348 -0
- package/src/cli/builder-flow-run-adapter.test.mjs +495 -0
- package/src/cli/builder-flow-runtime.test.mjs +213 -0
- package/src/cli/builder-run.ts +28 -0
- package/src/cli/codex-agent-routing.test.mjs +44 -0
- package/src/cli/codex-exit-code.test.mjs +207 -0
- package/src/cli/console-connect-options.test.mjs +329 -0
- package/src/cli/console-telemetry-validate.test.mjs +157 -0
- package/src/cli/effective-backlog-settings.ts +68 -2
- package/src/cli/flow-resolver-composition.test.mjs +101 -0
- package/src/cli/init.test.mjs +161 -0
- package/src/cli/init.ts +407 -62
- package/src/cli/kit-metadata-security.test.mjs +443 -0
- package/src/cli/kit.ts +50 -12
- package/src/cli/pull-work-provider.ts +377 -3
- package/src/cli/sidecar-pure-helpers.test.mjs +64 -0
- package/src/cli/skill-drift-check.ts +196 -0
- package/src/cli/telemetry-doctor.test.mjs +53 -0
- package/src/cli/telemetry-doctor.ts +50 -7
- package/src/cli/validate-hook-influence.ts +37 -6
- package/src/cli/workflow-sidecar.ts +1150 -151
- package/src/cli.ts +5 -0
- package/src/flow-kit/validate.ts +277 -38
- package/src/index.ts +19 -0
- package/src/lib/console-connect-options.ts +261 -0
- package/src/lib/console-telemetry-validate.ts +88 -0
- package/src/lib/flow-resolver.ts +153 -10
- package/src/lib/fs.ts +160 -0
- package/src/lib/local-artifact-root.ts +129 -3
- package/src/runtime-adapters.ts +113 -33
- package/src/tools/build-universal-bundles.ts +36 -2
- package/src/tools/codex-agent-routing.ts +48 -0
- package/src/tools/generate-context-map.ts +1 -0
- package/src/tools/validate-source-tree.ts +26 -1
|
@@ -0,0 +1,331 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# economics-record.sh — per-run kit-economics record emitter (#349, console ADR 0003 calls 1/2/3/6).
|
|
3
|
+
#
|
|
4
|
+
# Assembles ONE immutable `kontour.console.economics` v0.1 record per run — cost, time, iterations,
|
|
5
|
+
# and defects caught — the measurement substrate for the value proof (I32–I35). Modeled byte-for-byte
|
|
6
|
+
# on scripts/liveness/relay.sh (#295): source ${TELEMETRY_DIR}/lib/transport.sh, build the record with
|
|
7
|
+
# a single `jq -c` filter (valid JSON + \u-escaping of every untrusted field), then hand off to the
|
|
8
|
+
# SHARED `console_post_json` core. Do NOT hand-roll a curl; do NOT fork the transport.
|
|
9
|
+
#
|
|
10
|
+
# LOCAL-FIRST (ADR 0003 call 6): the record is ALWAYS written to the local economics log FIRST. The
|
|
11
|
+
# console POST is a detached, opt-in, best-effort fire that can NEVER block or fail a run — every
|
|
12
|
+
# failure path is a quiet `exit 0`. flow-agents emits a per-run FACT, never a rollup (call 3);
|
|
13
|
+
# aggregation/value is a console projection. Tenancy is stamped console-side from the principal (call
|
|
14
|
+
# 2); tenant_id here is self-description only.
|
|
15
|
+
#
|
|
16
|
+
# Ground-truth tokens/cost come from the `session.usage` event's .usage block (parsed upstream by
|
|
17
|
+
# usage_parse_transcript from each assistant message's .message.usage — never re-estimated). Defects
|
|
18
|
+
# join from the review sidecar critique.json; verdict/slug from state.json.
|
|
19
|
+
#
|
|
20
|
+
# Invoked (detached, best-effort) from scripts/telemetry/telemetry.sh after the session.usage event is
|
|
21
|
+
# emitted at run end. Usage:
|
|
22
|
+
# economics-record.sh '<session.usage-event-json>' [--state PATH] [--acceptance PATH]
|
|
23
|
+
# [--critique PATH] [--agents-dir DIR]
|
|
24
|
+
# (the session.usage event may also arrive on stdin instead of $1)
|
|
25
|
+
#
|
|
26
|
+
# --agents-dir DIR (#415 slice 1): the run's `<slug>/agents` directory. When present, the emitter
|
|
27
|
+
# assembles a `delegations[]` FACT — one entry per delegated sub-agent {agent_id, role, resolved_model,
|
|
28
|
+
# summary, escalated_from?} joined from each `<DIR>/<agent-id>/events.jsonl` (latest delegation/
|
|
29
|
+
# escalation event wins). This is a per-run FACT only (ADR 0003 call 3): cost-per-(role,model) rollups
|
|
30
|
+
# and per-delegation outcome are console projections / a later slice, NOT fabricated here — telemetry
|
|
31
|
+
# does not isolate per-sub-agent token usage today, so no per-delegation cost is invented.
|
|
32
|
+
set -uo pipefail
|
|
33
|
+
|
|
34
|
+
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" || exit 0
|
|
35
|
+
|
|
36
|
+
# transport.sh sources ${TELEMETRY_DIR}/lib/redact.sh at load — point it at the telemetry lib dir,
|
|
37
|
+
# then reuse the SAME console POST core the telemetry mirror + liveness relay use (#356: one core,
|
|
38
|
+
# never forked). config.sh gives us TELEMETRY_DATA_DIR for the local log location.
|
|
39
|
+
export TELEMETRY_DIR="${TELEMETRY_DIR:-$SCRIPT_DIR}"
|
|
40
|
+
[[ -f "$TELEMETRY_DIR/lib/transport.sh" ]] || exit 0
|
|
41
|
+
# shellcheck source=/dev/null
|
|
42
|
+
source "$TELEMETRY_DIR/lib/config.sh" 2>/dev/null || true
|
|
43
|
+
# shellcheck source=/dev/null
|
|
44
|
+
source "$TELEMETRY_DIR/lib/transport.sh" 2>/dev/null || exit 0
|
|
45
|
+
|
|
46
|
+
# jq is mandatory — it is what guarantees valid JSON + \u-escaping of untrusted fields. No jq ⇒ no-op.
|
|
47
|
+
command -v jq >/dev/null 2>&1 || exit 0
|
|
48
|
+
|
|
49
|
+
# --- parse args: the session.usage event (positional or stdin) + optional sidecar paths -------------
|
|
50
|
+
usage_event=""
|
|
51
|
+
state_path=""
|
|
52
|
+
acceptance_path=""
|
|
53
|
+
critique_path=""
|
|
54
|
+
agents_dir=""
|
|
55
|
+
while [[ $# -gt 0 ]]; do
|
|
56
|
+
case "$1" in
|
|
57
|
+
--state) state_path="${2:-}"; shift 2 ;;
|
|
58
|
+
--acceptance) acceptance_path="${2:-}"; shift 2 ;;
|
|
59
|
+
--critique) critique_path="${2:-}"; shift 2 ;;
|
|
60
|
+
--agents-dir) agents_dir="${2:-}"; shift 2 ;;
|
|
61
|
+
--) shift ;;
|
|
62
|
+
*) [[ -z "$usage_event" ]] && usage_event="$1"; shift ;;
|
|
63
|
+
esac
|
|
64
|
+
done
|
|
65
|
+
# Fall back to stdin when no positional event was supplied.
|
|
66
|
+
if [[ -z "$usage_event" && ! -t 0 ]]; then
|
|
67
|
+
usage_event="$(cat 2>/dev/null || true)"
|
|
68
|
+
fi
|
|
69
|
+
[[ -z "$usage_event" ]] && exit 0
|
|
70
|
+
|
|
71
|
+
# --- read sidecars as JSON args (never the token source; join sources only) -------------------------
|
|
72
|
+
# Each is `null` when missing/unreadable/unparseable so the jq filter defaults cleanly.
|
|
73
|
+
read_json_or_null() {
|
|
74
|
+
local p="$1" out
|
|
75
|
+
[[ -n "$p" && -f "$p" ]] || { printf 'null'; return; }
|
|
76
|
+
out="$(jq -c '.' < "$p" 2>/dev/null)" || { printf 'null'; return; }
|
|
77
|
+
[[ -z "$out" ]] && out='null'
|
|
78
|
+
printf '%s' "$out"
|
|
79
|
+
}
|
|
80
|
+
state_json="$(read_json_or_null "$state_path")"
|
|
81
|
+
critique_json="$(read_json_or_null "$critique_path")"
|
|
82
|
+
# acceptance.json is read for future criteria/goal_fit joins; carried but not yet a required field.
|
|
83
|
+
acceptance_json="$(read_json_or_null "$acceptance_path")"
|
|
84
|
+
|
|
85
|
+
# --- delegations[] FACT (#415): per-sub-agent role/model + honestly-derived outcome ------------------
|
|
86
|
+
# Scan each per-agent event log; group ALL events by agent_id. Role/model come from the latest
|
|
87
|
+
# delegation/escalation event (an escalation supersedes the initial delegation, carrying escalated_from).
|
|
88
|
+
#
|
|
89
|
+
# `outcome` is derived ONLY from real recorded signals — NEVER fabricated (see harness-capability-matrix).
|
|
90
|
+
# Crucially these are ORCHESTRATOR-OBSERVABLE — the orchestrator knows what it dispatched, how often it
|
|
91
|
+
# re-dispatched, and how it corrected — so they hold WITHOUT peeking inside the sub-agent (which most
|
|
92
|
+
# harnesses forbid). dispatch_count = how many times this agent_id was (re)dispatched (delegation +
|
|
93
|
+
# escalation events); >1 means the orchestrator re-prompted it.
|
|
94
|
+
# diverged — an explicit supersession marker (kind=="supersession" or status=="diverged") exists.
|
|
95
|
+
# rework — an escalation happened, OR the orchestrator re-dispatched the agent (dispatch_count>1).
|
|
96
|
+
# failed — the latest terminal verdict event (kind evidence/verdict) for the agent is a FAIL.
|
|
97
|
+
# accepted — the latest terminal verdict event is a PASS (and no escalation/redispatch/supersession).
|
|
98
|
+
# unavailable— no terminal verdict was recorded for this agent on this harness. NOT assumed accepted.
|
|
99
|
+
# Per-delegation COST is still not attributed here — token usage IS sub-agent-internal and no runtime
|
|
100
|
+
# isolates it today (see the `signals` block); cost-per-(role,model) is a console projection.
|
|
101
|
+
# Any read/parse failure degrades to an empty array; never fatal (local-first, best-effort).
|
|
102
|
+
delegations_json='[]'
|
|
103
|
+
if [[ -n "$agents_dir" && -d "$agents_dir" ]]; then
|
|
104
|
+
assembled="$(
|
|
105
|
+
{ for f in "$agents_dir"/*/events.jsonl; do [[ -f "$f" ]] && cat "$f"; done; } 2>/dev/null \
|
|
106
|
+
| jq -s -c '
|
|
107
|
+
[ .[] | select(type == "object") | select(.agent_id != null) ]
|
|
108
|
+
| group_by(.agent_id)
|
|
109
|
+
| map(
|
|
110
|
+
. as $all
|
|
111
|
+
| [ $all[] | select((.kind == "delegation" or .kind == "escalation")
|
|
112
|
+
and (.role != null) and (.model != null)) ] as $routes
|
|
113
|
+
| select(($routes | length) > 0)
|
|
114
|
+
| ($routes | sort_by(.timestamp // "") | last) as $l
|
|
115
|
+
| ($routes | length) as $dispatch_count
|
|
116
|
+
| (any($all[]; .kind == "escalation")) as $escalated
|
|
117
|
+
| (any($all[]; .kind == "supersession" or .status == "diverged")) as $diverged
|
|
118
|
+
| ([ $all[] | select(.kind == "evidence" or .kind == "verdict") ]
|
|
119
|
+
| sort_by(.timestamp // "") | last) as $verdict
|
|
120
|
+
| (($verdict.status // "") | ascii_downcase) as $vs
|
|
121
|
+
| (if $diverged then "diverged"
|
|
122
|
+
elif ($escalated or $dispatch_count > 1) then "rework"
|
|
123
|
+
elif ($verdict != null and $vs == "fail") then "failed"
|
|
124
|
+
elif ($verdict != null and $vs == "pass") then "accepted"
|
|
125
|
+
else "unavailable" end) as $outcome
|
|
126
|
+
| {
|
|
127
|
+
agent_id: $l.agent_id,
|
|
128
|
+
role: $l.role,
|
|
129
|
+
resolved_model: $l.model,
|
|
130
|
+
summary: ($l.summary // null),
|
|
131
|
+
dispatch_count: $dispatch_count,
|
|
132
|
+
outcome: $outcome
|
|
133
|
+
}
|
|
134
|
+
+ (if ($l.escalated_from // null) != null then { escalated_from: $l.escalated_from } else {} end)
|
|
135
|
+
)
|
|
136
|
+
' 2>/dev/null
|
|
137
|
+
)"
|
|
138
|
+
[[ -n "$assembled" && "$assembled" != "null" ]] && delegations_json="$assembled"
|
|
139
|
+
fi
|
|
140
|
+
|
|
141
|
+
tenant_self="${CONSOLE_TENANT_ID:-${FLOW_AGENTS_CONSOLE_TENANT:-}}"
|
|
142
|
+
|
|
143
|
+
# --- assemble the record with ONE jq -c filter (injection-safe, valid JSON) -------------------------
|
|
144
|
+
# Untrusted fields (task_slug, model names, finding text) flow through jq string handling so hostile
|
|
145
|
+
# control bytes are \u-escaped rather than emitted raw. NEVER printf/concatenate JSON.
|
|
146
|
+
record="$(printf '%s' "$usage_event" | jq -c \
|
|
147
|
+
--argjson state "$state_json" \
|
|
148
|
+
--argjson critique "$critique_json" \
|
|
149
|
+
--argjson acceptance "$acceptance_json" \
|
|
150
|
+
--argjson delegations "$delegations_json" \
|
|
151
|
+
--arg tenant "$tenant_self" '
|
|
152
|
+
. as $e
|
|
153
|
+
| ($e.usage // {}) as $u
|
|
154
|
+
# findings_by_severity: group critique.json .critiques[].findings[] on .severity; missing → low.
|
|
155
|
+
| ([($critique.critiques // [])[].findings[]?
|
|
156
|
+
| (.severity // "low" | ascii_downcase)]
|
|
157
|
+
| reduce .[] as $s (
|
|
158
|
+
{critical:0, high:0, medium:0, low:0};
|
|
159
|
+
if ($s=="critical" or $s=="high" or $s=="medium" or $s=="low")
|
|
160
|
+
then .[$s] += 1 else .low += 1 end)) as $sev
|
|
161
|
+
# distinct caught false-completions: findings flagged as such (category/type/false_completion).
|
|
162
|
+
| ([($critique.critiques // [])[].findings[]?
|
|
163
|
+
| select((.false_completion == true)
|
|
164
|
+
or ((.category // "") | ascii_downcase | test("false[_-]?completion"))
|
|
165
|
+
or ((.type // "") | ascii_downcase | test("false[_-]?completion")))]
|
|
166
|
+
| length) as $cfc
|
|
167
|
+
# verification verdict: explicit sidecar field wins; else null-derived → NOT_VERIFIED.
|
|
168
|
+
| (($critique.verification_verdict // $acceptance.verification_verdict
|
|
169
|
+
// $state.verification_verdict // $state.verify_verdict) // null) as $vv0
|
|
170
|
+
| (if ($vv0 == "PASS" or $vv0 == "FAIL" or $vv0 == "NOT_VERIFIED") then $vv0 else "NOT_VERIFIED" end) as $vv
|
|
171
|
+
# phase attribution: when state.json carries a phase, one bucket for it; else all → unattributed.
|
|
172
|
+
# The single bucket carries the full cost/time totals so the phase-sum invariant holds by construction.
|
|
173
|
+
| (($state.phase // null) | if . == null or . == "" then "unattributed" else . end) as $phase
|
|
174
|
+
| (($u.input_tokens // 0)) as $it
|
|
175
|
+
| (($u.output_tokens // 0)) as $ot
|
|
176
|
+
| (($u.cache_creation_input_tokens // 0)) as $cc
|
|
177
|
+
| (($u.cache_read_input_tokens // 0)) as $cr
|
|
178
|
+
| (($u.estimated_cost_usd // 0)) as $ec
|
|
179
|
+
| (($u.duration_s // 0)) as $wc
|
|
180
|
+
| {
|
|
181
|
+
schema: "kontour.console.economics",
|
|
182
|
+
version: "0.1",
|
|
183
|
+
run_id: ($e.session_id // "unknown"),
|
|
184
|
+
at: ($e.timestamp // null),
|
|
185
|
+
task_slug: ($state.task_slug // null),
|
|
186
|
+
model: ($u.model // null),
|
|
187
|
+
pricing_version: ($u.pricing_version // null),
|
|
188
|
+
cost: {
|
|
189
|
+
input_tokens: $it,
|
|
190
|
+
output_tokens: $ot,
|
|
191
|
+
cache_creation_input_tokens: $cc,
|
|
192
|
+
cache_read_input_tokens: $cr,
|
|
193
|
+
estimated_cost_usd: $ec,
|
|
194
|
+
by_model: ($u.by_model // [])
|
|
195
|
+
},
|
|
196
|
+
time: {
|
|
197
|
+
wall_clock_s: $wc,
|
|
198
|
+
human_wait_s: (($state.human_wait_s // $e.usage.human_wait_s) // 0)
|
|
199
|
+
},
|
|
200
|
+
phases: [ {
|
|
201
|
+
phase: $phase,
|
|
202
|
+
input_tokens: $it,
|
|
203
|
+
output_tokens: $ot,
|
|
204
|
+
cache_creation_input_tokens: $cc,
|
|
205
|
+
cache_read_input_tokens: $cr,
|
|
206
|
+
estimated_cost_usd: $ec,
|
|
207
|
+
wall_clock_s: $wc
|
|
208
|
+
} ],
|
|
209
|
+
iterations: {
|
|
210
|
+
count: (($state.iterations.count // $state.iteration_count) // 1),
|
|
211
|
+
route_backs: (($state.iterations.route_backs // $state.route_backs) // 0)
|
|
212
|
+
},
|
|
213
|
+
defects: {
|
|
214
|
+
gate_fires: (($critique.gate_fires // $state.gate_fires) // 0),
|
|
215
|
+
findings_by_severity: $sev,
|
|
216
|
+
caught_false_completions: (($critique.caught_false_completions // $cfc) // 0),
|
|
217
|
+
verification_verdict: $vv
|
|
218
|
+
},
|
|
219
|
+
delegations: ($delegations // []),
|
|
220
|
+
# signals: what telemetry the CURRENT runtime actually exposed, so a consumer can tell a real zero
|
|
221
|
+
# from a harness-blind gap (never fabricate — see docs/specs/harness-capability-matrix.md).
|
|
222
|
+
# per_delegation_tokens: no runtime isolates per-sub-agent token usage today → cost-per-delegation
|
|
223
|
+
# is unavailable; the console attributes at (role,model) granularity via cost.by_model instead.
|
|
224
|
+
# per_delegation_outcome: coverage of the outcome signal on THIS run — "full" (every delegation has
|
|
225
|
+
# a real outcome), "partial" (some do), "none" (delegations exist but none had a verdict/escalation),
|
|
226
|
+
# "n/a" (no delegations observed).
|
|
227
|
+
signals: {
|
|
228
|
+
runtime: ($e.agent.runtime // null),
|
|
229
|
+
per_delegation_tokens: false,
|
|
230
|
+
per_delegation_outcome: (
|
|
231
|
+
($delegations // []) as $d
|
|
232
|
+
| ($d | length) as $n
|
|
233
|
+
| if $n == 0 then "n/a"
|
|
234
|
+
else ([ $d[] | select(.outcome != null and .outcome != "unavailable") ] | length) as $known
|
|
235
|
+
| if $known == 0 then "none" elif $known == $n then "full" else "partial" end
|
|
236
|
+
end)
|
|
237
|
+
},
|
|
238
|
+
tenant_id: (if $tenant == "" then null else $tenant end)
|
|
239
|
+
}' 2>/dev/null)" || exit 0
|
|
240
|
+
[[ -z "$record" || "$record" == "null" ]] && exit 0
|
|
241
|
+
|
|
242
|
+
# --- LOCAL-FIRST: append the record to the local economics log BEFORE any network attempt -----------
|
|
243
|
+
# This write must happen and must NOT depend on the POST. TELEMETRY_DATA_DIR comes from config.sh
|
|
244
|
+
# and is ALREADY the fully-qualified `.../.kontourai/telemetry` data dir (lib/config.sh:47), so only
|
|
245
|
+
# `economics.jsonl` is appended to it here — do NOT re-append `.kontourai/telemetry/...` on top of an
|
|
246
|
+
# already-fully-qualified TELEMETRY_DATA_DIR (that doubled the suffix, #469). The TELEMETRY_DIR-
|
|
247
|
+
# relative last resort below only applies when TELEMETRY_DATA_DIR truly isn't set (config.sh failed
|
|
248
|
+
# to source), in which case it legitimately needs the full `.kontourai/telemetry/economics.jsonl`
|
|
249
|
+
# suffix since it resolves from the workspace root, not a data dir.
|
|
250
|
+
if [[ -n "${TELEMETRY_ECONOMICS_LOG_FILE:-}" ]]; then
|
|
251
|
+
economics_log="$TELEMETRY_ECONOMICS_LOG_FILE"
|
|
252
|
+
elif [[ -n "${TELEMETRY_DATA_DIR:-}" ]]; then
|
|
253
|
+
economics_log="${TELEMETRY_DATA_DIR}/economics.jsonl"
|
|
254
|
+
else
|
|
255
|
+
economics_log="${TELEMETRY_DIR}/../../.kontourai/telemetry/economics.jsonl"
|
|
256
|
+
fi
|
|
257
|
+
mkdir -p "$(dirname "$economics_log")" 2>/dev/null || true
|
|
258
|
+
printf '%s\n' "$record" >> "$economics_log" 2>/dev/null || true
|
|
259
|
+
|
|
260
|
+
# --- BEST-EFFORT console relay: opt-in + only when a console endpoint is configured ------------------
|
|
261
|
+
# Off by default (local-first). Endpoint resolution copies relay.sh: explicit override, else base + /records.
|
|
262
|
+
case "${FLOW_AGENTS_CONSOLE_ECONOMICS_RELAY:-}" in
|
|
263
|
+
1 | true | TRUE | yes | on) ;;
|
|
264
|
+
*) exit 0 ;;
|
|
265
|
+
esac
|
|
266
|
+
|
|
267
|
+
# --- suppression guard: never relay a record with neither attribution nor cost/token/defect signal
|
|
268
|
+
# An "unattributed" $0/no-token/no-defect record carries no signal for the console ROI view and
|
|
269
|
+
# only dilutes cross-run aggregates (firstPassRate, totalCostUsd, defectsCaught). Suppress the
|
|
270
|
+
# POST — NOT the local write above, which stays an unconditional immutable per-run fact — only
|
|
271
|
+
# when task_slug is empty/"unattributed" AND cost is zero AND there is NO token volume (input +
|
|
272
|
+
# output + cache all zero — cost legitimately degrades to null/0 on an unpriced/new model while
|
|
273
|
+
# TOKENS remain source-of-truth, so a token-volume leg is required or real usage on unpriced
|
|
274
|
+
# models is wrongly suppressed) AND there is no defect/gate signal. ANY of real attribution, real
|
|
275
|
+
# cost, real token volume, or a real defect signal is enough to still relay unchanged. This guard
|
|
276
|
+
# is itself best-effort and fails OPEN toward RELAYING: dropping a real record is worse than an
|
|
277
|
+
# extra empty one reaching the console, so we suppress ONLY when jq successfully computed an
|
|
278
|
+
# explicit `false` (a genuinely-empty record); a jq/read failure (non-zero exit, or any output
|
|
279
|
+
# other than the literal "false") falls through and RELAYS unchanged — never toward silently
|
|
280
|
+
# swallowing real data (see docs/specs/economics-record-contract.md).
|
|
281
|
+
has_signal="$(printf '%s' "$record" | jq -r '
|
|
282
|
+
((.task_slug // "") | ascii_downcase) as $slug
|
|
283
|
+
| ($slug != "" and $slug != "unattributed") as $attributed
|
|
284
|
+
| ((.cost.estimated_cost_usd // 0) > 0) as $has_cost
|
|
285
|
+
| ((((.cost.input_tokens // 0) + (.cost.output_tokens // 0)
|
|
286
|
+
+ (.cost.cache_creation_input_tokens // 0)
|
|
287
|
+
+ (.cost.cache_read_input_tokens // 0)) > 0)) as $has_tokens
|
|
288
|
+
| ((.defects.gate_fires // 0) > 0) as $has_gate_fires
|
|
289
|
+
| ((.defects.caught_false_completions // 0) > 0) as $has_cfc
|
|
290
|
+
| (([.defects.findings_by_severity[]?] | add // 0) > 0) as $has_findings
|
|
291
|
+
| ($attributed or $has_cost or $has_tokens or $has_gate_fires or $has_cfc or $has_findings)
|
|
292
|
+
' 2>/dev/null)"
|
|
293
|
+
jq_rc=$?
|
|
294
|
+
if [[ $jq_rc -eq 0 && "$has_signal" == "false" ]]; then
|
|
295
|
+
[[ "${TELEMETRY_ECONOMICS_DEBUG:-}" == "1" ]] && echo "economics-record: suppressing console relay (genuinely-empty record: no attribution/cost/tokens/defects)" >&2
|
|
296
|
+
exit 0
|
|
297
|
+
fi
|
|
298
|
+
# jq error (non-zero rc), or any signal present ($has_signal == "true") -> fall through and RELAY
|
|
299
|
+
# (fail-open toward relaying real data, never toward silently dropping a record on guard failure).
|
|
300
|
+
|
|
301
|
+
# The economics ingress is the shared kind-routed `<origin>/records` endpoint — NOT the same path as
|
|
302
|
+
# the telemetry endpoint (`.../api/telemetry/records`) — so CONSOLE_TELEMETRY_ENDPOINT_URL can only
|
|
303
|
+
# contribute an origin here (its path is stripped), never be reused verbatim (#469 review).
|
|
304
|
+
endpoint="${FLOW_AGENTS_CONSOLE_ECONOMICS_ENDPOINT_URL:-}"
|
|
305
|
+
if [[ -z "$endpoint" ]]; then
|
|
306
|
+
base="${FLOW_AGENTS_CONSOLE_URL:-${CONSOLE_TELEMETRY_URL:-${CONSOLE_URL:-}}}"
|
|
307
|
+
if [[ -z "$base" && -n "${CONSOLE_TELEMETRY_ENDPOINT_URL:-}" ]]; then
|
|
308
|
+
base="$(printf '%s' "$CONSOLE_TELEMETRY_ENDPOINT_URL" | sed -E 's#^(https?://[^/]+).*#\1#')"
|
|
309
|
+
fi
|
|
310
|
+
[[ -z "$base" ]] && exit 0
|
|
311
|
+
endpoint="${base%/}/records"
|
|
312
|
+
fi
|
|
313
|
+
|
|
314
|
+
# Auth reuses the env names console_post_json reads (CONSOLE_TELEMETRY_TOKEN / CONSOLE_TENANT_ID),
|
|
315
|
+
# accepting FLOW_AGENTS_CONSOLE_* aliases and an optional token file — never fatal on a missing token.
|
|
316
|
+
if [[ -z "${CONSOLE_TELEMETRY_TOKEN:-}" ]]; then
|
|
317
|
+
token_file="${FLOW_AGENTS_CONSOLE_TOKEN_FILE:-${CONSOLE_TELEMETRY_TOKEN_FILE:-}}"
|
|
318
|
+
if [[ -n "$token_file" && -r "$token_file" ]]; then
|
|
319
|
+
CONSOLE_TELEMETRY_TOKEN="$(tr -d '\r\n' < "$token_file" 2>/dev/null || true)"
|
|
320
|
+
export CONSOLE_TELEMETRY_TOKEN
|
|
321
|
+
fi
|
|
322
|
+
fi
|
|
323
|
+
if [[ -z "${CONSOLE_TENANT_ID:-}" && -n "${FLOW_AGENTS_CONSOLE_TENANT:-}" ]]; then
|
|
324
|
+
export CONSOLE_TENANT_ID="$FLOW_AGENTS_CONSOLE_TENANT"
|
|
325
|
+
fi
|
|
326
|
+
|
|
327
|
+
# Optional field redaction (default none — jq escaping already neutralizes injection), then the shared
|
|
328
|
+
# best-effort POST. console_post_json enforces the https/localhost endpoint-allow gate + detached fire.
|
|
329
|
+
processed="$(redact_event "$record" "${FLOW_AGENTS_CONSOLE_ECONOMICS_REDACT:-none}")"
|
|
330
|
+
console_post_json "$endpoint" "$processed"
|
|
331
|
+
exit 0
|
|
@@ -15,6 +15,10 @@ Options:
|
|
|
15
15
|
--console-token-file PATH
|
|
16
16
|
File containing the bearer token.
|
|
17
17
|
--console-tenant ID Tenant identifier for hosted Console routing.
|
|
18
|
+
--no-economics-relay Write console_economics_relay=0 to opt out of the
|
|
19
|
+
automatic kit-economics relay (default-on once a
|
|
20
|
+
Console telemetry sink is configured; see
|
|
21
|
+
lib/config.sh).
|
|
18
22
|
EOF
|
|
19
23
|
}
|
|
20
24
|
|
|
@@ -105,6 +109,7 @@ main() {
|
|
|
105
109
|
local console_token=""
|
|
106
110
|
local console_token_file="${FLOW_AGENTS_CONSOLE_TOKEN_FILE:-${CONSOLE_TELEMETRY_TOKEN_FILE:-}}"
|
|
107
111
|
local console_tenant="${FLOW_AGENTS_CONSOLE_TENANT:-${CONSOLE_TENANT_ID:-}}"
|
|
112
|
+
local no_economics_relay=0
|
|
108
113
|
telemetry_sinks=()
|
|
109
114
|
if [[ -n "${FLOW_AGENTS_TELEMETRY_SINKS:-}" ]]; then
|
|
110
115
|
append_sinks "$FLOW_AGENTS_TELEMETRY_SINKS"
|
|
@@ -139,6 +144,10 @@ main() {
|
|
|
139
144
|
console_tenant="$2"
|
|
140
145
|
shift 2
|
|
141
146
|
;;
|
|
147
|
+
--no-economics-relay)
|
|
148
|
+
no_economics_relay=1
|
|
149
|
+
shift
|
|
150
|
+
;;
|
|
142
151
|
--help|-h)
|
|
143
152
|
usage
|
|
144
153
|
exit 0
|
|
@@ -181,7 +190,12 @@ main() {
|
|
|
181
190
|
done
|
|
182
191
|
[[ "$console_sink_count" -le 1 ]] || die "select at most one Console telemetry sink"
|
|
183
192
|
|
|
184
|
-
if [[
|
|
193
|
+
if [[ "$console_sink_count" -gt 0 && -z "$console_tenant" ]]; then
|
|
194
|
+
echo "warning: install-console-config.sh: a Console telemetry sink was selected with no --console-tenant; records will be untenanted" >&2
|
|
195
|
+
fi
|
|
196
|
+
|
|
197
|
+
if [[ -z "$console_url" && -z "$console_endpoint" && -z "$console_token" && -z "$console_tenant" \
|
|
198
|
+
&& "$no_economics_relay" -eq 0 ]]; then
|
|
185
199
|
return 0
|
|
186
200
|
fi
|
|
187
201
|
if [[ -z "$console_url" && -z "$console_endpoint" && ( -n "$console_token" || -n "$console_tenant" ) ]]; then
|
|
@@ -199,15 +213,22 @@ main() {
|
|
|
199
213
|
set_config_key "$conf" "console_telemetry_endpoint_url" "$console_endpoint"
|
|
200
214
|
set_config_key "$conf" "console_telemetry_token" "$console_token"
|
|
201
215
|
set_config_key "$conf" "console_tenant_id" "$console_tenant"
|
|
216
|
+
if [[ "$no_economics_relay" -eq 1 ]]; then
|
|
217
|
+
set_config_key "$conf" "console_economics_relay" "0"
|
|
218
|
+
fi
|
|
202
219
|
if [[ -n "$console_token" ]]; then
|
|
203
220
|
chmod 600 "$conf" 2>/dev/null || true
|
|
204
221
|
fi
|
|
205
222
|
|
|
206
223
|
local target="${console_endpoint:-$console_url}"
|
|
207
|
-
if [[ -n "$
|
|
208
|
-
|
|
224
|
+
if [[ -n "$target" ]]; then
|
|
225
|
+
if [[ -n "$console_tenant" ]]; then
|
|
226
|
+
echo "Configured Console telemetry in $conf for $target (tenant: $console_tenant)"
|
|
227
|
+
else
|
|
228
|
+
echo "Configured Console telemetry in $conf for $target"
|
|
229
|
+
fi
|
|
209
230
|
else
|
|
210
|
-
echo "
|
|
231
|
+
echo "Recorded economics-relay opt-out in $conf"
|
|
211
232
|
fi
|
|
212
233
|
}
|
|
213
234
|
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# learning-review-decide.sh — record a human ratify/reject/defer decision against a
|
|
3
|
+
# learning-review-proposals.sh ledger entry, in place (#352).
|
|
4
|
+
#
|
|
5
|
+
# Never mutates kit/gate/flow config; only rewrites the matching ledger line's `decision`
|
|
6
|
+
# object (and `follow_on_ref`, ratify-only). Writes atomically (temp file + mv). Refuses,
|
|
7
|
+
# with no write, to: record a decision for an unknown proposal-id; accept more than one of
|
|
8
|
+
# --ratify/--reject/--defer; or accept --follow-on-ref without --ratify (AC3's
|
|
9
|
+
# ratify-before-follow-on ordering guard).
|
|
10
|
+
#
|
|
11
|
+
# Usage:
|
|
12
|
+
# learning-review-decide.sh <ledger-path> <proposal-id> --ratify|--reject|--defer \
|
|
13
|
+
# --decided-by NAME [--rationale TEXT] [--follow-on-ref REF]
|
|
14
|
+
set -uo pipefail
|
|
15
|
+
|
|
16
|
+
usage() {
|
|
17
|
+
cat <<'USAGE'
|
|
18
|
+
Usage: learning-review-decide.sh <ledger-path> <proposal-id> --ratify|--reject|--defer \
|
|
19
|
+
--decided-by NAME [--rationale TEXT] [--follow-on-ref REF]
|
|
20
|
+
|
|
21
|
+
Rewrites the matching ledger line's `decision` object in place (status, decided_by,
|
|
22
|
+
decided_at, rationale). `--follow-on-ref` is only accepted together with `--ratify`
|
|
23
|
+
(AC3: a follow-on work item may never be linked to an unratified proposal). Exits non-zero,
|
|
24
|
+
writing nothing, if: <proposal-id> is not found in <ledger-path>; more than one of
|
|
25
|
+
--ratify/--reject/--defer is given; or --follow-on-ref is given without --ratify.
|
|
26
|
+
|
|
27
|
+
See docs/specs/learning-review-proposals-contract.md for the full contract.
|
|
28
|
+
USAGE
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
command -v jq >/dev/null 2>&1 || { echo "learning-review-decide: jq is required" >&2; exit 1; }
|
|
32
|
+
|
|
33
|
+
if [[ "${1:-}" == "-h" || "${1:-}" == "--help" ]]; then usage; exit 0; fi
|
|
34
|
+
|
|
35
|
+
ledger_path="${1:-}"
|
|
36
|
+
proposal_id="${2:-}"
|
|
37
|
+
if [[ -z "$ledger_path" || -z "$proposal_id" ]]; then
|
|
38
|
+
echo "learning-review-decide: <ledger-path> and <proposal-id> are required" >&2
|
|
39
|
+
usage >&2
|
|
40
|
+
exit 1
|
|
41
|
+
fi
|
|
42
|
+
shift 2 || true
|
|
43
|
+
|
|
44
|
+
status=""
|
|
45
|
+
decided_by=""
|
|
46
|
+
rationale=""
|
|
47
|
+
follow_on_ref=""
|
|
48
|
+
have_follow_on_ref=0
|
|
49
|
+
decision_flags=0
|
|
50
|
+
|
|
51
|
+
while [[ "$#" -gt 0 ]]; do
|
|
52
|
+
case "$1" in
|
|
53
|
+
--ratify) status="ratified"; decision_flags=$((decision_flags + 1)); shift ;;
|
|
54
|
+
--reject) status="rejected"; decision_flags=$((decision_flags + 1)); shift ;;
|
|
55
|
+
--defer) status="deferred"; decision_flags=$((decision_flags + 1)); shift ;;
|
|
56
|
+
--decided-by) decided_by="${2:-}"; shift 2 ;;
|
|
57
|
+
--rationale) rationale="${2:-}"; shift 2 ;;
|
|
58
|
+
--follow-on-ref) follow_on_ref="${2:-}"; have_follow_on_ref=1; shift 2 ;;
|
|
59
|
+
-h|--help) usage; exit 0 ;;
|
|
60
|
+
*) echo "learning-review-decide: unknown argument: $1" >&2; usage >&2; exit 1 ;;
|
|
61
|
+
esac
|
|
62
|
+
done
|
|
63
|
+
|
|
64
|
+
if [[ "$decision_flags" -ne 1 ]]; then
|
|
65
|
+
echo "learning-review-decide: exactly one of --ratify/--reject/--defer is required" >&2
|
|
66
|
+
exit 1
|
|
67
|
+
fi
|
|
68
|
+
|
|
69
|
+
if [[ -z "$decided_by" ]]; then
|
|
70
|
+
echo "learning-review-decide: --decided-by NAME is required" >&2
|
|
71
|
+
exit 1
|
|
72
|
+
fi
|
|
73
|
+
|
|
74
|
+
if [[ "$have_follow_on_ref" -eq 1 && "$status" != "ratified" ]]; then
|
|
75
|
+
echo "learning-review-decide: --follow-on-ref requires --ratify (AC3: no follow-on before a recorded ratify decision)" >&2
|
|
76
|
+
exit 1
|
|
77
|
+
fi
|
|
78
|
+
|
|
79
|
+
if [[ ! -f "$ledger_path" ]]; then
|
|
80
|
+
echo "learning-review-decide: ledger not found: $ledger_path" >&2
|
|
81
|
+
exit 1
|
|
82
|
+
fi
|
|
83
|
+
|
|
84
|
+
found="$(jq -c --arg pid "$proposal_id" 'select(.proposal_id == $pid)' "$ledger_path" 2>/dev/null | head -n1)"
|
|
85
|
+
if [[ -z "$found" ]]; then
|
|
86
|
+
echo "learning-review-decide: proposal-id not found in ledger: $proposal_id" >&2
|
|
87
|
+
exit 1
|
|
88
|
+
fi
|
|
89
|
+
|
|
90
|
+
now_iso="$(date -u +%Y-%m-%dT%H:%M:%SZ)"
|
|
91
|
+
ledger_dir="$(dirname "$ledger_path")"
|
|
92
|
+
tmp_ledger="$(mktemp "${ledger_dir}/.learning-review-decide.XXXXXX")" || {
|
|
93
|
+
echo "learning-review-decide: failed to create temp file" >&2
|
|
94
|
+
exit 1
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
if ! jq -c \
|
|
98
|
+
--arg pid "$proposal_id" \
|
|
99
|
+
--arg status "$status" \
|
|
100
|
+
--arg decided_by "$decided_by" \
|
|
101
|
+
--arg decided_at "$now_iso" \
|
|
102
|
+
--arg rationale "$rationale" \
|
|
103
|
+
--argjson have_rationale "$([[ -n "$rationale" ]] && echo true || echo false)" \
|
|
104
|
+
--arg follow_on_ref "$follow_on_ref" \
|
|
105
|
+
--argjson have_follow_on_ref "$([[ "$have_follow_on_ref" -eq 1 ]] && echo true || echo false)" \
|
|
106
|
+
'
|
|
107
|
+
if .proposal_id == $pid then
|
|
108
|
+
.decision = {
|
|
109
|
+
status: $status,
|
|
110
|
+
decided_by: $decided_by,
|
|
111
|
+
decided_at: $decided_at,
|
|
112
|
+
rationale: (if $have_rationale then $rationale else null end)
|
|
113
|
+
}
|
|
114
|
+
| if $have_follow_on_ref and $status == "ratified" then .follow_on_ref = $follow_on_ref else . end
|
|
115
|
+
else . end
|
|
116
|
+
' "$ledger_path" > "$tmp_ledger"; then
|
|
117
|
+
echo "learning-review-decide: failed to rewrite ledger" >&2
|
|
118
|
+
rm -f "$tmp_ledger"
|
|
119
|
+
exit 1
|
|
120
|
+
fi
|
|
121
|
+
|
|
122
|
+
mv "$tmp_ledger" "$ledger_path"
|
|
123
|
+
echo "learning-review-decide: proposal_id=$proposal_id decision.status=$status recorded" >&2
|
|
124
|
+
jq -c --arg pid "$proposal_id" 'select(.proposal_id == $pid)' "$ledger_path"
|