@kontourai/flow-agents 2.3.0 → 3.0.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/CODEOWNERS +16 -0
- package/.github/workflows/ci.yml +44 -0
- package/.github/workflows/trust-reconcile.yml +62 -4
- package/AGENTS.md +4 -4
- package/CHANGELOG.md +62 -0
- package/CONTEXT.md +35 -0
- package/README.md +9 -3
- package/agents/dev.json +1 -1
- package/agents/tool-code-reviewer.json +1 -1
- package/agents/tool-planner.json +3 -3
- package/agents/tool-verifier.json +3 -3
- package/build/src/cli/assignment-provider.d.ts +1 -0
- package/build/src/cli/assignment-provider.js +748 -0
- package/build/src/cli/console-learning-projection.js +3 -2
- package/build/src/cli/effective-assignment-provider-settings.d.ts +1 -0
- package/build/src/cli/effective-assignment-provider-settings.js +125 -0
- package/build/src/cli/init.js +104 -21
- package/build/src/cli/kit.js +37 -6
- package/build/src/cli/pull-work-provider.js +1 -1
- package/build/src/cli/usage-feedback.js +3 -3
- package/build/src/cli/validate-hook-influence.js +1 -0
- package/build/src/cli/validate-workflow-artifacts.js +27 -7
- package/build/src/cli/workflow-artifact-cleanup-audit.js +36 -2
- package/build/src/cli/workflow-sidecar.d.ts +14 -7
- package/build/src/cli/workflow-sidecar.js +938 -168
- package/build/src/cli.js +6 -0
- package/build/src/flow-kit/validate.d.ts +23 -0
- package/build/src/flow-kit/validate.js +67 -1
- package/build/src/index.d.ts +1 -1
- package/build/src/index.js +1 -1
- package/build/src/lib/flow-resolver.d.ts +6 -4
- package/build/src/lib/flow-resolver.js +125 -58
- package/build/src/lib/local-artifact-root.d.ts +14 -2
- package/build/src/lib/local-artifact-root.js +22 -5
- package/build/src/lib/workflow-learning-projection.js +2 -2
- package/build/src/runtime-adapters.d.ts +12 -0
- package/build/src/runtime-adapters.js +56 -16
- package/build/src/tools/build-universal-bundles.js +49 -16
- package/build/src/tools/generate-context-map.js +10 -8
- package/build/src/tools/validate-source-tree.js +24 -1
- package/console.telemetry.json +22 -0
- package/context/contracts/artifact-contract.md +40 -7
- package/context/contracts/assignment-provider-contract.md +239 -0
- package/context/contracts/builder-kit-workflow-state-contract.md +5 -2
- package/context/contracts/decision-registry-contract.md +265 -0
- package/context/contracts/delivery-contract.md +3 -1
- package/context/contracts/execution-contract.md +25 -0
- package/context/contracts/governance-adapter-contract.md +2 -0
- package/context/contracts/knowledge-store-contract.md +197 -0
- package/context/contracts/planning-contract.md +4 -1
- package/context/contracts/review-contract.md +7 -3
- package/context/contracts/sandbox-policy.md +2 -0
- package/context/contracts/standing-directives.md +13 -0
- package/context/contracts/verification-contract.md +11 -1
- package/context/contracts/work-item-contract.md +3 -1
- package/context/deferred/parallelization.md +1 -1
- package/context/gate-awareness.md +4 -4
- package/context/scripts/git-status.sh +2 -2
- package/context/scripts/hooks/config-protection.js +502 -2
- package/context/scripts/hooks/lib/config-protection-remedies.js +69 -0
- package/context/scripts/hooks/run-hook.js +14 -1
- package/context/scripts/hooks/stop-goal-fit.js +1140 -99
- package/context/scripts/hooks/workflow-steering.js +212 -6
- package/context/settings/assignment-provider-settings.json +33 -0
- package/docs/adr/0007-flow-skill-kit-tool-boundary.md +2 -0
- package/docs/adr/0015-flow-flow-agents-boundary-reconciliation.md +35 -0
- package/docs/adr/0019-kit-dependency-ownership.md +62 -0
- package/docs/adr/0020-trust-reconcile-manifest-and-claim-classification.md +290 -0
- package/docs/adr/0021-assignment-leases-and-stale-claim-takeover.md +221 -0
- package/docs/adr/0022-fail-closed-delivery-reconciliation-with-governed-exemptions.md +463 -0
- package/docs/agent-system-guidebook.md +3 -3
- package/docs/agent-usage-feedback-loop.md +6 -6
- package/docs/context-map.md +10 -7
- package/docs/decisions/decision-records.md +57 -0
- package/docs/decisions/index.md +17 -0
- package/docs/decisions/knowledge-store-provider.md +51 -0
- package/docs/decisions/model-routing.md +63 -0
- package/docs/decisions/promotion-gate.md +52 -0
- package/docs/decisions/standing-directives.md +66 -0
- package/docs/developer-architecture.md +1 -1
- package/docs/fixture-ownership.md +4 -0
- package/docs/flow-kit-repository-contract.md +5 -5
- package/docs/getting-started.md +12 -2
- package/docs/integrations/harness-install.md +2 -1
- package/docs/kit-authoring-guide.md +64 -5
- package/docs/knowledge-kit.md +5 -1
- package/docs/learnings/2026-07-improvement-program.md +203 -0
- package/docs/migrations.md +6 -5
- package/docs/north-star.md +2 -2
- package/docs/operating-layers.md +4 -3
- package/docs/repository-structure.md +5 -5
- package/docs/skills-map.md +8 -8
- package/docs/spec/runtime-hook-surface.md +19 -15
- package/docs/spikes/graph-provider-2026-07.md +155 -0
- package/docs/standards-register.md +7 -7
- package/docs/trust-anchor-adoption.md +105 -4
- package/docs/work-item-adapters.md +1 -1
- package/docs/workflow-artifact-lifecycle.md +72 -9
- package/docs/workflow-eval-strategy.md +3 -3
- package/docs/workflow-shared-contracts.md +3 -2
- package/docs/workflow-usage-guide.md +260 -21
- package/evals/acceptance/DEMO-false-completion.md +20 -11
- package/evals/acceptance/prove-capture-teeth-declared.sh +15 -15
- package/evals/acceptance/prove-capture-teeth.sh +11 -11
- package/evals/acceptance/test_kiro_harness.sh +1 -1
- package/evals/ci/antigaming-suite.sh +5 -0
- package/evals/ci/run-baseline.sh +71 -1
- package/evals/fixtures/assignment-provider/actor-a.json +6 -0
- package/evals/fixtures/assignment-provider/actor-b.json +6 -0
- package/evals/fixtures/assignment-provider/github-issue-claimed.json +27 -0
- package/evals/fixtures/assignment-provider/github-issue-unassigned.json +7 -0
- package/evals/fixtures/assignment-provider/liveness-fresh.json +9 -0
- package/evals/fixtures/assignment-provider/liveness-stale.json +9 -0
- package/evals/fixtures/flow-kit-repository/invalid-bad-dependency/flows/review.flow.json +26 -0
- package/evals/fixtures/flow-kit-repository/invalid-bad-dependency/kit.json +13 -0
- package/evals/fixtures/flow-kit-repository/valid-with-dependency/flows/review.flow.json +26 -0
- package/evals/fixtures/flow-kit-repository/valid-with-dependency/kit.json +13 -0
- package/evals/fixtures/hook-influence/cases.json +32 -0
- package/evals/fixtures/pull-work-provider/github-issues.json +6 -6
- package/evals/fixtures/trust-reconcile-exploits/fabricated-attestation.json +59 -0
- package/evals/fixtures/trust-reconcile-exploits/no-label-bypass.json +36 -0
- package/evals/fixtures/trust-reconcile-exploits/skip-assumed-bypass.json +59 -0
- package/evals/fixtures/trust-reconcile-exploits/status-misassertion.json +48 -0
- package/evals/fixtures/trust-reconcile-exploits/waived-command-check.json +44 -0
- package/evals/fixtures/trust-reconcile-mixed-bundle/mixed-bundle.json +159 -0
- package/evals/fixtures/trust-reconcile-ws3/ws3-bundle.json +920 -0
- package/evals/integration/test_actor_identity.sh +254 -0
- package/evals/integration/test_assignment_provider_github.sh +318 -0
- package/evals/integration/test_assignment_provider_local_file.sh +222 -0
- package/evals/integration/test_builder_step_producers.sh +33 -23
- package/evals/integration/test_bundle_install.sh +22 -16
- package/evals/integration/test_bundle_lifecycle.sh +11 -11
- package/evals/integration/test_captured_fail_reconciliation.sh +50 -50
- package/evals/integration/test_checkpoint_signing.sh +43 -0
- package/evals/integration/test_claim_lookup.sh +4 -4
- package/evals/integration/test_codex_hook_resolution.sh +114 -0
- package/evals/integration/test_command_log_concurrency.sh +8 -8
- package/evals/integration/test_command_log_fork_classification.sh +3 -3
- package/evals/integration/test_command_log_integrity.sh +16 -16
- package/evals/integration/test_console_learning_projection.sh +1 -1
- package/evals/integration/test_critique_supersession_roundtrip.sh +182 -0
- package/evals/integration/test_dual_emit_flow_step.sh +145 -0
- package/evals/integration/test_enforcer_expects_driven.sh +12 -12
- package/evals/integration/test_evidence_capture_hook.sh +14 -13
- package/evals/integration/test_fixture_retirement_audit.sh +2 -2
- package/evals/integration/test_flow_agents_statusline.sh +2 -2
- package/evals/integration/test_flow_kit_install_git.sh +52 -0
- package/evals/integration/test_flow_kit_repository.sh +2 -0
- package/evals/integration/test_flowdef_session_activation.sh +6 -6
- package/evals/integration/test_flowdef_union_floor_regression.sh +429 -0
- package/evals/integration/test_gate_bypass_chain.sh +52 -40
- package/evals/integration/test_gate_lockdown.sh +97 -73
- package/evals/integration/test_gate_review_inquiry_records.sh +3 -3
- package/evals/integration/test_goal_fit_escape_hatch.sh +10 -10
- package/evals/integration/test_goal_fit_ghost_session.sh +104 -0
- package/evals/integration/test_goal_fit_hook.sh +32 -32
- package/evals/integration/test_goal_fit_rederive.sh +9 -9
- package/evals/integration/test_hook_category_behaviors.sh +103 -0
- package/evals/integration/test_hook_influence_cases.sh +1 -0
- package/evals/integration/test_install_merge.sh +262 -1
- package/evals/integration/test_kit_identity_trust.sh +6 -6
- package/evals/integration/test_liveness_conflict_injection.sh +587 -0
- package/evals/integration/test_liveness_heartbeat.sh +677 -0
- package/evals/integration/test_liveness_verdict.sh +394 -0
- package/evals/integration/test_local_flow_kit_install.sh +15 -0
- package/evals/integration/test_phase_map_and_gate_claim.sh +63 -11
- package/evals/integration/test_promote_gate.sh +178 -0
- package/evals/integration/test_publish_delivery.sh +23 -6
- package/evals/integration/test_pull_work_assignment_join.sh +132 -0
- package/evals/integration/test_pull_work_liveness_preflight.sh +394 -0
- package/evals/integration/test_pull_work_provider.sh +2 -2
- package/evals/integration/test_reconcile_soundness.sh +37 -13
- package/evals/integration/test_resolvefirststep_security.sh +22 -0
- package/evals/integration/test_runtime_adapter_activation.sh +87 -18
- package/evals/integration/test_session_resume_roundtrip.sh +147 -0
- package/evals/integration/test_sidecar_field_preservation.sh +249 -0
- package/evals/integration/test_trust_reconcile.sh +11 -10
- package/evals/integration/test_trust_reconcile_manifest.sh +79 -0
- package/evals/integration/test_trust_reconcile_mixed_bundle.sh +96 -0
- package/evals/integration/test_trust_reconcile_negatives.sh +759 -0
- package/evals/integration/test_usage_feedback_global.sh +5 -5
- package/evals/integration/test_validate_artifacts_portability.sh +60 -0
- package/evals/integration/test_verify_cli.sh +2 -2
- package/evals/integration/test_veritas_governance_kit.sh +117 -0
- package/evals/integration/test_workflow_artifact_cleanup_audit.sh +26 -1
- package/evals/integration/test_workflow_artifacts.sh +33 -0
- package/evals/integration/test_workflow_sidecar_writer.sh +802 -74
- package/evals/integration/test_workflow_steering_hook.sh +175 -5
- package/evals/lib/codex-provider.sh +1 -1
- package/evals/lib/node.sh +19 -1
- package/evals/run.sh +32 -0
- package/evals/static/test_decisions.sh +150 -0
- package/evals/static/test_flowdef_codeowners_coverage.sh +62 -0
- package/evals/static/test_knowledge_providers.sh +23 -0
- package/evals/static/test_package.sh +19 -10
- package/evals/static/test_universal_bundles.sh +48 -3
- package/evals/static/test_workflow_skills.sh +41 -8
- package/kits/builder/flows/build.flow.json +3 -74
- package/kits/builder/flows/publish-learn.flow.json +90 -0
- package/kits/builder/kit.json +11 -0
- package/kits/builder/skills/builder-shape/SKILL.md +1 -1
- package/kits/builder/skills/deliver/SKILL.md +32 -11
- package/kits/builder/skills/design-probe/SKILL.md +4 -4
- package/kits/builder/skills/evidence-gate/SKILL.md +26 -1
- package/kits/builder/skills/execute-plan/SKILL.md +2 -2
- package/kits/builder/skills/fix-bug/SKILL.md +2 -0
- package/kits/builder/skills/gate-review/SKILL.md +6 -6
- package/kits/builder/skills/idea-to-backlog/SKILL.md +1 -1
- package/kits/builder/skills/learning-review/SKILL.md +7 -7
- package/kits/builder/skills/pickup-probe/SKILL.md +5 -4
- package/kits/builder/skills/plan-work/SKILL.md +27 -5
- package/kits/builder/skills/pull-work/SKILL.md +148 -5
- package/kits/builder/skills/release-readiness/SKILL.md +4 -4
- package/kits/builder/skills/review-work/SKILL.md +24 -1
- package/kits/builder/skills/tdd-workflow/SKILL.md +2 -0
- package/kits/builder/skills/verify-work/SKILL.md +8 -1
- package/kits/catalog.json +6 -0
- package/kits/knowledge/kit.json +35 -0
- package/kits/knowledge/providers/conformance/fixtures/git-repo/CONTEXT.md +12 -0
- package/kits/knowledge/providers/conformance/fixtures/git-repo/docs/decisions/old-sprocket-shape.md +13 -0
- package/kits/knowledge/providers/conformance/fixtures/git-repo/docs/decisions/sprocket-shape.md +14 -0
- package/kits/knowledge/providers/conformance/fixtures/git-repo/docs/decisions/widget-format.md +14 -0
- package/kits/knowledge/providers/conformance/fixtures/git-repo/docs/learnings/fixture-learning.md +7 -0
- package/kits/knowledge/providers/conformance/fixtures/work-item/issues.json +30 -0
- package/kits/knowledge/providers/conformance/suite.test.js +125 -0
- package/kits/knowledge/providers/git-repo/index.js +236 -0
- package/kits/knowledge/providers/health/health-pass.test.js +99 -0
- package/kits/knowledge/providers/health/index.js +153 -0
- package/kits/knowledge/providers/index.js +24 -0
- package/kits/knowledge/providers/lib/model.js +91 -0
- package/kits/knowledge/providers/lib/schema-validate.js +119 -0
- package/kits/knowledge/providers/markdown-vault/index.js +169 -0
- package/kits/knowledge/providers/work-item/index.js +204 -0
- package/kits/veritas-governance/adapter/readiness-to-trust-bundle.mjs +178 -0
- package/kits/veritas-governance/docs/README.md +75 -0
- package/kits/veritas-governance/fixtures/readiness/not-ready.readiness-report.json +4645 -0
- package/kits/veritas-governance/fixtures/readiness/ready.readiness-report.json +4403 -0
- package/kits/veritas-governance/flows/readiness-check.flow.json +35 -0
- package/kits/veritas-governance/kit.json +16 -0
- package/package.json +11 -5
- package/packaging/README.md +2 -2
- package/packaging/conformance/fixtures/config-protection--allow-read-state-json.json +20 -0
- package/packaging/conformance/fixtures/config-protection--block-write-state-json.json +20 -0
- package/packaging/conformance/fixtures/stop-goal-fit--block-bundle-disputed-claim.json +1 -1
- package/packaging/conformance/fixtures/stop-goal-fit--block-capture-contradicts-claimed-pass.json +1 -1
- package/packaging/manifest.json +9 -10
- package/schemas/assignment-provider-settings.schema.json +125 -0
- package/schemas/decision-record.schema.json +109 -0
- package/schemas/knowledge/edge.schema.json +54 -0
- package/schemas/knowledge/health-report.schema.json +45 -0
- package/schemas/knowledge/node.schema.json +49 -0
- package/schemas/knowledge/proposal.schema.json +53 -0
- package/schemas/workflow-evidence.schema.json +1 -1
- package/schemas/workflow-state.schema.json +5 -0
- package/scripts/README.md +8 -3
- package/scripts/check-content-boundary.cjs +1 -1
- package/scripts/check-decisions.cjs +356 -0
- package/scripts/ci/derive-claim-status.mjs +74 -0
- package/scripts/ci/trust-reconcile.js +957 -103
- package/scripts/git-status.sh +2 -2
- package/scripts/hooks/claude-telemetry-hook.js +37 -3
- package/scripts/hooks/codex-telemetry-hook.js +36 -2
- package/scripts/hooks/config-protection.js +102 -30
- package/scripts/hooks/evidence-capture.js +3 -3
- package/scripts/hooks/lib/actor-identity.js +368 -0
- package/scripts/hooks/lib/config-protection-remedies.js +69 -0
- package/scripts/hooks/lib/liveness-heartbeat.js +361 -0
- package/scripts/hooks/lib/liveness-policy.js +127 -0
- package/scripts/hooks/lib/liveness-read.js +105 -12
- package/scripts/hooks/lib/liveness-write.js +46 -0
- package/scripts/hooks/lib/local-artifact-paths.js +12 -6
- package/scripts/hooks/opencode-telemetry-hook.js +27 -1
- package/scripts/hooks/pi-telemetry-hook.js +27 -1
- package/scripts/hooks/stop-goal-fit.js +157 -26
- package/scripts/hooks/workflow-steering.js +77 -3
- package/scripts/install-codex-home.sh +138 -38
- package/scripts/statusline/flow-agents-statusline.js +2 -2
- package/src/cli/assignment-provider.ts +845 -0
- package/src/cli/console-learning-projection.ts +3 -2
- package/src/cli/effective-assignment-provider-settings.ts +112 -0
- package/src/cli/init.ts +101 -21
- package/src/cli/kit.ts +37 -6
- package/src/cli/public-api.test.mjs +58 -6
- package/src/cli/pull-work-provider.ts +1 -1
- package/src/cli/trust-bundle-policy-order.test.mjs +87 -0
- package/src/cli/usage-feedback.ts +3 -3
- package/src/cli/validate-hook-influence.ts +1 -0
- package/src/cli/validate-workflow-artifacts.ts +27 -7
- package/src/cli/veritas-readiness-adapter.test.mjs +267 -0
- package/src/cli/workflow-artifact-cleanup-audit.ts +35 -2
- package/src/cli/workflow-sidecar.ts +933 -168
- package/src/cli.ts +6 -0
- package/src/flow-kit/validate.ts +74 -1
- package/src/index.ts +6 -2
- package/src/lib/flow-resolver.ts +123 -55
- package/src/lib/local-artifact-root.ts +24 -5
- package/src/lib/workflow-learning-projection.ts +2 -2
- package/src/runtime-adapters.ts +61 -15
- package/src/tools/build-universal-bundles.ts +49 -16
- package/src/tools/generate-context-map.ts +10 -8
- package/src/tools/validate-source-tree.ts +24 -1
|
@@ -0,0 +1,290 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: "ADR 0020: Trust-Reconcile Manifest, Claim Classification, and Waivers"
|
|
3
|
+
---
|
|
4
|
+
|
|
5
|
+
# ADR 0020: Trust-Reconcile Manifest, Claim Classification, and Waivers
|
|
6
|
+
|
|
7
|
+
Status: Accepted
|
|
8
|
+
|
|
9
|
+
Relates to: ADR 0004 (self-grading gate), ADR 0010 (trust.bundle as the primary
|
|
10
|
+
verification artifact), ADR 0017 (the anti-gaming trust security model + external CI
|
|
11
|
+
anchor). This ADR refines the CI anchor's *reconcile* step; it does not change the
|
|
12
|
+
layered-defense posture ADR 0017 established.
|
|
13
|
+
|
|
14
|
+
## Context
|
|
15
|
+
|
|
16
|
+
PR #264 was this repository's first real `trust.bundle` publication through the CI trust
|
|
17
|
+
anchor (`scripts/ci/trust-reconcile.js`, ADR 0017). It could not pass, and the failure was
|
|
18
|
+
architectural, not incidental:
|
|
19
|
+
|
|
20
|
+
1. **Single opaque composite command.** The reconciler resolved exactly one canonical
|
|
21
|
+
verify command — `package.json scripts["trust-reconcile-verify"]` = `npm run build &&
|
|
22
|
+
npm run eval:static` — and reconciled a claimed pass only when the claim's
|
|
23
|
+
`evidence.execution.label` was byte-identical to that composite string. Any honest,
|
|
24
|
+
granular, per-acceptance-criterion command (a single lane check, a narrower test, a
|
|
25
|
+
browser check) was, by construction, "a claimed pass CI never ran" → divergence.
|
|
26
|
+
|
|
27
|
+
2. **Every check hardcoded as `test_output`.** `buildTrustBundle`
|
|
28
|
+
(`src/cli/workflow-sidecar.ts`) stamped `evidenceType: "test_output"` on *every* check
|
|
29
|
+
regardless of `check.kind`, and only stamped `execution.label` when the local
|
|
30
|
+
command-log capture happened to run it. A browser check, a manual verification, a
|
|
31
|
+
provider/CI check, or a diff review therefore looked identical to a `test_output`
|
|
32
|
+
claim CI never ran.
|
|
33
|
+
|
|
34
|
+
3. **No waiver path.** An honestly-accepted gap had no sanctioned, visible way through the
|
|
35
|
+
anchor.
|
|
36
|
+
|
|
37
|
+
The net effect: no bundle carrying honest, granular, mixed evidence could pass, and the
|
|
38
|
+
deliver skill's own publish step was unsatisfiable for any session reporting more than one
|
|
39
|
+
composite command. PR #264's bundle was withdrawn.
|
|
40
|
+
|
|
41
|
+
## Decision
|
|
42
|
+
|
|
43
|
+
Redesign the producer/reconciler contract. **Consume, never fork** — every new concept
|
|
44
|
+
reuses an existing canonical vocabulary rather than inventing a parallel one.
|
|
45
|
+
|
|
46
|
+
### 1. The manifest is the existing CI check registry, not a new file
|
|
47
|
+
|
|
48
|
+
`evals/ci/run-baseline.sh` already defines a CI-verified, named, individually-re-runnable
|
|
49
|
+
command registry: the `CHECKS` array of `"Label|command"` pairs, partitioned into `LANE_*`
|
|
50
|
+
arrays that `.github/workflows/ci.yml`'s jobs invoke via `--check <slug>`. The merge-gating
|
|
51
|
+
lanes (`LANE_SOURCE_AND_STATIC`, `LANE_WORKFLOW_CONTRACTS`, `LANE_RUNTIME_AND_KIT`) are
|
|
52
|
+
*structurally* wired into a required job — the anti-gaming property ("a manifest command must
|
|
53
|
+
actually run in a required lane") is achieved **by construction** rather than by a new rule
|
|
54
|
+
that itself needs policing. The advisory `LANE_USAGE_FEEDBACK` lane runs `continue-on-error`
|
|
55
|
+
and does **not** gate a merge, so its checks are **excluded** from `--manifest-json`: a
|
|
56
|
+
`test_output` claim must never be able to reconcile against a non-gating command.
|
|
57
|
+
(`test_trust_reconcile_manifest.sh` asserts the emitted manifest equals the *required-lane*
|
|
58
|
+
`--check` set, usage-feedback excluded from both sides.)
|
|
59
|
+
|
|
60
|
+
`run-baseline.sh` gains a machine-readable `--manifest-json` emit mode (lane-covered checks
|
|
61
|
+
only, as `{id, command, lanes[]}`). `trust-reconcile.js` gains a manifest resolver with the
|
|
62
|
+
priority order:
|
|
63
|
+
|
|
64
|
+
1. CLI `--manifest <json>`
|
|
65
|
+
2. env `TRUST_RECONCILE_MANIFEST <json>`
|
|
66
|
+
3. `package.json "trust-reconcile-manifest"` — an inline `{id, command}[]` array (for
|
|
67
|
+
adopters without a `run-baseline.sh`-shaped registry), **or** a string command that
|
|
68
|
+
emits that JSON. This repo declares it as `bash evals/ci/run-baseline.sh
|
|
69
|
+
--manifest-json`, so its live manifest *is* the run-baseline registry.
|
|
70
|
+
4. `evals/ci/run-baseline.sh --manifest-json` (auto-detected — this repo's dogfood path).
|
|
71
|
+
5. Legacy fallback: the resolved single fresh-verify command(s) as a manifest of size N —
|
|
72
|
+
so the historical single-command behavior is a strict subset (backward compat by
|
|
73
|
+
construction).
|
|
74
|
+
|
|
75
|
+
The standalone fresh-verify (Step 1, the anchor's own independent CI truth) is unchanged.
|
|
76
|
+
The manifest governs Step 2 (per-command reconcile): a `test_output` claim may reconcile
|
|
77
|
+
only against a manifest command, and a manifest command not already run in Step 1 is
|
|
78
|
+
re-run **fresh on demand** (bounded by the registry) so the reconcile is against real CI
|
|
79
|
+
results, never the agent's self-report.
|
|
80
|
+
|
|
81
|
+
### 2. Claim classification reuses Surface's `evidenceType`/`method`, not a new taxonomy
|
|
82
|
+
|
|
83
|
+
`@kontourai/surface`'s `evidence.schema.json` already defines
|
|
84
|
+
`evidenceType: [source_excerpt, test_output, human_attestation, attestation,
|
|
85
|
+
calculation_trace, document_citation, crawl_observation, policy_rule]` and a `method`
|
|
86
|
+
enum. `buildTrustBundle` now derives `evidenceType`/`method` from `check.kind`:
|
|
87
|
+
|
|
88
|
+
| `check.kind` | `evidenceType` | `method` | reconcilable? |
|
|
89
|
+
| --- | --- | --- | --- |
|
|
90
|
+
| build, types, lint, test, command | `test_output` | `validation` | yes |
|
|
91
|
+
| security (command-backed) | `test_output` | `validation` | yes |
|
|
92
|
+
| security (no command) | `attestation` | `corroboration` | no |
|
|
93
|
+
| browser (command-backed) | `test_output` | `validation` | yes |
|
|
94
|
+
| browser (no command) | `crawl_observation` | `observation` | no |
|
|
95
|
+
| runtime (command-backed) | `test_output` | `validation` | yes |
|
|
96
|
+
| runtime (no command) | `attestation` | `attestation` | no |
|
|
97
|
+
| diff | `source_excerpt` | `extraction` | no |
|
|
98
|
+
| policy | `policy_rule` | `auditability` | no |
|
|
99
|
+
| external | `attestation` | `corroboration` | no |
|
|
100
|
+
|
|
101
|
+
A command-backed check now **always** carries `execution.label` (even when the local
|
|
102
|
+
command-log capture did not run it) so the reconciler has a stable match key. A claim is
|
|
103
|
+
**reconcilable** iff its evidence is `evidenceType: "test_output"` **and** its command
|
|
104
|
+
matches a manifest entry; everything else is **session-local by construction** — no new
|
|
105
|
+
boolean, no new enum. A `test_output` claim whose command is *not* in the manifest is
|
|
106
|
+
still a divergence (an agent cannot self-label an arbitrary command `test_output` to dodge
|
|
107
|
+
the manifest). The existing exit-code-laundering check runs *before* classification and is
|
|
108
|
+
unchanged.
|
|
109
|
+
|
|
110
|
+
**Iteration-2 hardening (soundness fixes found in review).**
|
|
111
|
+
|
|
112
|
+
- **A `test_output` claim reconciles or it is a divergence — never session-local (finding 1).**
|
|
113
|
+
The not-run guard triggers for ANY pass-asserting claim whose evidence is
|
|
114
|
+
`evidenceType: test_output` (Surface's default when unset) that lacks a manifest-matched
|
|
115
|
+
`execution.label` — not only the literal claimType `workflow.check.command`. A fabricated
|
|
116
|
+
`kind:"test"` claim with no command still emits `test_output` evidence with no label; it is
|
|
117
|
+
now a `not-run` divergence instead of silently passing as session-local.
|
|
118
|
+
- **The reconciler re-derives status CI-side and never trusts `claim.status` (finding 3).**
|
|
119
|
+
For every session-local claim, `trust-reconcile.js` re-runs Surface's canonical
|
|
120
|
+
`deriveClaimStatus` over the bundle's OWN evidence/events/policies (via
|
|
121
|
+
`scripts/ci/derive-claim-status.mjs`, resolving Surface from the anchor's own node_modules)
|
|
122
|
+
and compares the result to the asserted status. A mismatch is a `status-misassertion`
|
|
123
|
+
divergence; if re-derivation is unavailable the claim fails closed (`status-underivable`).
|
|
124
|
+
The bundle's self-reported `status` field is never the basis for a pass.
|
|
125
|
+
- **Policy construction is order-independent (AC1 cache defect).** `ensurePolicy` keys its
|
|
126
|
+
cache by `(claimType, requiredEvidence)` rather than by `claimType` alone, so two checks of
|
|
127
|
+
the same legacy claimType that differ in command-presence (a command-backed browser check →
|
|
128
|
+
`test_output` vs a no-command browser check → `crawl_observation`) get distinct policies
|
|
129
|
+
instead of the first-seen `requiredEvidence` silently corrupting the second claim's derived
|
|
130
|
+
status. Merging is deliberately NOT used — Surface's `requiredEvidence` is all-of, so a union
|
|
131
|
+
would over-constrain both claims. `trust-bundle-policy-order.test.mjs` proves two same-kind
|
|
132
|
+
checks produce identical bundles in both record orders.
|
|
133
|
+
|
|
134
|
+
### 3. Waivers reuse the existing `accepted_gap` status
|
|
135
|
+
|
|
136
|
+
`record-evidence`/`record-gate-claim` gain `--accepted-gap-reason <text>` and `--waived-by
|
|
137
|
+
<actor>` (both required together — an accepted gap with no justification or no approver is
|
|
138
|
+
refused; no silent/default waiver). When present, the recorded claim is classified
|
|
139
|
+
session-local (`attestation`), its status is the existing `accepted_gap → assumed` mapping,
|
|
140
|
+
and `claim.metadata.waiver = {reason, approved_by, approved_at}` is stamped
|
|
141
|
+
(`claim.metadata` is free-form per `claim.schema.json` — no schema fork). The reconciler
|
|
142
|
+
prints every waived claim on a distinct, un-suppressible `[trust-reconcile] WAIVED: ...`
|
|
143
|
+
line so it is visible in the required job's own log — reviewed, justified, never silent.
|
|
144
|
+
|
|
145
|
+
A session-local claim that is **not** waived must resolve a real, CI-RE-DERIVED `verified`
|
|
146
|
+
status to pass; a `disputed`/`rejected` claim still blocks. **`assumed` alone is no longer a
|
|
147
|
+
silent pass (finding 2):** because the `skip → assumed` mapping means an unjustified skip
|
|
148
|
+
resolves `assumed`, an `assumed` claim is accepted ONLY when it also carries a waiver (printed
|
|
149
|
+
on the loud `WAIVED` line). An unwaived `assumed` claim is an `unwaived-assumed` divergence —
|
|
150
|
+
restoring the pre-WS8 semantics where `assumed` alone never satisfied a claimed pass. An
|
|
151
|
+
unwaived session-local claim asserting pass without a re-derived `verified` status is an
|
|
152
|
+
`unwaived-session-local` divergence. Session-local classification is **not** a pass bypass.
|
|
153
|
+
|
|
154
|
+
**A command-backed check cannot be waived (finding 4).** A waiver documents an *accepted gap*
|
|
155
|
+
in something that was not re-runnably verified; a `test_output` (command-backed) check
|
|
156
|
+
reconciles against CI or fails, so waiving it would let an agent skip the real run. Both the
|
|
157
|
+
producer (`record-evidence`/`record-gate-claim` reject `--accepted-gap-reason`/`--waived-by`
|
|
158
|
+
on any check whose evidence classifies as `test_output`) and the reconciler (a waiver on a
|
|
159
|
+
claim backed by `test_output` evidence is a `waiver-on-command-check` divergence) enforce this.
|
|
160
|
+
|
|
161
|
+
**Authority-binding residual (honest).** `approved_by` is currently free text — the reconciler
|
|
162
|
+
verifies a waiver is *present, justified, and named*, and prints it loudly for human review,
|
|
163
|
+
but it does NOT yet cryptographically bind the approver to an authenticated identity. Tying
|
|
164
|
+
`approved_by` to an Assurance-profile identity (Surface's authority/identity model), so a
|
|
165
|
+
waiver's approver is verifiable rather than self-asserted, is deferred to that profile's
|
|
166
|
+
adoption and tracked as follow-up; until then a waiver's accountability rests on the required
|
|
167
|
+
job's visible `WAIVED` line and CODEOWNERS review, not on identity attestation. (See Residuals
|
|
168
|
+
below — this is the same authenticated-identity gap that also underlies the fabricated-
|
|
169
|
+
attestation residual.)
|
|
170
|
+
|
|
171
|
+
### 4. Iteration-4 hardening: loud disclosure of non-command-backed passes (converged finding)
|
|
172
|
+
|
|
173
|
+
Both iteration-3 gates (the review critique and an independently-corroborating adversarial
|
|
174
|
+
re-verification) converged on one finding against the redesign above: a session-local claim
|
|
175
|
+
that re-derives `verified` with **no waiver** — i.e. every `human_attestation` / `attestation`
|
|
176
|
+
/ `external`-evidenced claim for a no-`command` check (§2's table: `security`/`browser`/
|
|
177
|
+
`runtime`/`external` without a command, `diff`, `policy`) — was printed as a quiet
|
|
178
|
+
`SESSION-LOCAL OK` line, identical in weight and format to a genuinely CI-reconciled
|
|
179
|
+
`RECONCILED` line. `deriveClaimStatus` (finding 3, §"Iteration-2 hardening") re-derives status
|
|
180
|
+
from the bundle's OWN evidence/events/policies — it proves the bundle is **internally
|
|
181
|
+
self-consistent**, never that the underlying real-world attestation is **true**. A fully
|
|
182
|
+
hand-fabricated claim+evidence+event triple (a single `record-evidence --check-json
|
|
183
|
+
'{"kind":"security","status":"pass",...}'` call with no `command` field, run through the real
|
|
184
|
+
producer CLI) is, at this re-derivation layer, indistinguishable from a genuine one — and
|
|
185
|
+
because pre-WS8 every check was hardcoded `test_output` (§ Context, point 2), this specific
|
|
186
|
+
silent-pass path did not exist before this redesign; WS8 newly opened it for exactly the
|
|
187
|
+
high-impact, no-command kinds this ADR added first-class support for.
|
|
188
|
+
|
|
189
|
+
The fix is **disclosure, not a new block**: blocking every non-command-backed claim at this
|
|
190
|
+
layer would break every honest human-attestation/external-check use the manifest+
|
|
191
|
+
classification redesign exists to support (§1–§2's whole point). Instead, `trust-reconcile.js`
|
|
192
|
+
now prints every such claim on a loud, distinct, `grep`-stable line —
|
|
193
|
+
|
|
194
|
+
```
|
|
195
|
+
[trust-reconcile] ATTESTED (not independently verifiable at L0): '<claimId>' (<claimType>) evidenceType=<type> — accepted on bundle-internal consistency only; see ADR 0020 Residuals
|
|
196
|
+
```
|
|
197
|
+
|
|
198
|
+
— and a summary count after the reconcile step (`N attested claim(s) accepted without
|
|
199
|
+
independent verification`), always emitted (including `0`) so its absence is never mistaken
|
|
200
|
+
for "count line not implemented." Exit codes are unchanged — this is visibility, not a new
|
|
201
|
+
gate. `evals/integration/test_trust_reconcile_negatives.sh` freezes a fabricated-attestation
|
|
202
|
+
fixture asserting BOTH the pass (exit 0) AND the loud marker + summary line, and
|
|
203
|
+
`test_trust_reconcile_mixed_bundle.sh`'s shipped honest bundle asserts its own `ATTESTED` line
|
|
204
|
+
so the honest path is proven, not just the exploit.
|
|
205
|
+
|
|
206
|
+
## Residuals (honest — ADR 0017 §Residuals style; a bar-raiser, not a wall)
|
|
207
|
+
|
|
208
|
+
- **Fabricated self-consistent attestation bundles pass, loudly marked (inherent to L0).**
|
|
209
|
+
"L0" here means this reconciler's baseline reconcile/re-derivation layer — distinct from
|
|
210
|
+
ADR 0017's Layer 1–4 defense-in-depth naming. At L0, a non-command-backed claim
|
|
211
|
+
(`human_attestation`/`attestation`/`external`/etc.) can only be checked for *internal*
|
|
212
|
+
self-consistency (does the bundle's own evidence/event agree with the asserted status);
|
|
213
|
+
L0 has no independent oracle for whether the underlying real-world review/attestation
|
|
214
|
+
actually happened. A hand-fabricated bundle that is internally consistent therefore passes
|
|
215
|
+
— by design, since L0 cannot tell it apart from a genuine one. **The remedy is a higher
|
|
216
|
+
Assurance profile, not a stronger L0 heuristic:** an L1/L2 profile in which
|
|
217
|
+
non-command-backed evidence is cryptographically signed by an authenticated identity at
|
|
218
|
+
capture time (extending Layer 3's keyless-Sigstore minting, ADR 0017, from CI's own fresh
|
|
219
|
+
results to individual human-attestation events) would let the reconciler verify the
|
|
220
|
+
attestation's *provenance*, not merely the bundle's internal arithmetic. That profile does
|
|
221
|
+
not exist yet; until it does, every `ATTESTED` line is this ADR's explicit, permanent
|
|
222
|
+
disclosure that the claim rests on bundle-internal consistency only — a human reviewer,
|
|
223
|
+
not the reconciler, is the check.
|
|
224
|
+
- **Free-text `approved_by` on waivers is unauthenticated (existing disclosure, folded in
|
|
225
|
+
here).** As noted in §3 above: a waiver's `approved_by` is self-asserted free text, not
|
|
226
|
+
bound to an authenticated identity. *Mitigated by:* the loud, un-suppressible `WAIVED` line
|
|
227
|
+
in the required job's own log, plus CODEOWNERS review — not by identity attestation. Same
|
|
228
|
+
underlying gap and same Assurance-profile remedy as the attestation residual above.
|
|
229
|
+
- **The evidenceType-laundering route (explicitly named).** §2's classification table routes
|
|
230
|
+
`security`/`browser`/`runtime` checks to `test_output` (CI-reconcilable) only `hasCommand`;
|
|
231
|
+
otherwise they fall to `attestation`/`crawl_observation` (session-local). An agent that
|
|
232
|
+
wants a high-impact check to avoid CI re-execution can simply omit `command` from the
|
|
233
|
+
`record-evidence --check-json` call — the check is not "downgraded," it legitimately *is*
|
|
234
|
+
session-local (no command to reconcile), but the practical effect is the same evasion
|
|
235
|
+
surface as the fabricated-attestation residual above: a `kind:"security"`/`"runtime"`/
|
|
236
|
+
`"external"` claim with no command was, pre-iteration-4, indistinguishable from a
|
|
237
|
+
test_output-equivalent pass. *Mitigated by:* the iteration-4 `ATTESTED` marker makes every
|
|
238
|
+
claim that took this route visible and countable in the required job's own log — a reviewer
|
|
239
|
+
(or a future CI policy keyed on the summary count) can now see exactly how many high-impact
|
|
240
|
+
claims in a passing bundle went through the no-command/session-local path rather than a
|
|
241
|
+
fresh CI re-run. Not closed; disclosed. Closing it fully requires either (a) the
|
|
242
|
+
Assurance-profile signing remedy above, or (b) a policy decision to require `command` for
|
|
243
|
+
specific `impactLevel`/`kind` combinations (a future, separate ADR — not implemented here,
|
|
244
|
+
as it would need to weigh against the honest no-command use cases §1–§2 exist to support).
|
|
245
|
+
|
|
246
|
+
The consistent principle (same as ADR 0017): **the reconciler's L0 baseline cannot manufacture
|
|
247
|
+
independent truth for non-command-backed evidence; disclosure (the loud `ATTESTED` marker +
|
|
248
|
+
summary count) and human review are the closing mechanism until a signed-attestation
|
|
249
|
+
Assurance profile exists.**
|
|
250
|
+
|
|
251
|
+
## Consequences
|
|
252
|
+
|
|
253
|
+
- The exact capability PR #264 lacked now exists and is proven end-to-end:
|
|
254
|
+
`evals/integration/test_trust_reconcile_mixed_bundle.sh` builds a bundle with one
|
|
255
|
+
manifest-matched `test_output` claim, one honest `human_attestation` session-local claim,
|
|
256
|
+
and one waived `accepted_gap` claim, and passes the *real* `trust-reconcile.js`
|
|
257
|
+
entrypoint (exit 0) with distinct `RECONCILED` / `ATTESTED (not independently verifiable at
|
|
258
|
+
L0)` / `WAIVED` lines (the `SESSION-LOCAL OK` line from earlier iterations was replaced by
|
|
259
|
+
the loud `ATTESTED` marker in iteration-4 — see §4 above).
|
|
260
|
+
- The anti-gaming self-check `evals/integration/test_trust_reconcile_manifest.sh` asserts
|
|
261
|
+
the manifest id set and ci.yml's required `--check` set are identical, and runs in a
|
|
262
|
+
required lane (`antigaming-suite.sh`). Removing a check from every `LANE_*` array fails
|
|
263
|
+
it.
|
|
264
|
+
- Every live exploit reproduced in review is frozen as a permanent negative regression in
|
|
265
|
+
`evals/integration/test_trust_reconcile_negatives.sh` (wired into the required
|
|
266
|
+
`antigaming-suite.sh`): a no-label `test_output` bypass, a `skip → assumed` silent pass, a
|
|
267
|
+
`status-misassertion`, a waived command-backed check, the real ws3 old-style bundle
|
|
268
|
+
(AC6), and (iteration-4) a fabricated-attestation bundle asserting it PASSES (exit 0) while
|
|
269
|
+
emitting the loud `ATTESTED` marker and summary count — a visibility assertion, not a
|
|
270
|
+
divergence assertion — each of the first five asserting a non-zero exit and its specific
|
|
271
|
+
divergence string.
|
|
272
|
+
- Backward compat holds by construction: single-command adopters and old all-`test_output`
|
|
273
|
+
bundles reconcile identically (the legacy path is a manifest of size 1). The existing
|
|
274
|
+
laundering, checkpoint-bypass, and never-captured-command divergences are unchanged.
|
|
275
|
+
- CODEOWNERS protection extends to the new `trust-reconcile-manifest` declaration, matching
|
|
276
|
+
ADR 0017's owner-review posture for the verify config.
|
|
277
|
+
- **Trade-off (honest):** the reconciler re-runs matched manifest commands fresh on demand.
|
|
278
|
+
This is bounded by the registry and only the claimed commands are run. Reusing ci.yml's
|
|
279
|
+
already-produced `status.tsv` (rather than re-running) is a future optimization the
|
|
280
|
+
manifest design anticipates but this ADR does not implement.
|
|
281
|
+
|
|
282
|
+
## Alternatives Considered
|
|
283
|
+
|
|
284
|
+
- **A new manifest file / a new `claimType` taxonomy / a new waiver status value** — all
|
|
285
|
+
rejected as forks of existing canonical vocabulary (`run-baseline.sh`'s registry,
|
|
286
|
+
Surface's `evidenceType`, the `accepted_gap` status). Each would create a parallel
|
|
287
|
+
concept needing its own policing.
|
|
288
|
+
- **Making the reconciler re-run the full registry in `trust-reconcile.yml`** — rejected:
|
|
289
|
+
it would duplicate the entire ci.yml suite in a second required job. On-demand fresh runs
|
|
290
|
+
of only the claimed manifest commands give the same soundness at bounded cost.
|
|
@@ -0,0 +1,221 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: "ADR 0021: Assignment Leases and Stale-Claim Takeover"
|
|
3
|
+
---
|
|
4
|
+
|
|
5
|
+
# ADR 0021: Assignment Leases and Stale-Claim Takeover
|
|
6
|
+
|
|
7
|
+
**Date:** 2026-07-02
|
|
8
|
+
**Status:** Draft (shaped with Brian Anderson, 2026-07-02). Completes the coordination loop
|
|
9
|
+
designed in [ADR 0012](./0012-agent-coordination-as-liveness-claims.md); gated on the same
|
|
10
|
+
Surface ≥1.2.x bump.
|
|
11
|
+
|
|
12
|
+
---
|
|
13
|
+
|
|
14
|
+
## Context
|
|
15
|
+
|
|
16
|
+
ADR 0012 designed the primitive — an advisory liveness claim stream (`claim` / `heartbeat` /
|
|
17
|
+
`release`, TTL-reaped, recomputed via the Surface status function) — and the implementation
|
|
18
|
+
exists (`src/cli/workflow-sidecar.ts` `liveness` command, `scripts/hooks/lib/liveness-read.js`,
|
|
19
|
+
the `workflow-steering.js` SessionStart warning). But the loop is unwired in practice:
|
|
20
|
+
|
|
21
|
+
1. **Nothing emits.** Lifecycle auto-emit is gated behind `FLOW_AGENTS_LIVENESS` (default off);
|
|
22
|
+
no skill calls `liveness claim`; the live runtime has no `liveness/` stream at all.
|
|
23
|
+
2. **`pull-work` never reads.** Selection writes no claim, label, or assignee (provider
|
|
24
|
+
mutation is forbidden by the skill), so N concurrent sessions independently classify the
|
|
25
|
+
same issue "ready" and all take it (#166, under epic #151).
|
|
26
|
+
3. **Actor identity collapses.** `FLOW_AGENTS_ACTOR` defaults to the shared literal `"local"`,
|
|
27
|
+
so co-located sessions are indistinguishable — held/conflict detection is structurally
|
|
28
|
+
defeated on exactly the machine where it matters most.
|
|
29
|
+
4. **The sidecar directory is a collision point.** `workItemSlug()` is deterministic per work
|
|
30
|
+
item, so two sessions on one issue race last-writer-wins on the same `state.json` and the
|
|
31
|
+
single global `current.json` pointer.
|
|
32
|
+
5. **The merge race is unguarded.** No branch convention (templates default `branch: main`),
|
|
33
|
+
no hold check before push/PR/merge — a lapsed-and-superseded session can still publish over
|
|
34
|
+
its successor's work.
|
|
35
|
+
6. **Durable assignment is missing entirely.** The liveness stream is local and ephemeral by
|
|
36
|
+
design; there is no cross-machine, human-visible, crash-surviving record of *who owns what*.
|
|
37
|
+
|
|
38
|
+
The user-level requirement: run X concurrent sessions unattended, with no duplicate pickup, no
|
|
39
|
+
clobbered merges, visibility into who holds what — and **no lost locks**: a claim must never
|
|
40
|
+
outlive its holder's usefulness just because a terminal window was forgotten.
|
|
41
|
+
|
|
42
|
+
## Decision
|
|
43
|
+
|
|
44
|
+
### 1. Effective claim state is a join: `assignment ⋈ liveness`. Neither layer alone is truth.
|
|
45
|
+
|
|
46
|
+
Two layers with distinct jobs, never conflated:
|
|
47
|
+
|
|
48
|
+
| Layer | Records | Lifetime | Medium |
|
|
49
|
+
|---|---|---|---|
|
|
50
|
+
| **Assignment** | intent / ownership | durable, survives crashes | provider (issue assignee/label/comment) |
|
|
51
|
+
| **Liveness** | presence / freshness | ephemeral, TTL-reaped | ADR 0012 claim stream |
|
|
52
|
+
|
|
53
|
+
Readers always compute the join; provider state is **never trusted alone**:
|
|
54
|
+
|
|
55
|
+
| Assignment | Liveness | Effective state | `pull-work` treatment |
|
|
56
|
+
|---|---|---|---|
|
|
57
|
+
| assigned | fresh heartbeat | **held** | excluded |
|
|
58
|
+
| assigned | stale / absent | **reclaimable** | offered, via takeover protocol (§5) |
|
|
59
|
+
| assigned (human) | n/a (humans don't heartbeat) | **human-held** | surfaced, never auto-taken (§6) |
|
|
60
|
+
| unassigned | fresh (claim only) | **held** (assignment lagging) | excluded |
|
|
61
|
+
| unassigned | absent | **free** | offered |
|
|
62
|
+
|
|
63
|
+
This join rule is what makes lost locks structurally impossible: staleness — not assignment —
|
|
64
|
+
is what excludes, so an orphaned assignee/label from a dead session can never gate work.
|
|
65
|
+
|
|
66
|
+
### 2. Assignment is a provider abstraction — GitHub is an implementation, not the model.
|
|
67
|
+
|
|
68
|
+
A third provider leg beside the existing backlog and change providers
|
|
69
|
+
(`backlog-provider-settings.schema.json`, `ChangeProvider`):
|
|
70
|
+
|
|
71
|
+
```
|
|
72
|
+
AssignmentProvider:
|
|
73
|
+
claim(subjectId, actor, meta) # assign + attach machine-readable claim record
|
|
74
|
+
release(subjectId, actor, meta) # unassign + handoff note
|
|
75
|
+
supersede(subjectId, from, to, meta) # reassign with audit trail
|
|
76
|
+
status(subjectId) -> {assignee?, actor?, claimedAt?, meta?}
|
|
77
|
+
list(actor?) -> [subjectId]
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
The **GitHub implementation**: assignee (notification/board hook) + a single `agent:claimed`
|
|
81
|
+
label (board filter) + a **machine-readable claim comment** carrying the full actor struct,
|
|
82
|
+
`claimed_at`, TTL, branch, and artifact-dir pointer. The comment carries identity because the
|
|
83
|
+
assignee field cannot: N agent sessions typically share one GitHub account, so per-session
|
|
84
|
+
identity lives in the attached record, and each provider decides what it can natively
|
|
85
|
+
represent versus what goes in the record. Other implementations (Linear, Jira, GitLab,
|
|
86
|
+
local-file for tracker-less repos) map the same operations.
|
|
87
|
+
|
|
88
|
+
**Actor identity is runtime-agnostic**: `{runtime, session_id, host, human?}` — Claude Code,
|
|
89
|
+
codex, opencode, and pi sessions (all of which already have telemetry hook adapters) and
|
|
90
|
+
humans are all actors. The SessionStart hook derives and exports `FLOW_AGENTS_ACTOR` from the
|
|
91
|
+
runtime session id automatically; the `"local"` default is retired as an error, not a fallback.
|
|
92
|
+
|
|
93
|
+
### 3. Liveness is cross-cutting: ambient reads, lifecycle writes, one hard gate.
|
|
94
|
+
|
|
95
|
+
Five touchpoints, one policy:
|
|
96
|
+
|
|
97
|
+
| Touchpoint | Direction | Behavior |
|
|
98
|
+
|---|---|---|
|
|
99
|
+
| `workflow-steering.js`, every turn | read | one-line liveness digest; "superseded" interrupt for the held subject |
|
|
100
|
+
| `pull-work` | read + write | exclude held; claim (liveness + provider) on selection |
|
|
101
|
+
| `ensure-session` / pickup | read + write | refuse entry to a session dir under a fresh other-actor claim; supersede-then-enter if stale |
|
|
102
|
+
| `advance-state` + tool activity | write | heartbeat rides existing writes; no bespoke timer |
|
|
103
|
+
| **publish (push / PR / merge)** | **read, blocking** | **verify-hold**: hard-stop if not the fresh, non-superseded holder |
|
|
104
|
+
|
|
105
|
+
Once an actor holds a fresh claim it is "heads down": the ambient check narrows from "what is
|
|
106
|
+
everyone doing" to "have I been superseded". The verify-hold gate is the fencing token — the
|
|
107
|
+
advisory layer's single mutex moment, placed at the integration layer exactly where ADR 0012 §4
|
|
108
|
+
assigns real serialization. It is what defeats the zombie: a session that sleeps past TTL, gets
|
|
109
|
+
superseded, and wakes is caught deterministically before it can publish over its successor.
|
|
110
|
+
|
|
111
|
+
`FLOW_AGENTS_LIVENESS` flips to default-on. Branch becomes a first-class routing field in
|
|
112
|
+
`state.json` with the convention `agent/<actor>/<slug>`; `current.json` becomes per-actor
|
|
113
|
+
rather than one global last-writer-wins pointer.
|
|
114
|
+
|
|
115
|
+
### 4. Provider state is corrected lazily at claim transitions — plus a janitor for humans.
|
|
116
|
+
|
|
117
|
+
Nothing updates the provider on a timer; every mutation has a responsible actor:
|
|
118
|
+
|
|
119
|
+
| Transition | Mutator | When |
|
|
120
|
+
|---|---|---|
|
|
121
|
+
| claim | the claiming session | at selection |
|
|
122
|
+
| clean release | the incumbent (Stop hook / terminal `advance-state`) | session end — unassign + handoff comment |
|
|
123
|
+
| supersede | the **successor**, inside the takeover protocol | after the grace beat |
|
|
124
|
+
| crash, no successor | nobody, initially | corrected by the next actor to want the subject, or the janitor |
|
|
125
|
+
|
|
126
|
+
Lazy correction is *safe* by §1 (stale assignment excludes nothing) but leaves the human board
|
|
127
|
+
lying in the crash case. The supplement is a **janitor**: a sweep that joins `agent:claimed`
|
|
128
|
+
subjects against the liveness stream and relabels/unassigns past-TTL claims with an explanatory
|
|
129
|
+
comment. Locally this is a cron sweep; for fleets it is the natural first coordination duty of
|
|
130
|
+
the hosted Console relay (§7), which is the one vantage with global heartbeat visibility.
|
|
131
|
+
Heartbeats are **never** written to the provider (rate-limit abuse for no benefit); at most the
|
|
132
|
+
claim comment refreshes on phase transitions, doubling as board-level progress.
|
|
133
|
+
|
|
134
|
+
### 5. Takeover is resumption, not restart — `continue-work` triggered by staleness.
|
|
135
|
+
|
|
136
|
+
ADR 0012 §3 established that durable evidence outlives the ephemeral claim. This ADR makes the
|
|
137
|
+
resume path concrete. On seeing `assigned + stale`:
|
|
138
|
+
|
|
139
|
+
1. Emit a supersede claim on the subject.
|
|
140
|
+
2. **Grace beat**: wait one heartbeat interval; if the incumbent revives, back off (the
|
|
141
|
+
"laptop just woke" race resolves in the incumbent's favor).
|
|
142
|
+
3. `supersede()` via the provider — reassign with the audit-trail comment ("superseded actor X,
|
|
143
|
+
last seen T, resuming from trust bundle").
|
|
144
|
+
4. Resume through the existing handoff/trust-bundle machinery — the deterministic work-item
|
|
145
|
+
slug means the successor inherits the same artifact dir by construction (the prior
|
|
146
|
+
collision hazard becomes the resume feature once claims gate entry).
|
|
147
|
+
5. **Continue the incumbent's branch** (fetched from `state.json`'s branch field), never a
|
|
148
|
+
parallel one — otherwise takeover recreates the duplicate work it exists to prevent.
|
|
149
|
+
|
|
150
|
+
The woken zombie is then caught twice: its next turn's steering digest says "superseded", and
|
|
151
|
+
the verify-hold gate blocks it at publish regardless.
|
|
152
|
+
|
|
153
|
+
### 6. Human assignees are never auto-superseded.
|
|
154
|
+
|
|
155
|
+
Humans do not heartbeat, so `assigned-to-human + no-liveness` is normal, not stale. Agents
|
|
156
|
+
**surface** idle human assignments ("assigned to brian 3 days ago, no linked activity —
|
|
157
|
+
reclaim?") and act only on explicit confirmation. The threshold and behavior are a policy knob
|
|
158
|
+
in the assignment-provider settings; the default is ask-first.
|
|
159
|
+
|
|
160
|
+
### 7. Local-first stands; the Console is the fleet tier, never required.
|
|
161
|
+
|
|
162
|
+
Per ADR 0012 §5 the sink ladder is local file → git ref → optional hosted relay. Everything in
|
|
163
|
+
§§1–6 works on a single machine with the local JSONL stream and the GitHub provider. The hosted
|
|
164
|
+
Console (console.kontourai.io) adds, strictly as a relay/projection of the advisory stream —
|
|
165
|
+
consistent with its "never becomes the authority" stance:
|
|
166
|
+
|
|
167
|
+
- **liveness relay**: sessions mirror claim/heartbeat/release events (its existing idempotent
|
|
168
|
+
ingest + tenant auth + SSE substrate fits without new authority semantics);
|
|
169
|
+
- **fleet view**: active actors, held/reclaimable subjects, per-session cost it already has;
|
|
170
|
+
- **the janitor** (§4), the first genuinely cross-machine duty.
|
|
171
|
+
|
|
172
|
+
This is the deliberate product boundary: single-machine parallelism is fully unlocked locally;
|
|
173
|
+
multi-machine fleets, human oversight, and history are what the hosted tier sells.
|
|
174
|
+
|
|
175
|
+
## Consequences
|
|
176
|
+
|
|
177
|
+
- Running X unattended concurrent sessions becomes safe by construction: duplicate pickup is
|
|
178
|
+
excluded by the join rule, clobbered merges by verify-hold, lost locks by TTL + Stop-hook
|
|
179
|
+
release, and abandoned work is resumed — not restarted — via takeover.
|
|
180
|
+
- The Stop hook gains a duty: emit `release`-with-handoff on every non-terminal session end, so
|
|
181
|
+
the common case (closed terminal) frees work instantly and TTL only covers real crashes. This
|
|
182
|
+
keeps TTLs generous, which ADR 0012 §4 identifies as the operational risk.
|
|
183
|
+
- `pull-work` loses its blanket no-provider-mutation rule in favor of one narrow, audited
|
|
184
|
+
mutation: the claim at selection.
|
|
185
|
+
- New surface to maintain: the `AssignmentProvider` contract + settings schema, the actor
|
|
186
|
+
struct, a branch-naming convention, per-actor `current.json`, and the janitor.
|
|
187
|
+
- Cross-repo dependency: the Console relay/fleet-view/janitor land in the console repo; this
|
|
188
|
+
repo only gains the optional relay sink.
|
|
189
|
+
|
|
190
|
+
## Alternatives Considered
|
|
191
|
+
|
|
192
|
+
- **Liveness-only (no durable assignment).** Fails cross-machine and human visibility; a crash
|
|
193
|
+
leaves no record of ownership anywhere a human looks. Rejected — it is the presence layer,
|
|
194
|
+
not the ownership layer.
|
|
195
|
+
- **Provider-only (assignee/label, no liveness).** ADR 0012 already rejected as insufficient;
|
|
196
|
+
additionally it has no freshness, so it *creates* the lost-lock problem this ADR exists to
|
|
197
|
+
prevent. TTL logic bolted onto provider comments is the same design with worse atomicity.
|
|
198
|
+
- **Hard leases (provider-side CAS, lock server).** Re-rejected per ADR 0012 — stale leases
|
|
199
|
+
orphan work; the verify-hold gate gets mutex safety at the single point that needs it.
|
|
200
|
+
- **Heartbeating to the provider.** Rejected: rate limits, comment spam, and it duplicates the
|
|
201
|
+
stream that already exists.
|
|
202
|
+
- **Auto-superseding humans symmetrically with agents.** Rejected: humans lack heartbeats, so
|
|
203
|
+
symmetry manufactures false reclaims against exactly the actors least able to defend a claim.
|
|
204
|
+
|
|
205
|
+
## References
|
|
206
|
+
|
|
207
|
+
- [ADR 0012: Agent Coordination as Hachure Liveness Claims](./0012-agent-coordination-as-liveness-claims.md) — the primitive this ADR wires and completes. (Its reference to
|
|
208
|
+
#137 for pull-work claim wiring is stale; the live tracking is #166 under epic #151.)
|
|
209
|
+
- #151 — the ADR 0012 coordination epic; this ADR shapes its remaining slices. #166
|
|
210
|
+
(pull-work claim emit/exclude) is extended by §1's join rule; #167 (advisory surface,
|
|
211
|
+
warn-never-block) is deliberately upgraded by §3 at exactly one point — the verify-hold
|
|
212
|
+
publish gate; #153 (resume from the durable record) is the substrate §5's takeover
|
|
213
|
+
builds on; #161 (deterministic work-item slug) is the landed foundation.
|
|
214
|
+
- `src/cli/workflow-sidecar.ts` (`liveness`, `withLock`, `writeCurrent`, `workItemSlug`);
|
|
215
|
+
`scripts/hooks/workflow-steering.js` (ambient read side, already live);
|
|
216
|
+
`scripts/hooks/stop-goal-fit.js` (release-with-handoff injection point).
|
|
217
|
+
- `docs/kontour-resource-contract.md` `ScopeOverlap` — the declared-file-scope seam for
|
|
218
|
+
work-*area* (vs work-item) conflict detection; claims should carry declared scope so
|
|
219
|
+
`pull-work`'s `global_conflicts` can consult live claims, closing the gap ADR 0012 names.
|
|
220
|
+
- Console: `docs/integrations/flow-agents-console.md`, console repo `POST /records` +
|
|
221
|
+
`/ingest/flow` + SSE (the relay substrate).
|