@kontourai/flow-agents 2.4.0 → 3.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (195) hide show
  1. package/.github/CODEOWNERS +8 -0
  2. package/.github/workflows/ci.yml +20 -0
  3. package/.github/workflows/trust-reconcile.yml +62 -4
  4. package/CHANGELOG.md +38 -0
  5. package/CONTEXT.md +88 -1
  6. package/README.md +4 -0
  7. package/build/src/cli/assignment-provider.d.ts +144 -0
  8. package/build/src/cli/assignment-provider.js +805 -0
  9. package/build/src/cli/effective-assignment-provider-settings.d.ts +1 -0
  10. package/build/src/cli/effective-assignment-provider-settings.js +125 -0
  11. package/build/src/cli/validate-workflow-artifacts.js +5 -1
  12. package/build/src/cli/workflow-sidecar.d.ts +2 -2
  13. package/build/src/cli/workflow-sidecar.js +549 -145
  14. package/build/src/cli.js +6 -0
  15. package/build/src/lib/flow-resolver.d.ts +12 -6
  16. package/build/src/lib/flow-resolver.js +30 -14
  17. package/build/src/tools/validate-source-tree.js +2 -0
  18. package/context/contracts/artifact-contract.md +2 -0
  19. package/context/contracts/assignment-provider-contract.md +248 -0
  20. package/context/contracts/builder-kit-workflow-state-contract.md +2 -0
  21. package/context/contracts/decision-registry-contract.md +2 -0
  22. package/context/contracts/delivery-contract.md +2 -0
  23. package/context/contracts/execution-contract.md +25 -0
  24. package/context/contracts/governance-adapter-contract.md +2 -0
  25. package/context/contracts/knowledge-store-contract.md +197 -0
  26. package/context/contracts/planning-contract.md +2 -0
  27. package/context/contracts/probe-docs-write-contract.md +187 -0
  28. package/context/contracts/review-contract.md +2 -0
  29. package/context/contracts/sandbox-policy.md +2 -0
  30. package/context/contracts/standing-directives.md +13 -0
  31. package/context/contracts/verification-contract.md +2 -0
  32. package/context/contracts/work-item-contract.md +2 -0
  33. package/context/scripts/hooks/config-protection.js +14 -1
  34. package/context/scripts/hooks/lib/config-protection-remedies.js +9 -0
  35. package/context/scripts/hooks/stop-goal-fit.js +4 -2
  36. package/context/scripts/hooks/workflow-steering.js +42 -0
  37. package/context/settings/assignment-provider-settings.json +33 -0
  38. package/docs/adr/0001-flow-agents-consumes-flow.md +2 -0
  39. package/docs/adr/0002-flow-kits-as-extension-unit.md +2 -0
  40. package/docs/adr/0003-flow-agents-coordinates-kits-and-adapters.md +2 -0
  41. package/docs/adr/0004-gates-expect-surface-claims.md +2 -0
  42. package/docs/adr/0005-kubernetes-inspired-resource-contracts.md +2 -0
  43. package/docs/adr/0006-typescript-first-source-policy.md +2 -0
  44. package/docs/adr/0007-flow-skill-kit-tool-boundary.md +2 -0
  45. package/docs/adr/0007-skill-audit.md +2 -0
  46. package/docs/adr/0008-kit-operation-boundary.md +2 -0
  47. package/docs/adr/0009-canonical-hook-core-kit-boundary.md +2 -0
  48. package/docs/adr/0010-workflow-trust-state-as-hachure-bundle.md +2 -0
  49. package/docs/adr/0011-mcp-posture.md +2 -0
  50. package/docs/adr/0012-agent-coordination-as-liveness-claims.md +2 -0
  51. package/docs/adr/0013-context-lifecycle.md +2 -0
  52. package/docs/adr/0014-core-vs-domain-kit-boundary.md +2 -0
  53. package/docs/adr/0015-flow-flow-agents-boundary-reconciliation.md +2 -0
  54. package/docs/adr/0016-three-hard-boundary-model.md +2 -0
  55. package/docs/adr/0017-anti-gaming-trust-security-model.md +2 -0
  56. package/docs/adr/0018-freeze-local-shell-heuristics.md +2 -0
  57. package/docs/adr/0019-kit-dependency-ownership.md +2 -0
  58. package/docs/adr/0020-trust-reconcile-manifest-and-claim-classification.md +2 -0
  59. package/docs/adr/0021-assignment-leases-and-stale-claim-takeover.md +3 -1
  60. package/docs/adr/0022-fail-closed-delivery-reconciliation-with-governed-exemptions.md +182 -0
  61. package/docs/adr/README.md +49 -0
  62. package/docs/adr/index.md +34 -0
  63. package/docs/context-map.md +1 -0
  64. package/docs/decisions/agent-coordination.md +20 -0
  65. package/docs/decisions/anti-gaming-trust-security.md +20 -0
  66. package/docs/decisions/context-lifecycle.md +18 -0
  67. package/docs/decisions/core-domain-kit-boundary.md +18 -0
  68. package/docs/decisions/flow-flow-agents-boundary.md +18 -0
  69. package/docs/decisions/flow-kit.md +20 -0
  70. package/docs/decisions/flow-skill-kit-tool-boundary.md +20 -0
  71. package/docs/decisions/graph-knowledge-provider.md +63 -0
  72. package/docs/decisions/hook-core-kit-boundary.md +18 -0
  73. package/docs/decisions/index.md +22 -0
  74. package/docs/decisions/kit-dependency-ownership.md +18 -0
  75. package/docs/decisions/kit-operation-boundary.md +18 -0
  76. package/docs/decisions/knowledge-promote-sub-flow.md +59 -0
  77. package/docs/decisions/knowledge-store-provider.md +51 -0
  78. package/docs/decisions/kontour-resource-contract.md +18 -0
  79. package/docs/decisions/mcp-posture.md +18 -0
  80. package/docs/decisions/model-routing.md +63 -0
  81. package/docs/decisions/standing-directives.md +66 -0
  82. package/docs/decisions/three-hard-boundary-model.md +18 -0
  83. package/docs/decisions/trust-reconcile.md +20 -0
  84. package/docs/decisions/typescript-source-policy.md +48 -0
  85. package/docs/decisions/workflow-enforcement.md +18 -0
  86. package/docs/decisions/workflow-trust-state.md +20 -0
  87. package/docs/fixture-ownership.md +1 -0
  88. package/docs/workflow-shared-contracts.md +2 -1
  89. package/docs/workflow-usage-guide.md +7 -1
  90. package/evals/ci/run-baseline.sh +10 -0
  91. package/evals/fixtures/assignment-provider/actor-a.json +6 -0
  92. package/evals/fixtures/assignment-provider/actor-b.json +6 -0
  93. package/evals/fixtures/assignment-provider/github-issue-claimed.json +27 -0
  94. package/evals/fixtures/assignment-provider/github-issue-unassigned.json +7 -0
  95. package/evals/fixtures/assignment-provider/hostile-effective-state.json +11 -0
  96. package/evals/fixtures/assignment-provider/liveness-fresh.json +9 -0
  97. package/evals/fixtures/assignment-provider/liveness-stale.json +9 -0
  98. package/evals/integration/test_assignment_provider_github.sh +318 -0
  99. package/evals/integration/test_assignment_provider_local_file.sh +222 -0
  100. package/evals/integration/test_critique_supersession_roundtrip.sh +182 -0
  101. package/evals/integration/test_current_json_per_actor.sh +516 -0
  102. package/evals/integration/test_ensure_session_ownership_guard.sh +435 -0
  103. package/evals/integration/test_fixture_retirement_audit.sh +2 -2
  104. package/evals/integration/test_gate_lockdown.sh +10 -0
  105. package/evals/integration/test_publish_delivery.sh +21 -4
  106. package/evals/integration/test_pull_work_assignment_join.sh +132 -0
  107. package/evals/integration/test_pull_work_liveness_preflight.sh +20 -7
  108. package/evals/integration/test_reconcile_soundness.sh +33 -9
  109. package/evals/integration/test_trust_reconcile.sh +9 -8
  110. package/evals/integration/test_trust_reconcile_negatives.sh +608 -0
  111. package/evals/integration/test_workflow_sidecar_writer.sh +91 -3
  112. package/evals/run.sh +14 -0
  113. package/evals/static/test_flowdef_codeowners_coverage.sh +6 -0
  114. package/evals/static/test_knowledge_providers.sh +32 -0
  115. package/evals/static/test_workflow_skills.sh +15 -2
  116. package/kits/builder/skills/deliver/SKILL.md +36 -0
  117. package/kits/builder/skills/design-probe/SKILL.md +37 -0
  118. package/kits/builder/skills/pickup-probe/SKILL.md +4 -4
  119. package/kits/builder/skills/pull-work/SKILL.md +78 -10
  120. package/kits/knowledge/adapters/default-store/index.js +92 -4
  121. package/kits/knowledge/adapters/obsidian-store/index.js +69 -4
  122. package/kits/knowledge/adapters/shared/codec.js +141 -0
  123. package/kits/knowledge/docs/README.md +121 -2
  124. package/kits/knowledge/docs/store-contract.md +112 -4
  125. package/kits/knowledge/evals/contract-suite/suite.test.js +176 -0
  126. package/kits/knowledge/flows/promote.flow.json +84 -0
  127. package/kits/knowledge/kit.json +50 -0
  128. package/kits/knowledge/promote/distill.js +96 -0
  129. package/kits/knowledge/promote/fixtures/contradiction-registry/CONTEXT.md +7 -0
  130. package/kits/knowledge/promote/fixtures/contradiction-registry/docs/decisions/cache-eviction-policy.md +12 -0
  131. package/kits/knowledge/promote/fixtures/contradiction-registry/docs/decisions/cache-eviction.md +12 -0
  132. package/kits/knowledge/promote/fixtures/repo/CONTEXT.md +11 -0
  133. package/kits/knowledge/promote/fixtures/repo/docs/decisions/index.md +9 -0
  134. package/kits/knowledge/promote/fixtures/repo/docs/decisions/liveness-claim.md +12 -0
  135. package/kits/knowledge/promote/fixtures/session/acceptance.json +9 -0
  136. package/kits/knowledge/promote/fixtures/session/agents/tool-planner/events.jsonl +1 -0
  137. package/kits/knowledge/promote/fixtures/session/agents/tool-verifier/events.jsonl +1 -0
  138. package/kits/knowledge/promote/fixtures/session/agents/tool-worker-1-1/events.jsonl +1 -0
  139. package/kits/knowledge/promote/fixtures/session/kontourai-flow-agents-287--deliver.md +47 -0
  140. package/kits/knowledge/promote/fixtures/session/learning.json +35 -0
  141. package/kits/knowledge/promote/fixtures/session/state.json +19 -0
  142. package/kits/knowledge/promote/health.js +137 -0
  143. package/kits/knowledge/promote/index.js +176 -0
  144. package/kits/knowledge/promote/ingest.js +103 -0
  145. package/kits/knowledge/promote/lib.js +132 -0
  146. package/kits/knowledge/promote/link.js +84 -0
  147. package/kits/knowledge/promote/promote.test.js +174 -0
  148. package/kits/knowledge/providers/conformance/fixtures/git-repo/CONTEXT.md +12 -0
  149. package/kits/knowledge/providers/conformance/fixtures/git-repo/docs/decisions/old-sprocket-shape.md +13 -0
  150. package/kits/knowledge/providers/conformance/fixtures/git-repo/docs/decisions/sprocket-shape.md +14 -0
  151. package/kits/knowledge/providers/conformance/fixtures/git-repo/docs/decisions/widget-format.md +14 -0
  152. package/kits/knowledge/providers/conformance/fixtures/git-repo/docs/learnings/fixture-learning.md +7 -0
  153. package/kits/knowledge/providers/conformance/fixtures/work-item/issues.json +30 -0
  154. package/kits/knowledge/providers/conformance/suite.test.js +143 -0
  155. package/kits/knowledge/providers/git-repo/index.js +236 -0
  156. package/kits/knowledge/providers/health/health-pass.test.js +99 -0
  157. package/kits/knowledge/providers/health/index.js +153 -0
  158. package/kits/knowledge/providers/index.js +25 -0
  159. package/kits/knowledge/providers/lib/model.js +91 -0
  160. package/kits/knowledge/providers/lib/schema-validate.js +119 -0
  161. package/kits/knowledge/providers/markdown-vault/index.js +169 -0
  162. package/kits/knowledge/providers/neo4j/connection.js +121 -0
  163. package/kits/knowledge/providers/neo4j/cypher.js +190 -0
  164. package/kits/knowledge/providers/neo4j/fake-driver.js +123 -0
  165. package/kits/knowledge/providers/neo4j/fixtures/spike-ground-truth.json +29 -0
  166. package/kits/knowledge/providers/neo4j/graph-queries.js +210 -0
  167. package/kits/knowledge/providers/neo4j/index.js +280 -0
  168. package/kits/knowledge/providers/neo4j/integration.test.js +122 -0
  169. package/kits/knowledge/providers/neo4j/neo4j.test.js +219 -0
  170. package/kits/knowledge/providers/neo4j/sync.js +235 -0
  171. package/kits/knowledge/providers/work-item/index.js +204 -0
  172. package/package.json +7 -3
  173. package/schemas/assignment-provider-settings.schema.json +125 -0
  174. package/schemas/knowledge/edge.schema.json +54 -0
  175. package/schemas/knowledge/health-report.schema.json +45 -0
  176. package/schemas/knowledge/node.schema.json +49 -0
  177. package/schemas/knowledge/proposal.schema.json +53 -0
  178. package/scripts/README.md +1 -0
  179. package/scripts/ci/trust-reconcile.js +521 -24
  180. package/scripts/freeze-adrs.mjs +364 -0
  181. package/scripts/hooks/config-protection.js +14 -1
  182. package/scripts/hooks/evidence-capture.js +4 -1
  183. package/scripts/hooks/lib/config-protection-remedies.js +9 -0
  184. package/scripts/hooks/lib/current-pointer.js +123 -0
  185. package/scripts/hooks/lib/liveness-heartbeat.js +75 -35
  186. package/scripts/hooks/stop-goal-fit.js +4 -2
  187. package/scripts/hooks/workflow-steering.js +42 -0
  188. package/scripts/statusline/flow-agents-statusline.js +3 -1
  189. package/src/cli/assignment-provider.ts +927 -0
  190. package/src/cli/effective-assignment-provider-settings.ts +112 -0
  191. package/src/cli/validate-workflow-artifacts.ts +5 -1
  192. package/src/cli/workflow-sidecar.ts +577 -139
  193. package/src/cli.ts +6 -0
  194. package/src/lib/flow-resolver.ts +35 -14
  195. package/src/tools/validate-source-tree.ts +2 -0
package/build/src/cli.js CHANGED
@@ -1,6 +1,8 @@
1
1
  #!/usr/bin/env node
2
2
  import { basename } from "node:path";
3
3
  import { main as effectiveBacklogSettings } from "./cli/effective-backlog-settings.js";
4
+ import { main as effectiveAssignmentProviderSettings } from "./cli/effective-assignment-provider-settings.js";
5
+ import { main as assignmentProvider } from "./cli/assignment-provider.js";
4
6
  import { main as consoleLearningProjection } from "./cli/console-learning-projection.js";
5
7
  import { main as kit } from "./cli/kit.js";
6
8
  import { main as fixtureRetirementAudit } from "./cli/fixture-retirement-audit.js";
@@ -24,6 +26,8 @@ const availableCommands = new Map([
24
26
  ["build-bundles", () => buildBundles()],
25
27
  ["console-learning-projection", consoleLearningProjection],
26
28
  ["context-map", contextMap],
29
+ ["assignment-provider", assignmentProvider],
30
+ ["effective-assignment-provider-settings", effectiveAssignmentProviderSettings],
27
31
  ["effective-backlog-settings", effectiveBacklogSettings],
28
32
  ["fixture-retirement-audit", fixtureRetirementAudit],
29
33
  ["kit", kit],
@@ -46,6 +50,8 @@ const aliases = new Map([
46
50
  ["flow-agents-build-bundles", "build-bundles"],
47
51
  ["flow-agents-console-learning-projection", "console-learning-projection"],
48
52
  ["flow-agents-context-map", "context-map"],
53
+ ["flow-agents-assignment-provider", "assignment-provider"],
54
+ ["flow-agents-effective-assignment-provider-settings", "effective-assignment-provider-settings"],
49
55
  ["flow-agents-effective-backlog-settings", "effective-backlog-settings"],
50
56
  ["flow-agents-fixture-retirement-audit", "fixture-retirement-audit"],
51
57
  ["flow-agents-kit", "kit"],
@@ -72,16 +72,22 @@ export declare function resolvePhaseMap(flowId: string, repoRoot: string): Recor
72
72
  /**
73
73
  * Resolve the active flow step from current.json.
74
74
  *
75
- * Reads active_flow_id and active_step_id from <flowAgentsDir>/current.json.
76
- * If both are present, delegates to resolveFlowStep. The repoRoot is derived by
77
- * walking upward from flowAgentsDir to find the nearest ancestor containing kits/,
78
- * with a fallback to process.cwd(). This handles temp dirs, CI workspaces, and
79
- * subproject layouts without hardcoding the repo structure.
75
+ * Reads active_flow_id and active_step_id via the shared per-actor current-pointer helper
76
+ * (#291 Wave 2 Task 2.2): when `actorKey` is a resolved actor with its own
77
+ * `current/<actor>.json` projection, that file is preferred; otherwise (no actorKey, an
78
+ * unresolved actor, or no per-actor file yet) this falls straight back to the legacy global
79
+ * `<flowAgentsDir>/current.json` IDENTICAL to this function's pre-#291 behavior for every
80
+ * caller that does not pass `actorKey`. The repoRoot is derived by walking upward from
81
+ * flowAgentsDir to find the nearest ancestor containing kits/, with a fallback to
82
+ * process.cwd(). This handles temp dirs, CI workspaces, and subproject layouts without
83
+ * hardcoding the repo structure.
80
84
  *
81
85
  * @param flowAgentsDir Path to the runtime artifact root directory (contains current.json).
86
+ * @param actorKey Optional resolved actor identity (Wave 2 Task 2.1's `writeCurrent()` dual-write
87
+ * key) — when omitted, behavior is unchanged (legacy-file-only, exactly as before #291).
82
88
  * @returns ActiveFlowStep or null when fields are absent or resolution fails.
83
89
  */
84
- export declare function resolveActiveFlowStep(flowAgentsDir: string): ActiveFlowStep | null;
90
+ export declare function resolveActiveFlowStep(flowAgentsDir: string, actorKey?: string): ActiveFlowStep | null;
85
91
  /** The resolved route-back policy for a phase transition. */
86
92
  export type RouteBackPolicy = {
87
93
  /** Maximum allowed route-back attempts for this transition key. */
@@ -14,6 +14,8 @@
14
14
  */
15
15
  import * as fs from "node:fs";
16
16
  import * as path from "node:path";
17
+ import { createRequire } from "node:module";
18
+ import { fileURLToPath } from "node:url";
17
19
  // ─── Security: Layer 1 traversal defense ─────────────────────────────────────
18
20
  //
19
21
  // Both kitId and flowName originate from agent-writable sources (active_flow_id
@@ -270,30 +272,44 @@ function findRepoRoot(startDir) {
270
272
  // Fallback: process.cwd() covers the common "run from repo root" case
271
273
  return process.cwd();
272
274
  }
275
+ /**
276
+ * Delegate to the shared pure-CJS per-actor current-pointer reader
277
+ * (scripts/hooks/lib/current-pointer.js), mirroring the exact createRequire idiom
278
+ * workflow-sidecar.ts already uses for its own cross-boundary CJS helper reuse (Wave 2 Task 2.2,
279
+ * #291). Deliberately NO inline duplicate fallback — this is the single choke point for the
280
+ * per-actor-first/legacy-fallback compat-shim rule; a second hand-rolled reader here would let it
281
+ * drift from every other consumer.
282
+ */
283
+ function loadCurrentPointerHelper() {
284
+ const _req = createRequire(import.meta.url);
285
+ const helperPath = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "../../../scripts/hooks/lib/current-pointer.js");
286
+ return _req(helperPath);
287
+ }
273
288
  /**
274
289
  * Resolve the active flow step from current.json.
275
290
  *
276
- * Reads active_flow_id and active_step_id from <flowAgentsDir>/current.json.
277
- * If both are present, delegates to resolveFlowStep. The repoRoot is derived by
278
- * walking upward from flowAgentsDir to find the nearest ancestor containing kits/,
279
- * with a fallback to process.cwd(). This handles temp dirs, CI workspaces, and
280
- * subproject layouts without hardcoding the repo structure.
291
+ * Reads active_flow_id and active_step_id via the shared per-actor current-pointer helper
292
+ * (#291 Wave 2 Task 2.2): when `actorKey` is a resolved actor with its own
293
+ * `current/<actor>.json` projection, that file is preferred; otherwise (no actorKey, an
294
+ * unresolved actor, or no per-actor file yet) this falls straight back to the legacy global
295
+ * `<flowAgentsDir>/current.json` IDENTICAL to this function's pre-#291 behavior for every
296
+ * caller that does not pass `actorKey`. The repoRoot is derived by walking upward from
297
+ * flowAgentsDir to find the nearest ancestor containing kits/, with a fallback to
298
+ * process.cwd(). This handles temp dirs, CI workspaces, and subproject layouts without
299
+ * hardcoding the repo structure.
281
300
  *
282
301
  * @param flowAgentsDir Path to the runtime artifact root directory (contains current.json).
302
+ * @param actorKey Optional resolved actor identity (Wave 2 Task 2.1's `writeCurrent()` dual-write
303
+ * key) — when omitted, behavior is unchanged (legacy-file-only, exactly as before #291).
283
304
  * @returns ActiveFlowStep or null when fields are absent or resolution fails.
284
305
  */
285
- export function resolveActiveFlowStep(flowAgentsDir) {
306
+ export function resolveActiveFlowStep(flowAgentsDir, actorKey) {
286
307
  if (!flowAgentsDir)
287
308
  return null;
288
- const currentFile = path.join(flowAgentsDir, "current.json");
289
- let current;
290
- try {
291
- const raw = fs.readFileSync(currentFile, "utf8");
292
- current = JSON.parse(raw);
293
- }
294
- catch {
309
+ const pointer = loadCurrentPointerHelper().readCurrentPointer(flowAgentsDir, actorKey);
310
+ if (pointer.source === "none" || !pointer.payload)
295
311
  return null;
296
- }
312
+ const current = pointer.payload;
297
313
  const flowId = typeof current["active_flow_id"] === "string" ? current["active_flow_id"] : null;
298
314
  const stepId = typeof current["active_step_id"] === "string" ? current["active_step_id"] : null;
299
315
  if (!flowId || !stepId)
@@ -83,6 +83,7 @@ const hookFilePolicies = new Map([
83
83
  ["scripts/hooks/lib/actor-identity.js", { category: "shared hook library", requiredNeedles: ["resolveActor", "FLOW_AGENTS_ACTOR"] }],
84
84
  ["scripts/hooks/lib/audit-transport.sh", { category: "shared hook library", requiredNeedles: ["audit_emit"] }],
85
85
  ["scripts/hooks/lib/config-protection-remedies.js", { category: "shared hook library", requiredNeedles: ["SANCTIONED_REMEDIES", "REMEDY_COMMAND_CANDIDATES"] }],
86
+ ["scripts/hooks/lib/current-pointer.js", { category: "shared hook library", requiredNeedles: ["readCurrentPointer", "perActorCurrentFile"] }],
86
87
  ["scripts/hooks/lib/hook-flags.js", { category: "shared hook library", requiredNeedles: ["isHookEnabled"] }],
87
88
  ["scripts/hooks/lib/liveness-heartbeat.js", { category: "shared hook library", requiredNeedles: ["maybeEmitHeartbeat", "resolveHeartbeatThrottleSeconds"] }],
88
89
  ["scripts/hooks/lib/liveness-policy.js", { category: "shared hook library", requiredNeedles: ["isLivenessEnabled", "resolveTtlSeconds"] }],
@@ -93,6 +94,7 @@ const hookFilePolicies = new Map([
93
94
  ["scripts/hooks/lib/resolve-formatter.js", { category: "shared hook library", requiredNeedles: ["resolveFormatter"] }],
94
95
  ]);
95
96
  const fixtureOwnerPolicies = new Map([
97
+ ["evals/fixtures/assignment-provider", { owners: ["evals/integration/test_assignment_provider_local_file.sh", "evals/integration/test_assignment_provider_github.sh", "evals/integration/test_pull_work_assignment_join.sh", "evals/integration/test_ensure_session_ownership_guard.sh"], classification: "AssignmentProvider local-file and GitHub render/status fixtures (#290); hostile-effective-state.json is the #291 ensure-session ownership guard's AC9 sanitization fixture" }],
96
98
  ["evals/fixtures/backlog-provider-settings", { owners: ["evals/integration/test_effective_backlog_settings.sh"], classification: "settings precedence fixtures" }],
97
99
  ["evals/fixtures/builder-kit-workflow-state", { owners: ["evals/static/test_workflow_skills.sh"], classification: "Builder Kit workflow-state fixtures" }],
98
100
  ["evals/fixtures/console-learning-projection", { owners: ["evals/integration/test_console_learning_projection.sh"], classification: "console learning projection fixtures" }],
@@ -1,5 +1,7 @@
1
1
  # Workflow Artifact Contract
2
2
 
3
+ > Read [`context/contracts/standing-directives.md`](standing-directives.md) — ratified owner directives that override default engineering conservatism.
4
+
3
5
  Workflow artifacts are the resumable local handoff surface between orchestrators, workers, verifiers, hooks, evals, and future sessions.
4
6
 
5
7
  ## Artifact Root
@@ -0,0 +1,248 @@
1
+ # Assignment Provider Contract
2
+
3
+ This contract defines the provider-neutral vocabulary for durable work-item ownership: who has
4
+ claimed a subject, how that claim is represented, and how it is read back safely. It formalizes
5
+ [ADR 0021: Assignment Leases and Stale-Claim Takeover](../../docs/adr/0021-assignment-leases-and-stale-claim-takeover.md)
6
+ as the third provider leg beside `WorkItemProvider` and `BoardProvider`
7
+ (`context/contracts/work-item-contract.md`) and `ChangeProvider`. GitHub is the first concrete
8
+ mapping for this contract, not the generic vocabulary — Linear, Jira, GitLab, and a local-file
9
+ provider for tracker-less repos and evals all map the same operations.
10
+
11
+ ## Terminology: three distinct "claim" concepts
12
+
13
+ This repository uses the word "claim" for three unrelated things. Always qualify in prose; never
14
+ write bare "claim" where the meaning is not obvious from surrounding context:
15
+
16
+ | Term | Layer | Meaning | Where it lives |
17
+ | --- | --- | --- | --- |
18
+ | **assignment claim** | this contract | Durable, provider-recorded ownership of a work item | `AssignmentProvider.claim()`, this doc |
19
+ | **liveness claim** | ADR 0012 | Ephemeral, TTL-reaped presence/heartbeat event | `liveness claim` (`workflow-sidecar.ts`), `freshHolders` (`liveness-read.js`) |
20
+ | **trust claim** | ADR 0010 / ADR 0017 | A Hachure `trust.bundle` evidence claim (kit-typed, verification status) | `workflow-sidecar.ts claim <id> <dir>` (`claimLookup`) |
21
+
22
+ An assignment claim's *effective state* is never computed from the assignment layer alone — it is
23
+ always joined against a liveness claim's freshness (see the join table below).
24
+
25
+ ## AssignmentProvider Operations
26
+
27
+ Provider-neutral interface, verbatim from ADR 0021 §2:
28
+
29
+ ```
30
+ AssignmentProvider:
31
+ claim(subjectId, actor, meta) # assign + attach machine-readable claim record
32
+ release(subjectId, actor, meta) # unassign + handoff note
33
+ supersede(subjectId, from, to, meta) # reassign with audit trail
34
+ status(subjectId) -> {assignee?, actor?, claimedAt?, meta?}
35
+ list(actor?) -> [subjectId]
36
+ ```
37
+
38
+ | Operation | Arguments | Effect | Returns |
39
+ | --- | --- | --- | --- |
40
+ | `claim` | `subjectId`, `actor`, `meta` | Records durable ownership: assignee (or local-file record), a claim marker, and a versioned claim record carrying the full actor struct. | void (caller re-reads via `status` to confirm) |
41
+ | `release` | `subjectId`, `actor`, `meta` | Clears durable ownership and leaves a handoff note. Normally invoked by the incumbent actor at clean session end. | void |
42
+ | `supersede` | `subjectId`, `from`, `to`, `meta` | Reassigns ownership from a lapsed actor to a successor, with an audit-trail note explaining why (staleness, explicit human confirmation). | void |
43
+ | `status` | `subjectId` | Reads current assignment state without joining liveness. | `{ assignee?, actor?, claimedAt?, meta? }` — the raw assignment-layer read |
44
+ | `list` | `actor?` (optional filter) | Enumerates subject ids currently claimed, optionally filtered to one actor. | `[subjectId]` |
45
+
46
+ `status()` alone is **assignment-layer truth only** — it does not tell a caller whether the claim
47
+ is fresh. Callers that need to decide "is this work available" must compute the join described
48
+ next, not call `status()` in isolation.
49
+
50
+ **Actor identity is runtime-agnostic**: `{ runtime, session_id, host, human? }`. Claude Code,
51
+ codex, opencode, and pi sessions, and humans, are all actors. Reuse the exact struct
52
+ `scripts/hooks/lib/actor-identity.js`'s `resolveActor`/`serializeActor` already define — do not
53
+ fork a second actor concept. `subject_id` reuses `workItemSlug`'s `owner/repo#id` convention
54
+ (the same string the deterministic session slug derives from) rather than inventing a second
55
+ identifier scheme.
56
+
57
+ ## The assignment ⋈ liveness join
58
+
59
+ Effective claim state is always a **join** of two layers with distinct jobs. Neither layer is
60
+ trusted alone:
61
+
62
+ | Layer | Records | Lifetime | Medium |
63
+ | --- | --- | --- | --- |
64
+ | **Assignment** | intent / ownership | durable, survives crashes | provider (issue assignee/label/comment, or local-file record) |
65
+ | **Liveness** | presence / freshness | ephemeral, TTL-reaped | ADR 0012 claim stream (`freshHolders`) |
66
+
67
+ Readers compute the join from `{ assignment: AssignmentStatus, freshHolders: FreshHolder[] }` to
68
+ one of five effective states:
69
+
70
+ | Assignment | Liveness | Effective state | `pull-work` treatment |
71
+ | --- | --- | --- | --- |
72
+ | assigned | fresh heartbeat | **held** | excluded |
73
+ | assigned | stale / absent | **reclaimable** | offered, via takeover protocol (ADR 0021 §5, out of this contract's scope) |
74
+ | assigned (human) | n/a (humans don't heartbeat) | **human-held** | surfaced, never auto-taken (see below) |
75
+ | unassigned | fresh (claim only) | **held** (assignment lagging) | excluded |
76
+ | unassigned | absent | **free** | offered |
77
+
78
+ This join rule is what makes lost locks structurally impossible: staleness — not assignment — is
79
+ what excludes, so an orphaned assignee/label/record from a dead session can never gate work.
80
+
81
+ The join is a pure function; reuse `scripts/hooks/lib/liveness-read.js`'s exported
82
+ `freshHolders(events, slug, selfActor, nowMs)` for the liveness half rather than re-implementing
83
+ TTL/staleness arithmetic. The join table itself (assignment status × liveness freshness →
84
+ effective state) is the only new logic — it has no other implementation in this repository.
85
+
86
+ ## Lazy-correction transition table
87
+
88
+ Nothing updates the provider on a timer; every mutation has a responsible actor, invoked at a
89
+ specific lifecycle moment (ADR 0021 §4). This table names every transition for completeness; it
90
+ also records which issue implements each row so a future reader does not assume any single issue
91
+ delivers all four:
92
+
93
+ | Transition | Mutator | When | Owning issue |
94
+ | --- | --- | --- | --- |
95
+ | `claim` | the claiming session | at selection | **#290 (this issue)** |
96
+ | `claim` / `supersede` | `ensure-session`'s pre-entry ownership guard (a SECOND mutator, alongside `assignment-provider claim`) | on session entry, before any session directory is created — `free` establishes a claim, `reclaimable` requires explicit `--supersede-stale` | **#291** |
97
+ | `clean_release` | the incumbent (Stop hook / terminal `advance-state`) | session end — unassign + handoff comment | #292 |
98
+ | `supersede` | the successor, inside the takeover protocol | after the grace beat | #294 |
99
+ | `crash_no_successor` | nobody, initially; corrected by the next actor to want the subject, or the janitor | lazily, or on a janitor sweep | out of scope — the Console relay's first cross-machine duty (ADR 0021 §4/§7) |
100
+
101
+ `ensure-session`'s guard reuses this file's own `computeEffectiveState`/`performLocalClaim`/
102
+ `performLocalSupersede` (Wave 1 exports) rather than a parallel implementation — see
103
+ `docs/adr/0021-assignment-leases-and-stale-claim-takeover.md` §3 for why a second claim point
104
+ was needed (a session entered without going through `pull-work` previously got no durable claim
105
+ at all) and the canonical `actor_key` field (below) that keeps its self-recognition consistent
106
+ with every other tool that reads or writes a claim record.
107
+
108
+ Lazy correction is *safe* by the join rule above (stale assignment excludes nothing), but leaves
109
+ the human-visible board stale in the crash case until the next claim attempt or a janitor sweep
110
+ corrects it.
111
+
112
+ ## Human-assignee ask-first policy
113
+
114
+ Humans do not heartbeat, so `assigned-to-human + no-liveness` is **normal, not stale** — it must
115
+ never be treated as `reclaimable`. Agents surface idle human assignments ("assigned to `brian` 3
116
+ days ago, no linked activity — reclaim?") and act only on explicit confirmation.
117
+
118
+ - Default behavior: `ask_first`.
119
+ - The threshold (`idle_threshold_days`, default `3`, matching ADR 0021 §6's own illustrative
120
+ example) and behavior (`ask_first` | `never_reclaim`) are a policy knob in
121
+ `assignment-provider-settings.schema.json` (`policy.human_assignee_policy`), not a hardcoded
122
+ constant.
123
+ - **Non-goal:** never auto-supersede a human. The join function must gate on the actor struct's
124
+ `human` field being present (not absent/null) — never on a heuristic over the GitHub login name
125
+ — so a human assignee is always classified `human-held`, regardless of idle duration, and is
126
+ never silently reclaimed by any automated path.
127
+ - This contract and its settings schema define the policy knob and the read-side data a caller
128
+ needs to surface the question (assignee identity, idle duration). The actual "ask the user"
129
+ interaction belongs to whichever skill consumes the join result (`pull-work`), which records it
130
+ as an alignment question rather than a new UI surface.
131
+
132
+ ## Versioned claim-record format
133
+
134
+ The durable claim record is a single JSON object, versioned via `schema_version` so a reader can
135
+ detect an incompatible future shape before parsing fields it does not understand:
136
+
137
+ ```json
138
+ {
139
+ "schema_version": "1.0",
140
+ "role": "AssignmentClaimRecord",
141
+ "subject_id": "kontourai/flow-agents#290",
142
+ "actor": { "runtime": "claude-code", "session_id": "...", "host": "...", "human": null },
143
+ "claimed_at": "2026-07-02T00:00:00Z",
144
+ "ttl_seconds": 1800,
145
+ "branch": "agent/<actor>/<slug>",
146
+ "artifact_dir": ".kontourai/flow-agents/<slug>",
147
+ "status": "claimed"
148
+ }
149
+ ```
150
+
151
+ | Field | Required | Description |
152
+ | --- | --- | --- |
153
+ | `schema_version` | yes | Version of this record shape, `"1.0"`. |
154
+ | `role` | yes | Constant `"AssignmentClaimRecord"`, for readers scanning mixed content (e.g. a GitHub comment thread) for this record type. |
155
+ | `subject_id` | yes | The claimed work item, in `owner/repo#id` form — the same string `workItemSlug` derives the deterministic session slug from. |
156
+ | `actor` | yes | `{ runtime, session_id, host, human? }` — the exact struct `actor-identity.js` defines. `human` is set (non-null) only for a human assignee; its presence, not a username heuristic, gates the human-held join state. |
157
+ | `actor_key` | no (additive, #291) | The canonical `resolveActor(env).actor` string for the claiming actor — the same flat/bare token `liveness whoami`, `liveness claim --actor`, per-actor `current.json`, and pull-work's `--self-actor` all use. When present, `computeEffectiveState` compares against THIS (not a re-serialization of `actor`) for both self-recognition and the liveness join, because `serializeActor(actor)` and `resolveActor(env).actor` diverge for an explicit-override actor (a bare token vs. a `explicit-override:<value>:<host>` triple) while agreeing for a derived actor. Absent on any pre-#291 record or fixture — `computeEffectiveState` falls back to `serializeActor(actor)` in that case, reproducing pre-#291 behavior exactly. |
158
+ | `claimed_at` | yes | ISO-8601 timestamp the claim was recorded. Mirrors the liveness stream's own claim-event field so a reader compares freshness with one mental model across both layers, even though the two are stored in different media. |
159
+ | `ttl_seconds` | yes | Same field name/semantics as the liveness stream's `ttlSeconds` (default `1800`). |
160
+ | `branch` | yes | The branch this actor is working on, per the `agent/<actor>/<slug>` convention. |
161
+ | `artifact_dir` | yes | The deterministic workflow artifact directory for this subject, so a successor inherits the same session. |
162
+ | `status` | yes | `"claimed"` today; future transitions may add `"released"` / `"superseded"` values as the corresponding mutator (see the transition table) is implemented. |
163
+
164
+ **Versioning rule:** bump `schema_version` only on an incompatible change (matching
165
+ `artifact-contract.md`'s existing sidecar rule: "Keep `schema_version` at `1.0` until the schema
166
+ changes incompatibly"). Additive, optional fields do not require a bump.
167
+
168
+ ### GitHub mapping
169
+
170
+ The GitHub implementation represents the record as:
171
+
172
+ - **assignee**: the notification/board hook.
173
+ - **a single `agent:claimed` label** (default name, configurable via
174
+ `policy.label_name`): the board filter.
175
+ - **a machine-readable claim comment**: human-readable prose above a fenced JSON block containing
176
+ the record above, located via a fixed marker (default
177
+ `<!-- flow-agents:assignment-claim -->`, configurable via `policy.claim_comment_marker`) so the
178
+ board stays legible to a human without tooling, while remaining machine-parseable.
179
+
180
+ The comment carries identity because the assignee field cannot: N agent sessions typically share
181
+ one GitHub account, so per-session identity lives in the attached record. Each provider decides
182
+ what it can natively represent versus what goes in the record.
183
+
184
+ ### local-file mapping
185
+
186
+ For tracker-less repos and evals, the same record is written to a JSON file under the artifact
187
+ root (`<artifact-root>/assignment/<sanitized-subject-id>.json`), full read-modify-write, no
188
+ external process involved. This path is the one that does real I/O inside the CLI, since there is
189
+ no external mutation to defer to a skill.
190
+
191
+ **`--actor-json` asymmetry (fix-plan iteration 1, F5, intentional):** local-file `claim`/`release`
192
+ falls back to auto-resolving the actor from the live environment (`resolveActor(process.env)`) when
193
+ `--actor-json` is omitted; `render-claim`/`render-supersede` (GitHub) always require `--actor-json`
194
+ explicitly and fail loud without it. This is intentional, not a footgun to fix later — the
195
+ currently-wired `pull-work` skill always passes `--actor-json` for both provider kinds, so the
196
+ asymmetry has no effect on the shipped path today, but a future direct-CLI caller relying on
197
+ local-file `claim` to fail the same way `render-claim` does when the actor can't be determined
198
+ should pass `--actor-json` explicitly rather than relying on the auto-resolve fallback.
199
+
200
+ ## Implementation Note
201
+
202
+ `schemas/assignment-provider-settings.schema.json` is the settings schema companion to this
203
+ contract — it configures `provider.kind` (`github` | `local-file`), the label name, the claim
204
+ comment marker, and the human-assignee policy knob described above.
205
+
206
+ The GitHub write path follows this repository's `publish-change-helper.ts` precedent — **render,
207
+ don't execute**: the CLI emits the exact, deterministic `gh` argv and comment body for a mutation
208
+ as a pure function; the calling skill runs that exact argv via its Bash tool (never freehand `gh`
209
+ text), then re-fetches and feeds the result back into the CLI's `status` parser to close the round
210
+ trip. This is a deliberate divergence from a literal "CLI shells out to `gh` itself" design: it
211
+ matches the one existing same-shaped precedent in this repository, keeps operational concerns
212
+ (auth, rate limiting, retries, partial-failure sequencing across separate `gh` calls) at the skill
213
+ layer where Bash-tool error handling already lives, and avoids introducing this repository's first
214
+ `execFileSync`-to-`gh` code path inside compiled code without a dedicated decision. A future
215
+ implementer of the GitHub mutation path should read this note, not re-derive the reasoning from
216
+ scratch — see the `#290` plan artifact's Design Decision 1 for the full evidence trail if more
217
+ detail is needed.
218
+
219
+ **`gh_commands` execution contract (fix-plan iteration 1, F4):** every `render-*` subcommand's
220
+ `gh_commands` field is a JSON array of **argv arrays** (one element per `gh` argument), not a
221
+ shell-command string. The calling skill MUST execute each entry as argv — every element passed as
222
+ its own separate Bash-tool argument — and MUST NOT concatenate the elements into a single shell
223
+ string or run them via `bash -c`/shell re-interpretation. This matters specifically because
224
+ `claim_comment_body` (and, for `render-supersede`, `previous_record`) can carry attacker-influenced
225
+ text: any GitHub user who can comment on the issue can post a forged claim-marker comment (see the
226
+ sanitization note below), and reconstructing a shell string from that content would reintroduce a
227
+ shell-injection surface the render/execute split is designed to avoid. This is stated explicitly in
228
+ `kits/builder/skills/pull-work/SKILL.md`'s "Assignment Claim On Selection" section as well — this
229
+ paragraph is the contract-side mirror of that instruction, not a duplicate source of truth to drift
230
+ from it.
231
+
232
+ **Untrusted claim-record fields (fix-plan iteration 1, F2):** `extractClaimRecord()` parses the
233
+ fenced JSON claim record out of *any* issue comment matching the claim marker — posting a comment
234
+ requires no elevated access, unlike the assignee/label mutations this contract otherwise gates. Its
235
+ returned record's string fields (`subject_id`, `branch`, `artifact_dir`, `actor.*`, including
236
+ `actor.human`, and each `audit_trail` entry's actor/`reason`) are therefore treated as untrusted
237
+ display input and are run through a control-character-strip + length-cap sanitizer (mirroring
238
+ `workflow-sidecar.ts`'s `stripControlCharsForDisplay`, the established #287/#320 mitigation for
239
+ this input class) before they leave the module in `status`/`list` output. This is display-only — it
240
+ never changes the assignment ⋈ liveness join's classification logic, only the surfaced string
241
+ content.
242
+
243
+ No part of this system writes to the provider on a timer or heartbeats to it (ADR 0021 §4/§7).
244
+ Every write is triggered by a specific lifecycle moment named in the transition table above. The
245
+ one optional exception is a comment-body refresh at an explicit phase transition
246
+ (`policy.comment_refresh_on_phase_transition`, default `false`) — cheap because it reuses the same
247
+ render path already built for `claim`, just called again with an updated `status`/`claimed_at`
248
+ field.
@@ -1,5 +1,7 @@
1
1
  # Builder Kit Workflow State Contract
2
2
 
3
+ > Read [`context/contracts/standing-directives.md`](standing-directives.md) — ratified owner directives that override default engineering conservatism.
4
+
3
5
  This contract defines the durable routing state for Builder Kit build-flow pickup and resume. It keeps `state.json` limited to canonical Flow Agents workflow routing and stores rich pickup decisions in a documented Probe record sidecar referenced by `state.json`, `handoff.json`, and the session artifact.
4
6
 
5
7
  Builder Kit owns product-level build-flow coordination. Flow Agents owns the workflow harness, artifact conventions, primitive execution, and evidence gates. Flow remains the owner of Flow gate semantics. Builder Kit contracts may route to Flow Agents primitives, but must not redefine pass, fail, approval, or release authority semantics.
@@ -1,5 +1,7 @@
1
1
  # Decision Registry Contract
2
2
 
3
+ > Read [`context/contracts/standing-directives.md`](standing-directives.md) — ratified owner directives that override default engineering conservatism.
4
+
3
5
  Normative contract for the topic-keyed decision registry. This is the source of
4
6
  truth for how NEW decisions are recorded in a Kontour repository. Numbered ADRs
5
7
  under `docs/adr/` are frozen history and are never written for new decisions
@@ -1,5 +1,7 @@
1
1
  # Delivery Contract
2
2
 
3
+ > Read [`context/contracts/standing-directives.md`](standing-directives.md) — ratified owner directives that override default engineering conservatism.
4
+
3
5
  Delivery chains planning, execution, review, verification, Goal Fit, and final acceptance until the user-facing goal is genuinely handled.
4
6
 
5
7
  ## Workflow
@@ -1,5 +1,7 @@
1
1
  # Execution Contract
2
2
 
3
+ > Read [`context/contracts/standing-directives.md`](standing-directives.md) — ratified owner directives that override default engineering conservatism.
4
+
3
5
  Execution turns an approved plan artifact into code and local evidence while preserving parallel safety.
4
6
 
5
7
  ## Required Inputs
@@ -42,6 +44,29 @@ Execution may upgrade to a stronger mode when risk increases. Downgrades require
42
44
  - Worker delegation must name the exact worker role (`tool-worker`) rather than spawning an unnamed/default implementation agent.
43
45
  - After each wave, collect results, check conflicts, and update the session artifact before starting the next wave.
44
46
 
47
+ ## Delegation: Model Routing
48
+
49
+ Model-routing policy is data, not code: `.datum/config.json` (schema:
50
+ `@kontourai/datum`, provider/role registry) is the source of truth for which
51
+ model backs which delegate role. This contract only says where to read it.
52
+
53
+ - When spawning a delegate, resolve its role via `npx @kontourai/datum resolve <role> --json`
54
+ (or the `@kontourai/datum` library's `resolve()`) and pass the resolved
55
+ `model` explicitly to the delegate invocation. Do not hardcode a model name
56
+ in a skill, agent definition, or generated file — the role name is the
57
+ stable reference; the model it resolves to can change without touching
58
+ delegation call sites.
59
+ - Role selection follows task shape, not delegate identity: fully-specified
60
+ mechanical tasks resolve `delegate-mechanical`; precisely-planned
61
+ implementation tasks resolve `delegate-implementation`; tasks needing design
62
+ latitude resolve `delegate-design`. The orchestrator's own model (planning,
63
+ gates, adversarial verification) resolves `orchestrator`, typically inherited
64
+ rather than overridden.
65
+ - When `datum` (the CLI/library) or `.datum/config.json` is absent or a role
66
+ fails to resolve, fall back to the runtime's inherited model and note the
67
+ fallback in the session/task artifact — do not block delegation on datum
68
+ being present.
69
+
45
70
  ## Completion Rules
46
71
 
47
72
  Execution is complete only when:
@@ -1,5 +1,7 @@
1
1
  # Governance Adapter Contract
2
2
 
3
+ > Read [`context/contracts/standing-directives.md`](standing-directives.md) — ratified owner directives that override default engineering conservatism.
4
+
3
5
  Governance adapters let Flow Agents ask an external tool for policy, proof, or trust evidence without baking repo-specific policy into Flow Agents core.
4
6
 
5
7
  ## Boundary